package com.topjohnwu.magisk.adapters; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.collection.ArraySet; import androidx.recyclerview.widget.RecyclerView; import com.buildware.widget.indeterm.IndeterminateCheckBox; import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.Config; import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.uicomponents.ArrowExpandable; import com.topjohnwu.magisk.uicomponents.Expandable; import com.topjohnwu.magisk.utils.Event; import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import butterknife.BindView; import java9.util.Comparators; import java9.util.Lists; import java9.util.Objects; import java9.util.Sets; import java9.util.stream.Collectors; import java9.util.stream.Stream; import java9.util.stream.StreamSupport; public class ApplicationAdapter extends SectionedAdapter { private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable"; private static final String GMS_PACKAGE = "com.google.android.gms"; private static boolean old_hide = false; /* A list of apps that should not be shown as hide-able */ private static final List HIDE_BLACKLIST = Lists.of( App.self.getPackageName(), "android", "com.android.chrome", "com.chrome.beta", "com.chrome.dev", "com.chrome.canary", "com.android.webview", "com.google.android.webview" ); private static final List DEFAULT_HIDELIST = Lists.of( SAFETYNET_PROCESS ); private static int BOTTOM_MARGIN = -1; private List fullList, showList; private List hideList; private PackageManager pm; private boolean showSystem; public ApplicationAdapter(Context context) { fullList = showList = Collections.emptyList(); hideList = Collections.emptyList(); pm = context.getPackageManager(); showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP); AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps); } private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) { return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams(); } @Override public int getSectionCount() { return showList.size(); } @Override public int getItemCount(int section) { HideAppInfo app = showList.get(section); return app.expanded ? app.processList.size() : 0; } @Override public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false); AppViewHolder vh = new AppViewHolder(v); if (BOTTOM_MARGIN < 0) BOTTOM_MARGIN = getMargins(vh).bottomMargin; return vh; } @Override public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false); return new ProcessViewHolder(v); } @Override public void onBindSectionViewHolder(AppViewHolder holder, int section) { HideAppInfo app = showList.get(section); holder.app_name.setText(app.name); holder.app_icon.setImageDrawable(app.info.loadIcon(pm)); holder.package_name.setText(app.info.packageName); holder.checkBox.setOnStateChangedListener(null); holder.checkBox.setState(app.getState()); holder.ex.setExpanded(app.expanded); int index = getItemPosition(section, 0); holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> { if (status != null) { setHide(status, app); if (app.expanded) notifyItemRangeChanged(index, app.processList.size()); } }); if (app.processList.size() > 1) { holder.arrow.setVisibility(View.VISIBLE); holder.trigger.setOnClickListener((v) -> { if (app.expanded) { app.expanded = false; notifyItemRangeRemoved(index, app.processList.size()); holder.ex.collapse(); } else { app.expanded = true; notifyItemRangeInserted(index, app.processList.size()); holder.ex.expand(); } }); } else { holder.arrow.setVisibility(View.GONE); holder.trigger.setOnClickListener(null); } } @Override public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) { HideAppInfo app = showList.get(section); HideProcessInfo target = app.processList.get(position); holder.process.setText(target.name); holder.checkbox.setOnCheckedChangeListener(null); holder.checkbox.setChecked(target.hidden); holder.checkbox.setOnCheckedChangeListener((v, checked) -> { setHide(checked, app, target); notifyItemChanged(getSectionPosition(section)); }); getMargins(holder).bottomMargin = position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0; } public void filter(String constraint) { AsyncTask.SERIAL_EXECUTOR.execute(() -> { Stream s = StreamSupport.stream(fullList) .filter(this::systemFilter) .filter(t -> nameFilter(t, constraint)); UiThreadHandler.run(() -> { showList = s.collect(Collectors.toList()); notifyDataSetChanged(); }); }); } public void setShowSystem(boolean b) { showSystem = b; } public void refresh() { AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps); } private void setHide(boolean add, HideAppInfo app) { if (add) { StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p)); } else { if (StreamSupport.stream(app.processList) .anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) { StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p)); } else { // Quick removal Shell.su("magiskhide --rm " + app.info.packageName).submit(); StreamSupport.stream(app.processList).forEach(p -> p.hidden = false); } } } private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) { // Don't remove SafetyNet if (!add && DEFAULT_HIDELIST.contains(process.name)) return; Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm", app.info.packageName, process.name)).submit(); process.hidden = add; } private void addProcesses(Set set, ComponentInfo[] infos) { if (infos != null) for (ComponentInfo info : infos) set.add(info.processName); } private PackageInfo getPackageInfo(String pkg) { // Try super hard to get as much info as possible try { return pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS); } catch (Exception e1) { try { PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES); info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services; info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers; info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers; return info; } catch (Exception e2) { return null; } } } @WorkerThread private void loadApps() { hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut()) .map(HideTarget::new) .collect(Collectors.toList()); fullList = StreamSupport.stream(pm.getInstalledApplications(0)) .filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled) .map(info -> { if (old_hide) { return new HideAppInfo(info, Sets.of(info.packageName)); } else { Set set = new ArraySet<>(); PackageInfo pkg = getPackageInfo(info.packageName); if (pkg != null) { addProcesses(set, pkg.activities); addProcesses(set, pkg.services); addProcesses(set, pkg.receivers); addProcesses(set, pkg.providers); } if (set.isEmpty()) return null; return new HideAppInfo(info, set); } }).filter(Objects::nonNull).sorted() .collect(Collectors.toList()); Event.trigger(false, Event.MAGISK_HIDE_DONE); } // True if not system app or user already hidden it private boolean systemFilter(HideAppInfo target) { return showSystem || target.haveHidden() || (target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0; } private boolean contains(String s, String filter) { return s.toLowerCase().contains(filter); } private boolean nameFilter(HideAppInfo target, String filter) { if (filter == null || filter.isEmpty()) return true; filter = filter.toLowerCase(); if (contains(target.name, filter)) return true; for (HideProcessInfo p : target.processList) { if (contains(p.name, filter)) return true; } return contains(target.info.packageName, filter); } class HideAppInfo implements Comparable { String name; ApplicationInfo info; List processList; boolean expanded; IndeterminateCheckBox.OnStateChangedListener listener; HideAppInfo(ApplicationInfo appInfo, Set set) { info = appInfo; name = Utils.getAppLabel(info, pm); expanded = false; processList = StreamSupport.stream(set) .map(process -> new HideProcessInfo(info.packageName, process)) .sorted().collect(Collectors.toList()); listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> { if (status != null) { for (HideProcessInfo p : processList) { String cmd = Utils.fmt("magiskhide --%s %s %s", status ? "add" : "rm", info.packageName, p.name); Shell.su(cmd).submit(); p.hidden = status; } } }; } @Override public int compareTo(HideAppInfo o) { Comparator c; c = Comparators.comparing(HideAppInfo::haveHidden); c = Comparators.reversed(c); c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase); c = Comparators.thenComparing(c, t -> t.info.packageName); return c.compare(this, o); } Boolean getState() { boolean all = true; boolean hidden = false; for (HideProcessInfo p : processList) { if (!p.hidden) all = false; else hidden = true; } if (all) return true; return hidden ? null : false; } boolean haveHidden() { Boolean c = getState(); return c == null ? true : c; } } class HideProcessInfo implements Comparable { String name; boolean hidden; HideProcessInfo(String pkg, String process) { this.name = process; for (HideTarget t : hideList) { if (t.pkg.equals(pkg) && t.process.equals(process)) { hidden = true; break; } } } @Override public int compareTo(HideProcessInfo o) { Comparator c; c = Comparators.comparing((HideProcessInfo t) -> t.hidden); c = Comparators.reversed(c); c = Comparators.thenComparing(c, t -> t.name); return c.compare(this, o); } } class HideTarget { String pkg; String process; HideTarget(String line) { String[] split = line.split("\\|", 2); pkg = split[0]; if (split.length == 2) { process = split[1]; } else { // Backwards compatibility old_hide = true; process = pkg; } } } class AppViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.app_icon) ImageView app_icon; @BindView(R.id.app_name) TextView app_name; @BindView(R.id.package_name) TextView package_name; @BindView(R.id.checkbox) IndeterminateCheckBox checkBox; @BindView(R.id.trigger) View trigger; @BindView(R.id.arrow) ImageView arrow; Expandable ex; AppViewHolder(@NonNull View itemView) { super(itemView); new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView); ex = new ArrowExpandable(new Expandable() { @Override protected void onExpand() { getMargins(AppViewHolder.this).bottomMargin = 0; } @Override protected void onCollapse() { getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN; } }, arrow); } } class ProcessViewHolder extends RecyclerView.ViewHolder { @BindView(R.id.process) TextView process; @BindView(R.id.checkbox) CheckBox checkbox; ProcessViewHolder(@NonNull View itemView) { super(itemView); new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView); } } }