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:
parent
d7c86402d8
commit
3a95ba5945
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
11
app/src/main/res/anim/scale_appear.xml
Normal file
11
app/src/main/res/anim/scale_appear.xml
Normal 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>
|
11
app/src/main/res/anim/scale_hide.xml
Normal file
11
app/src/main/res/anim/scale_hide.xml
Normal 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>
|
12
app/src/main/res/drawable/circle_accent.xml
Normal file
12
app/src/main/res/drawable/circle_accent.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
Loading…
Reference in a new issue