Skip to content

Commit 3d502a9

Browse files
committed
[Profile] Implement filter-based profile
Filter-based profiles are very similar to the apps-based profile except when the profile is being applied, the list of applications are retrieved and filtered using the filter options. To facilitate the user, a real-time filtered list of applications are displayed in the profile (editor) page. Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 5674d8e commit 3d502a9

12 files changed

Lines changed: 135 additions & 26 deletions

app/src/main/java/io/github/muntashirakon/AppManager/filters/FilterableAppInfo.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import android.content.pm.PermissionInfo;
1313
import android.content.pm.ProviderInfo;
1414
import android.content.pm.ServiceInfo;
15+
import android.graphics.drawable.Drawable;
1516
import android.os.Build;
1617
import android.os.RemoteException;
1718
import android.os.UserHandleHidden;
@@ -124,6 +125,12 @@ public String getAppLabel() {
124125
return mAppLabel;
125126
}
126127

128+
@NonNull
129+
@Override
130+
public Drawable getAppIcon() {
131+
return mApplicationInfo.loadIcon(mPm);
132+
}
133+
127134
@Override
128135
@Nullable
129136
public String getVersionName() {

app/src/main/java/io/github/muntashirakon/AppManager/filters/IFilterableAppInfo.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import android.content.pm.ComponentInfo;
66
import android.content.pm.FeatureInfo;
7+
import android.graphics.drawable.Drawable;
78
import android.os.Build;
89

910
import androidx.annotation.NonNull;
@@ -29,6 +30,9 @@ public interface IFilterableAppInfo {
2930
@NonNull
3031
String getAppLabel();
3132

33+
@NonNull
34+
Drawable getAppIcon();
35+
3236
@Nullable
3337
String getVersionName();
3438

app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsBaseProfileActivity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
231231
int itemId = item.getItemId();
232232
if (itemId == R.id.action_apps) {
233233
mViewPager.setCurrentItem(0, true);
234-
} else if (itemId == R.id.action_filters) {
234+
} else if (itemId == R.id.action_preview) {
235235
mViewPager.setCurrentItem(0, true);
236236
} else if (itemId == R.id.action_conf) {
237237
mViewPager.setCurrentItem(1, true);

app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsFilterProfileActivity.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import androidx.fragment.app.Fragment;
1212

1313
import io.github.muntashirakon.AppManager.R;
14+
import io.github.muntashirakon.AppManager.filters.EditFiltersDialogFragment;
15+
import io.github.muntashirakon.AppManager.filters.FilterItem;
1416

15-
public class AppsFilterProfileActivity extends AppsBaseProfileActivity {
17+
public class AppsFilterProfileActivity extends AppsBaseProfileActivity implements EditFiltersDialogFragment.OnSaveDialogButtonInterface {
1618
@NonNull
1719
public static Intent getProfileIntent(@NonNull Context context, @NonNull String profileId) {
1820
Intent intent = new Intent(context, AppsFilterProfileActivity.class);
@@ -51,7 +53,24 @@ public void loadNewProfile(@NonNull String newProfileName, @NonNull Intent inten
5153
protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
5254
super.onAuthenticated(savedInstanceState);
5355
bottomNavigationView.getMenu().removeItem(R.id.action_apps);
54-
fab.setImageResource(R.drawable.ic_edit);
55-
fab.setContentDescription(getString(R.string.item_edit));
56+
fab.setImageResource(R.drawable.ic_filter_menu);
57+
fab.setContentDescription(getString(R.string.filters));
58+
fab.setOnClickListener(v -> {
59+
EditFiltersDialogFragment dialog = new EditFiltersDialogFragment();
60+
dialog.setOnSaveDialogButtonInterface(this);
61+
dialog.show(getSupportFragmentManager(), EditFiltersDialogFragment.TAG);
62+
});
63+
}
64+
65+
@NonNull
66+
@Override
67+
public FilterItem getFilterItem() {
68+
return model.getFilterItem();
69+
}
70+
71+
@Override
72+
public void onItemAltered(@NonNull FilterItem item) {
73+
model.setModified(true);
74+
model.loadPackages();
5675
}
5776
}

app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsFragment.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Objects;
2424

2525
import io.github.muntashirakon.AppManager.R;
26+
import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo;
2627
import io.github.muntashirakon.AppManager.self.imagecache.ImageLoader;
2728
import io.github.muntashirakon.AppManager.utils.UIUtils;
2829
import io.github.muntashirakon.util.AdapterUtils;
@@ -36,6 +37,7 @@ public static class AppsFragmentItem {
3637
@Nullable
3738
public CharSequence label;
3839
public ApplicationInfo applicationInfo;
40+
public IFilterableAppInfo filterableAppInfo;
3941

4042
public AppsFragmentItem(@NonNull String packageName) {
4143
this.packageName = packageName;
@@ -104,7 +106,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
104106
public void onResume() {
105107
super.onResume();
106108
if (mActivity.getSupportActionBar() != null) {
107-
mActivity.getSupportActionBar().setSubtitle(R.string.apps);
109+
mActivity.getSupportActionBar().setSubtitle(mModel.getPreviewTitleString());
108110
}
109111
mActivity.fab.show();
110112
}
@@ -116,11 +118,13 @@ public void onRefresh() {
116118
}
117119

118120
public class AppsRecyclerAdapter extends RecyclerView.Adapter<AppsRecyclerAdapter.ViewHolder> {
119-
PackageManager pm;
121+
final PackageManager pm;
120122
final ArrayList<AppsFragmentItem> packages = new ArrayList<>();
123+
final ImageLoader.DefaultImageDrawable defaultImage;
121124

122125
private AppsRecyclerAdapter() {
123126
pm = mActivity.getPackageManager();
127+
defaultImage = new ImageLoader.DefaultImageDrawable("android_default_icon", pm.getDefaultActivityIcon());
124128
}
125129

126130
void setList(List<AppsFragmentItem> list) {
@@ -138,7 +142,12 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
138142
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
139143
AppsFragmentItem fragmentItem = packages.get(position);
140144
holder.icon.setTag(fragmentItem);
141-
ImageLoader.getInstance().displayImage(fragmentItem.packageName, fragmentItem.applicationInfo, holder.icon);
145+
if (fragmentItem.applicationInfo != null) {
146+
ImageLoader.getInstance().displayImage(fragmentItem.packageName, fragmentItem.applicationInfo, holder.icon);
147+
} else {
148+
ImageLoader.getInstance().displayImage(fragmentItem.packageName, holder.icon, tag ->
149+
new ImageLoader.ImageFetcherResult(tag, UIUtils.getBitmapFromDrawable(fragmentItem.filterableAppInfo.getAppIcon()), true, true, defaultImage));
150+
}
142151
CharSequence label = fragmentItem.label;
143152
holder.title.setText(label != null ? label : fragmentItem.packageName);
144153
if (label != null) {
@@ -147,19 +156,21 @@ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
147156
} else {
148157
holder.subtitle.setVisibility(View.GONE);
149158
}
150-
holder.itemView.setOnClickListener(v -> {
151-
});
152-
holder.itemView.setOnLongClickListener(v -> {
153-
PopupMenu popupMenu = new PopupMenu(mActivity, holder.itemView);
154-
popupMenu.setForceShowIcon(true);
155-
popupMenu.getMenu().add(R.string.delete).setIcon(R.drawable.ic_trash_can)
156-
.setOnMenuItemClickListener(item -> {
157-
mModel.deletePackage(fragmentItem.packageName);
158-
return true;
159-
});
160-
popupMenu.show();
161-
return true;
162-
});
159+
if (fragmentItem.applicationInfo != null) {
160+
holder.itemView.setOnClickListener(v -> {
161+
});
162+
holder.itemView.setOnLongClickListener(v -> {
163+
PopupMenu popupMenu = new PopupMenu(mActivity, holder.itemView);
164+
popupMenu.setForceShowIcon(true);
165+
popupMenu.getMenu().add(R.string.delete).setIcon(R.drawable.ic_trash_can)
166+
.setOnMenuItemClickListener(item -> {
167+
mModel.deletePackage(fragmentItem.packageName);
168+
return true;
169+
});
170+
popupMenu.show();
171+
return true;
172+
});
173+
}
163174
}
164175

165176
@Override

app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsProfileActivity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void loadNewProfile(@NonNull String newProfileName, @NonNull Intent inten
6969
@Override
7070
protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
7171
super.onAuthenticated(savedInstanceState);
72-
bottomNavigationView.getMenu().removeItem(R.id.action_filters);
72+
bottomNavigationView.getMenu().removeItem(R.id.action_preview);
7373
if (getIntent().hasExtra(EXTRA_SHORTCUT_TYPE)) {
7474
// Compatibility mode for shortcut
7575
@ProfileApplierActivity.ShortcutType String shortcutType = getIntent().getStringExtra(EXTRA_SHORTCUT_TYPE);

app/src/main/java/io/github/muntashirakon/AppManager/profiles/AppsProfileViewModel.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static io.github.muntashirakon.AppManager.compat.PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES;
88
import static io.github.muntashirakon.AppManager.utils.PackageUtils.getAppOpNames;
99

10+
import android.annotation.SuppressLint;
1011
import android.app.Application;
1112
import android.content.pm.ApplicationInfo;
1213
import android.content.pm.PackageManager;
@@ -17,6 +18,7 @@
1718
import androidx.annotation.GuardedBy;
1819
import androidx.annotation.NonNull;
1920
import androidx.annotation.Nullable;
21+
import androidx.annotation.StringRes;
2022
import androidx.annotation.WorkerThread;
2123
import androidx.core.util.Pair;
2224
import androidx.lifecycle.AndroidViewModel;
@@ -40,6 +42,9 @@
4042
import io.github.muntashirakon.AppManager.R;
4143
import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat;
4244
import io.github.muntashirakon.AppManager.compat.PackageManagerCompat;
45+
import io.github.muntashirakon.AppManager.filters.FilterItem;
46+
import io.github.muntashirakon.AppManager.filters.FilteringUtils;
47+
import io.github.muntashirakon.AppManager.filters.IFilterableAppInfo;
4348
import io.github.muntashirakon.AppManager.logs.Log;
4449
import io.github.muntashirakon.AppManager.profiles.struct.AppsBaseProfile;
4550
import io.github.muntashirakon.AppManager.profiles.struct.AppsFilterProfile;
@@ -88,6 +93,8 @@ public boolean isModified() {
8893
return mIsModified;
8994
}
9095

96+
@SuppressLint("WrongThread") // main thread check present
97+
@AnyThread
9198
public void setModified(boolean modified) {
9299
if (mIsModified != modified) {
93100
mIsModified = modified;
@@ -379,6 +386,23 @@ public List<String> getCurrentPackages() {
379386
return Arrays.asList(((AppsProfile) mProfile).packages);
380387
}
381388

389+
public FilterItem getFilterItem() {
390+
assert mProfile instanceof AppsFilterProfile;
391+
return ((AppsFilterProfile) mProfile).getFilterItem();
392+
}
393+
394+
@StringRes
395+
public int getPreviewTitleString() {
396+
if (mProfile instanceof AppsProfile) {
397+
return R.string.apps;
398+
} else if (mProfile instanceof AppsFilterProfile) {
399+
return R.string.preview;
400+
}
401+
if (mProfile != null) {
402+
throw new UnsupportedOperationException("Invalid profile type: " + mProfile.type);
403+
} else throw new UnsupportedOperationException("Profile not initialized");
404+
}
405+
382406
@AnyThread
383407
public void loadPackages() {
384408
if (mLoadProfileResult != null) {
@@ -389,7 +413,7 @@ public void loadPackages() {
389413
if (mProfile instanceof AppsProfile) {
390414
packagesLiveData.postValue(loadAppsPackages((AppsProfile) mProfile));
391415
} else if (mProfile instanceof AppsFilterProfile) {
392-
// TODO: 7/11/25
416+
packagesLiveData.postValue(loadAppsFilteredPackages((AppsFilterProfile) mProfile));
393417
}
394418
}
395419
});
@@ -433,6 +457,25 @@ private ArrayList<AppsFragment.AppsFragmentItem> loadAppsPackages(@NonNull AppsP
433457
return items;
434458
}
435459

460+
List<IFilterableAppInfo> mFilterableAppInfoList;
461+
462+
@NonNull
463+
private ArrayList<AppsFragment.AppsFragmentItem> loadAppsFilteredPackages(@NonNull AppsFilterProfile profile) {
464+
int[] users = profile.users != null ? profile.users : Users.getUsersIds();
465+
if (mFilterableAppInfoList == null) {
466+
mFilterableAppInfoList = FilteringUtils.loadFilterableAppInfo(users);
467+
}
468+
List<FilterItem.FilteredItemInfo> filteredItems = profile.getFilterItem().getFilteredList(mFilterableAppInfoList);
469+
ArrayList<AppsFragment.AppsFragmentItem> items = new ArrayList<>(filteredItems.size());
470+
for (FilterItem.FilteredItemInfo itemInfo : filteredItems) {
471+
AppsFragment.AppsFragmentItem item = new AppsFragment.AppsFragmentItem(itemInfo.info.getPackageName());
472+
item.label = itemInfo.info.getAppLabel();
473+
item.filterableAppInfo = itemInfo.info;
474+
items.add(item);
475+
}
476+
return items;
477+
}
478+
436479
public void putBoolean(@NonNull String key, boolean value) {
437480
if (mProfile == null) return;
438481
setModified(true);

app/src/main/java/io/github/muntashirakon/AppManager/profiles/NewProfileDialogFragment.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
5959
TextInputLayout editTextLayout = TextInputLayoutCompat.fromTextInputEditText(mEditText);
6060
editTextLayout.setHelperText(requireContext().getText(R.string.input_profile_name_description));
6161
MaterialSpinner spinner = mDialogView.findViewById(R.id.type_selector_spinner);
62-
spinner.setVisibility(View.GONE);
6362
ArrayAdapter<CharSequence> spinnerAdapter = SelectedArrayAdapter.createFromResource(requireContext(),
6463
R.array.profile_types, io.github.muntashirakon.ui.R.layout.auto_complete_dropdown_item);
6564
spinner.setAdapter(spinnerAdapter);

app/src/main/java/io/github/muntashirakon/AppManager/profiles/struct/AppsFilterProfile.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.github.muntashirakon.AppManager.users.Users;
2121

2222
public class AppsFilterProfile extends AppsBaseProfile {
23+
@NonNull
2324
private final FilterItem mFilterItem;
2425

2526
protected AppsFilterProfile(@NonNull String profileId, @NonNull String profileName) {
@@ -37,6 +38,10 @@ protected AppsFilterProfile(@NonNull String profileId, @NonNull String profileNa
3738
}
3839
}
3940

41+
public FilterItem getFilterItem() {
42+
return mFilterItem;
43+
}
44+
4045
@Override
4146
public ProfileApplierResult apply(@NonNull String state, @Nullable ProfileLogger logger, @Nullable ProgressHandler progressHandler) {
4247
// Filter results
@@ -48,9 +53,18 @@ public ProfileApplierResult apply(@NonNull String state, @Nullable ProfileLogger
4853
}
4954
List<String> packages = new ArrayList<>(filteredList.size());
5055
List<Integer> assocUsers = new ArrayList<>(filteredList.size());
56+
if (logger != null) {
57+
logger.println("====> Filtered packages: " + filteredList.size());
58+
}
59+
StringBuilder sb = new StringBuilder();
5160
for (FilterItem.FilteredItemInfo info : filteredList) {
5261
packages.add(info.info.getPackageName());
5362
assocUsers.add(info.info.getUserId());
63+
sb.append("(").append(info.info.getPackageName()).append(", ")
64+
.append(info.info.getUserId()).append("), ");
65+
}
66+
if (logger != null) {
67+
logger.println(sb);
5468
}
5569
return apply(packages, assocUsers, state, logger, progressHandler);
5670
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- SPDX-License-Identifier: Apache-2.0 -->
2+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:width="24dp"
4+
android:height="24dp"
5+
android:viewportWidth="24"
6+
android:viewportHeight="24"
7+
android:tint="?attr/colorControlNormal">
8+
<path
9+
android:fillColor="@android:color/white"
10+
android:pathData="M12,9A3,3 0,0 0,9 12A3,3 0,0 0,12 15A3,3 0,0 0,15 12A3,3 0,0 0,12 9M12,17A5,5 0,0 1,7 12A5,5 0,0 1,12 7A5,5 0,0 1,17 12A5,5 0,0 1,12 17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
11+
</vector>

0 commit comments

Comments
 (0)