Shelter/app/src/main/java/net/typeblog/shelter/ui/AppListAdapter.java
2019-02-26 23:34:04 +01:00

346 lines
12 KiB
Java

package net.typeblog.shelter.ui;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.typeblog.shelter.R;
import net.typeblog.shelter.services.ILoadIconCallback;
import net.typeblog.shelter.services.IShelterService;
import net.typeblog.shelter.util.ApplicationInfoWrapper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHolder> {
class ViewHolder extends RecyclerView.ViewHolder {
private ImageView mIcon;
private TextView mTitle;
private TextView mPackage;
// This text view shows the order of all selected items
private TextView mSelectOrder;
int mIndex = -1;
ViewHolder(View view) {
super(view);
mIcon = view.findViewById(R.id.list_app_icon);
mTitle = view.findViewById(R.id.list_app_title);
mPackage = view.findViewById(R.id.list_app_package);
mSelectOrder = view.findViewById(R.id.list_app_select_order);
view.setOnClickListener((v) -> onClick());
if (mAllowMultiSelect) {
view.setOnLongClickListener((v) -> onLongClick());
}
}
void onClick() {
if (mIndex == -1) return;
if (!mMultiSelectMode) {
// Show available operations via the Fragment
// pass the full info to it, since we can't be sure
// the index won't change
if (mContextMenuHandler != null) {
mContextMenuHandler.showContextMenu(mList.get(mIndex), itemView);
}
} else {
// In multi-select mode, single clicks just adds to the selection
// or cancels the selection if already selected
if (!mSelectedIndices.contains(mIndex)) {
select();
} else {
deselect();
}
}
}
boolean onLongClick() {
if (mIndex == -1) return false;
// If we have an action mode handler, we notify it to enter
// action mode on long click, and register this adapter
// to be in multi-select mode
if (!mMultiSelectMode && mActionModeHandler != null && mActionModeHandler.createActionMode()) {
mMultiSelectMode = true;
select();
return true;
} else {
return false;
}
}
// When the user selects the item
// we need to play the animation of the "select order" appearing
// on the right side of the item view
void select() {
mSelectedIndices.add(mIndex);
mSelectOrder.clearAnimation();
mSelectOrder.startAnimation(AnimationUtils.loadAnimation(itemView.getContext(), R.anim.scale_appear));
showSelectOrder();
}
// When the user deselects the item
void deselect() {
mSelectedIndices.remove((Integer) mIndex);
mSelectOrder.clearAnimation();
setUnselectedBackground();
Animation anim = AnimationUtils.loadAnimation(itemView.getContext(), R.anim.scale_hide);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mActionModeCancelHandler != null && mSelectedIndices.size() == 0) {
// If there is no selection left, tell parent to cancel the action mode.
mActionModeCancelHandler.cancelActionMode();
}
// The selection index of items other than this one
// can be changed because of the removal of the current one
// Thus, we just notify that the data set has been changed,
// to force redraw all of them.
notifyDataSetChanged();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
mSelectOrder.startAnimation(anim);
}
// When an item should be displayed in selected state
// (not necessarily when the user clicked on it; the view might have been recycled)
void showSelectOrder() {
if (!mList.get(mIndex).isHidden()) {
itemView.setBackgroundResource(R.color.selectedAppBackground);
} else {
// The app is both frozen and selected
// we use a blended color of the two for its background
itemView.setBackgroundResource(R.color.selectedAndDisabledAppBackground);
}
mSelectOrder.setVisibility(View.VISIBLE);
mSelectOrder.setText(String.valueOf(mSelectedIndices.indexOf(mIndex) + 1));
}
// When an item should be displayed in deselected state
void hideSelectOrder() {
setUnselectedBackground();
mSelectOrder.setVisibility(View.GONE);
}
// Set the background when not in the selected state
void setUnselectedBackground() {
if (!mList.get(mIndex).isHidden()) {
itemView.setBackground(null);
} else {
itemView.setBackgroundResource(R.color.disabledAppBackground);
}
}
void setIndex(final int index) {
mIndex = index;
if (mIndex >= 0) {
// Clear all animations first
mSelectOrder.clearAnimation();
ApplicationInfoWrapper info = mList.get(mIndex);
mPackage.setText(info.getPackageName());
if (info.isHidden()) {
String label = String.format(mLabelDisabled, info.getLabel());
mTitle.setText(label);
} else {
mTitle.setText(info.getLabel());
}
// Special logic when in multi-select mode and this item is selected
if (mMultiSelectMode && mSelectedIndices.contains(mIndex)) {
showSelectOrder();
} else {
hideSelectOrder();
}
// Load the application icon from cache
// or populate the cache through the service
if (mIconCache.containsKey(info.getPackageName())) {
mIcon.setImageBitmap(mIconCache.get(info.getPackageName()));
} else {
mIcon.setImageDrawable(mDefaultIcon);
try {
mService.loadIcon(info, new ILoadIconCallback.Stub() {
@Override
public void callback(Bitmap icon) {
if (index == mIndex) {
mHandler.post(() -> mIcon.setImageBitmap(icon));
}
synchronized (AppListAdapter.class) {
mIconCache.put(info.getPackageName(), icon);
}
}
});
} catch (RemoteException e) {
// Do Nothing
}
}
}
}
}
interface ContextMenuHandler {
void showContextMenu(ApplicationInfoWrapper info, View view);
}
interface ActionModeHandler {
boolean createActionMode();
}
interface ActionModeCancelHandler {
void cancelActionMode();
}
// The ORIGINAL list of applications without filtering
private List<ApplicationInfoWrapper> mOrigList = new ArrayList<>();
// The list of applications that is ACTUALLY displayed
// (after filtering by search query if applicable)
private List<ApplicationInfoWrapper> mList = new ArrayList<>();
private String mSearchQuery = null;
private IShelterService mService;
private Drawable mDefaultIcon;
private String mLabelDisabled;
private Map<String, Bitmap> mIconCache = new HashMap<>();
private ContextMenuHandler mContextMenuHandler = null;
private ActionModeHandler mActionModeHandler = null;
private ActionModeCancelHandler mActionModeCancelHandler = null;
private Handler mHandler = new Handler(Looper.getMainLooper());
// Multi-selection mode
private boolean mAllowMultiSelect = false;
private boolean mMultiSelectMode = false;
private List<Integer> mSelectedIndices = new ArrayList<>();
AppListAdapter(IShelterService service, Drawable defaultIcon) {
mService = service;
mDefaultIcon = defaultIcon;
}
void setContextMenuHandler(ContextMenuHandler handler) {
mContextMenuHandler = handler;
}
// When we enter multi-select mode, we have to notify our parent fragment
// to enter action mode, in order to show the specific menus
void setActionModeHandler(ActionModeHandler handler) {
mActionModeHandler = handler;
}
// When there is no selection left, we have to notify our parent fragment
// to exit action mode
void setActionModeCancelHandler(ActionModeCancelHandler handler) {
mActionModeCancelHandler = handler;
}
void allowMultiSelect() {
mAllowMultiSelect = true;
}
boolean isMultiSelectMode() {
return mMultiSelectMode;
}
void cancelMultiSelectMode() {
mMultiSelectMode = false;
mSelectedIndices.clear();
notifyDataSetChanged();
}
List<ApplicationInfoWrapper> getSelectedItems() {
if (!mMultiSelectMode) return null;
if (mSelectedIndices.size() == 0) return null;
return mSelectedIndices.stream()
.map((idx) -> mList.get(idx))
.collect(Collectors.toList());
}
void setData(List<ApplicationInfoWrapper> apps) {
mOrigList.clear();
mList.clear();
mIconCache.clear();
mOrigList.addAll(apps);
notifyChange();
}
// null = clear search query
void setSearchQuery(String query) {
mSearchQuery = query;
notifyChange();
}
// Call this on ACTUAL data set change and/or search query change
private void notifyChange() {
mList.clear();
if (mSearchQuery == null) {
// No search query, do not filter
mList.addAll(mOrigList);
} else {
// Filter by search query
mList.addAll(mOrigList.stream()
.filter((app) ->
app.getPackageName().toLowerCase().contains(mSearchQuery)
|| app.getLabel().toLowerCase().contains(mSearchQuery))
.collect(Collectors.toList()));
}
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return mList.size();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
if (mLabelDisabled == null) {
mLabelDisabled = viewGroup.getContext().getString(R.string.list_item_disabled);
}
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
View view = inflater.inflate(R.layout.app_list_item, viewGroup, false);
ViewHolder vh = new ViewHolder(view);
vh.setIndex(i);
return vh;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
viewHolder.setIndex(i);
}
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
super.onViewRecycled(holder);
holder.setIndex(-1);
}
}