diff --git a/app/build.gradle b/app/build.gradle index 9a605a1fd..8c0fc89f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation 'com.caverock:androidsvg-aar:1.3' implementation 'org.kamranzafar:jtar:2.3' implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0' + implementation 'me.drakeet.multitype:multitype:3.5.0' + implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5' def androidXVersion = "1.0.0" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' diff --git a/app/src/main/java/com/topjohnwu/magisk/adapters/AppViewBinder.java b/app/src/main/java/com/topjohnwu/magisk/adapters/AppViewBinder.java new file mode 100644 index 000000000..eba09d32f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/adapters/AppViewBinder.java @@ -0,0 +1,170 @@ +package com.topjohnwu.magisk.adapters; + +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.buildware.widget.indeterm.IndeterminateCheckBox; +import com.topjohnwu.magisk.R; +import com.topjohnwu.superuser.Shell; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import butterknife.BindView; +import java9.util.Comparators; +import me.drakeet.multitype.ItemViewBinder; + +public class AppViewBinder extends ItemViewBinder { + + private final List items; + + AppViewBinder(List items) { + this.items = items; + } + + + @Override + protected @NonNull + ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new ViewHolder(inflater.inflate(R.layout.list_item_hide_app, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull App app) { + IndeterminateCheckBox.OnStateChangedListener listener = + (IndeterminateCheckBox indeterminateCheckBox, @Nullable Boolean stat) -> { + if (stat != null && stat) { + for (ProcessViewBinder.Process process : app.processes) { + Shell.su("magiskhide --add " + process.fullname).submit(); + process.hidden = true; + } + } else if (stat != null) { + for (ProcessViewBinder.Process process : app.processes) { + Shell.su("magiskhide --rm " + process.fullname).submit(); + process.hidden = false; + } + } + app.getStatBool(true); + }; + holder.app_name.setText(app.name); + holder.app_icon.setImageDrawable(app.icon); + holder.package_name.setText(app.packageName); + holder.checkBox.setOnStateChangedListener(null); + holder.checkBox.setState(app.getStatBool(false)); + holder.checkBox.setOnStateChangedListener(listener); + if (app.expand) { + holder.checkBox.setVisibility(View.GONE); + setBottomMargin(holder.itemView, 0); + } else { + holder.checkBox.setVisibility(View.VISIBLE); + setBottomMargin(holder.itemView, 2); + } + holder.itemView.setOnClickListener((v) -> { + int index = getPosition(holder); + if (app.expand) { + app.expand = false; + items.removeAll(app.processes); + getAdapter().notifyItemRangeRemoved(index + 1, app.processes.size()); + setBottomMargin(holder.itemView, 2); + holder.checkBox.setOnStateChangedListener(null); + holder.checkBox.setVisibility(View.VISIBLE); + holder.checkBox.setState(app.getStatBool(true)); + holder.checkBox.setOnStateChangedListener(listener); + } else { + holder.checkBox.setVisibility(View.GONE); + setBottomMargin(holder.itemView, 0); + app.expand = true; + items.addAll(index + 1, app.processes); + getAdapter().notifyItemRangeInserted(index + 1, app.processes.size()); + } + }); + } + + private void setBottomMargin(View view, int dp) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + ViewGroup.MarginLayoutParams marginParams; + if (params instanceof ViewGroup.MarginLayoutParams) { + marginParams = (ViewGroup.MarginLayoutParams) params; + } else { + marginParams = new ViewGroup.MarginLayoutParams(params); + } + int px = (int) (0.5f + dp * Resources.getSystem().getDisplayMetrics().density); + marginParams.bottomMargin = px; + view.setLayoutParams(marginParams); + } + + static class ViewHolder 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; + + ViewHolder(View itemView) { + super(itemView); + new AppViewBinder$ViewHolder_ViewBinding(this, itemView); + } + } + + public static class App implements Comparable { + Drawable icon; + String name; + String packageName; + int flags; + List processes; + boolean expand = false; + int stat = -1; + + App(Drawable icon, String name, String packageName, int flags) { + this.icon = icon; + this.name = name; + this.packageName = packageName; + this.flags=flags; + this.processes = new ArrayList<>(); + } + + int getStat(boolean update) { + if (stat > 1 && !update) return stat; + int n = 0; + for (ProcessViewBinder.Process process : processes) { + if (process.hidden) n++; + } + if (n == processes.size()) stat = 2; + else if (n > 0) stat = 1; + else stat = 0; + return stat; + } + + Boolean getStatBool(boolean update) { + int stat = getStat(update); + switch (stat) { + case 2: + return true; + case 1: + return null; + case 0: + default: + return false; + } + } + + @Override + public int compareTo(App o) { + Comparator c; + c = Comparators.comparing((App t) -> t.stat); + c = Comparators.reversed(c); + c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase); + return c.compare(this, o); + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java b/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java index 512f700cf..3e388b5aa 100644 --- a/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java +++ b/app/src/main/java/com/topjohnwu/magisk/adapters/ApplicationAdapter.java @@ -6,70 +6,64 @@ 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.WorkerThread; import androidx.collection.ArraySet; -import androidx.recyclerview.widget.RecyclerView; import com.topjohnwu.magisk.App; import com.topjohnwu.magisk.Config; -import com.topjohnwu.magisk.R; import com.topjohnwu.magisk.utils.Topic; -import com.topjohnwu.magisk.utils.Utils; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.internal.UiThreadHandler; import java.util.ArrayList; import java.util.Arrays; 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.stream.Collectors; import java9.util.stream.Stream; import java9.util.stream.StreamSupport; +import me.drakeet.multitype.MultiTypeAdapter; -public class ApplicationAdapter extends RecyclerView.Adapter { +import static com.topjohnwu.magisk.utils.Utils.getAppLabel; + + +public class ApplicationAdapter { /* A list of apps that should not be shown as hide-able */ - private static final List HIDE_BLACKLIST = Arrays.asList( + private static final List HIDE_BLACKLIST = Arrays.asList( 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 String SAFETYNET_PROCESS = "com.google.android.gms.unstable"; private static final String GMS_PACKAGE = "com.google.android.gms"; - private List fullList, showList; + private List fullList; private List hideList; + private List showList; + private MultiTypeAdapter adapter; private PackageManager pm; private boolean showSystem; - public ApplicationAdapter(Context context) { - showList = Collections.emptyList(); + public ApplicationAdapter(Context context, MultiTypeAdapter adapter) { hideList = Collections.emptyList(); fullList = new ArrayList<>(); + showList = new ArrayList<>(); + this.adapter = adapter; + this.adapter.setItems(showList); pm = context.getPackageManager(); showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP); AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps); - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false); - return new ViewHolder(v); + adapter.register(AppViewBinder.App.class, new AppViewBinder(showList)); + adapter.register(ProcessViewBinder.Process.class, new ProcessViewBinder()); } private void addProcesses(Set set, ComponentInfo[] infos) { @@ -83,7 +77,7 @@ public class ApplicationAdapter extends RecyclerView.Adapter set = new ArraySet<>(); PackageInfo pkg = getPackageInfo(info.packageName); if (pkg != null) { @@ -119,9 +117,17 @@ public class ApplicationAdapter extends RecyclerView.Adapter new HideAppInfo(info, process)) - .collect(Collectors.toList())); + if (set.isEmpty()) fullList.remove(app); + for (String proc : set) { + boolean hidden = false; + for (HideTarget tgt : hideList) { + if (info.packageName.equals(tgt.pkg) && proc.equals(tgt.process)) + hidden = true; + } + app.processes.add(new ProcessViewBinder.Process(proc, hidden, info.packageName)); + } + app.getStat(true); + Collections.sort(app.processes); } } @@ -133,122 +139,47 @@ public class ApplicationAdapter extends RecyclerView.Adapter holder.checkBox.setChecked(true)); - } else { - holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> { - String pair = Utils.fmt("%s %s", target.info.packageName, target.process); - if (isChecked) { - Shell.su("magiskhide --add " + pair).submit(); - target.hidden = true; - } else { - Shell.su("magiskhide --rm " + pair).submit(); - target.hidden = false; - } - }); - } - } - - @Override - public int getItemCount() { - return showList.size(); - } // True if not system app and have launch intent, or user already hidden it - private boolean systemFilter(HideAppInfo target) { - return showSystem || target.hidden || - ((target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0 && - pm.getLaunchIntentForPackage(target.info.packageName) != null); + private boolean systemFilter(AppViewBinder.App target) { + return showSystem || target.stat != 0 || + ((target.flags & ApplicationInfo.FLAG_SYSTEM) == 0 && + pm.getLaunchIntentForPackage(target.packageName) != null); } private boolean contains(String s, String filter) { return s.toLowerCase().contains(filter); } - private boolean nameFilter(HideAppInfo target, String filter) { + private boolean nameFilter(AppViewBinder.App target, String filter) { if (filter == null || filter.isEmpty()) return true; filter = filter.toLowerCase(); return contains(target.name, filter) || - contains(target.process, filter) || - contains(target.info.packageName, filter); + contains(target.packageName, filter); } public void filter(String constraint) { AsyncTask.SERIAL_EXECUTOR.execute(() -> { - Stream s = StreamSupport.stream(fullList) + Stream s = StreamSupport.stream(fullList) .filter(this::systemFilter) .filter(t -> nameFilter(t, constraint)); UiThreadHandler.run(() -> { - showList = s.collect(Collectors.toList()); - notifyDataSetChanged(); + showList.clear(); + for (AppViewBinder.App target : s.collect(Collectors.toList())) { + if (target.expand) { + showList.add(target); + showList.addAll(target.processes); + } else showList.add(target); + } + adapter.notifyDataSetChanged(); }); }); } public void refresh() { - AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps); - } - - static class ViewHolder extends RecyclerView.ViewHolder { - - @BindView(R.id.app_icon) ImageView appIcon; - @BindView(R.id.app_name) TextView appName; - @BindView(R.id.process) TextView process; - @BindView(R.id.package_name) TextView appPackage; - @BindView(R.id.checkbox) CheckBox checkBox; - - ViewHolder(View itemView) { - super(itemView); - new ApplicationAdapter$ViewHolder_ViewBinding(this, itemView); - } - } - - class HideAppInfo implements Comparable { - String process; - String name; - ApplicationInfo info; - boolean hidden; - - HideAppInfo(ApplicationInfo info, String process) { - this.process = process; - this.info = info; - name = Utils.getAppLabel(info, pm); - for (HideTarget tgt : hideList) { - if (tgt.process.equals(process)) { - hidden = true; - break; - } - } - } - - @Override - public int compareTo(HideAppInfo o) { - Comparator c; - c = Comparators.comparing((HideAppInfo t) -> t.hidden); - c = Comparators.reversed(c); - c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase); - c = Comparators.thenComparing(c, t -> t.info.packageName); - c = Comparators.thenComparing(c, t -> t.process); - return c.compare(this, o); - } + Collections.sort(fullList); + Topic.publish(false, Topic.MAGISK_HIDE_DONE); } class HideTarget { diff --git a/app/src/main/java/com/topjohnwu/magisk/adapters/ProcessViewBinder.java b/app/src/main/java/com/topjohnwu/magisk/adapters/ProcessViewBinder.java new file mode 100644 index 000000000..b20917801 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/adapters/ProcessViewBinder.java @@ -0,0 +1,80 @@ +package com.topjohnwu.magisk.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.topjohnwu.magisk.R; +import com.topjohnwu.magisk.utils.Utils; +import com.topjohnwu.superuser.Shell; + +import java.util.Comparator; + +import butterknife.BindView; +import java9.util.Comparators; +import me.drakeet.multitype.ItemViewBinder; + +public class ProcessViewBinder extends ItemViewBinder { + + @Override + protected @NonNull + ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new ViewHolder(inflater.inflate(R.layout.list_item_hide_process, parent, false)); + } + + @Override + protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Process process) { + holder.process.setText(process.name); + holder.checkbox.setOnCheckedChangeListener(null); + holder.checkbox.setChecked(process.hidden); + holder.checkbox.setOnCheckedChangeListener((v, isChecked) -> { + if (isChecked) { + Shell.su("magiskhide --add " + process.fullname).submit(); + process.hidden = true; + } else { + Shell.su("magiskhide --rm " + process.fullname).submit(); + process.hidden = false; + } + + }); + + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + @BindView(R.id.process) TextView process; + @BindView(R.id.checkbox) CheckBox checkbox; + + ViewHolder(View itemView) { + super(itemView); + new ProcessViewBinder$ViewHolder_ViewBinding(this, itemView); + } + } + + public static class Process implements Comparable { + String name; + boolean hidden; + String fullname; + String packageName; + + Process(String name, boolean hidden, String packageName) { + this.name = name; + this.hidden = hidden; + this.packageName=packageName; + this.fullname = Utils.fmt("%s %s", packageName, name); + } + + @Override + public int compareTo(Process o) { + Comparator c; + c = Comparators.comparing((Process t) -> !t.name.startsWith(t.packageName)); + c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase); + return c.compare(this, o); + } + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/fragments/MagiskHideFragment.java b/app/src/main/java/com/topjohnwu/magisk/fragments/MagiskHideFragment.java index c7bae793a..5b6c2e0e2 100644 --- a/app/src/main/java/com/topjohnwu/magisk/fragments/MagiskHideFragment.java +++ b/app/src/main/java/com/topjohnwu/magisk/fragments/MagiskHideFragment.java @@ -21,6 +21,7 @@ import com.topjohnwu.magisk.components.BaseFragment; import com.topjohnwu.magisk.utils.Topic; import butterknife.BindView; +import me.drakeet.multitype.MultiTypeAdapter; public class MagiskHideFragment extends BaseFragment implements Topic.Subscriber { @@ -28,7 +29,7 @@ public class MagiskHideFragment extends BaseFragment implements Topic.Subscriber @BindView(R.id.recyclerView) RecyclerView recyclerView; private SearchView search; - private ApplicationAdapter adapter; + private ApplicationAdapter applicationAdapter; private SearchView.OnQueryTextListener searchListener; @Override @@ -43,22 +44,23 @@ public class MagiskHideFragment extends BaseFragment implements Topic.Subscriber View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false); unbinder = new MagiskHideFragment_ViewBinding(this, view); - adapter = new ApplicationAdapter(requireActivity()); + MultiTypeAdapter adapter = new MultiTypeAdapter(); + applicationAdapter = new ApplicationAdapter(requireActivity(), adapter); recyclerView.setAdapter(adapter); mSwipeRefreshLayout.setRefreshing(true); - mSwipeRefreshLayout.setOnRefreshListener(adapter::refresh); + mSwipeRefreshLayout.setOnRefreshListener(applicationAdapter::refresh); searchListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { - adapter.filter(query); + applicationAdapter.filter(query); return false; } @Override public boolean onQueryTextChange(String newText) { - adapter.filter(newText); + applicationAdapter.filter(newText); return false; } }; @@ -82,8 +84,8 @@ public class MagiskHideFragment extends BaseFragment implements Topic.Subscriber boolean showSystem = !item.isChecked(); item.setChecked(showSystem); Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem); - adapter.setShowSystem(showSystem); - adapter.filter(search.getQuery().toString()); + applicationAdapter.setShowSystem(showSystem); + applicationAdapter.filter(search.getQuery().toString()); } return true; } @@ -96,6 +98,6 @@ public class MagiskHideFragment extends BaseFragment implements Topic.Subscriber @Override public void onPublish(int topic, Object[] result) { mSwipeRefreshLayout.setRefreshing(false); - adapter.filter(search.getQuery().toString()); + applicationAdapter.filter(search.getQuery().toString()); } } diff --git a/app/src/main/res/layout/list_item_app.xml b/app/src/main/res/layout/list_item_hide_app.xml similarity index 77% rename from app/src/main/res/layout/list_item_app.xml rename to app/src/main/res/layout/list_item_hide_app.xml index f36189993..7180fc7f3 100644 --- a/app/src/main/res/layout/list_item_app.xml +++ b/app/src/main/res/layout/list_item_hide_app.xml @@ -1,6 +1,7 @@ - - - - + app:layout_constraintTop_toBottomOf="@+id/app_name" /> - - + /> diff --git a/app/src/main/res/layout/list_item_hide_process.xml b/app/src/main/res/layout/list_item_hide_process.xml new file mode 100644 index 000000000..273a6740b --- /dev/null +++ b/app/src/main/res/layout/list_item_hide_process.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + +