[app] Optimize module and app list loading (#1401)

Co-authored-by: LoveSy <shana@zju.edu.cn>
This commit is contained in:
Howard Wu 2021-11-21 00:47:35 +08:00 committed by GitHub
parent 806bdcf4fd
commit 4bf7c04ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 442 additions and 317 deletions

View File

@ -167,7 +167,7 @@ dependencies {
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
implementation("androidx.core:core:1.7.0")
implementation("androidx.fragment:fragment:1.3.6")
implementation("androidx.fragment:fragment:1.4.0-rc01")
implementation("androidx.navigation:navigation-fragment:$navVersion")
implementation("androidx.navigation:navigation-ui:$navVersion")
implementation("androidx.preference:preference:1.1.1")

View File

@ -27,6 +27,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Looper;
import android.os.Process;
import android.system.Os;
import android.text.TextUtils;
@ -36,6 +37,7 @@ import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.lsposed.hiddenapibypass.HiddenApiBypass;
import org.lsposed.manager.adapters.AppHelper;
import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.ui.activity.CrashReportActivity;
import org.lsposed.manager.util.DoHDNS;
@ -84,6 +86,33 @@ public class App extends Application {
// TODO: set specific class name
HiddenApiBypass.addHiddenApiExemptions("");
}
Looper.myQueue().addIdleHandler(() -> {
if (App.getInstance() == null || App.getExecutorService() == null) return true;
App.getExecutorService().submit(() -> {
var list = AppHelper.getAppList(false);
var pm = App.getInstance().getPackageManager();
list.parallelStream().forEach(i -> AppHelper.getAppLabel(i, pm));
});
return false;
});
Looper.myQueue().addIdleHandler(() -> {
if (App.getInstance() == null || App.getExecutorService() == null) return true;
App.getExecutorService().submit(() -> {
AppHelper.getDenyList(false);
});
return false;
});
Looper.myQueue().addIdleHandler(() -> {
if (App.getInstance() == null || App.getExecutorService() == null) return true;
App.getExecutorService().submit((Runnable) ModuleUtil::getInstance);
return false;
});
Looper.myQueue().addIdleHandler(() -> {
if (App.getInstance() == null || App.getExecutorService() == null) return true;
App.getExecutorService().submit((Runnable) RepoLoader::getInstance);
return false;
});
}
public static final String TAG = "LSPosedManager";
@ -91,7 +120,7 @@ public class App extends Application {
private static OkHttpClient okHttpClient;
private static Cache okHttpCache;
private SharedPreferences pref;
private ExecutorService executorService;
private final ExecutorService executorService = Executors.newCachedThreadPool();
public static App getInstance() {
return instance;
@ -143,8 +172,6 @@ public class App extends Application {
instance = this;
executorService = Executors.newCachedThreadPool();
pref = PreferenceManager.getDefaultSharedPreferences(this);
if ("CN".equals(Locale.getDefault().getCountry())) {
if (!pref.contains("doh")) {

View File

@ -39,6 +39,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.github.xposed.xposedservice.utils.ParceledListSlice;
@ -103,7 +104,7 @@ public class ConfigManager {
}
}
public static boolean setModuleScope(String packageName, HashSet<ScopeAdapter.ApplicationWithEquals> applications) {
public static boolean setModuleScope(String packageName, Set<ScopeAdapter.ApplicationWithEquals> applications) {
try {
List<Application> list = new ArrayList<>();
applications.forEach(application -> {

View File

@ -26,14 +26,18 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Looper;
import android.view.MenuItem;
import org.lsposed.lspd.models.Application;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public class AppHelper {
@ -41,6 +45,7 @@ public class AppHelper {
public static final int FLAG_SHOW_FOR_ALL_USERS = 0x0400;
private static List<String> denyList;
private static List<PackageInfo> appList;
private static final ConcurrentHashMap<PackageInfo, CharSequence> appLabel = new ConcurrentHashMap<>();
public static Intent getSettingsIntent(String packageName, int userId) {
Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
@ -132,17 +137,22 @@ public class AppHelper {
}
}
public static List<PackageInfo> getAppList(boolean force) {
synchronized public static List<PackageInfo> getAppList(boolean force) {
if (appList == null || force) {
appList = ConfigManager.getInstalledPackagesFromAllUsers(PackageManager.GET_META_DATA | PackageManager.MATCH_UNINSTALLED_PACKAGES, true);
}
return appList;
}
public static List<String> getDenyList(boolean force) {
synchronized public static List<String> getDenyList(boolean force) {
if (denyList == null || force) {
denyList = ConfigManager.getDenyListPackages();
}
return denyList;
}
public static CharSequence getAppLabel(PackageInfo info, PackageManager pm) {
if (info == null || info.applicationInfo == null) return null;
return appLabel.computeIfAbsent(info, i->i.applicationInfo.loadLabel(pm));
}
}

View File

@ -72,25 +72,25 @@ import org.lsposed.manager.R;
import org.lsposed.manager.databinding.ItemModuleBinding;
import org.lsposed.manager.ui.fragment.AppListFragment;
import org.lsposed.manager.ui.fragment.CompileDialogFragment;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import org.lsposed.manager.util.GlideApp;
import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.SimpleStatefulAdaptor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.Set;
import java.util.stream.Collectors;
import rikka.core.util.ResourceUtils;
import rikka.widget.switchbar.SwitchBar;
@SuppressLint("NotifyDataSetChanged")
public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder> implements Filterable {
public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ScopeAdapter.ViewHolder> implements Filterable {
private final Activity activity;
private final AppListFragment fragment;
@ -102,11 +102,11 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
private final ModuleUtil.InstalledModule module;
private final HashSet<ApplicationWithEquals> recommendedList = new HashSet<>();
private final HashSet<ApplicationWithEquals> checkedList = new HashSet<>();
private final ConcurrentLinkedQueue<AppInfo> searchList = new ConcurrentLinkedQueue<>();
private final List<AppInfo> showList = new ArrayList<>();
private final List<String> denyList = new ArrayList<>();
private Set<ApplicationWithEquals> recommendedList = new HashSet<>();
private Set<ApplicationWithEquals> checkedList = new HashSet<>();
private List<AppInfo> searchList = new ArrayList<>();
private List<AppInfo> showList = new ArrayList<>();
private List<String> denyList = new ArrayList<>();
private final SwitchBar.OnCheckedChangeListener switchBarOnCheckedChangeListener = new SwitchBar.OnCheckedChangeListener() {
@Override
@ -119,21 +119,6 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
return true;
}
};
private final Runnable dataReadyRunnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
if (fragment == null || fragment.binding == null) {
return;
}
fragment.binding.progress.setIndeterminate(false);
fragment.binding.swipeRefreshLayout.setRefreshing(false);
String queryStr = fragment.searchView != null ? fragment.searchView.getQuery().toString() : "";
getFilter().filter(queryStr);
this.notify();
}
}
};
private ApplicationInfo selectedInfo;
private boolean refreshing = false;
@ -188,7 +173,7 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
return preferences.getBoolean("filter_system_apps", true) && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}
private void sortApps(List<AppInfo> list) {
private int sortApps(AppInfo x, AppInfo y) {
Comparator<PackageInfo> comparator = AppHelper.getAppListComparator(preferences.getInt("list_sort", 0), pm);
Comparator<AppInfo> frameworkComparator = (a, b) -> {
if (a.packageName.equals("android") == b.packageName.equals("android")) {
@ -210,23 +195,26 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
return 1;
}
};
list.sort((a, b) -> {
boolean aChecked = checkedList.contains(a.application);
boolean bChecked = checkedList.contains(b.application);
if (aChecked == bChecked) {
return recommendedComparator.compare(a, b);
} else if (aChecked) {
return -1;
} else {
return 1;
}
});
boolean aChecked = checkedList.contains(x.application);
boolean bChecked = checkedList.contains(y.application);
if (aChecked == bChecked) {
return recommendedComparator.compare(x, y);
} else if (aChecked) {
return -1;
} else {
return 1;
}
}
private void checkRecommended() {
checkedList.removeIf(i -> i.userId == module.userId);
checkedList.addAll(recommendedList);
ConfigManager.setModuleScope(module.packageName, checkedList);
fragment.runAsync(() -> {
var tmpChkList = new HashSet<>(checkedList);
tmpChkList.removeIf(i -> i.userId == module.userId);
tmpChkList.addAll(recommendedList);
ConfigManager.setModuleScope(module.packageName, tmpChkList);
checkedList = tmpChkList;
fragment.runOnUiThread(this::notifyDataSetChanged);
});
}
public boolean onOptionsItemSelected(MenuItem item) {
@ -237,13 +225,11 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
.setMessage(R.string.use_recommended_message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
checkRecommended();
notifyDataSetChanged();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
checkRecommended();
notifyDataSetChanged();
}
return true;
} else if (itemId == R.id.item_filter_system) {
@ -363,6 +349,12 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
}
}
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
holder.checkbox.setOnCheckedChangeListener(null);
super.onViewRecycled(holder);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
AppInfo appInfo = showList.get(position);
@ -444,10 +436,10 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
}
});
holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(checkedList.contains(appInfo.application));
holder.checkbox.setOnCheckedChangeListener((v, isChecked) -> onCheckedChange(v, isChecked, appInfo));
holder.itemView.setOnClickListener(v -> {
if (enabled) holder.checkbox.toggle();
});
@ -495,82 +487,85 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
fragment.binding.masterSwitch.setOnCheckedChangeListener(switchBarOnCheckedChangeListener);
loadAppListHandler.post(() -> {
List<PackageInfo> appList = AppHelper.getAppList(force);
checkedList.clear();
recommendedList.clear();
denyList.clear();
denyList.addAll(AppHelper.getDenyList(force));
var tmpList = new ArrayList<AppInfo>();
checkedList.addAll(ConfigManager.getModuleScope(module.packageName));
HashSet<ApplicationWithEquals> installedList = new HashSet<>();
denyList = AppHelper.getDenyList(force);
var tmpRecList = new HashSet<ApplicationWithEquals>();
var tmpChkList = new HashSet<>(ConfigManager.getModuleScope(module.packageName));
final var tmpList = new ArrayList<AppInfo>();
final HashSet<ApplicationWithEquals> installedList = new HashSet<>();
List<String> scopeList = module.getScopeList();
boolean emptyCheckedList = checkedList.isEmpty();
for (PackageInfo info : appList) {
boolean emptyCheckedList = tmpChkList.isEmpty();
appList.parallelStream().forEach(info -> {
int userId = info.applicationInfo.uid / 100000;
String packageName = info.packageName;
if (packageName.equals("android") && userId != 0 ||
packageName.equals(module.packageName) ||
packageName.equals(BuildConfig.APPLICATION_ID)) {
continue;
return;
}
ApplicationWithEquals application = new ApplicationWithEquals(packageName, userId);
installedList.add(application);
synchronized (installedList) {
installedList.add(application);
}
if (userId != module.userId) {
continue;
return;
}
if (scopeList != null && scopeList.contains(packageName)) {
recommendedList.add(application);
synchronized (tmpRecList) {
tmpRecList.add(application);
}
if (emptyCheckedList) {
checkedList.add(application);
synchronized (tmpChkList) {
tmpChkList.add(application);
}
}
} else if (shouldHideApp(info, application)) {
continue;
return;
}
AppInfo appInfo = new AppInfo();
appInfo.packageInfo = info;
appInfo.label = info.applicationInfo.loadLabel(pm);
appInfo.label = AppHelper.getAppLabel(info, pm);
appInfo.application = application;
appInfo.packageName = info.packageName;
appInfo.applicationInfo = info.applicationInfo;
tmpList.add(appInfo);
}
checkedList.retainAll(installedList);
synchronized (tmpList) {
tmpList.add(appInfo);
}
});
tmpChkList.retainAll(installedList);
if (emptyCheckedList) {
ConfigManager.setModuleScope(module.packageName, checkedList);
}
sortApps(tmpList);
searchList.clear();
searchList.addAll(tmpList);
synchronized (dataReadyRunnable) {
synchronized (this) {
refreshing = false;
}
activity.runOnUiThread(dataReadyRunnable);
try {
dataReadyRunnable.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
ConfigManager.setModuleScope(module.packageName, tmpChkList);
}
checkedList = tmpChkList;
recommendedList = tmpRecList;
searchList = tmpList.parallelStream().sorted(this::sortApps).collect(Collectors.toList());
String queryStr = fragment.searchView != null ? fragment.searchView.getQuery().toString() : "";
getFilter().filter(queryStr, count -> {
refreshing = false;
fragment.runOnUiThread((this::notifyDataSetChanged));
});
});
}
protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, AppInfo appInfo) {
var tmpChkList = new HashSet<>(checkedList);
if (isChecked) {
checkedList.add(appInfo.application);
tmpChkList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
tmpChkList.remove(appInfo.application);
}
if (!ConfigManager.setModuleScope(module.packageName, checkedList)) {
if (!ConfigManager.setModuleScope(module.packageName, tmpChkList)) {
Snackbar.make(fragment.binding.snackbar, R.string.failed_to_save_scope_list, Snackbar.LENGTH_SHORT).show();
if (!isChecked) {
checkedList.add(appInfo.application);
tmpChkList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
tmpChkList.remove(appInfo.application);
}
buttonView.setChecked(!isChecked);
} else if (appInfo.packageName.equals("android")) {
@ -581,6 +576,12 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
Snackbar.make(fragment.binding.snackbar, activity.getString(R.string.deny_list, appInfo.label), Snackbar.LENGTH_SHORT)
.show();
}
checkedList = tmpChkList;
}
@Override
public boolean isLoaded() {
return !refreshing;
}
static class ViewHolder extends RecyclerView.ViewHolder {
@ -631,10 +632,8 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
showList.clear();
//noinspection unchecked
showList.addAll((Collection<AppInfo>) results.values);
notifyDataSetChanged();
showList = (List<AppInfo>) results.values;
}
}
@ -662,7 +661,6 @@ public class ScopeAdapter extends SimpleStatefulAdaptor<ScopeAdapter.ViewHolder>
if (!recommendedList.isEmpty()) {
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
checkRecommended();
notifyDataSetChanged();
});
} else {
builder.setPositiveButton(android.R.string.cancel, null);

View File

@ -34,6 +34,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
@ -78,6 +79,15 @@ public class AppListFragment extends BaseFragment {
scopeAdapter = new ScopeAdapter(this, module);
scopeAdapter.setHasStableIds(true);
scopeAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
if (binding != null && scopeAdapter != null) {
binding.progress.setVisibility(scopeAdapter.isLoaded() ? View.GONE : View.VISIBLE);
binding.swipeRefreshLayout.setRefreshing(!scopeAdapter.isLoaded());
}
}
});
binding.recyclerView.setAdapter(scopeAdapter);
binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));

View File

@ -56,11 +56,12 @@ import java.util.Locale;
import rikka.core.util.ResourceUtils;
public class HomeFragment extends BaseFragment implements RepoLoader.Listener {
public class HomeFragment extends BaseFragment implements RepoLoader.Listener, ModuleUtil.ModuleListener {
private FragmentHomeBinding binding;
private static final RepoLoader repoLoader = RepoLoader.getInstance();
private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -109,6 +110,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener {
updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
repoLoader.addListener(this);
moduleUtil.addListener(this);
if (repoLoader.isRepoLoaded()) {
repoLoaded();
}
@ -198,7 +200,9 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener {
public void repoLoaded() {
final int[] count = new int[]{0};
HashSet<String> processedModules = new HashSet<>();
ModuleUtil.getInstance().getModules().forEach((k, v) -> {
var modules = moduleUtil.getModules();
if (modules == null) return;
modules.forEach((k, v) -> {
if (!processedModules.contains(k.first)) {
var ver = repoLoader.getModuleLatestVersion(k.first);
if (ver != null && ver.upgradable(v.versionCode, v.versionName)) {
@ -222,6 +226,11 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener {
runOnUiThread(() -> binding.downloadSummary.setText(getResources().getString(R.string.module_repo_up_to_date)));
}
@Override
public void onModulesReloaded() {
setModulesSummary(moduleUtil.getEnabledModulesCount());
}
private class StartFragmentListener implements View.OnClickListener {
boolean requireInstalled;
int fragment;
@ -244,19 +253,20 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener {
@Override
public void onResume() {
super.onResume();
int moduleCount;
if (ConfigManager.isBinderAlive()) {
moduleCount = ModuleUtil.getInstance().getEnabledModulesCount();
} else {
moduleCount = 0;
}
binding.modulesSummary.setText(getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount));
setModulesSummary(moduleUtil.getEnabledModulesCount());
} else setModulesSummary(0);
}
private void setModulesSummary(int moduleCount) {
runOnUiThread(() -> binding.modulesSummary.setText(getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount)));
}
@Override
public void onDestroyView() {
super.onDestroyView();
repoLoader.removeListener(this);
moduleUtil.removeListener(this);
binding = null;
}
}

View File

@ -20,6 +20,7 @@
package org.lsposed.manager.ui.fragment;
import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY;
import android.annotation.SuppressLint;
import android.content.Intent;
@ -82,26 +83,31 @@ import org.lsposed.manager.util.ModuleUtil;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt;
import rikka.widget.borderview.BorderRecyclerView;
public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener {
private static final PackageManager pm = App.getInstance().getPackageManager();
private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
private static final RepoLoader repoLoader = RepoLoader.getInstance();
private static final List<UserInfo> users = ConfigManager.getUsers();
protected FragmentPagerBinding binding;
protected SearchView searchView;
private SearchView.OnQueryTextListener searchListener;
private final ArrayList<ModuleAdapter> adapters = new ArrayList<>();
private final ArrayList<String> tabTitles = new ArrayList<>();
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
updateProgress();
}
};
private ModuleUtil.InstalledModule selectedModule;
@ -121,6 +127,23 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
return false;
}
};
if (users != null) {
for (var user : users) {
var adapter = new ModuleAdapter(user);
adapter.setHasStableIds(true);
adapter.setStateRestorationPolicy(PREVENT_WHEN_EMPTY);
adapters.add(adapter);
adapter.registerAdapterDataObserver(observer);
}
}
}
private void updateProgress() {
if (binding != null) {
var position = binding.viewPager.getCurrentItem();
binding.progress.setVisibility(adapters.get(position).isLoaded ? View.GONE : View.VISIBLE);
}
}
@Override
@ -133,71 +156,54 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentPagerBinding.inflate(inflater, container, false);
setupToolbar(binding.toolbar, R.string.Modules, R.menu.menu_modules);
binding.viewPager.setAdapter(new PagerAdapter(this));
binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
BorderRecyclerView recyclerView = binding.viewPager.findViewWithTag(position);
if (position > 0) {
binding.fab.show();
} else {
binding.fab.hide();
}
updateProgress();
}
});
var users = ConfigManager.getUsers();
if (users != null) {
adapters.clear();
if (users.size() != 1) {
tabTitles.clear();
for (var user : users) {
var adapter = new ModuleAdapter(user);
adapter.setHasStableIds(true);
adapters.add(adapter);
tabTitles.add(user.name);
}
new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> {
if (position < tabTitles.size()) {
tab.setText(tabTitles.get(position));
}
}).attach();
binding.viewPager.setUserInputEnabled(true);
binding.tabLayout.setVisibility(View.VISIBLE);
} else {
var adapter = new ModuleAdapter(null);
adapter.setHasStableIds(true);
adapters.add(adapter);
binding.viewPager.setUserInputEnabled(false);
binding.tabLayout.setVisibility(View.GONE);
new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> {
if (position < adapters.size()) {
tab.setText(adapters.get(position).getUser().name);
}
}).attach();
if (users != null && users.size() != 1) {
binding.viewPager.setUserInputEnabled(true);
binding.tabLayout.setVisibility(View.VISIBLE);
binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
if (tabLayoutWidth <= binding.getRoot().getWidth()) {
binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
}
});
binding.fab.show();
} else {
binding.viewPager.setUserInputEnabled(false);
binding.tabLayout.setVisibility(View.GONE);
}
binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
if (tabLayoutWidth <= binding.getRoot().getWidth()) {
binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
}
});
binding.fab.setOnClickListener(v -> {
var pickAdaptor = new ModuleAdapter(null, true);
var pickAdaptor = new ModuleAdapter(adapters.get(binding.viewPager.getCurrentItem()).getUser(), true);
var position = binding.viewPager.getCurrentItem();
var snapshot = adapters.get(position).snapshot().stream().map(m -> m.packageName).collect(Collectors.toSet());
var user = adapters.get(position).getUser();
pickAdaptor.setFilter(m -> !snapshot.contains(m.packageName));
var binding = DialogRecyclerviewBinding.inflate(getLayoutInflater());
binding.list.setAdapter(pickAdaptor);
binding.list.setLayoutManager(new LinearLayoutManager(requireActivity()));
pickAdaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.progress.setVisibility(pickAdaptor.isLoaded() ? View.GONE : View.VISIBLE);
}
});
pickAdaptor.refresh();
var rv = DialogRecyclerviewBinding.inflate(getLayoutInflater()).getRoot();
rv.setAdapter(pickAdaptor);
rv.setLayoutManager(new LinearLayoutManager(requireActivity()));
var dialog = new MaterialAlertDialogBuilder(requireActivity())
.setTitle(getString(R.string.install_to_user, user.name))
.setView(rv)
.setView(binding.getRoot())
.setNegativeButton(android.R.string.cancel, null)
.show();
pickAdaptor.setOnPickListener(picked -> {
@ -229,8 +235,13 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
@Override
public void onSingleInstalledModuleReloaded() {
adapters.forEach(adapter -> adapter.refresh(true));
public void onSingleInstalledModuleReloaded(ModuleUtil.InstalledModule module) {
adapters.forEach(ModuleAdapter::refresh);
}
@Override
public void onModulesReloaded() {
adapters.forEach(ModuleAdapter::refresh);
}
@Override
@ -341,16 +352,18 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
binding.recyclerView.setAdapter(fragment.adapters.get(position));
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(layoutManager);
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE && position > 0) {
fragment.binding.fab.show();
} else {
fragment.binding.fab.hide();
if (users != null && users.size() != 1) {
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
fragment.binding.fab.show();
} else {
fragment.binding.fab.hide();
}
}
}
});
});
}
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
return binding.getRoot();
}
@ -384,15 +397,13 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
private class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ModuleAdapter.ViewHolder> implements Filterable {
private final ConcurrentLinkedQueue<ModuleUtil.InstalledModule> searchList = new ConcurrentLinkedQueue<>();
private final List<ModuleUtil.InstalledModule> showList = new ArrayList<>();
private List<ModuleUtil.InstalledModule> searchList = new ArrayList<>();
private List<ModuleUtil.InstalledModule> showList = new ArrayList<>();
private final UserInfo user;
private final boolean isPick;
private boolean isLoaded;
private View.OnClickListener onPickListener;
private Predicate<ModuleUtil.InstalledModule> customFilter = m -> true;
ModuleAdapter(UserInfo user) {
this(user, false);
}
@ -412,6 +423,10 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
return new ModuleAdapter.ViewHolder(ItemModuleBinding.inflate(getLayoutInflater(), parent, false));
}
public boolean isPick() {
return isPick;
}
@Override
public void onBindViewHolder(@NonNull ModuleAdapter.ViewHolder holder, int position) {
ModuleUtil.InstalledModule item = showList.get(position);
@ -539,6 +554,12 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
}
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
holder.itemView.setTag(null);
super.onViewRecycled(holder);
}
@Override
public int getItemCount() {
return showList.size();
@ -555,49 +576,71 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
return new ModuleAdapter.ApplicationFilter();
}
public void setFilter(@NonNull Predicate<ModuleUtil.InstalledModule> filter) {
this.customFilter = filter;
}
public void setOnPickListener(View.OnClickListener onPickListener) {
this.onPickListener = onPickListener;
}
public List<ModuleUtil.InstalledModule> snapshot() {
return new ArrayList<>(searchList);
}
public void refresh() {
refresh(false);
}
public void refresh(boolean force) {
if (force) moduleUtil.reloadInstalledModules();
runOnUiThread(reloadModules);
if (force) runAsync(moduleUtil::reloadInstalledModules);
runAsync(reloadModules);
}
private final Runnable reloadModules = new Runnable() {
public void run() {
var tmpList = moduleUtil.getModules().values().stream().filter(module -> user == null ? module.userId == 0 : module.userId == user.id).filter(customFilter).collect(Collectors.toCollection(ArrayList::new));
Comparator<PackageInfo> cmp = AppHelper.getAppListComparator(0, pm);
tmpList.sort((a, b) -> {
boolean aChecked = moduleUtil.isModuleEnabled(a.packageName);
boolean bChecked = moduleUtil.isModuleEnabled(b.packageName);
if (aChecked == bChecked) {
return cmp.compare(a.pkg, b.pkg);
} else if (aChecked) {
return -1;
} else {
return 1;
private final Runnable reloadModules = () -> {
var modules = moduleUtil.getModules();
if (modules == null) return;
Comparator<PackageInfo> cmp = AppHelper.getAppListComparator(0, pm);
setLoaded(false);
var tmpList = new ArrayList<ModuleUtil.InstalledModule>();
modules.values().parallelStream()
.sorted((a, b) -> {
boolean aChecked = moduleUtil.isModuleEnabled(a.packageName);
boolean bChecked = moduleUtil.isModuleEnabled(b.packageName);
if (aChecked == bChecked) {
var c = cmp.compare(a.pkg, b.pkg);
if (c == 0) {
if (a.userId == getUser().id) return -1;
if (b.userId == getUser().id) return 1;
else return Integer.compare(a.userId, b.userId);
}
return c;
} else if (aChecked) {
return -1;
} else {
return 1;
}
}).forEachOrdered(new Consumer<>() {
private final HashSet<String> uniquer = new HashSet<>();
@Override
public void accept(ModuleUtil.InstalledModule module) {
if (isPick()) {
if (!uniquer.contains(module.packageName)) {
uniquer.add(module.packageName);
if (module.userId != getUser().id)
tmpList.add(module);
}
} else if (module.userId == getUser().id) {
tmpList.add(module);
}
});
searchList.clear();
searchList.addAll(tmpList);
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
runOnUiThread(() -> getFilter().filter(queryStr));
}
}
});
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
searchList = tmpList;
runOnUiThread(() -> getFilter().filter(queryStr, count -> setLoaded(true)));
};
@SuppressLint("NotifyDataSetChanged")
private void setLoaded(boolean loaded) {
runOnUiThread(() -> {
isLoaded = loaded;
notifyDataSetChanged();
});
}
@Override
public boolean isLoaded() {
return isLoaded;
@ -651,14 +694,10 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
return filterResults;
}
@SuppressLint("NotifyDataSetChanged")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
showList.clear();
//noinspection unchecked
showList.addAll((List<ModuleUtil.InstalledModule>) results.values);
isLoaded = true;
notifyDataSetChanged();
showList = (List<ModuleUtil.InstalledModule>) results.values;
}
}
}

View File

@ -63,6 +63,7 @@ import org.lsposed.manager.repo.model.Collaborator;
import org.lsposed.manager.repo.model.OnlineModule;
import org.lsposed.manager.repo.model.Release;
import org.lsposed.manager.repo.model.ReleaseAsset;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import org.lsposed.manager.ui.widget.LinkifyTextView;
import org.lsposed.manager.util.NavUtil;
import org.lsposed.manager.util.SimpleStatefulAdaptor;
@ -210,7 +211,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
public void moduleReleasesLoaded(OnlineModule module) {
this.module = module;
if (releaseAdapter != null) {
runOnUiThread(() -> releaseAdapter.loadItems());
runAsync(releaseAdapter::loadItems);
if (isResumed() && module.getReleases().size() == 1) {
Snackbar.make(binding.snackbar, R.string.module_release_no_more, Snackbar.LENGTH_SHORT).show();
}
@ -220,7 +221,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@Override
public void onThrowable(Throwable t) {
if (releaseAdapter != null) {
runOnUiThread(() -> releaseAdapter.loadItems());
runAsync(releaseAdapter::loadItems);
if (isResumed()) {
Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show();
}
@ -321,12 +322,12 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
}
}
private class ReleaseAdapter extends SimpleStatefulAdaptor<ReleaseAdapter.ViewHolder> {
private List<Release> items;
private class ReleaseAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ReleaseAdapter.ViewHolder> {
private List<Release> items = new ArrayList<>();
private final Resources resources = App.getInstance().getResources();
public ReleaseAdapter() {
loadItems();
runAsync(this::loadItems);
}
public void loadItems() {
@ -345,7 +346,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
return !name.startsWith("snapshot") && !name.startsWith("nightly");
}).collect(Collectors.toList());
} else this.items = releases;
notifyDataSetChanged();
runOnUiThread(this::notifyDataSetChanged);
}
@NonNull
@ -400,6 +401,11 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
return !module.releasesLoaded && position == getItemCount() - 1 ? 1 : 0;
}
@Override
public boolean isLoaded() {
return module.releasesLoaded;
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView title;
WebView description;

View File

@ -38,23 +38,6 @@ import rikka.core.util.ResourceUtils;
public class EmptyStateRecyclerView extends StatefulRecyclerView {
private final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
private final String emptyText;
private final AdapterDataObserver emptyObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
Adapter<?> adapter = getAdapter();
if (adapter != null) {
boolean newEmpty = adapter.getItemCount() == 0;
if (empty != newEmpty) {
empty = newEmpty;
invalidate();
}
}
}
};
private boolean empty = false;
public EmptyStateRecyclerView(Context context) {
this(context, null);
@ -74,26 +57,11 @@ public class EmptyStateRecyclerView extends StatefulRecyclerView {
emptyText = context.getString(R.string.list_empty);
}
@Override
public void setAdapter(Adapter adapter) {
var oldAdapter = getAdapter();
if (oldAdapter != null) {
oldAdapter.unregisterAdapterDataObserver(emptyObserver);
}
super.setAdapter(adapter);
if (adapter != null) {
adapter.registerAdapterDataObserver(emptyObserver);
if (adapter instanceof EmptyStateAdapter && ((EmptyStateAdapter<?>) adapter).isLoaded()) {
emptyObserver.onChanged();
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (empty) {
var adapter = getAdapter();
if (adapter instanceof EmptyStateAdapter && ((EmptyStateAdapter<?>) adapter).isLoaded() && adapter.getItemCount() == 0) {
final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
@ -108,7 +76,6 @@ public class EmptyStateRecyclerView extends StatefulRecyclerView {
}
}
public abstract static class EmptyStateAdapter<T extends ViewHolder> extends SimpleStatefulAdaptor<T> {
abstract public boolean isLoaded();
}

View File

@ -27,6 +27,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import org.lsposed.manager.App;
@ -58,7 +59,7 @@ public final class ModuleUtil {
public static synchronized ModuleUtil getInstance() {
if (instance == null) {
instance = new ModuleUtil();
instance.reloadInstalledModules();
App.getExecutorService().submit(instance::reloadInstalledModules);
}
return instance;
}
@ -101,10 +102,13 @@ public final class ModuleUtil {
installedModules = modules;
enabledModules = new HashSet<>(Arrays.asList(ConfigManager.getEnabledModules()));
synchronized (this) {
isReloading = false;
}
for (var listener: listeners) {
listener.onModulesReloaded();
}
}
public InstalledModule reloadSingleModule(String packageName, int userId) {
@ -122,7 +126,7 @@ public final class ModuleUtil {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) {
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded();
listener.onSingleInstalledModuleReloaded(old);
}
}
return null;
@ -133,14 +137,14 @@ public final class ModuleUtil {
InstalledModule module = new InstalledModule(pkg);
installedModules.put(Pair.create(packageName, userId), module);
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded();
listener.onSingleInstalledModuleReloaded(module);
}
return module;
} else {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) {
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded();
listener.onSingleInstalledModuleReloaded(old);
}
}
return null;
@ -155,8 +159,9 @@ public final class ModuleUtil {
return getModule(packageName, 0);
}
public Map<Pair<String, Integer>, InstalledModule> getModules() {
return installedModules;
@Nullable
synchronized public Map<Pair<String, Integer>, InstalledModule> getModules() {
return isReloading ? null : installedModules;
}
public boolean setModuleEnabled(String packageName, boolean enabled) {
@ -193,7 +198,13 @@ public final class ModuleUtil {
* Called whenever one (previously or now) installed module has been
* reloaded
*/
void onSingleInstalledModuleReloaded();
default void onSingleInstalledModuleReloaded(InstalledModule module) {
}
default void onModulesReloaded() {
}
}
public class InstalledModule {

View File

@ -33,6 +33,7 @@ public abstract class SimpleStatefulAdaptor<T extends RecyclerView.ViewHolder> e
super.onViewRecycled(holder);
}
@CallSuper
@Override
public final void onBindViewHolder(@NonNull T holder, int position, @NonNull List<Object> payloads) {
var state = states.remove(holder.getItemId());

View File

@ -16,12 +16,29 @@
~
~ Copyright (C) 2021 LSPosed Contributors
-->
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:minHeight="?attr/listPreferredItemHeight"
android:fadeScrollbars="true"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical" />
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="visible"
app:hideAnimationBehavior="inward"
app:showAnimationBehavior="outward" />
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="true"
android:minHeight="?attr/listPreferredItemHeight"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical" />
</FrameLayout>

View File

@ -73,7 +73,7 @@
android:layout_height="match_parent"
android:id="@+id/swipeRefreshLayout">
<org.lsposed.manager.ui.widget.StatefulRecyclerView
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -38,19 +38,19 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
android:minHeight="?attr/actionBarSize"
app:logo="@drawable/ic_launcher"
app:contentInsetStart="24dp"
app:titleMarginStart="48dp"
android:elevation="0dp" />
app:logo="@drawable/ic_launcher"
app:titleMarginStart="48dp" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingTop="?actionBarSize"
app:fitsSystemWindowsInsets="top|bottom"
tools:ignore="MissingPrefix">
@ -59,9 +59,9 @@
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:borderTopVisibility="whenTop"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderBottomVisibility="never">
app:borderTopVisibility="whenTop">
<LinearLayout
android:layout_width="match_parent"
@ -72,9 +72,9 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/status"
style="@style/HomeCard.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Primary">
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
@ -97,9 +97,9 @@
android:layout_marginStart="24dp"
android:layout_toEndOf="@id/status_icon"
android:fontFamily="sans-serif-medium"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@color/primary_text_material_inverse" />
android:textColor="@color/primary_text_material_inverse"
android:textSize="16sp" />
<TextView
android:id="@+id/status_summary"
@ -114,9 +114,9 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/modules"
style="@style/HomeCard.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Secondary">
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
@ -138,8 +138,8 @@
android:layout_marginStart="24dp"
android:layout_toEndOf="@id/modules_icon"
android:text="@string/Modules"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem" />
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
<TextView
android:id="@+id/modules_summary"
@ -147,15 +147,16 @@
android:layout_height="wrap_content"
android:layout_below="@id/modules_title"
android:layout_alignStart="@id/modules_title"
android:text="@string/module_repo_loading"
android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/download"
style="@style/HomeCard.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Secondary">
android:layout_height="wrap_content">
<RelativeLayout
android:layout_width="match_parent"
@ -177,8 +178,8 @@
android:layout_marginStart="24dp"
android:layout_toEndOf="@id/download_icon"
android:text="@string/module_repo"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem" />
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
<TextView
android:id="@+id/download_summary"
@ -193,16 +194,16 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/logs"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Tertiary">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:padding="16dp">
<ImageView
android:layout_width="24dp"
@ -215,23 +216,23 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/Logs"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem" />
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/settings"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Tertiary">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:padding="16dp">
<ImageView
android:layout_width="24dp"
@ -250,16 +251,16 @@
<com.google.android.material.card.MaterialCardView
android:id="@+id/issue"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/HomeCard.Tertiary">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:layout_gravity="center_vertical">
android:padding="16dp">
<ImageView
android:layout_width="24dp"
@ -272,8 +273,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/report_issue"
android:textSize="16sp"
android:textAppearance="?android:attr/textAppearanceListItem" />
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -38,10 +38,10 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:elevation="0dp"
app:layout_scrollFlags="scroll|enterAlways"
app:layout_scrollEffect="none" />
android:minHeight="?attr/actionBarSize"
app:layout_scrollEffect="none"
app:layout_scrollFlags="scroll|enterAlways" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
@ -54,11 +54,25 @@
app:tabMode="scrollable" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</FrameLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
@ -68,6 +82,6 @@
android:layout_margin="16dp"
android:contentDescription="@string/add_module_to_user"
android:src="@drawable/ic_baseline_add_24"
android:visibility="invisible"
android:visibility="gone"
app:layout_fitsSystemWindowsInsets="bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -17,7 +17,7 @@
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<org.lsposed.manager.ui.widget.StatefulRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/recyclerView"
android:layout_width="match_parent"

View File

@ -173,7 +173,7 @@ dependencies {
implementation("com.android.tools.build:apksig:$agpVersion")
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("de.upb.cs.swt:axml:2.1.1")
compileOnly("androidx.annotation:annotation:1.2.0")
compileOnly("androidx.annotation:annotation:1.3.0")
compileOnly(project(":hiddenapi-stubs"))
implementation(project(":hiddenapi-bridge"))
implementation(project(":manager-service"))

View File

@ -21,6 +21,7 @@ package org.lsposed.lspd.service;
import static android.content.Context.BIND_AUTO_CREATE;
import static org.lsposed.lspd.service.ServiceManager.TAG;
import static org.lsposed.lspd.service.ServiceManager.getExecutorService;
import android.annotation.SuppressLint;
import android.app.INotificationManager;
@ -406,10 +407,10 @@ public class LSPManagerService extends ILSPManagerService.Stub {
// we do it by cancelling the launch (return false)
// and start activity in a new thread
pendingManager = true;
new Thread(() -> {
getExecutorService().submit(() -> {
ensureWebViewPermission();
stopAndStartActivity(pkgName, intent, true);
}).start();
});
Log.d(TAG, "requested to launch manager");
return false;
}

View File

@ -21,6 +21,7 @@ package org.lsposed.lspd.service;
import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE;
import static org.lsposed.lspd.service.ServiceManager.TAG;
import static org.lsposed.lspd.service.ServiceManager.getExecutorService;
import android.app.IApplicationThread;
import android.content.IIntentReceiver;
@ -216,7 +217,7 @@ public class LSPosedService extends ILSPosedService.Stub {
var receiver = new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
new Thread(() -> dispatchPackageChanged(intent)).start();
getExecutorService().submit(() -> dispatchPackageChanged(intent));
try {
ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
} catch (Throwable e) {
@ -241,7 +242,7 @@ public class LSPosedService extends ILSPosedService.Stub {
ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
new Thread(() -> dispatchUserUnlocked(intent)).start();
getExecutorService().submit(() -> dispatchUserUnlocked(intent));
try {
ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
} catch (Throwable e) {
@ -263,7 +264,7 @@ public class LSPosedService extends ILSPosedService.Stub {
ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
new Thread(() -> dispatchConfigurationChanged(intent)).start();
getExecutorService().submit(() -> dispatchConfigurationChanged(intent));
try {
ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
} catch (Throwable e) {
@ -287,7 +288,7 @@ public class LSPosedService extends ILSPosedService.Stub {
ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
new Thread(() -> dispatchSecretCodeReceive()).start();
getExecutorService().submit(() -> dispatchSecretCodeReceive());
try {
ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
} catch (Throwable e) {
@ -309,14 +310,14 @@ public class LSPosedService extends ILSPosedService.Stub {
ActivityManagerService.registerReceiver("android", null, new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
new Thread(() -> {
getExecutorService().submit(() -> {
try {
var am = ActivityManagerService.getActivityManager();
if (am != null) am.setActivityController(null, false);
} catch (Throwable e) {
Log.e(TAG, "setActivityController", e);
}
}).start();
});
try {
ActivityManagerService.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
} catch (Throwable e) {

View File

@ -59,6 +59,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import io.github.xposed.xposedservice.utils.ParceledListSlice;
@ -134,14 +135,15 @@ public class PackageService {
res.addAll(pm.getInstalledPackages(flags, user.id).getList());
}
if (filterNoProcess) {
res.removeIf(packageInfo -> {
return new ParceledListSlice<>(res.parallelStream().filter(packageInfo -> {
try {
PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE);
return fetchProcesses(pkgInfo).isEmpty();
return !fetchProcesses(pkgInfo).isEmpty();
} catch (RemoteException e) {
return false;
Log.w(TAG, "filter failed", e);
return true;
}
});
}).collect(Collectors.toList()));
}
return new ParceledListSlice<>(res);
}

View File

@ -33,6 +33,9 @@ import org.lsposed.lspd.BuildConfig;
import java.io.File;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import hidden.HiddenApiBridge;
@ -47,6 +50,12 @@ public class ServiceManager {
private static LSPSystemServerService systemServerService = null;
private static LogcatService logcatService = null;
private static final ExecutorService executorService = Executors.newCachedThreadPool();
public static ExecutorService getExecutorService() {
return executorService;
}
private static void waitSystemService(String name) {
while (android.os.ServiceManager.getService(name) == null) {
try {