Shelter/app/src/main/java/net/typeblog/shelter/ui/AppListFragment.java

347 lines
14 KiB
Java

package net.typeblog.shelter.ui;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import net.typeblog.shelter.R;
import net.typeblog.shelter.services.IAppInstallCallback;
import net.typeblog.shelter.services.IGetAppsCallback;
import net.typeblog.shelter.services.ILoadIconCallback;
import net.typeblog.shelter.services.IShelterService;
import net.typeblog.shelter.services.ShelterService;
import net.typeblog.shelter.util.ApplicationInfoWrapper;
import net.typeblog.shelter.util.LocalStorageManager;
import net.typeblog.shelter.util.Utility;
import java.util.List;
public class AppListFragment extends Fragment {
private static final String BROADCAST_REFRESH = "net.typeblog.shelter.broadcast.REFRESH";
// Menu Items
private static final int MENU_ITEM_CLONE = 10001;
private static final int MENU_ITEM_UNINSTALL = 10002;
private static final int MENU_ITEM_FREEZE = 10003;
private static final int MENU_ITEM_UNFREEZE = 10004;
private static final int MENU_ITEM_LAUNCH = 10005;
private static final int MENU_ITEM_CREATE_UNFREEZE_SHORTCUT = 10006;
private static final int MENU_ITEM_AUTO_FREEZE = 10007;
private IShelterService mService = null;
private boolean mIsRemote = false;
private boolean mRefreshing = false;
private Drawable mDefaultIcon = null;
private ApplicationInfoWrapper mSelectedApp = null;
// Views
private RecyclerView mList = null;
private AppListAdapter mAdapter = null;
private SwipeRefreshLayout mSwipeRefresh = null;
// Receiver for Refresh events
// used for app changes
private BroadcastReceiver mRefreshReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
refresh();
}
};
// Receiver for context menu closed event
private BroadcastReceiver mContextMenuClosedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mSelectedApp = null;
}
};
static AppListFragment newInstance(IShelterService service, boolean isRemote) {
AppListFragment fragment = new AppListFragment();
Bundle args = new Bundle();
args.putBinder("service", service.asBinder());
args.putBoolean("is_remote", isRemote);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDefaultIcon = getActivity().getPackageManager().getDefaultActivityIcon();
IBinder service = getArguments().getBinder("service");
mService = IShelterService.Stub.asInterface(service);
mIsRemote = getArguments().getBoolean("is_remote");
}
@Override
public void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(getContext())
.registerReceiver(mRefreshReceiver, new IntentFilter(BROADCAST_REFRESH));
LocalBroadcastManager.getInstance(getContext())
.registerReceiver(mContextMenuClosedReceiver,
new IntentFilter(MainActivity.BROADCAST_CONTEXT_MENU_CLOSED));
refresh();
}
@Override
public void onPause() {
super.onPause();
mSelectedApp = null;
LocalBroadcastManager.getInstance(getContext())
.unregisterReceiver(mRefreshReceiver);
LocalBroadcastManager.getInstance(getContext())
.unregisterReceiver(mContextMenuClosedReceiver);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_list, container, false);
// Save the views
mList = view.findViewById(R.id.fragment_list_recycler_view);
mSwipeRefresh = view.findViewById(R.id.fragment_swipe_refresh);
mAdapter = new AppListAdapter(mService, mDefaultIcon);
mAdapter.setContextMenuHandler((info, v) -> {
mSelectedApp = info;
mList.showContextMenuForChild(v);
});
mList.setAdapter(mAdapter);
mList.setLayoutManager(new LinearLayoutManager(getActivity()));
mList.setHasFixedSize(true);
mSwipeRefresh.setOnRefreshListener(this::refresh);
registerForContextMenu(mList);
return view;
}
void refresh() {
if (mAdapter == null) return;
if (mRefreshing) return;
mRefreshing = true;
mSwipeRefresh.setRefreshing(true);
try {
mService.getApps(new IGetAppsCallback.Stub() {
@Override
public void callback(List<ApplicationInfoWrapper> apps) {
if (mIsRemote) {
Utility.deleteMissingApps(
LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE,
apps);
}
getActivity().runOnUiThread(() -> {
mSwipeRefresh.setRefreshing(false);
mAdapter.setData(apps);
mRefreshing = false;
});
}
});
} catch (RemoteException e) {
// Just... do nothing for now
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (mSelectedApp == null) return;
if (mIsRemote) {
if (!mSelectedApp.isSystem())
menu.add(Menu.NONE, MENU_ITEM_CLONE, Menu.NONE, R.string.clone_to_main_profile);
// Freezing / Unfreezing is only available in profiles that we can control
if (mSelectedApp.isHidden()) {
menu.add(Menu.NONE, MENU_ITEM_UNFREEZE, Menu.NONE, R.string.unfreeze_app);
menu.add(Menu.NONE, MENU_ITEM_LAUNCH, Menu.NONE, R.string.unfreeze_and_launch);
} else {
menu.add(Menu.NONE, MENU_ITEM_FREEZE, Menu.NONE, R.string.freeze_app);
menu.add(Menu.NONE, MENU_ITEM_LAUNCH, Menu.NONE, R.string.launch);
}
// TODO: If we implement God Mode (i.e. Shelter as device owner), we should
// TODO: use two different lists to store auto freeze apps because we'll be
// TODO: able to freeze apps in main profile.
MenuItem autoFreeze = menu.add(Menu.NONE, MENU_ITEM_AUTO_FREEZE, Menu.NONE, R.string.auto_freeze);
autoFreeze.setCheckable(true);
autoFreeze.setChecked(
LocalStorageManager.getInstance().stringListContains(
LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, mSelectedApp.getPackageName()));
menu.add(Menu.NONE, MENU_ITEM_CREATE_UNFREEZE_SHORTCUT, Menu.NONE, R.string.create_unfreeze_shortcut);
} else {
menu.add(Menu.NONE, MENU_ITEM_CLONE, Menu.NONE, R.string.clone_to_work_profile);
}
if (!mSelectedApp.isSystem()) {
// We can't uninstall system apps in both cases
// but we'll be able to "freeze" them
menu.add(Menu.NONE, MENU_ITEM_UNINSTALL, Menu.NONE, R.string.uninstall_app);
}
if (menu.size() > 0) {
// Only set title when the menu is not empty
// this ensures that no menu will be shown
// if no operation available
menu.setHeaderTitle(
getString(R.string.app_context_menu_title, mSelectedApp.getLabel()));
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (mSelectedApp == null) return false;
switch (item.getItemId()) {
case MENU_ITEM_CLONE:
if (Utility.isMIUI() && !mSelectedApp.isSystem()) {
// Cannot clone non-system apps on MIUI
new AlertDialog.Builder(getContext())
.setMessage(R.string.miui_cannot_clone)
.setPositiveButton(android.R.string.ok, null)
.show();
} else {
installOrUninstall(mSelectedApp, true);
}
return true;
case MENU_ITEM_UNINSTALL:
installOrUninstall(mSelectedApp, false);
return true;
case MENU_ITEM_FREEZE:
try {
mService.freezeApp(mSelectedApp);
} catch (RemoteException e) {
}
Toast.makeText(getContext(),
getString(R.string.freeze_success, mSelectedApp.getLabel()), Toast.LENGTH_SHORT).show();
refresh();
return true;
case MENU_ITEM_UNFREEZE:
try {
mService.unfreezeApp(mSelectedApp);
} catch (RemoteException e) {
}
Toast.makeText(getContext(),
getString(R.string.unfreeze_success, mSelectedApp.getLabel()), Toast.LENGTH_SHORT).show();
refresh();
return true;
case MENU_ITEM_LAUNCH:
// LAUNCH and UNFREEZE_AND_LAUNCH share the same ID
// because the implementation of UNFREEZE_AND_LAUNCH in DummyActivity
// will work for both
Intent intent = new Intent(DummyActivity.UNFREEZE_AND_LAUNCH);
intent.setComponent(new ComponentName(getContext(), DummyActivity.class));
intent.putExtra("packageName", mSelectedApp.getPackageName());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
DummyActivity.registerSameProcessRequest(intent);
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
}
return true;
case MENU_ITEM_AUTO_FREEZE:
boolean orig = LocalStorageManager.getInstance().stringListContains(
LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, mSelectedApp.getPackageName());
if (!orig) {
LocalStorageManager.getInstance().appendStringList(
LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, mSelectedApp.getPackageName());
} else {
LocalStorageManager.getInstance().removeFromStringList(
LocalStorageManager.PREF_AUTO_FREEZE_LIST_WORK_PROFILE, mSelectedApp.getPackageName());
}
return true;
}
return super.onContextItemSelected(item);
}
void installOrUninstall(final ApplicationInfoWrapper app, final boolean isInstall) {
mSelectedApp = null;
IAppInstallCallback.Stub callback = new IAppInstallCallback.Stub() {
@Override
public void callback(int result) {
getActivity().runOnUiThread(() ->
installAppCallback(result, app, isInstall));
}
};
try {
if (isInstall) {
((MainActivity) getActivity()).getOtherService(mIsRemote)
.installApp(app, callback);
} else {
mService.uninstallApp(app, callback);
}
} catch (RemoteException e) {
// TODO: Maybe tell the user?
}
}
void installAppCallback(int result, ApplicationInfoWrapper app, boolean isInstall) {
if (result == Activity.RESULT_OK) {
String message = getString(isInstall ? R.string.clone_success : R.string.uninstall_success);
message = String.format(message, app.getLabel());
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
LocalBroadcastManager.getInstance(getContext())
.sendBroadcast(new Intent(BROADCAST_REFRESH));
} else if (result == ShelterService.RESULT_CANNOT_INSTALL_SYSTEM_APP) {
Toast.makeText(getContext(),
getString(isInstall ? R.string.clone_fail_system_app :
R.string.uninstall_fail_system_app), Toast.LENGTH_SHORT).show();
}
}
void addUnfreezeShortcut(ApplicationInfoWrapper app, Bitmap icon) {
// 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());
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(),
app.getLabel());
}
}