diff --git a/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppListFragment.java b/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppListFragment.java index 20385ecc3..be042e95c 100644 --- a/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppListFragment.java +++ b/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppListFragment.java @@ -3,12 +3,14 @@ import static com.nononsenseapps.filepicker.FilePickerActivityUtils.pickSingleDirIntent; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -20,6 +22,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.appcompat.view.menu.MenuPopupHelper; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; @@ -27,6 +31,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import com.elvishew.xlog.XLog; +import com.google.android.material.chip.Chip; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.collect.Lists; import com.google.common.io.Files; @@ -47,6 +52,7 @@ import github.tornaco.android.thanos.apps.PackageSetChooserDialog; import github.tornaco.android.thanos.common.AppItemActionListener; import github.tornaco.android.thanos.common.AppListModel; +import github.tornaco.android.thanos.common.sort.AppSort; import github.tornaco.android.thanos.core.app.ThanosManager; import github.tornaco.android.thanos.core.pm.AppInfo; import github.tornaco.android.thanos.core.pm.PackageManager; @@ -91,6 +97,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c Bundle data = getArguments(); String pkgSetId = Objects.requireNonNull(data, "Missing arg: " + ARG_PKG_SET).getString(ARG_PKG_SET, null); setupViewModel(pkgSetId); + onSetupSorter(binding.sortChipContainer.sortChip); return binding.getRoot(); } @@ -147,7 +154,60 @@ public void onAppItemSwitchStateChange(AppInfo appInfo, boolean checked) { binding.swipe.setOnRefreshListener(() -> viewModel.start()); binding.swipe.setColorSchemeColors(getResources().getIntArray(github.tornaco.android.thanos.module.common.R.array.common_swipe_refresh_colors)); + } + + @SuppressLint("RestrictedApi") + protected void onSetupSorter(Chip sorterAnchor) { + AppSort[] appSortArray = AppSort.values(); + AppSort currentSort = viewModel.getCurrentAppSort(); + sorterAnchor.setText(currentSort.labelRes); + + sorterAnchor.setOnClickListener(view -> { + MenuBuilder menuBuilder = new MenuBuilder(requireActivity()); + MenuPopupHelper menuPopupHelper = new MenuPopupHelper(requireActivity(), menuBuilder, view); + menuPopupHelper.setForceShowIcon(true); + + int reverseItemId = 10086; + MenuItem reverseItem = menuBuilder.add(1000, + reverseItemId, + Menu.NONE, + github.tornaco.android.thanos.module.common.R.string.common_sort_reverse); + reverseItem.setCheckable(true); + reverseItem.setChecked(viewModel.isSortReverse()); + reverseItem.setIcon(github.tornaco.android.thanos.module.common.R.drawable.module_common_ic_arrow_up_down_line); + + for (int i = 0; i < appSortArray.length; i++) { + AppSort sort = appSortArray[i]; + MenuItem sortItem = menuBuilder.add(1000, + i, + Menu.NONE, + sort.labelRes); + boolean isSelected = viewModel.getCurrentAppSort() == sort; + if (isSelected) { + sortItem.setTitle(getString(sort.labelRes) + " \uD83C\uDFAF"); + } + } + menuBuilder.setCallback(new MenuBuilder.Callback() { + @Override + public boolean onMenuItemSelected(@NonNull MenuBuilder menu, @NonNull MenuItem item) { + int index = item.getItemId(); + if (index == reverseItemId) { + viewModel.setSortReverse(!viewModel.isSortReverse()); + item.setChecked(viewModel.isSortReverse()); + } else { + viewModel.setAppSort(appSortArray[index]); + sorterAnchor.setText(appSortArray[index].labelRes); + } + return true; + } + @Override + public void onMenuModeChange(@NonNull MenuBuilder menu) { + // Noop + } + }); + menuPopupHelper.show(); + }); } @Verify diff --git a/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppsViewModel.java b/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppsViewModel.java index f3e318eb6..dbed45257 100644 --- a/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppsViewModel.java +++ b/android/app/src/main/java/github/tornaco/android/thanos/power/SmartFreezeAppsViewModel.java @@ -1,6 +1,7 @@ package github.tornaco.android.thanos.power; import android.app.Application; +import android.app.usage.UsageStats; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -30,7 +31,9 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -38,6 +41,7 @@ import github.tornaco.android.rhino.plugin.Verify; import github.tornaco.android.thanos.common.AppLabelSearchFilter; import github.tornaco.android.thanos.common.AppListModel; +import github.tornaco.android.thanos.common.sort.AppSort; import github.tornaco.android.thanos.core.app.ThanosManager; import github.tornaco.android.thanos.core.pm.AppInfo; import github.tornaco.android.thanos.core.pm.PackageSet; @@ -58,7 +62,6 @@ import util.Consumer; import util.IoUtils; import util.JsonFormatter; -import util.PinyinComparatorUtils; @SuppressWarnings("UnstableApiUsage") public class SmartFreezeAppsViewModel extends AndroidViewModel { @@ -72,6 +75,9 @@ public class SmartFreezeAppsViewModel extends AndroidViewModel { private final ObservableField queryText = new ObservableField<>(""); private final AppLabelSearchFilter appLabelSearchFilter = new AppLabelSearchFilter(); + private final ObservableBoolean sortReverse = new ObservableBoolean(false); + private final ObservableField currentSort = new ObservableField<>(AppSort.Default); + public SmartFreezeAppsViewModel(@NonNull Application application) { super(application); } @@ -99,10 +105,6 @@ private void loadModels() { disposables.add(Single .create((SingleOnSubscribe>) emitter -> emitter.onSuccess(getSmartFreezeApps())) - .map(listModels -> { - listModels.sort((o1, o2) -> PinyinComparatorUtils.compare(o1.appInfo.getAppLabel(), o2.appInfo.getAppLabel())); - return listModels; - }) .flatMapObservable((Function, ObservableSource>) Observable::fromIterable) .filter(listModel -> { String query = queryText.get(); @@ -130,9 +132,43 @@ private List getSmartFreezeApps() { res.add(model); } } + + AppSort sort = currentSort.get(); + if (sort != null) { + // inflate usage stats if necessary. + if (sort.relyOnUsageStats()) { + inflateAppUsageStats(res); + } + + AppSort.AppSorterProvider appSorterProvider = sort.provider; + if (appSorterProvider != null) { + res.sort(appSorterProvider.comparator(getApplication())); + if (sortReverse.get()) { + Collections.reverse(res); + } + } + } + return res; } + private void inflateAppUsageStats(List res) { + XLog.d("inflateAppUsageStats"); + ThanosManager thanox = ThanosManager.from(getApplication()); + if (thanox.isServiceInstalled()) { + Map statsMap = thanox.getUsageStatsManager().queryAndAggregateUsageStats(0, System.currentTimeMillis()); + for (AppListModel app : res) { + if (statsMap.containsKey(app.appInfo.getPkgName())) { + UsageStats stats = statsMap.get(app.appInfo.getPkgName()); + if (stats != null) { + app.lastUsedTimeMills = stats.getLastTimeUsed(); + app.totalUsedTimeMills = stats.getTotalTimeInForeground(); + } + } + } + } + } + void createShortcutStubApkForAsync(AppInfo appInfo, String appLabel, String versionName, @@ -366,4 +402,22 @@ public void importPackageListFromFile(Uri uri, Consumer onRes) { } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(onRes::accept)); } + + public boolean isSortReverse() { + return sortReverse.get(); + } + + public void setSortReverse(boolean reverse) { + sortReverse.set(reverse); + start(); + } + + public AppSort getCurrentAppSort() { + return currentSort.get(); + } + + public void setAppSort(AppSort sort) { + currentSort.set(sort); + start(); + } } diff --git a/android/app/src/main/res/layout/activity_smart_freeze_apps.xml b/android/app/src/main/res/layout/activity_smart_freeze_apps.xml index b19ecb1ef..4dee25b3c 100755 --- a/android/app/src/main/res/layout/activity_smart_freeze_apps.xml +++ b/android/app/src/main/res/layout/activity_smart_freeze_apps.xml @@ -73,6 +73,20 @@ android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + +