AppListFragment: allow creating "linked unfreezing" shortcuts

Allow creating an "unfreeze & launch" shortcut that contains several extra apps to unfreeze before launching the main one. This can be useful for apps that have dependency relationships on other apps and the user wants a quick way to unfreeze them all while still pertaining Shelter's auto-freeze feature.
This commit is contained in:
Peter Cai 2018-10-14 13:22:29 +08:00
parent d7c86402d8
commit 3a95ba5945
No known key found for this signature in database
GPG key ID: 71F5FB4E4F3FD54F
9 changed files with 316 additions and 23 deletions

View file

@ -8,6 +8,8 @@ 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;
@ -23,6 +25,7 @@ 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 {
@ -30,6 +33,8 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
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(ViewGroup view) {
super(view);
@ -37,34 +42,127 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
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;
// 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), mView);
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), mView);
}
} 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(mView.getContext(), R.anim.scale_appear));
showSelectOrder();
}
// When the user deselects the item
void deselect() {
mSelectedIndices.remove((Integer) mIndex);
mSelectOrder.clearAnimation();
mView.setBackgroundResource(android.R.color.transparent);
Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.scale_hide);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mIndex != -1) {
hideSelectOrder(mList.get(mIndex));
}
}
@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() {
mView.setBackgroundResource(R.color.selectedAppBackground);
mSelectOrder.setVisibility(View.VISIBLE);
mSelectOrder.setText(String.valueOf(mSelectedIndices.indexOf(mIndex) + 1));
}
// When an item should be displayed in deselected state
void hideSelectOrder(ApplicationInfoWrapper info) {
// First, determine the hidden (frozen) state
if (!info.isHidden()) {
mView.setBackground(null);
} else {
mView.setBackgroundResource(R.color.disabledAppBackground);
}
mSelectOrder.setVisibility(View.GONE);
}
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);
mView.setBackgroundResource(R.color.disabledAppBackground);
} else {
mTitle.setText(info.getLabel());
mView.setBackground(null);
}
// Special logic when in multi-select mode and this item is selected
if (mMultiSelectMode && mSelectedIndices.contains(mIndex)) {
showSelectOrder();
} else {
hideSelectOrder(info);
}
// Load the application icon from cache
@ -99,14 +197,24 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
void showContextMenu(ApplicationInfoWrapper info, View view);
}
interface ActionModeHandler {
boolean createActionMode();
}
private List<ApplicationInfoWrapper> mList = new ArrayList<>();
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 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;
@ -116,6 +224,35 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.ViewHold
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;
}
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) {
mList.clear();
mIconCache.clear();

View file

@ -23,6 +23,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -40,6 +42,7 @@ import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
import java.util.List;
import java.util.stream.Collectors;
public class AppListFragment extends Fragment {
private static final String BROADCAST_REFRESH = "net.typeblog.shelter.broadcast.REFRESH";
@ -133,6 +136,12 @@ public class AppListFragment extends Fragment {
mSelectedApp = info;
mList.showContextMenuForChild(v);
});
if (mIsRemote) {
// Allow multi-select actions if this is in the work profile
// to allow things like multi-app unfreeze shortcuts
mAdapter.allowMultiSelect();
mAdapter.setActionModeHandler(this::createMultiSelectActionMode);
}
mList.setAdapter(mAdapter);
mList.setLayoutManager(new LinearLayoutManager(getActivity()));
mList.setHasFixedSize(true);
@ -146,6 +155,7 @@ public class AppListFragment extends Fragment {
void refresh() {
if (mAdapter == null) return;
if (mRefreshing) return;
if (mAdapter.isMultiSelectMode()) return; // Disallow refreshing when we are multi-selecting
mRefreshing = true;
mSwipeRefresh.setRefreshing(true);
@ -170,6 +180,50 @@ public class AppListFragment extends Fragment {
}
}
// Enter multi-select mode for work profile
boolean createMultiSelectActionMode() {
((AppCompatActivity) getActivity()).startSupportActionMode(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
menu.add(Menu.NONE, MENU_ITEM_CREATE_UNFREEZE_SHORTCUT, Menu.NONE, R.string.create_unfreeze_shortcut)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
List<ApplicationInfoWrapper> list = mAdapter.getSelectedItems();
if (list == null) {
// We can't perform any action on nothing
return false;
}
switch (item.getItemId()) {
case MENU_ITEM_CREATE_UNFREEZE_SHORTCUT:
// When multiple apps are selected for creating unfreeze & launch shortcut
// the shortcut will launch the first one, before which all the others
// will be unfrozen. This helps apps that has dependency relationships.
loadIconAndAddUnfreezeShortcut(list.get(0), list);
mode.finish();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mAdapter.cancelMultiSelectMode();
}
});
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
@ -267,18 +321,7 @@ public class AppListFragment extends Fragment {
startActivity(intent);
return true;
case MENU_ITEM_CREATE_UNFREEZE_SHORTCUT:
final ApplicationInfoWrapper app = mSelectedApp;
try {
// Call the service to load the latest icon
mService.loadIcon(app, new ILoadIconCallback.Stub() {
@Override
public void callback(Bitmap icon) {
getActivity().runOnUiThread(() -> addUnfreezeShortcut(app, icon));
}
});
} catch (RemoteException e) {
// Ignore
}
loadIconAndAddUnfreezeShortcut(mSelectedApp, null);
return true;
case MENU_ITEM_AUTO_FREEZE:
boolean orig = LocalStorageManager.getInstance().stringListContains(
@ -333,16 +376,41 @@ public class AppListFragment extends Fragment {
}
}
void addUnfreezeShortcut(ApplicationInfoWrapper app, Bitmap icon) {
void loadIconAndAddUnfreezeShortcut(final ApplicationInfoWrapper app, final List<ApplicationInfoWrapper> linkedApps) {
try {
// Call the service to load the latest icon
mService.loadIcon(app, new ILoadIconCallback.Stub() {
@Override
public void callback(Bitmap icon) {
getActivity().runOnUiThread(() -> addUnfreezeShortcut(app, linkedApps, icon));
}
});
} catch (RemoteException e) {
// Ignore
}
}
void addUnfreezeShortcut(ApplicationInfoWrapper app, List<ApplicationInfoWrapper> linkedApps, Bitmap icon) {
String id = "shelter-" + app.getPackageName();
// First, create an Intent to be sent when clicking on the shortcut
Intent launchIntent = new Intent(DummyActivity.PUBLIC_UNFREEZE_AND_LAUNCH);
launchIntent.setComponent(new ComponentName(getContext(), DummyActivity.class));
launchIntent.putExtra("packageName", app.getPackageName());
if (linkedApps != null) {
String appListStr = linkedApps.stream()
.map(ApplicationInfoWrapper::getPackageName).collect(Collectors.joining(","));
id += appListStr.hashCode();
// Multiple apps can be added so that
// these "linked" apps are all unfrozen
// before launching the main app
// Note: PersistableBundle doesn't support String array lists inside them
launchIntent.putExtra("linkedPackages", appListStr);
}
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Then tell the launcher to add the shortcut
Utility.createLauncherShortcut(getContext(), launchIntent,
Icon.createWithBitmap(icon), "shelter-" + app.getPackageName(),
Icon.createWithBitmap(icon), id,
app.getLabel());
}
}

View file

@ -319,11 +319,44 @@ public class DummyActivity extends Activity {
SettingsManager.getInstance().getAutoFreezeServiceEnabled() &&
LocalStorageManager.getInstance()
.stringListContains(LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, packageName));
if (getIntent().hasExtra("linkedPackages")) {
// Multiple apps should be unfrozen here
String[] packages = getIntent().getStringExtra("linkedPackages").split(",");
boolean[] packagesShouldFreeze = new boolean[packages.length];
for (int i = 0; i < packages.length; i++) {
// Apps in linkedPackages may also need to be auto-frozen
// thus, we loop through them and fetch the settings
packagesShouldFreeze[i] = SettingsManager.getInstance().getAutoFreezeServiceEnabled() &&
LocalStorageManager.getInstance()
.stringListContains(LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, packages[i]);
}
intent.putExtra("linkedPackages", packages);
intent.putExtra("linkedPackagesShouldFreeze", packagesShouldFreeze);
}
startActivity(intent);
finish();
return;
}
// If we have multiple linked apps to unfreeze before launching the main one
if (getIntent().hasExtra("linkedPackages")) {
String[] packages = getIntent().getStringArrayExtra("linkedPackages");
boolean[] packagesShouldFreeze = getIntent().getBooleanArrayExtra("linkedPackagesShouldFreeze");
for (int i = 0; i < packages.length; i++) {
// Unfreeze everything
mPolicyManager.setApplicationHidden(
new ComponentName(this, ShelterDeviceAdminReceiver.class),
packages[i], false);
// Register freeze service
if (packagesShouldFreeze[i]) {
registerAppToFreeze(packages[i]);
}
}
}
// Here is the main package to launch
String packageName = getIntent().getStringExtra("packageName");
// Unfreeze the app first
@ -336,8 +369,7 @@ public class DummyActivity extends Activity {
if (launchIntent != null) {
if (getIntent().getBooleanExtra("shouldFreeze", false)) {
FreezeService.registerAppToFreeze(packageName);
startService(new Intent(this, FreezeService.class));
registerAppToFreeze(packageName);
}
startActivity(launchIntent);
}
@ -345,6 +377,11 @@ public class DummyActivity extends Activity {
finish();
}
private void registerAppToFreeze(String packageName) {
FreezeService.registerAppToFreeze(packageName);
startService(new Intent(this, FreezeService.class));
}
private void actionPublicFreezeAll() {
// For now we only support freezing apps in work profile
// so forward this to DummyActivity in work profile

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="200"
android:fromXScale="0.0"
android:fromYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:duration="200"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="0.0"
android:toYScale="0.0" />
</set>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/colorAccent"/>
<size
android:width="120dp"
android:height="120dp"/>
</shape>

View file

@ -44,4 +44,19 @@
app:layout_constraintTop_toBottomOf="@id/list_app_title"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/list_app_select_order"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="16dp"
android:textColor="@android:color/white"
android:textStyle="bold"
android:background="@drawable/circle_accent"
android:gravity="center"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -6,4 +6,5 @@
<color name="black">#333333</color>
<color name="grey">#999999</color>
<color name="disabledAppBackground">#E0F2F1</color>
<color name="selectedAppBackground">#EEEEEE</color>
</resources>

View file

@ -6,6 +6,7 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionModeOverlay">true</item>
</style>
<!-- Theme for ActionBar -->