[app] More UI fix (#1438)

This commit is contained in:
LoveSy 2021-11-27 09:15:23 +08:00 committed by GitHub
parent 48fd4c042c
commit 5d782a7680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 4065 additions and 1039 deletions

View File

@ -187,7 +187,7 @@ dependencies {
implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.google.android.material:material:1.5.0-beta01")
implementation("com.google.code.gson:gson:2.8.9")

View File

@ -195,7 +195,6 @@ public class App extends Application {
}, new IntentFilter(Intent.ACTION_PACKAGE_CHANGED));
UpdateUtil.loadRemoteVersion();
RepoLoader.getInstance().loadRemoteData();
executorService.submit(HTML_TEMPLATE);
executorService.submit(HTML_TEMPLATE_DARK);

View File

@ -36,7 +36,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

View File

@ -26,11 +26,8 @@ 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;
@ -153,6 +150,6 @@ public class AppHelper {
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));
return appLabel.computeIfAbsent(info, i -> i.applicationInfo.loadLabel(pm));
}
}

View File

@ -33,8 +33,6 @@ import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@ -61,7 +59,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.lspd.models.Application;
import org.lsposed.manager.App;
@ -96,8 +93,6 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
private final AppListFragment fragment;
private final PackageManager pm;
private final SharedPreferences preferences;
private final HandlerThread handlerThread = new HandlerThread("appList");
private final Handler loadAppListHandler;
private final ModuleUtil moduleUtil;
private final ModuleUtil.InstalledModule module;
@ -121,7 +116,7 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
};
private ApplicationInfo selectedInfo;
private boolean refreshing = false;
private boolean isLoaded = false;
private boolean enabled = true;
public ScopeAdapter(AppListFragment fragment, ModuleUtil.InstalledModule module) {
@ -129,8 +124,6 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
this.activity = fragment.requireActivity();
this.module = module;
moduleUtil = ModuleUtil.getInstance();
handlerThread.start();
loadAppListHandler = new Handler(handlerThread.getLooper());
preferences = App.getPreferences();
pm = activity.getPackageManager();
}
@ -217,15 +210,21 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
});
}
@SuppressLint("NotifyDataSetChanged")
private void setLoaded(boolean loaded) {
fragment.runOnUiThread(() -> {
isLoaded = loaded;
notifyDataSetChanged();
});
}
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.use_recommended) {
if (!checkedList.isEmpty()) {
new BlurBehindDialogBuilder(activity)
.setMessage(R.string.use_recommended_message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
checkRecommended();
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended())
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
@ -244,14 +243,6 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
} else if (itemId == R.id.item_filter_denylist) {
item.setChecked(!item.isChecked());
preferences.edit().putBoolean("filter_denylist", item.isChecked()).apply();
} else if (itemId == R.id.menu_launch) {
Intent launchIntent = AppHelper.getSettingsIntent(module.packageName, module.userId);
if (launchIntent != null) {
ConfigManager.startActivityAsUserWithFeature(launchIntent, module.userId);
} else {
Snackbar.make(fragment.binding.snackbar, R.string.module_no_ui, Snackbar.LENGTH_LONG).show();
}
return true;
} else if (itemId == R.id.backup) {
LocalDateTime now = LocalDateTime.now();
fragment.backupLauncher.launch(String.format(Locale.ROOT,
@ -279,7 +270,7 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
ConfigManager.startActivityAsUserWithFeature(launchIntent, module.userId);
}
} else if (itemId == R.id.menu_compile_speed) {
CompileDialogFragment.speed(fragment.getChildFragmentManager(), info, fragment.binding.snackbar);
CompileDialogFragment.speed(fragment.getChildFragmentManager(), info);
} else if (itemId == R.id.menu_other_app) {
var intent = new Intent(Intent.ACTION_SHOW_APP_INFO);
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, module.packageName);
@ -305,10 +296,6 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
}
public void onPrepareOptionsMenu(@NonNull Menu menu) {
Intent intent = AppHelper.getSettingsIntent(module.packageName, module.userId);
if (intent == null) {
menu.removeItem(R.id.menu_launch);
}
List<String> scopeList = module.getScopeList();
if (scopeList == null || scopeList.isEmpty()) {
menu.removeItem(R.id.use_recommended);
@ -465,26 +452,17 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
return showList.size();
}
public void onDestroy() {
loadAppListHandler.removeCallbacksAndMessages(null);
handlerThread.quit();
public void refresh() {
refresh(false);
}
public void refresh() {
synchronized (this) {
if (refreshing) {
return;
}
refreshing = true;
}
loadAppListHandler.removeCallbacksAndMessages(null);
boolean force = fragment.binding.swipeRefreshLayout.isRefreshing();
if (!force) fragment.binding.progress.setIndeterminate(true);
public void refresh(boolean force) {
setLoaded(false);
enabled = moduleUtil.isModuleEnabled(module.packageName);
fragment.binding.masterSwitch.setOnCheckedChangeListener(null);
fragment.binding.masterSwitch.setChecked(enabled);
fragment.binding.masterSwitch.setOnCheckedChangeListener(switchBarOnCheckedChangeListener);
loadAppListHandler.post(() -> {
fragment.runAsync(() -> {
List<PackageInfo> appList = AppHelper.getAppList(force);
denyList = AppHelper.getDenyList(force);
var tmpRecList = new HashSet<ApplicationWithEquals>();
@ -545,10 +523,7 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
String queryStr = fragment.searchView != null ? fragment.searchView.getQuery().toString() : "";
getFilter().filter(queryStr, count -> {
refreshing = false;
fragment.runOnUiThread((this::notifyDataSetChanged));
});
fragment.runOnUiThread(() -> getFilter().filter(queryStr));
});
}
@ -560,7 +535,7 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
tmpChkList.remove(appInfo.application);
}
if (!ConfigManager.setModuleScope(module.packageName, tmpChkList)) {
Snackbar.make(fragment.binding.snackbar, R.string.failed_to_save_scope_list, Snackbar.LENGTH_SHORT).show();
fragment.showHint(R.string.failed_to_save_scope_list, true);
if (!isChecked) {
tmpChkList.add(appInfo.application);
} else {
@ -568,19 +543,16 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
}
buttonView.setChecked(!isChecked);
} else if (appInfo.packageName.equals("android")) {
Snackbar.make(fragment.binding.snackbar, R.string.reboot_required, Snackbar.LENGTH_SHORT)
.setAction(R.string.reboot, v -> ConfigManager.reboot(false))
.show();
fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot(false));
} else if (denyList.contains(appInfo.packageName)) {
Snackbar.make(fragment.binding.snackbar, activity.getString(R.string.deny_list, appInfo.label), Snackbar.LENGTH_SHORT)
.show();
fragment.showHint(activity.getString(R.string.deny_list, appInfo.label), true);
}
checkedList = tmpChkList;
}
@Override
public boolean isLoaded() {
return !refreshing;
return isLoaded;
}
static class ViewHolder extends RecyclerView.ViewHolder {
@ -613,15 +585,11 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
List<AppInfo> filtered = new ArrayList<>();
if (constraint.toString().isEmpty()) {
filtered.addAll(searchList);
} else {
String filter = constraint.toString().toLowerCase();
for (AppInfo info : searchList) {
if (lowercaseContains(info.label.toString(), filter)
|| lowercaseContains(info.packageName, filter)) {
filtered.add(info);
}
String filter = constraint.toString().toLowerCase();
for (AppInfo info : searchList) {
if (lowercaseContains(info.label.toString(), filter)
|| lowercaseContains(info.packageName, filter)) {
filtered.add(info);
}
}
filterResults.values = filtered;
@ -633,6 +601,7 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
protected void publishResults(CharSequence constraint, FilterResults results) {
//noinspection unchecked
showList = (List<AppInfo>) results.values;
setLoaded(true);
}
}
@ -640,13 +609,13 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
return new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
refresh();
getFilter().filter(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
refresh();
public boolean onQueryTextChange(String query) {
getFilter().filter(query);
return true;
}
};
@ -654,13 +623,11 @@ public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<Scope
public void onBackPressed() {
fragment.searchView.clearFocus();
if (!refreshing && fragment.binding.masterSwitch.isChecked() && checkedList.isEmpty()) {
if (isLoaded && fragment.binding.masterSwitch.isChecked() && checkedList.isEmpty()) {
var builder = new BlurBehindDialogBuilder(activity);
builder.setMessage(!recommendedList.isEmpty() ? R.string.no_scope_selected_has_recommended : R.string.no_scope_selected);
if (!recommendedList.isEmpty()) {
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
checkRecommended();
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended());
} else {
builder.setPositiveButton(android.R.string.cancel, null);
}

View File

@ -23,6 +23,7 @@ package org.lsposed.manager.repo;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
@ -37,10 +38,9 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import okhttp3.Call;
import okhttp3.Callback;
@ -51,24 +51,25 @@ import okhttp3.ResponseBody;
public class RepoLoader {
private static RepoLoader instance = null;
private Map<String, OnlineModule> onlineModules = new HashMap<>();
private Map<String, ModuleVersion> latestVersion = new ConcurrentHashMap<>();
public static class ModuleVersion {
public String versionName;
public long versionCode;
private ModuleVersion(long versionCode, String versionName) {
this.versionName = versionName;
this.versionCode = versionCode;
}
public boolean upgradable(long versionCode, String versionName) {
return this.versionCode > versionCode || (this.versionCode == versionCode && !versionName.equals(this.versionName));
}
}
private final Map<String, ModuleVersion> latestVersion = new ConcurrentHashMap<>();
private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json");
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
private boolean isLoading = false;
private final Set<RepoListener> listeners = ConcurrentHashMap.newKeySet();
private boolean repoLoaded = false;
private static final String originRepoUrl = "https://modules.lsposed.org/";
private static final String backupRepoUrl = "https://cdn.jsdelivr.net/gh/Xposed-Modules-Repo/modules@gh-pages/";
@ -81,91 +82,78 @@ public class RepoLoader {
public static synchronized RepoLoader getInstance() {
if (instance == null) {
instance = new RepoLoader();
instance.loadRemoteData();
App.getExecutorService().submit(instance::loadRemoteData);
}
return instance;
}
public void loadRemoteData() {
synchronized (this) {
if (isLoading) {
return;
}
isLoading = true;
}
App.getOkHttpClient().newCall(new Request.Builder()
.url(repoUrl + "modules.json")
.build()).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.e(App.TAG, call.request().url().toString(), e);
for (Listener listener : listeners) {
listener.onThrowable(e);
}
synchronized (this) {
isLoading = false;
if (!repoUrl.equals(backupRepoUrl)) {
repoUrl = backupRepoUrl;
loadRemoteData();
}
}
}
synchronized public void loadRemoteData() {
repoLoaded = true;
try {
var response = App.getOkHttpClient().newCall(new Request.Builder()
.url(repoUrl + "modules.json")
.build()).execute();
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (body != null) {
try {
String bodyString = body.string();
Gson gson = new Gson();
Map<String, OnlineModule> modules = new HashMap<>();
OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class);
Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule));
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (body != null) {
try {
String bodyString = body.string();
Gson gson = new Gson();
Map<String, OnlineModule> modules = new HashMap<>();
OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class);
Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule));
latestVersion.clear();
for (var module : repoModules) {
var release = module.getLatestRelease();
if (release == null || release.isEmpty()) continue;
var splits = release.split("-", 2);
if (splits.length < 2) continue;
long verCode;
String verName;
try {
verCode = Long.parseLong(splits[0]);
verName = splits[1];
} catch (NumberFormatException ignored) {
continue;
}
String pkgName = module.getName();
latestVersion.put(pkgName, new ModuleVersion(verCode, verName));
Map<String, ModuleVersion> versions = new ConcurrentHashMap<>();
for (var module : repoModules) {
var release = module.getLatestRelease();
if (release == null || release.isEmpty()) continue;
var splits = release.split("-", 2);
if (splits.length < 2) continue;
long verCode;
String verName;
try {
verCode = Long.parseLong(splits[0]);
verName = splits[1];
} catch (NumberFormatException ignored) {
continue;
}
String pkgName = module.getName();
versions.put(pkgName, new ModuleVersion(verCode, verName));
}
onlineModules = modules;
Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8));
synchronized (this) {
repoLoaded = true;
}
for (Listener listener : listeners) {
listener.repoLoaded();
}
} catch (Throwable t) {
Log.e(App.TAG, Log.getStackTraceString(t));
for (Listener listener : listeners) {
listener.onThrowable(t);
}
latestVersion = versions;
onlineModules = modules;
Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8));
repoLoaded = true;
for (RepoListener listener : listeners) {
listener.onRepoLoaded();
}
} catch (Throwable t) {
Log.e(App.TAG, Log.getStackTraceString(t));
for (RepoListener listener : listeners) {
listener.onThrowable(t);
}
}
}
synchronized (this) {
isLoading = false;
}
}
});
} catch (Throwable e) {
Log.e(App.TAG, "load remote data", e);
for (RepoListener listener : listeners) {
listener.onThrowable(e);
}
if (!repoUrl.equals(backupRepoUrl)) {
repoUrl = backupRepoUrl;
loadRemoteData();
}
} finally {
repoLoaded = true;
}
}
@Nullable
public ModuleVersion getModuleLatestVersion(String packageName) {
return latestVersion.get(packageName);
return repoLoaded ? latestVersion.getOrDefault(packageName, null) : null;
}
public void loadRemoteReleases(String packageName) {
@ -179,7 +167,7 @@ public class RepoLoader {
repoUrl = backupRepoUrl;
loadRemoteReleases(packageName);
} else {
for (Listener listener : listeners) {
for (RepoListener listener : listeners) {
listener.onThrowable(e);
}
}
@ -196,12 +184,12 @@ public class RepoLoader {
OnlineModule module = gson.fromJson(bodyString, OnlineModule.class);
module.releasesLoaded = true;
onlineModules.replace(packageName, module);
for (Listener listener : listeners) {
listener.moduleReleasesLoaded(module);
for (RepoListener listener : listeners) {
listener.onModuleReleasesLoaded(module);
}
} catch (Throwable t) {
Log.e(App.TAG, Log.getStackTraceString(t));
for (Listener listener : listeners) {
for (RepoListener listener : listeners) {
listener.onThrowable(t);
}
}
@ -211,28 +199,30 @@ public class RepoLoader {
});
}
public void addListener(Listener listener) {
public void addListener(RepoListener listener) {
if (!listeners.contains(listener))
listeners.add(listener);
}
public void removeListener(Listener listener) {
public void removeListener(RepoListener listener) {
listeners.remove(listener);
}
@Nullable
public OnlineModule getOnlineModule(String packageName) {
return packageName == null ? null : onlineModules.get(packageName);
return !repoLoaded || packageName == null ? null : onlineModules.get(packageName);
}
@Nullable
public Collection<OnlineModule> getOnlineModules() {
return onlineModules.values();
return repoLoaded ? onlineModules.values() : null;
}
public interface Listener {
default void repoLoaded() {
public interface RepoListener {
default void onRepoLoaded() {
}
default void moduleReleasesLoaded(OnlineModule module) {
default void onModuleReleasesLoaded(OnlineModule module) {
}
default void onThrowable(Throwable t) {

View File

@ -21,6 +21,7 @@ import com.google.android.material.textview.MaterialTextView;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.DialogTitleBinding;
import org.lsposed.manager.databinding.DialogWarningBinding;
import java.io.BufferedReader;
@ -39,18 +40,23 @@ public class FlashDialogBuilder extends BlurBehindDialogBuilder {
var pref = App.getPreferences();
var notes = pref.getString("release_notes", "");
this.zipPath = pref.getString("zip_file", null);
setTitle(R.string.update_lsposed);
LayoutInflater inflater = LayoutInflater.from(context);
var title = DialogTitleBinding.inflate(inflater).getRoot();
title.setText(R.string.update_lsposed);
setCustomTitle(title);
textView = new MaterialTextView(context);
var text = notes + "\n\n\n" + context.getString(R.string.update_lsposed_msg) + "\n\n";
textView.setText(text);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setTextIsSelectable(true);
LayoutInflater inflater = LayoutInflater.from(context);
DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false);
binding.container.addView(textView);
rootView = binding.getRoot();
setView(rootView);
title.setOnClickListener(v -> rootView.smoothScrollTo(0, 0));
setNegativeButton(android.R.string.cancel, cancel);
setPositiveButton(R.string.install, null);

View File

@ -19,11 +19,14 @@
package org.lsposed.manager.ui.dialog;
import android.content.Context;
import android.app.Dialog;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import org.lsposed.manager.BuildConfig;
import org.lsposed.manager.ConfigManager;
@ -34,12 +37,14 @@ import java.util.Locale;
import rikka.core.util.ClipboardUtils;
public class InfoDialogBuilder extends BlurBehindDialogBuilder {
public class InfoDialogBuilder extends DialogFragment {
public InfoDialogBuilder(@NonNull Context context) {
super(context);
setTitle(R.string.info);
DialogInfoBinding binding = DialogInfoBinding.inflate(LayoutInflater.from(context), null, false);
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
var activity = requireActivity();
var builder = new BlurBehindDialogBuilder(activity).setTitle(R.string.info);
DialogInfoBinding binding = DialogInfoBinding.inflate(LayoutInflater.from(activity), null, false);
if (ConfigManager.isBinderAlive()) {
binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion()));
@ -61,37 +66,38 @@ public class InfoDialogBuilder extends BlurBehindDialogBuilder {
binding.device.setText(getDevice());
binding.systemAbi.setText(Build.SUPPORTED_ABIS[0]);
setView(binding.getRoot());
builder.setView(binding.getRoot());
setPositiveButton(android.R.string.ok, null);
String info = context.getString(R.string.info_api_version) +
builder.setPositiveButton(android.R.string.ok, null);
String info = activity.getString(R.string.info_api_version) +
"\n" +
binding.apiVersion.getText() +
"\n\n" +
context.getString(R.string.info_api) +
activity.getString(R.string.info_api) +
"\n" +
binding.api.getText() +
"\n\n" +
context.getString(R.string.info_framework_version) +
activity.getString(R.string.info_framework_version) +
"\n" +
binding.frameworkVersion.getText() +
"\n\n" +
context.getString(R.string.info_manager_version) +
activity.getString(R.string.info_manager_version) +
"\n" +
binding.managerVersion.getText() +
"\n\n" +
context.getString(R.string.info_system_version) +
activity.getString(R.string.info_system_version) +
"\n" +
binding.systemVersion.getText() +
"\n\n" +
context.getString(R.string.info_device) +
activity.getString(R.string.info_device) +
"\n" +
binding.device.getText() +
"\n\n" +
context.getString(R.string.info_system_abi) +
activity.getString(R.string.info_system_abi) +
"\n" +
binding.systemAbi.getText();
setNeutralButton(android.R.string.copy, (dialog, which) -> ClipboardUtils.put(context, info));
builder.setNeutralButton(android.R.string.copy, (dialog, which) -> ClipboardUtils.put(activity, info));
return builder.create();
}
private String getDevice() {

View File

@ -0,0 +1,63 @@
/*
* This file is part of LSPosed.
*
* LSPosed is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LSPosed is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2021 LSPosed Contributors
*/
package org.lsposed.manager.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.receivers.LSPManagerServiceHolder;
public class ShortcutDialog extends DialogFragment {
private static boolean shown = false;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new BlurBehindDialogBuilder(requireContext())
.setTitle(R.string.parasitic_recommend)
.setMessage(R.string.parasitic_recommend_summary)
.setNegativeButton(R.string.never_show, (dialog, which) ->
App.getPreferences().edit().putBoolean("never_show_shortcut", true).apply())
.setNeutralButton(R.string.create_shortcut, (dialog, which) -> {
try {
LSPManagerServiceHolder.getService().createShortcut();
} catch (RemoteException ignored) {
}
})
.setPositiveButton(android.R.string.ok, null).create();
}
public static void showIfNeed(FragmentManager fm) {
if (App.isParasitic() || !ConfigManager.isBinderAlive()) return;
if (App.getPreferences().getBoolean("never_show_shortcut", false)) return;
if (shown) return;
shown = true;
new ShortcutDialog().show(fm, "shortcut");
}
}

View File

@ -1,38 +0,0 @@
package org.lsposed.manager.ui.dialog;
import android.content.Context;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.receivers.LSPManagerServiceHolder;
public class ShortcutDialogBuilder extends BlurBehindDialogBuilder {
private static boolean shown = false;
private ShortcutDialogBuilder(@NonNull Context context) {
super(context);
setTitle(R.string.parasitic_recommend);
setMessage(R.string.parasitic_recommend_summary);
setNegativeButton(R.string.never_show, (dialog, which) ->
App.getPreferences().edit().putBoolean("never_show_shortcut", true).apply());
setNeutralButton(R.string.create_shortcut, (dialog, which) -> {
try {
LSPManagerServiceHolder.getService().createShortcut();
} catch (RemoteException ignored) {
}
});
setPositiveButton(android.R.string.ok, null);
}
public static void showIfNeed(@NonNull Context context) {
if (App.isParasitic() || !ConfigManager.isBinderAlive()) return;
if (App.getPreferences().getBoolean("never_show_shortcut", false)) return;
if (shown) return;
shown = true;
new ShortcutDialogBuilder(context).show();
}
}

View File

@ -19,13 +19,15 @@
package org.lsposed.manager.ui.dialog;
import android.app.Activity;
import android.content.Context;
import android.app.Dialog;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.DialogFragment;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
@ -33,14 +35,15 @@ import org.lsposed.manager.databinding.DialogItemBinding;
import org.lsposed.manager.databinding.DialogWarningBinding;
import org.lsposed.manager.util.chrome.LinkTransformationMethod;
public class WarningDialogBuilder extends BlurBehindDialogBuilder {
public class WarningDialogBuilder extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
var activity = requireActivity();
var builder = new BlurBehindDialogBuilder(activity).
setTitle(R.string.partial_activated);
public WarningDialogBuilder(@NonNull Context context) {
super(context);
Activity activity = (Activity) context;
setTitle(R.string.partial_activated);
LayoutInflater inflater = LayoutInflater.from(context);
LayoutInflater inflater = LayoutInflater.from(activity);
DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false);
if (!ConfigManager.isSepolicyLoaded()) {
@ -65,7 +68,9 @@ public class WarningDialogBuilder extends BlurBehindDialogBuilder {
item.value.setTransformationMethod(new LinkTransformationMethod(activity));
}
setView(binding.getRoot());
setPositiveButton(android.R.string.ok, null);
builder.setView(binding.getRoot());
builder.setPositiveButton(android.R.string.ok, null);
builder.setNeutralButton(R.string.info, (dialog, which) -> new InfoDialogBuilder().show(getParentFragmentManager(), "info"));
return builder.create();
}
}

View File

@ -19,13 +19,13 @@
package org.lsposed.manager.ui.fragment;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
@ -36,10 +36,10 @@ import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.adapters.AppHelper;
import org.lsposed.manager.adapters.ScopeAdapter;
import org.lsposed.manager.databinding.FragmentAppListBinding;
import org.lsposed.manager.util.BackupUtils;
@ -60,6 +60,15 @@ public class AppListFragment extends BaseFragment {
public ActivityResultLauncher<String> backupLauncher;
public ActivityResultLauncher<String[]> restoreLauncher;
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
if (binding != null && scopeAdapter != null) {
binding.swipeRefreshLayout.setRefreshing(!scopeAdapter.isLoaded());
}
}
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -68,7 +77,6 @@ public class AppListFragment extends BaseFragment {
return binding.getRoot();
}
binding.appBar.setLiftable(true);
binding.appBar.setLifted(true);
String title;
if (module.userId != 0) {
title = String.format(Locale.ROOT, "%s (%d)", module.getAppName(), module.userId);
@ -79,24 +87,33 @@ 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());
}
}
});
scopeAdapter.registerAdapterDataObserver(observer);
binding.recyclerView.setAdapter(scopeAdapter);
binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
binding.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh());
binding.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh(true));
binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
Intent intent = AppHelper.getSettingsIntent(module.packageName, module.userId);
if (intent == null) {
binding.fab.setVisibility(View.GONE);
} else {
binding.fab.setVisibility(View.VISIBLE);
binding.fab.setOnClickListener(v -> ConfigManager.startActivityAsUserWithFeature(intent, module.userId));
}
searchListener = scopeAdapter.getSearchListener();
setupToolbar(binding.toolbar, title, R.menu.menu_app_list, view -> requireActivity().getOnBackPressedDispatcher().onBackPressed());
setupToolbar(binding.toolbar, binding.clickView, title, R.menu.menu_app_list, view -> requireActivity().getOnBackPressedDispatcher().onBackPressed());
View.OnClickListener l = v -> {
if (searchView.isIconified()) {
binding.recyclerView.smoothScrollToPosition(0);
binding.appBar.setExpanded(true, true);
}
};
binding.toolbar.setOnClickListener(l);
binding.clickView.setOnClickListener(l);
return binding.getRoot();
}
@ -116,6 +133,8 @@ public class AppListFragment extends BaseFragment {
int moduleUserId = args.getModuleUserId();
module = ModuleUtil.getInstance().getModule(modulePackageName, moduleUserId);
if (module == null)
getNavController().navigate(R.id.action_app_list_fragment_to_modules_fragment);
backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
uri -> {
@ -125,11 +144,7 @@ public class AppListFragment extends BaseFragment {
BackupUtils.backup(uri, modulePackageName);
} catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
showHint(text, false);
}
});
});
@ -141,11 +156,7 @@ public class AppListFragment extends BaseFragment {
BackupUtils.restore(uri, modulePackageName);
} catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
showHint(text, false);
}
});
});
@ -164,17 +175,10 @@ public class AppListFragment extends BaseFragment {
if (scopeAdapter != null) scopeAdapter.refresh();
}
@Override
public void onDestroy() {
if (scopeAdapter != null) scopeAdapter.onDestroy();
super.onDestroy();
}
@Override
public void onDestroyView() {
super.onDestroyView();
scopeAdapter.unregisterAdapterDataObserver(observer);
binding = null;
}
@ -191,6 +195,18 @@ public class AppListFragment extends BaseFragment {
super.onPrepareOptionsMenu(menu);
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(searchListener);
searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View arg0) {
binding.appBar.setExpanded(false, true);
binding.recyclerView.setNestedScrollingEnabled(false);
}
@Override
public void onViewDetachedFromWindow(View v) {
binding.recyclerView.setNestedScrollingEnabled(true);
}
});
scopeAdapter.onPrepareOptionsMenu(menu);
}

View File

@ -21,12 +21,16 @@ package org.lsposed.manager.ui.fragment;
import android.app.Activity;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App;
import org.lsposed.manager.R;
@ -41,22 +45,24 @@ public class BaseFragment extends Fragment {
return NavHostFragment.findNavController(this);
}
public void setupToolbar(Toolbar toolbar, int title) {
setupToolbar(toolbar, getString(title), -1);
public void setupToolbar(Toolbar toolbar, View tipsView, int title) {
setupToolbar(toolbar, tipsView, getString(title), -1);
}
public void setupToolbar(Toolbar toolbar, int title, int menu) {
setupToolbar(toolbar, getString(title), menu, null);
public void setupToolbar(Toolbar toolbar, View tipsView, int title, int menu) {
setupToolbar(toolbar, tipsView, getString(title), menu, null);
}
public void setupToolbar(Toolbar toolbar, String title, int menu) {
setupToolbar(toolbar, title, menu, null);
public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu) {
setupToolbar(toolbar, tipsView, title, menu, null);
}
public void setupToolbar(Toolbar toolbar, String title, int menu, View.OnClickListener navigationOnClickListener) {
public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu, View.OnClickListener navigationOnClickListener) {
toolbar.setNavigationOnClickListener(navigationOnClickListener == null ? (v -> navigateUp()) : navigationOnClickListener);
toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
toolbar.setTitle(title);
toolbar.setTooltipText(title);
if (tipsView != null) tipsView.setTooltipText(title);
if (menu != -1) {
toolbar.inflateMenu(menu);
toolbar.setOnMenuItemClickListener(this::onOptionsItemSelected);
@ -74,4 +80,29 @@ public class BaseFragment extends Fragment {
activity.runOnUiThread(runnable);
}
}
public void showHint(@StringRes int res, boolean lengthShort, @StringRes int actionRes, View.OnClickListener action) {
showHint(getString(res), lengthShort, getString(actionRes), action);
}
public void showHint(@StringRes int res, boolean lengthShort) {
showHint(getString(res), lengthShort, null, null);
}
public void showHint(CharSequence str, boolean lengthShort) {
showHint(str, lengthShort, null, null);
}
public void showHint(CharSequence str, boolean lengthShort, CharSequence actionStr, View.OnClickListener action) {
if (isResumed()) {
var container = requireActivity().findViewById(R.id.container);
if (container != null) {
var snackbar = Snackbar.make(container, str, lengthShort ? Snackbar.LENGTH_SHORT : Snackbar.LENGTH_LONG);
if (actionStr != null && action != null) snackbar.setAction(actionStr, action);
snackbar.show();
return;
}
}
Toast.makeText(requireContext(), str, lengthShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
}
}

View File

@ -31,11 +31,10 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentCompileDialogBinding;
@ -47,19 +46,21 @@ import java.lang.ref.WeakReference;
@SuppressWarnings("deprecation")
public class CompileDialogFragment extends AppCompatDialogFragment {
private ApplicationInfo appInfo;
private View snackBar;
public static void speed(FragmentManager fragmentManager, ApplicationInfo info, View snackBar) {
public static void speed(FragmentManager fragmentManager, ApplicationInfo info) {
CompileDialogFragment fragment = new CompileDialogFragment();
fragment.setCancelable(false);
fragment.appInfo = info;
fragment.snackBar = snackBar;
var bundle = new Bundle();
bundle.putParcelable("appInfo", info);
fragment.setArguments(bundle);
fragment.show(fragmentManager, "compile_dialog");
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
var arguments = getArguments();
appInfo = arguments != null ? arguments.getParcelable("appInfo") : null;
if (appInfo == null) {
throw new IllegalStateException("appInfo should not be null.");
}
@ -67,7 +68,6 @@ public class CompileDialogFragment extends AppCompatDialogFragment {
FragmentCompileDialogBinding binding = FragmentCompileDialogBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
final PackageManager pm = requireContext().getPackageManager();
var builder = new BlurBehindDialogBuilder(requireActivity())
.setIcon(appInfo.loadIcon(pm))
.setTitle(appInfo.loadLabel(pm))
.setView(binding.getRoot());
@ -75,8 +75,8 @@ public class CompileDialogFragment extends AppCompatDialogFragment {
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
new CompileTask(this).executeOnExecutor(App.getExecutorService(), appInfo.packageName);
}
@ -118,9 +118,8 @@ public class CompileDialogFragment extends AppCompatDialogFragment {
if (fragment != null) {
fragment.dismissAllowingStateLoss();
var parent = fragment.getParentFragment();
if (fragment.snackBar != null && parent != null && parent.isResumed()) {
Snackbar.make(fragment.snackBar, text, Snackbar.LENGTH_LONG).show();
return;
if (parent instanceof BaseFragment) {
((BaseFragment) parent).showHint(text, true);
}
}
Toast.makeText(context, text, Toast.LENGTH_LONG).show();

View File

@ -20,17 +20,18 @@
package org.lsposed.manager.ui.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.os.Build;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.snackbar.Snackbar;
@ -44,7 +45,7 @@ import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.dialog.FlashDialogBuilder;
import org.lsposed.manager.ui.dialog.InfoDialogBuilder;
import org.lsposed.manager.ui.dialog.ShortcutDialogBuilder;
import org.lsposed.manager.ui.dialog.ShortcutDialog;
import org.lsposed.manager.ui.dialog.WarningDialogBuilder;
import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.NavUtil;
@ -56,7 +57,7 @@ import java.util.Locale;
import rikka.core.util.ResourceUtils;
public class HomeFragment extends BaseFragment implements RepoLoader.Listener, ModuleUtil.ModuleListener {
public class HomeFragment extends BaseFragment implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
private FragmentHomeBinding binding;
@ -66,15 +67,16 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ShortcutDialogBuilder.showIfNeed(requireContext());
ShortcutDialog.showIfNeed(getChildFragmentManager());
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentHomeBinding.inflate(inflater, container, false);
setupToolbar(binding.toolbar, getString(R.string.app_name), R.menu.menu_home);
setupToolbar(binding.toolbar, null, R.string.app_name);
binding.toolbar.setNavigationIcon(null);
binding.toolbar.setOnClickListener(v -> showAbout());
binding.appBar.setLiftable(true);
binding.nestedScrollView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
@ -82,9 +84,9 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.status.setOnClickListener(v -> {
if (ConfigManager.isBinderAlive() && !UpdateUtil.needUpdate()) {
if (!ConfigManager.isSepolicyLoaded() || !ConfigManager.systemServerRequested() || !ConfigManager.dex2oatFlagsLoaded()) {
new WarningDialogBuilder(activity).show();
new WarningDialogBuilder().show(getChildFragmentManager(), "warning");
} else {
new InfoDialogBuilder(activity).show();
new InfoDialogBuilder().show(getChildFragmentManager(), "info");
}
} else {
if (UpdateUtil.canInstall()) {
@ -105,43 +107,16 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.download.setOnClickListener(new StartFragmentListener(R.id.action_repo_fragment, false));
binding.logs.setOnClickListener(new StartFragmentListener(R.id.action_logs_fragment, true));
binding.settings.setOnClickListener(new StartFragmentListener(R.id.action_settings_fragment, false));
binding.issue.setOnClickListener(view -> NavUtil.startURL(activity, "https://github.com/LSPosed/LSPosed/issues"));
binding.issue.setOnClickListener(view -> NavUtil.startURL(activity, "https://github.com/LSPosed/LSPosed/issues/new/choose"));
updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
repoLoader.addListener(this);
moduleUtil.addListener(this);
if (repoLoader.isRepoLoaded()) {
repoLoaded();
}
onModulesReloaded();
return binding.getRoot();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_refresh) {
updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
} else if (itemId == R.id.menu_info) {
new InfoDialogBuilder(requireActivity()).setTitle(R.string.info).show();
} else if (itemId == R.id.menu_about) {
Activity activity = requireActivity();
DialogAboutBinding binding = DialogAboutBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
binding.designAboutTitle.setText(R.string.app_name);
binding.designAboutInfo.setMovementMethod(LinkMovementMethod.getInstance());
binding.designAboutInfo.setTransformationMethod(new LinkTransformationMethod(activity));
binding.designAboutInfo.setText(HtmlCompat.fromHtml(getString(
R.string.about_view_source_code,
"<b><a href=\"https://github.com/LSPosed/LSPosed\">GitHub</a></b>",
"<b><a href=\"https://t.me/LSPosed\">Telegram</a></b>"), HtmlCompat.FROM_HTML_MODE_LEGACY));
binding.designAboutVersion.setText(String.format(Locale.ROOT, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
new BlurBehindDialogBuilder(activity)
.setView(binding.getRoot())
.show();
}
return super.onOptionsItemSelected(item);
}
private void updateStates(Activity activity, boolean binderAlive, boolean needUpdate) {
int cardBackgroundColor;
if (binderAlive) {
@ -186,7 +161,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.download.setVisibility(View.GONE);
}
binding.statusIcon.setImageResource(R.drawable.ic_round_error_outline_24);
Snackbar.make(binding.snackbar, R.string.lsposed_not_active, Snackbar.LENGTH_INDEFINITE).show();
showHint(R.string.lsposed_not_active, false);
}
cardBackgroundColor = MaterialColors.harmonizeWithPrimary(activity, cardBackgroundColor);
binding.status.setCardBackgroundColor(cardBackgroundColor);
@ -194,10 +169,33 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.status.setOutlineSpotShadowColor(cardBackgroundColor);
binding.status.setOutlineAmbientShadowColor(cardBackgroundColor);
}
binding.about.setOnClickListener(v -> showAbout());
}
public static class AboutDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
DialogAboutBinding binding = DialogAboutBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
binding.designAboutTitle.setText(R.string.app_name);
binding.designAboutInfo.setMovementMethod(LinkMovementMethod.getInstance());
binding.designAboutInfo.setTransformationMethod(new LinkTransformationMethod(requireActivity()));
binding.designAboutInfo.setText(HtmlCompat.fromHtml(getString(
R.string.about_view_source_code,
"<b><a href=\"https://github.com/LSPosed/LSPosed\">GitHub</a></b>",
"<b><a href=\"https://t.me/LSPosed\">Telegram</a></b>"), HtmlCompat.FROM_HTML_MODE_LEGACY));
binding.designAboutVersion.setText(String.format(Locale.ROOT, "%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
return new BlurBehindDialogBuilder(requireContext())
.setView(binding.getRoot()).create();
}
}
private void showAbout() {
new AboutDialog().show(getChildFragmentManager(), "about");
}
@Override
public void repoLoaded() {
public void onRepoLoaded() {
final int[] count = new int[]{0};
HashSet<String> processedModules = new HashSet<>();
var modules = moduleUtil.getModules();
@ -228,7 +226,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override
public void onModulesReloaded() {
if (repoLoader.isRepoLoaded()) repoLoaded();
onRepoLoaded();
setModulesSummary(moduleUtil.getEnabledModulesCount());
}
@ -244,7 +242,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override
public void onClick(View v) {
if (requireInstalled && !ConfigManager.isBinderAlive()) {
Snackbar.make(binding.snackbar, R.string.lsposed_not_active, Snackbar.LENGTH_LONG).show();
showHint(R.string.lsposed_not_active, false);
} else {
getNavController().navigate(fragment);
}
@ -260,7 +258,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
}
private void setModulesSummary(int moduleCount) {
runOnUiThread(() -> binding.modulesSummary.setText(moduleCount == - 1? getString(R.string.loading) : getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount)));
runOnUiThread(() -> binding.modulesSummary.setText(moduleCount == -1 ? getString(R.string.loading) : getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount)));
}
@Override

View File

@ -14,57 +14,74 @@
* You should have received a copy of the GNU General Public License
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2020 EdXposed Contributors
* Copyright (C) 2021 LSPosed Contributors
*/
package org.lsposed.manager.ui.fragment;
import static org.lsposed.manager.App.TAG;
import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
import android.widget.Toast;
import android.widget.HorizontalScrollView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.textview.MaterialTextView;
import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentLogsBinding;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.databinding.FragmentPagerBinding;
import org.lsposed.manager.databinding.ItemLogTextviewBinding;
import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import java.io.ByteArrayOutputStream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import rikka.core.os.FileUtils;
import rikka.recyclerview.RecyclerViewKt;
public class LogsFragment extends BaseFragment {
private boolean verbose = false;
private final Handler handler = new Handler(Looper.getMainLooper());
private FragmentLogsBinding binding;
private FragmentPagerBinding binding;
private LogPageAdapter adapter;
private MenuItem wordWrap;
interface OptionsItemSelectListener {
boolean onOptionsItemSelected(@NonNull MenuItem item);
}
private OptionsItemSelectListener optionsItemSelectListener;
private final ActivityResultLauncher<String> saveLogsLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument(),
uri -> {
@ -78,11 +95,7 @@ public class LogsFragment extends BaseFragment {
os.finish();
} catch (IOException e) {
var text = context.getString(R.string.logs_save_failed2, e.getMessage());
if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
}
showHint(text, false);
}
});
});
@ -90,54 +103,57 @@ public class LogsFragment extends BaseFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentLogsBinding.inflate(inflater, container, false);
setupToolbar(binding.toolbar, R.string.Logs, R.menu.menu_logs);
binding.slidingTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
verbose = tab.getPosition() == 1;
reloadLogs();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
binding = FragmentPagerBinding.inflate(inflater, container, false);
binding.appBar.setLiftable(true);
setupToolbar(binding.toolbar, binding.clickView, R.string.Logs, R.menu.menu_logs);
binding.toolbar.setSubtitle(ConfigManager.isVerboseLogEnabled() ? R.string.enabled_verbose_log : R.string.disabled_verbose_log);
adapter = new LogPageAdapter(this);
binding.viewPager.setAdapter(adapter);
new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText((int) adapter.getItemId(position))).attach();
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);
}
});
return binding.getRoot();
}
public void setOptionsItemSelectListener(OptionsItemSelectListener optionsItemSelectListener) {
this.optionsItemSelectListener = optionsItemSelectListener;
}
@SuppressLint("NotifyDataSetChanged")
@Override
public void onResume() {
super.onResume();
reloadLogs();
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
var itemId = item.getItemId();
if (itemId == R.id.menu_save) {
save();
return true;
} else if (itemId == R.id.menu_word_wrap) {
item.setChecked(!item.isChecked());
App.getPreferences().edit().putBoolean("enable_word_wrap", item.isChecked()).apply();
binding.viewPager.setUserInputEnabled(item.isChecked());
adapter.refresh();
return true;
}
if (optionsItemSelectListener != null) {
if (optionsItemSelectListener.onOptionsItemSelected(item))
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_scroll_top) {
binding.scrollView.fullScroll(ScrollView.FOCUS_UP);
} else if (itemId == R.id.menu_scroll_down) {
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN);
} else if (itemId == R.id.menu_refresh) {
reloadLogs();
return true;
} else if (itemId == R.id.menu_save) {
save();
return true;
} else if (itemId == R.id.menu_clear) {
clear();
return true;
}
return super.onOptionsItemSelected(item);
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
wordWrap = menu.findItem(R.id.menu_word_wrap);
wordWrap.setChecked(App.getPreferences().getBoolean("enable_word_wrap", false));
binding.viewPager.setUserInputEnabled(wordWrap.isChecked());
}
@Override
@ -147,21 +163,6 @@ public class LogsFragment extends BaseFragment {
binding = null;
}
private void reloadLogs() {
var parcelFileDescriptor = ConfigManager.getLog(verbose);
if (parcelFileDescriptor != null)
new LogsReader().execute(parcelFileDescriptor);
}
private void clear() {
if (ConfigManager.clearLogs(verbose)) {
Snackbar.make(binding.snackbar, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
binding.body.setText("");
} else {
Snackbar.make(binding.snackbar, R.string.logs_clear_failed_2, Snackbar.LENGTH_SHORT).show();
}
}
private void save() {
LocalDateTime now = LocalDateTime.now();
String filename = String.format(Locale.ROOT, "LSPosed_%s.zip", now.toString());
@ -176,7 +177,7 @@ public class LogsFragment extends BaseFragment {
FileUtils.copy(is, os);
os.closeEntry();
} catch (IOException e) {
Log.w(TAG, name, e);
Log.w(App.TAG, name, e);
}
});
@ -187,53 +188,7 @@ public class LogsFragment extends BaseFragment {
FileUtils.copy(is, os);
os.closeEntry();
} catch (IOException e) {
Log.w(TAG, name, e);
}
}
@SuppressWarnings("deprecation")
@SuppressLint("StaticFieldLeak")
private class LogsReader extends AsyncTask<ParcelFileDescriptor, Integer, String> {
private AlertDialog mProgressDialog;
private final Runnable mRunnable = () -> {
synchronized (LogsReader.this) {
if (!requireActivity().isFinishing()) {
mProgressDialog.show();
}
}
};
@Override
synchronized protected void onPreExecute() {
mProgressDialog = new BlurBehindDialogBuilder(requireActivity()).create();
mProgressDialog.setMessage(getString(R.string.loading));
mProgressDialog.setCancelable(false);
handler.postDelayed(mRunnable, 300);
}
@Override
protected String doInBackground(ParcelFileDescriptor... log) {
Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2);
try (var pfd = log[0];
var inputStream = new FileInputStream(pfd.getFileDescriptor())) {
int size = Math.toIntExact(pfd.getStatSize()); // max 4MiB
var logs = new ByteArrayOutputStream(size);
FileUtils.copy(inputStream, logs);
return logs.toString();
} catch (IOException e) {
return requireActivity().getResources().getString(R.string.logs_cannot_read)
+ "\n" + Log.getStackTraceString(e);
}
}
@Override
synchronized protected void onPostExecute(String logs) {
binding.body.setText(logs);
handler.removeCallbacks(mRunnable);
if (mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
Log.w(App.TAG, name, e);
}
}
@ -243,19 +198,245 @@ public class LogsFragment extends BaseFragment {
super.onDestroy();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(LogsFragment.class.getName() + "." + "tab", binding.slidingTabs.getSelectedTabPosition());
public static class LogFragment extends BaseFragment {
public static final int SCROLL_THRESHOLD = 500;
protected boolean verbose;
protected SwiperefreshRecyclerviewBinding binding;
protected LogAdaptor adaptor;
protected LinearLayoutManager layoutManager;
class LogAdaptor extends EmptyStateRecyclerView.EmptyStateAdapter<LogAdaptor.ViewHolder> {
private List<CharSequence> log = Collections.emptyList();
private boolean isLoaded = false;
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(ItemLogTextviewBinding.inflate(getLayoutInflater(), parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.item.setText(log.get(position));
}
@Override
public int getItemCount() {
return log.size();
}
void refresh() {
isLoaded = true;
runOnUiThread(this::notifyDataSetChanged);
}
void fullRefresh() {
runAsync(() -> {
isLoaded = false;
try (var parcelFileDescriptor = ConfigManager.getLog(verbose);
var br = new BufferedReader(new InputStreamReader(new FileInputStream(parcelFileDescriptor != null ? parcelFileDescriptor.getFileDescriptor() : null)))) {
log = br.lines().parallel().collect(Collectors.toList());
} catch (Throwable e) {
log = Arrays.asList(Log.getStackTraceString(e).split("\n"));
} finally {
refresh();
}
});
}
@Override
public boolean isLoaded() {
return isLoaded;
}
class ViewHolder extends RecyclerView.ViewHolder {
final MaterialTextView item;
public ViewHolder(ItemLogTextviewBinding binding) {
super(binding.getRoot());
item = binding.logItem;
}
}
}
protected LogAdaptor createAdaptor() {
return new LogAdaptor();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
var arguments = getArguments();
if (arguments == null) return null;
verbose = arguments.getBoolean("verbose");
adaptor = createAdaptor();
binding.recyclerView.setAdapter(adaptor);
layoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(layoutManager);
binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
binding.swipeRefreshLayout.setOnRefreshListener(adaptor::fullRefresh);
adaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.swipeRefreshLayout.setRefreshing(!adaptor.isLoaded());
}
});
adaptor.fullRefresh();
return binding.getRoot();
}
public void scrollToTop(LogsFragment logsFragment) {
logsFragment.binding.appBar.setExpanded(true, true);
if (layoutManager.findFirstVisibleItemPosition() > SCROLL_THRESHOLD) {
binding.recyclerView.scrollToPosition(0);
} else {
binding.recyclerView.smoothScrollToPosition(0);
}
}
public void scrollToBottom(LogsFragment logsFragment) {
logsFragment.binding.appBar.setExpanded(false, true);
var end = Math.max(adaptor.getItemCount() - 1, 0);
if (adaptor.getItemCount() - layoutManager.findLastVisibleItemPosition() > SCROLL_THRESHOLD) {
binding.recyclerView.scrollToPosition(end);
} else {
binding.recyclerView.smoothScrollToPosition(end);
}
}
void attachListeners() {
var parent = getParentFragment();
if (parent instanceof LogsFragment) {
var logsFragment = (LogsFragment) parent;
logsFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder());
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> logsFragment.binding.appBar.setLifted(!top));
logsFragment.setOptionsItemSelectListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.menu_scroll_top) {
scrollToTop(logsFragment);
} else if (itemId == R.id.menu_scroll_down) {
scrollToBottom(logsFragment);
} else if (itemId == R.id.menu_clear) {
if (ConfigManager.clearLogs(verbose)) {
logsFragment.showHint(R.string.logs_cleared, true);
adaptor.fullRefresh();
} else {
logsFragment.showHint(R.string.logs_clear_failed_2, true);
}
return true;
}
return false;
});
View.OnClickListener l = v -> scrollToTop(logsFragment);
logsFragment.binding.clickView.setOnClickListener(l);
logsFragment.binding.toolbar.setOnClickListener(l);
}
}
void detachListeners() {
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
}
@Override
public void onStart() {
super.onStart();
attachListeners();
}
@Override
public void onResume() {
super.onResume();
adaptor.refresh();
attachListeners();
}
@Override
public void onPause() {
super.onPause();
detachListeners();
}
@Override
public void onStop() {
super.onStop();
detachListeners();
}
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
var tabPosition = savedInstanceState.getInt(LogsFragment.class.getName() + "." + "tab", 0);
if (tabPosition < binding.slidingTabs.getTabCount())
binding.slidingTabs.selectTab(binding.slidingTabs.getTabAt(tabPosition));
public static class UnwrapLogFragment extends LogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
var root = super.onCreateView(inflater, container, savedInstanceState);
binding.swipeRefreshLayout.removeView(binding.recyclerView);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
horizontalScrollView.setFillViewport(true);
horizontalScrollView.setHorizontalScrollBarEnabled(false);
binding.swipeRefreshLayout.addView(horizontalScrollView);
horizontalScrollView.addView(binding.recyclerView);
binding.recyclerView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
return root;
}
@Override
protected LogAdaptor createAdaptor() {
return new LogAdaptor() {
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
holder.item.measure(0, 0);
}
};
}
}
class LogPageAdapter extends FragmentStateAdapter {
public LogPageAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
var bundle = new Bundle();
bundle.putBoolean("verbose", verbose(position));
var f = getItemViewType(position) == 0 ? new LogFragment() : new UnwrapLogFragment();
f.setArguments(bundle);
return f;
}
@Override
public int getItemCount() {
return 2;
}
@Override
public long getItemId(int position) {
return verbose(position) ? R.string.nav_item_logs_lsp : R.string.nav_item_logs_module;
}
@Override
public boolean containsItem(long itemId) {
return itemId == R.string.nav_item_logs_lsp || itemId == R.string.nav_item_logs_module;
}
public boolean verbose(int position) {
return position != 0;
}
@Override
public int getItemViewType(int position) {
return wordWrap.isChecked() ? 0 : 1;
}
public void refresh() {
runOnUiThread(this::notifyDataSetChanged);
}
super.onViewStateRestored(savedInstanceState);
}
}

View File

@ -46,7 +46,6 @@ import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -65,7 +64,6 @@ import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
@ -74,10 +72,9 @@ import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R;
import org.lsposed.manager.adapters.AppHelper;
import org.lsposed.manager.databinding.DialogRecyclerviewBinding;
import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
import org.lsposed.manager.databinding.FragmentPagerBinding;
import org.lsposed.manager.databinding.ItemModuleBinding;
import org.lsposed.manager.databinding.ItemRepoRecyclerviewBinding;
import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
@ -94,7 +91,7 @@ import java.util.stream.IntStream;
import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt;
public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener {
public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener, RepoLoader.RepoListener {
private static final PackageManager pm = App.getInstance().getPackageManager();
private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
private static final RepoLoader repoLoader = RepoLoader.getInstance();
@ -103,14 +100,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected SearchView searchView;
private SearchView.OnQueryTextListener searchListener;
private final ArrayList<ModuleAdapter> adapters = new ArrayList<>();
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
updateProgress();
}
};
final ArrayList<ModuleAdapter> adapters = new ArrayList<>();
private ModuleUtil.InstalledModule selectedModule;
@ -125,8 +115,8 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
@Override
public boolean onQueryTextChange(String newText) {
adapters.forEach(adapter -> adapter.getFilter().filter(newText));
public boolean onQueryTextChange(String query) {
adapters.forEach(adapter -> adapter.getFilter().filter(query));
return false;
}
};
@ -137,7 +127,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
adapter.setHasStableIds(true);
adapter.setStateRestorationPolicy(PREVENT_WHEN_EMPTY);
adapters.add(adapter);
adapter.registerAdapterDataObserver(observer);
}
}
}
@ -154,29 +143,16 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
}
private void updateProgress() {
if (binding != null) {
var position = binding.viewPager.getCurrentItem();
binding.progress.setVisibility(adapters.get(position).isLoaded ? View.GONE : View.VISIBLE);
}
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
moduleUtil.addListener(this);
}
@Nullable
@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.appBar.setLiftable(true);
setupToolbar(binding.toolbar, binding.clickView, 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) {
updateProgress();
showFab();
}
});
@ -204,31 +180,18 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
binding.tabLayout.setVisibility(View.GONE);
}
binding.fab.setOnClickListener(v -> {
var pickAdaptor = new ModuleAdapter(adapters.get(binding.viewPager.getCurrentItem()).getUser(), true);
var position = binding.viewPager.getCurrentItem();
var user = adapters.get(position).getUser();
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 dialog = new BlurBehindDialogBuilder(requireActivity())
.setTitle(getString(R.string.install_to_user, user.name))
.setView(binding.getRoot())
.setNegativeButton(android.R.string.cancel, null)
.show();
pickAdaptor.setOnPickListener(picked -> {
var module = (ModuleUtil.InstalledModule) picked.getTag();
installModuleToUser(module, user);
dialog.dismiss();
});
var bundle = new Bundle();
var user = adapters.get(binding.viewPager.getCurrentItem()).getUser();
bundle.putParcelable("userInfo", user);
var f = new RecyclerViewDialogFragment();
f.setArguments(bundle);
f.show(getChildFragmentManager(), "install_to_user" + user.id);
});
moduleUtil.addListener(this);
repoLoader.addListener(this);
updateModuleSummary();
return binding.getRoot();
}
@ -236,6 +199,16 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
public void onPrepareOptionsMenu(Menu menu) {
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(searchListener);
searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View arg0) {
binding.appBar.setExpanded(false, true);
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
@Override
@ -245,31 +218,30 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
@Override
public void onDestroy() {
super.onDestroy();
moduleUtil.removeListener(this);
}
@Override
public void onSingleInstalledModuleReloaded(ModuleUtil.InstalledModule module) {
public void onSingleModuleReloaded(ModuleUtil.InstalledModule module) {
adapters.forEach(ModuleAdapter::refresh);
}
@Override
public void onModulesReloaded() {
adapters.forEach(ModuleAdapter::refresh);
updateModuleSummary();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_refresh) {
adapters.forEach(adapter -> adapter.refresh(true));
}
return super.onOptionsItemSelected(item);
public void onRepoLoaded() {
adapters.forEach(ModuleAdapter::refresh);
}
private void installModuleToUser(ModuleUtil.InstalledModule module, UserInfo user) {
private void updateModuleSummary() {
var moduleCount = moduleUtil.getEnabledModulesCount();
runOnUiThread(() -> {
binding.toolbar.setSubtitle(moduleCount == -1 ? getString(R.string.loading) : getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount));
binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle());
});
}
void installModuleToUser(ModuleUtil.InstalledModule module, UserInfo user) {
new BlurBehindDialogBuilder(requireActivity())
.setTitle(getString(R.string.install_to_user, user.name))
.setMessage(getString(R.string.install_to_user_message, module.getAppName(), user.name))
@ -279,11 +251,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
String text = success ?
getString(R.string.module_installed, module.getAppName(), user.name) :
getString(R.string.module_install_failed);
if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
showHint(text, false);
if (success)
moduleUtil.reloadSingleModule(module.packageName, user.id);
}))
@ -305,8 +273,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
Intent intent = AppHelper.getSettingsIntent(packageName, selectedModule.userId);
if (intent != null) {
ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
} else {
Snackbar.make(binding.snackbar, R.string.module_no_ui, Snackbar.LENGTH_LONG).show();
}
return true;
} else if (itemId == R.id.menu_other_app) {
@ -326,11 +292,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
runAsync(() -> {
boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId);
String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed);
if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
showHint(text, false);
if (success)
moduleUtil.reloadSingleModule(selectedModule.packageName, selectedModule.userId);
}))
@ -341,7 +303,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
getNavController().navigate(ModulesFragmentDirections.actionModulesFragmentToRepoItemFragment(selectedModule.packageName, selectedModule.getAppName()));
return true;
} else if (itemId == R.id.menu_compile_speed) {
CompileDialogFragment.speed(getChildFragmentManager(), selectedModule.pkg.applicationInfo, binding.snackbar);
CompileDialogFragment.speed(getChildFragmentManager(), selectedModule.pkg.applicationInfo);
}
return super.onContextItemSelected(item);
}
@ -349,12 +311,33 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
@Override
public void onDestroyView() {
super.onDestroyView();
moduleUtil.removeListener(this);
repoLoader.removeListener(this);
binding = null;
}
public static class ModuleListFragment extends Fragment {
public SwiperefreshRecyclerviewBinding binding;
private ModuleAdapter adapter;
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
}
};
private final View.OnAttachStateChangeListener searchViewLocker = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
binding.recyclerView.setNestedScrollingEnabled(false);
}
@Override
public void onViewDetachedFromWindow(View v) {
binding.recyclerView.setNestedScrollingEnabled(true);
}
};
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -364,13 +347,75 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
return null;
}
int position = arguments.getInt("position");
ItemRepoRecyclerviewBinding binding = ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
binding.recyclerView.setAdapter(fragment.adapters.get(position));
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(layoutManager);
binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
adapter = fragment.adapters.get(position);
binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
adapter.registerAdapterDataObserver(observer);
return binding.getRoot();
}
void attachListeners() {
var parent = getParentFragment();
if (parent instanceof ModulesFragment) {
var moduleFragment = (ModulesFragment) parent;
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> moduleFragment.binding.appBar.setLifted(!top));
moduleFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder());
moduleFragment.searchView.addOnAttachStateChangeListener(searchViewLocker);
binding.recyclerView.setNestedScrollingEnabled(moduleFragment.searchView.isIconified());
View.OnClickListener l = v -> {
if (moduleFragment.searchView.isIconified()) {
binding.recyclerView.smoothScrollToPosition(0);
moduleFragment.binding.appBar.setExpanded(true, true);
}
};
moduleFragment.binding.clickView.setOnClickListener(l);
moduleFragment.binding.toolbar.setOnClickListener(l);
}
}
void detachListeners() {
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
var parent = getParentFragment();
if (parent instanceof ModulesFragment) {
var moduleFragment = (ModulesFragment) parent;
moduleFragment.searchView.removeOnAttachStateChangeListener(searchViewLocker);
binding.recyclerView.setNestedScrollingEnabled(true);
}
}
@Override
public void onStart() {
super.onStart();
attachListeners();
}
@Override
public void onResume() {
super.onResume();
attachListeners();
}
@Override
public void onDestroyView() {
adapter.unregisterAdapterDataObserver(observer);
super.onDestroyView();
}
@Override
public void onPause() {
super.onPause();
detachListeners();
}
@Override
public void onStop() {
super.onStop();
detachListeners();
}
}
private class PagerAdapter extends FragmentStateAdapter {
@ -400,7 +445,11 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
}
private class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ModuleAdapter.ViewHolder> implements Filterable {
ModuleAdapter createPickModuleAdapter(UserInfo userInfo) {
return new ModuleAdapter(userInfo, true);
}
class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ModuleAdapter.ViewHolder> implements Filterable {
private List<ModuleUtil.InstalledModule> searchList = new ArrayList<>();
private List<ModuleUtil.InstalledModule> showList = new ArrayList<>();
private final UserInfo user;
@ -487,22 +536,20 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
if (repoLoader.isRepoLoaded()) {
var ver = repoLoader.getModuleLatestVersion(item.packageName);
if (ver != null && ver.upgradable(item.versionCode, item.versionName)) {
if (warningText != null) sb.append("\n");
String recommended = getString(R.string.update_available, ver.versionName);
sb.append(recommended);
final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorAccent));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
var ver = repoLoader.getModuleLatestVersion(item.packageName);
if (ver != null && ver.upgradable(item.versionCode, item.versionName)) {
if (warningText != null) sb.append("\n");
String recommended = getString(R.string.update_available, ver.versionName);
sb.append(recommended);
final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorAccent));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
if (sb.length() == 0) {
holder.hint.setVisibility(View.GONE);
@ -515,7 +562,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f);
holder.itemView.setOnClickListener(v -> {
searchView.clearFocus();
searchView.onActionViewCollapsed();
getNavController().navigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId));
});
holder.itemView.setOnLongClickListener(v -> {
@ -585,12 +631,15 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}
public void refresh() {
refresh(false);
runAsync(reloadModules);
}
public void refresh(boolean force) {
if (force) runAsync(moduleUtil::reloadInstalledModules);
runAsync(reloadModules);
public void fullRefresh() {
runAsync(() -> {
setLoaded(false);
moduleUtil.reloadInstalledModules();
refresh();
});
}
private final Runnable reloadModules = () -> {
@ -634,7 +683,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
});
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
searchList = tmpList;
runOnUiThread(() -> getFilter().filter(queryStr, count -> setLoaded(true)));
runOnUiThread(() -> getFilter().filter(queryStr));
};
@SuppressLint("NotifyDataSetChanged")
@ -647,7 +696,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
@Override
public boolean isLoaded() {
return isLoaded;
return isLoaded && moduleUtil.isModulesLoaded();
}
class ViewHolder extends RecyclerView.ViewHolder {
@ -681,16 +730,12 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
List<ModuleUtil.InstalledModule> filtered = new ArrayList<>();
if (constraint.toString().isEmpty()) {
filtered.addAll(searchList);
} else {
String filter = constraint.toString().toLowerCase();
for (ModuleUtil.InstalledModule info : searchList) {
if (lowercaseContains(info.getAppName(), filter) ||
lowercaseContains(info.packageName, filter) ||
lowercaseContains(info.getDescription(), filter)) {
filtered.add(info);
}
String filter = constraint.toString().toLowerCase();
for (ModuleUtil.InstalledModule info : searchList) {
if (lowercaseContains(info.getAppName(), filter) ||
lowercaseContains(info.packageName, filter) ||
lowercaseContains(info.getDescription(), filter)) {
filtered.add(info);
}
}
filterResults.values = filtered;
@ -702,6 +747,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected void publishResults(CharSequence constraint, FilterResults results) {
//noinspection unchecked
showList = (List<ModuleUtil.InstalledModule>) results.values;
setLoaded(true);
}
}
}

View File

@ -0,0 +1,85 @@
/*
* This file is part of LSPosed.
*
* LSPosed is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LSPosed is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
*
* Copyright (C) 2021 LSPosed Contributors
*/
package org.lsposed.manager.ui.fragment;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.lsposed.lspd.models.UserInfo;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.DialogTitleBinding;
import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.util.ModuleUtil;
public class RecyclerViewDialogFragment extends AppCompatDialogFragment {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
var parent = getParentFragment();
var arguments = getArguments();
if (!(parent instanceof ModulesFragment) || arguments == null) {
throw new IllegalStateException();
}
var modulesFragment = (ModulesFragment) parent;
var user = (UserInfo) arguments.getParcelable("userInfo");
var pickAdaptor = modulesFragment.createPickModuleAdapter(user);
var binding = SwiperefreshRecyclerviewBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
binding.recyclerView.setAdapter(pickAdaptor);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
pickAdaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.swipeRefreshLayout.setRefreshing(!pickAdaptor.isLoaded());
}
});
binding.swipeRefreshLayout.setOnRefreshListener(pickAdaptor::fullRefresh);
pickAdaptor.refresh();
var title = DialogTitleBinding.inflate(getLayoutInflater()).getRoot();
title.setText(getString(R.string.install_to_user, user.name));
var dialog = new BlurBehindDialogBuilder(requireActivity())
.setCustomTitle(title)
.setView(binding.getRoot())
.setNegativeButton(android.R.string.cancel, null)
.create();
title.setOnClickListener(s -> binding.recyclerView.smoothScrollToPosition(0));
pickAdaptor.setOnPickListener(picked -> {
var module = (ModuleUtil.InstalledModule) picked.getTag();
modulesFragment.installModuleToUser(module, user);
dialog.dismiss();
});
return dialog;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
}

View File

@ -44,26 +44,23 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App;
import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentRepoBinding;
import org.lsposed.manager.databinding.ItemOnlinemoduleBinding;
import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.repo.model.OnlineModule;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.SimpleStatefulAdaptor;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@ -71,7 +68,7 @@ import rikka.core.util.LabelComparator;
import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt;
public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
public class RepoFragment extends BaseFragment implements RepoLoader.RepoListener, ModuleUtil.ModuleListener {
protected FragmentRepoBinding binding;
protected SearchView searchView;
private SearchView.OnQueryTextListener mSearchListener;
@ -79,7 +76,14 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
private boolean preLoadWebview = true;
private final RepoLoader repoLoader = RepoLoader.getInstance();
private final ModuleUtil moduleUtil = ModuleUtil.getInstance();
private RepoAdapter adapter;
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -103,33 +107,78 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentRepoBinding.inflate(getLayoutInflater(), container, false);
setupToolbar(binding.toolbar, R.string.module_repo, R.menu.menu_repo);
binding.appBar.setLiftable(true);
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
setupToolbar(binding.toolbar, binding.clickView, R.string.module_repo, R.menu.menu_repo);
adapter = new RepoAdapter();
adapter.setHasStableIds(true);
adapter.registerAdapterDataObserver(observer);
binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
binding.progress.setVisibilityAfterHide(View.GONE);
binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
View.OnClickListener l = v -> {
if (searchView.isIconified()) {
binding.recyclerView.smoothScrollToPosition(0);
binding.appBar.setExpanded(true, true);
}
};
binding.toolbar.setOnClickListener(l);
binding.clickView.setOnClickListener(l);
repoLoader.addListener(this);
/*
CollapsingToolbarLayout consumes window insets, causing child views not
receiving window insets.
See https://github.com/material-components/material-components-android/issues/1310
Insets can be handled by RikkaX Insets, so we can manually set
OnApplyWindowInsetsListener to null.
*/
binding.collapsingToolbarLayout.setOnApplyWindowInsetsListener(null);
moduleUtil.addListener(this);
updateRepoSummary();
return binding.getRoot();
}
private void updateRepoSummary() {
final int[] count = new int[]{0};
HashSet<String> processedModules = new HashSet<>();
var modules = moduleUtil.getModules();
if (modules != null && repoLoader.isRepoLoaded()) {
modules.forEach((k, v) -> {
if (!processedModules.contains(k.first)) {
var ver = repoLoader.getModuleLatestVersion(k.first);
if (ver != null && ver.upgradable(v.versionCode, v.versionName)) {
++count[0];
}
processedModules.add(k.first);
}
}
);
} else {
count[0] = -1;
}
runOnUiThread(() -> {
if (count[0] > 0) {
binding.toolbar.setSubtitle(getResources().getQuantityString(R.plurals.module_repo_upgradable, count[0], count[0]));
} else if (count[0] == 0) {
binding.toolbar.setSubtitle(getResources().getString(R.string.module_repo_up_to_date));
} else {
binding.toolbar.setSubtitle(getResources().getString(R.string.loading));
}
binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle());
});
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(mSearchListener);
searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View arg0) {
binding.appBar.setExpanded(false, true);
binding.recyclerView.setNestedScrollingEnabled(false);
}
@Override
public void onViewDetachedFromWindow(View v) {
binding.recyclerView.setNestedScrollingEnabled(true);
}
});
int sort = App.getPreferences().getInt("repo_sort", 0);
if (sort == 0) {
menu.findItem(R.id.item_sort_by_name).setChecked(true);
@ -144,13 +193,15 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
mHandler.removeCallbacksAndMessages(null);
repoLoader.removeListener(this);
moduleUtil.removeListener(this);
adapter.unregisterAdapterDataObserver(observer);
binding = null;
}
@Override
public void onResume() {
super.onResume();
adapter.initData();
adapter.refresh();
if (preLoadWebview) {
mHandler.postDelayed(() -> new WebView(requireContext()), 500);
preLoadWebview = false;
@ -158,41 +209,41 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
}
@Override
public void repoLoaded() {
runOnUiThread(() -> {
binding.progress.hide();
adapter.setData(repoLoader.getOnlineModules());
});
public void onRepoLoaded() {
adapter.refresh();
updateRepoSummary();
}
@Override
public void onThrowable(Throwable t) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show();
}
showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
updateRepoSummary();
}
@Override
public void onModulesReloaded() {
updateRepoSummary();
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_refresh) {
binding.progress.show();
repoLoader.loadRemoteData();
} else if (itemId == R.id.item_sort_by_name) {
if (itemId == R.id.item_sort_by_name) {
item.setChecked(true);
App.getPreferences().edit().putInt("repo_sort", 0).apply();
adapter.setData(repoLoader.getOnlineModules());
adapter.refresh();
} else if (itemId == R.id.item_sort_by_update_time) {
item.setChecked(true);
App.getPreferences().edit().putInt("repo_sort", 1).apply();
adapter.setData(repoLoader.getOnlineModules());
adapter.refresh();
}
return super.onOptionsItemSelected(item);
}
private class RepoAdapter extends SimpleStatefulAdaptor<RepoAdapter.ViewHolder> implements Filterable {
private class RepoAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<RepoAdapter.ViewHolder> implements Filterable {
private List<OnlineModule> fullList, showList;
private final LabelComparator labelComparator = new LabelComparator();
private boolean isLoaded = false;
RepoAdapter() {
fullList = showList = Collections.emptyList();
@ -219,7 +270,7 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
holder.appDescription.setVisibility(View.VISIBLE);
holder.appDescription.setText(sb);
sb = new SpannableStringBuilder();
ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(module.getName());
ModuleUtil.InstalledModule installedModule = moduleUtil.getModule(module.getName());
if (installedModule != null) {
var ver = repoLoader.getModuleLatestVersion(installedModule.packageName);
if (ver != null && ver.upgradable(installedModule.versionCode, installedModule.versionName)) {
@ -245,9 +296,9 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
holder.itemView.setOnClickListener(v -> {
searchView.clearFocus();
searchView.onActionViewCollapsed();
getNavController().navigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName(), module.getDescription()));
});
holder.itemView.setTooltipText(module.getDescription());
}
@Override
@ -255,27 +306,37 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
return showList.size();
}
public void setData(Collection<OnlineModule> modules) {
fullList = new ArrayList<>(modules);
fullList = fullList.stream().filter((onlineModule -> !onlineModule.isHide() && !onlineModule.getReleases().isEmpty())).collect(Collectors.toList());
int sort = App.getPreferences().getInt("repo_sort", 0);
if (sort == 0) {
fullList.sort((o1, o2) -> labelComparator.compare(o1.getDescription(), o2.getDescription()));
} else if (sort == 1) {
fullList.sort(Collections.reverseOrder(Comparator.comparing(o -> Instant.parse(o.getReleases().get(0).getUpdatedAt()))));
}
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
requireActivity().runOnUiThread(() -> getFilter().filter(queryStr));
private void setLoaded(boolean isLoaded) {
this.isLoaded = isLoaded;
runOnUiThread(this::notifyDataSetChanged);
}
public void initData() {
Collection<OnlineModule> modules = repoLoader.getOnlineModules();
if (!repoLoader.isRepoLoaded()) {
binding.progress.show();
public void setData(Collection<OnlineModule> modules) {
if (modules == null) return;
setLoaded(false);
int sort = App.getPreferences().getInt("repo_sort", 0);
fullList = modules.parallelStream().filter((onlineModule -> !onlineModule.isHide() && !onlineModule.getReleases().isEmpty()))
.sorted((a, b) -> {
if (sort == 0) {
return labelComparator.compare(a.getDescription(), b.getDescription());
} else {
return Instant.parse(b.getReleases().get(0).getUpdatedAt()).compareTo(Instant.parse(a.getReleases().get(0).getUpdatedAt()));
}
}).collect(Collectors.toList());
String queryStr = searchView != null ? searchView.getQuery().toString() : "";
runOnUiThread(() -> getFilter().filter(queryStr));
}
public void fullRefresh() {
runAsync(() -> {
setLoaded(false);
repoLoader.loadRemoteData();
} else {
adapter.setData(modules);
}
refresh();
});
}
public void refresh() {
runAsync(() -> adapter.setData(repoLoader.getOnlineModules()));
}
@Override
@ -288,6 +349,11 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
return new RepoAdapter.ModuleFilter();
}
@Override
public boolean isLoaded() {
return isLoaded && repoLoader.isRepoLoaded();
}
class ViewHolder extends RecyclerView.ViewHolder {
ConstraintLayout root;
TextView appName;
@ -311,26 +377,26 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint.toString().isEmpty()) {
showList = fullList;
} else {
ArrayList<OnlineModule> filtered = new ArrayList<>();
String filter = constraint.toString().toLowerCase();
for (OnlineModule info : fullList) {
if (lowercaseContains(info.getDescription(), filter) ||
lowercaseContains(info.getName(), filter) ||
lowercaseContains(info.getSummary(), filter)) {
filtered.add(info);
}
FilterResults filterResults = new FilterResults();
ArrayList<OnlineModule> filtered = new ArrayList<>();
String filter = constraint.toString().toLowerCase();
for (OnlineModule info : fullList) {
if (lowercaseContains(info.getDescription(), filter) ||
lowercaseContains(info.getName(), filter) ||
lowercaseContains(info.getSummary(), filter)) {
filtered.add(info);
}
showList = filtered;
}
return null;
filterResults.values = filtered;
filterResults.count = filtered.size();
return filterResults;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyDataSetChanged();
//noinspection unchecked
showList = (List<OnlineModule>) results.values;
setLoaded(true);
}
}
}

View File

@ -19,6 +19,7 @@
package org.lsposed.manager.ui.fragment;
import android.app.Dialog;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
@ -36,16 +37,20 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
@ -83,13 +88,13 @@ import okhttp3.Request;
import okhttp3.Response;
import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt;
import rikka.widget.borderview.BorderNestedScrollView;
import rikka.widget.borderview.BorderRecyclerView;
import rikka.widget.borderview.BorderView;
public class RepoItemFragment extends BaseFragment implements RepoLoader.Listener {
public class RepoItemFragment extends BaseFragment implements RepoLoader.RepoListener {
FragmentPagerBinding binding;
private OnlineModule module;
OnlineModule module;
private ReleaseAdapter releaseAdapter;
private InformationAdapter informationAdapter;
@Nullable
@Override
@ -98,9 +103,11 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
if (module == null) return binding.getRoot();
String modulePackageName = module.getName();
String moduleName = module.getDescription();
setupToolbar(binding.toolbar, moduleName, R.menu.menu_repo_item);
binding.appBar.setLiftable(true);
setupToolbar(binding.toolbar, binding.clickView, moduleName, R.menu.menu_repo_item);
binding.clickView.setTooltipText(moduleName);
binding.toolbar.setSubtitle(modulePackageName);
binding.viewPager.setAdapter(new PagerAdapter());
binding.viewPager.setAdapter(new PagerAdapter(this));
int[] titles = new int[]{R.string.module_readme, R.string.module_releases, R.string.module_information};
new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(titles[position])).attach();
@ -112,15 +119,13 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
}
});
binding.toolbar.setOnClickListener(v -> binding.appBar.setExpanded(true, true));
releaseAdapter = new ReleaseAdapter();
informationAdapter = new InformationAdapter();
RepoLoader.getInstance().addListener(this);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
RepoLoader.getInstance().addListener(this);
@ -208,44 +213,35 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
}
@Override
public void moduleReleasesLoaded(OnlineModule module) {
public void onDestroyView() {
super.onDestroyView();
RepoLoader.getInstance().removeListener(this);
binding = null;
}
@Override
public void onModuleReleasesLoaded(OnlineModule module) {
this.module = module;
if (releaseAdapter != null) {
runAsync(releaseAdapter::loadItems);
if (isResumed() && module.getReleases().size() == 1) {
Snackbar.make(binding.snackbar, R.string.module_release_no_more, Snackbar.LENGTH_SHORT).show();
}
runAsync(releaseAdapter::loadItems);
if (module.getReleases().size() == 1) {
showHint(R.string.module_release_no_more, true);
}
}
@Override
public void onThrowable(Throwable t) {
if (releaseAdapter != null) {
runAsync(releaseAdapter::loadItems);
if (isResumed()) {
Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show();
}
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
RepoLoader.getInstance().removeListener(this);
binding = null;
runAsync(releaseAdapter::loadItems);
showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
}
private class InformationAdapter extends SimpleStatefulAdaptor<InformationAdapter.ViewHolder> {
private final OnlineModule module;
private int rowCount = 0;
private int homepageRow = -1;
private int collaboratorsRow = -1;
private int sourceUrlRow = -1;
public InformationAdapter(OnlineModule module) {
this.module = module;
public InformationAdapter() {
if (!TextUtils.isEmpty(module.getHomepageUrl())) {
homepageRow = rowCount++;
}
@ -260,7 +256,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@NonNull
@Override
public InformationAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new InformationAdapter.ViewHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false));
return new ViewHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false));
}
@Override
@ -302,7 +298,6 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
NavUtil.startURL(requireActivity(), module.getSourceUrl());
}
});
}
@Override
@ -322,6 +317,27 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
}
}
public static class DownloadDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
var args = getArguments();
if (args == null) throw new IllegalArgumentException();
return new BlurBehindDialogBuilder(requireActivity())
.setItems(args.getCharSequenceArray("names"), (dialog, which) -> NavUtil.startURL(requireActivity(), args.getStringArrayList("urls").get(which)))
.create();
}
static void create(FragmentManager fm, String[] names, ArrayList<String> urls) {
var f = new DownloadDialog();
var bundle = new Bundle();
bundle.putStringArray("names", names);
bundle.putStringArrayList("urls", urls);
f.setArguments(bundle);
f.show(fm, "download");
}
}
private class ReleaseAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ReleaseAdapter.ViewHolder> {
private List<Release> items = new ArrayList<>();
private final Resources resources = App.getInstance().getResources();
@ -353,9 +369,9 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@Override
public ReleaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == 0) {
return new ReleaseAdapter.ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false));
return new ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false));
} else {
return new ReleaseAdapter.LoadmoreViewHolder(ItemRepoLoadmoreBinding.inflate(getLayoutInflater(), parent, false));
return new LoadmoreViewHolder(ItemRepoLoadmoreBinding.inflate(getLayoutInflater(), parent, false));
}
}
@ -381,9 +397,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
holder.viewAssets.setOnClickListener(v -> {
ArrayList<String> names = new ArrayList<>();
assets.forEach(releaseAsset -> names.add(releaseAsset.getName()));
new BlurBehindDialogBuilder(requireActivity())
.setItems(names.toArray(new String[0]), (dialog, which) -> NavUtil.startURL(requireActivity(), assets.get(which).getDownloadUrl()))
.show();
DownloadDialog.create(getChildFragmentManager(), names.toArray(new String[0]), assets.stream().map(ReleaseAsset::getDownloadUrl).collect(Collectors.toCollection(ArrayList::new)));
});
} else {
holder.viewAssets.setVisibility(View.GONE);
@ -437,38 +451,27 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
}
}
private class PagerAdapter extends SimpleStatefulAdaptor<PagerAdapter.ViewHolder> {
private static class PagerAdapter extends FragmentStateAdapter {
public PagerAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public PagerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == 0) {
return new PagerAdapter.ReadmeViewHolder(ItemRepoReadmeBinding.inflate(getLayoutInflater(), parent, false));
public Fragment createFragment(int position) {
Bundle bundle = new Bundle();
bundle.putInt("position", position);
Fragment f;
if (position == 0) {
f = new ReadmeFragment();
} else if (position == 1) {
f = new RecyclerviewFragment();
} else {
return new PagerAdapter.RecyclerviewBinding(ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), parent, false));
}
}
@Override
public void onBindViewHolder(@NonNull PagerAdapter.ViewHolder holder, int position) {
switch (position) {
case 0:
if (module != null)
renderGithubMarkdown(holder.webView, module.getReadmeHTML());
break;
case 1:
case 2:
RecyclerView.Adapter adapter;
if (position == 1) {
adapter = releaseAdapter = new ReleaseAdapter();
} else {
adapter = new InformationAdapter(module);
}
holder.recyclerView.setAdapter(adapter);
holder.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
RecyclerViewKt.fixEdgeEffect(holder.recyclerView, false, true);
break;
f = new RecyclerviewFragment();
}
f.setArguments(bundle);
return f;
}
@Override
@ -481,29 +484,112 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
return position == 0 ? 0 : 1;
}
class ViewHolder extends RecyclerView.ViewHolder {
WebView webView;
BorderNestedScrollView scrollView;
BorderRecyclerView recyclerView;
@Override
public long getItemId(int position) {
return position;
}
}
public ViewHolder(@NonNull View itemView) {
super(itemView);
public static abstract class BorderFragment extends BaseFragment {
BorderView borderView;
void attachListeners() {
var parent = getParentFragment();
if (parent instanceof RepoItemFragment) {
var repoItemFragment = (RepoItemFragment) parent;
borderView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> repoItemFragment.binding.appBar.setLifted(!top));
repoItemFragment.binding.appBar.setLifted(!borderView.getBorderViewDelegate().isShowingTopBorder());
repoItemFragment.binding.toolbar.setOnClickListener(v -> {
repoItemFragment.binding.appBar.setExpanded(true, true);
scrollToTop();
});
}
}
class ReadmeViewHolder extends PagerAdapter.ViewHolder {
public ReadmeViewHolder(ItemRepoReadmeBinding binding) {
super(binding.getRoot());
webView = binding.readme;
scrollView = binding.scrollView;
}
abstract void scrollToTop();
void detachListeners() {
borderView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
}
class RecyclerviewBinding extends PagerAdapter.ViewHolder {
public RecyclerviewBinding(ItemRepoRecyclerviewBinding binding) {
super(binding.getRoot());
recyclerView = binding.recyclerView;
@Override
public void onResume() {
super.onResume();
attachListeners();
}
@Override
public void onStart() {
super.onStart();
attachListeners();
}
@Override
public void onStop() {
super.onStop();
detachListeners();
}
@Override
public void onPause() {
super.onPause();
detachListeners();
}
}
public static class ReadmeFragment extends BorderFragment {
ItemRepoReadmeBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
var parent = getParentFragment();
if (!(parent instanceof RepoItemFragment)) {
getNavController().navigate(R.id.action_repo_item_fragment_to_repo_fragment);
return null;
}
var repoItemFragment = (RepoItemFragment) parent;
binding = ItemRepoReadmeBinding.inflate(getLayoutInflater(), container, false);
repoItemFragment.renderGithubMarkdown(binding.readme, repoItemFragment.module.getReadmeHTML());
borderView = binding.scrollView;
return binding.getRoot();
}
@Override
void scrollToTop() {
binding.scrollView.fullScroll(ScrollView.FOCUS_UP);
}
}
public static class RecyclerviewFragment extends BorderFragment {
ItemRepoRecyclerviewBinding binding;
RecyclerView.Adapter<?> adapter;
@Override
void scrollToTop() {
binding.recyclerView.smoothScrollToPosition(0);
}
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
var arguments = getArguments();
var parent = getParentFragment();
if (arguments == null || !(parent instanceof RepoItemFragment)) {
getNavController().navigate(R.id.action_repo_item_fragment_to_repo_fragment);
return null;
}
var repoItemFragment = (RepoItemFragment) parent;
var position = arguments.getInt("position", 0);
if (position == 1)
adapter = repoItemFragment.releaseAdapter;
else if (position == 2)
adapter = repoItemFragment.informationAdapter;
else return null;
binding = ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
borderView = binding.recyclerView;
return binding.getRoot();
}
}
}

View File

@ -29,7 +29,6 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@ -42,7 +41,6 @@ import androidx.preference.SwitchPreference;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App;
import org.lsposed.manager.BuildConfig;
@ -72,22 +70,19 @@ public class SettingsFragment extends BaseFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentSettingsBinding.inflate(inflater, container, false);
setupToolbar(binding.toolbar, R.string.Settings);
binding.appBar.setLiftable(true);
setupToolbar(binding.toolbar, binding.clickView, R.string.Settings);
if (savedInstanceState == null) {
getChildFragmentManager().beginTransaction()
.add(R.id.container, new PreferenceFragment()).commitNow();
}
/*
CollapsingToolbarLayout consumes window insets, causing child views not
receiving window insets.
See https://github.com/material-components/material-components-android/issues/1310
Insets can be handled by RikkaX Insets, so we can manually set
OnApplyWindowInsetsListener to null.
*/
binding.collapsingToolbarLayout.setOnApplyWindowInsetsListener(null);
if (ConfigManager.isBinderAlive()) {
binding.toolbar.setSubtitle(String.format(Locale.ROOT, "%s (%d) - %s",
ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi()));
} else {
binding.toolbar.setSubtitle(String.format(Locale.ROOT, "%s (%d) - %s",
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed)));
}
return binding.getRoot();
}
@ -109,11 +104,7 @@ public class SettingsFragment extends BaseFragment {
BackupUtils.backup(uri);
} catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
if (parentFragment != null && parentFragment.binding != null && isResumed()) {
Snackbar.make(parentFragment.binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
parentFragment.showHint(text, false);
}
});
});
@ -125,11 +116,7 @@ public class SettingsFragment extends BaseFragment {
BackupUtils.restore(uri);
} catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
if (parentFragment != null && parentFragment.binding != null && isResumed()) {
Snackbar.make(parentFragment.binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
parentFragment.showHint(text, false);
}
});
});
@ -311,6 +298,17 @@ public class SettingsFragment extends BaseFragment {
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
BorderRecyclerView recyclerView = (BorderRecyclerView) super.onCreateRecyclerView(inflater, parent, savedInstanceState);
RecyclerViewKt.fixEdgeEffect(recyclerView, false, true);
recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> parentFragment.binding.appBar.setLifted(!top));
var fragment = getParentFragment();
if (fragment instanceof SettingsFragment) {
var settingsFragment = (SettingsFragment) fragment;
View.OnClickListener l = v -> {
settingsFragment.binding.appBar.setExpanded(true, true);
recyclerView.smoothScrollToPosition(0);
};
settingsFragment.binding.toolbar.setOnClickListener(l);
settingsFragment.binding.clickView.setOnClickListener(l);
}
return recyclerView;
}

View File

@ -40,22 +40,27 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class ModuleUtil {
// xposedminversion below this
public static int MIN_MODULE_VERSION = 2; // reject modules with
private static ModuleUtil instance = null;
private final PackageManager pm;
private final List<ModuleListener> listeners = new CopyOnWriteArrayList<>();
private final Set<ModuleListener> listeners = ConcurrentHashMap.newKeySet();
private HashSet<String> enabledModules = new HashSet<>();
private Map<Pair<String, Integer>, InstalledModule> installedModules = new HashMap<>();
private boolean isReloading = false;
private boolean modulesLoaded = false;
private ModuleUtil() {
pm = App.getInstance().getPackageManager();
}
public boolean isModulesLoaded() {
return modulesLoaded;
}
public static synchronized ModuleUtil getInstance() {
if (instance == null) {
instance = new ModuleUtil();
@ -76,16 +81,10 @@ public final class ModuleUtil {
return result;
}
public void reloadInstalledModules() {
synchronized (this) {
if (isReloading)
return;
isReloading = true;
}
synchronized public void reloadInstalledModules() {
modulesLoaded = false;
if (!ConfigManager.isBinderAlive()) {
synchronized (this) {
isReloading = false;
}
modulesLoaded = true;
return;
}
@ -102,11 +101,9 @@ public final class ModuleUtil {
installedModules = modules;
enabledModules = new HashSet<>(Arrays.asList(ConfigManager.getEnabledModules()));
synchronized (this) {
isReloading = false;
}
modulesLoaded = true;
for (var listener: listeners) {
for (var listener : listeners) {
listener.onModulesReloaded();
}
}
@ -126,7 +123,7 @@ public final class ModuleUtil {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) {
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded(old);
listener.onSingleModuleReloaded(old);
}
}
return null;
@ -137,31 +134,33 @@ public final class ModuleUtil {
InstalledModule module = new InstalledModule(pkg);
installedModules.put(Pair.create(packageName, userId), module);
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded(module);
listener.onSingleModuleReloaded(module);
}
return module;
} else {
InstalledModule old = installedModules.remove(Pair.create(packageName, userId));
if (old != null) {
for (ModuleListener listener : listeners) {
listener.onSingleInstalledModuleReloaded(old);
listener.onSingleModuleReloaded(old);
}
}
return null;
}
}
@Nullable
public InstalledModule getModule(String packageName, int userId) {
return installedModules.get(Pair.create(packageName, userId));
return modulesLoaded ? installedModules.get(Pair.create(packageName, userId)) : null;
}
@Nullable
public InstalledModule getModule(String packageName) {
return getModule(packageName, 0);
}
@Nullable
synchronized public Map<Pair<String, Integer>, InstalledModule> getModules() {
return isReloading ? null : installedModules;
return modulesLoaded ? installedModules : null;
}
public boolean setModuleEnabled(String packageName, boolean enabled) {
@ -181,12 +180,11 @@ public final class ModuleUtil {
}
public int getEnabledModulesCount() {
return isReloading ? -1 : enabledModules.size();
return modulesLoaded ? enabledModules.size() : -1;
}
public void addListener(ModuleListener listener) {
if (!listeners.contains(listener))
listeners.add(listener);
listeners.add(listener);
}
public void removeListener(ModuleListener listener) {
@ -198,7 +196,7 @@ public final class ModuleUtil {
* Called whenever one (previously or now) installed module has been
* reloaded
*/
default void onSingleInstalledModuleReloaded(InstalledModule module) {
default void onSingleModuleReloaded(InstalledModule module) {
}
@ -290,7 +288,7 @@ public final class ModuleUtil {
e.printStackTrace();
}
RepoLoader repoLoader = RepoLoader.getInstance();
if (scopeList == null && repoLoader.isRepoLoaded()) {
if (scopeList == null) {
OnlineModule module = repoLoader.getOnlineModule(packageName);
if (module != null && module.getScope() != null) {
scopeList = module.getScope();

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View File

@ -19,13 +19,13 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="?attr/colorSurface"/>
<solid android:color="?attr/colorSurface" />
<corners android:radius="?popupBackgroundRadius" />
</shape>
</item>
<item>
<shape>
<solid android:color="@color/m3_popupmenu_overlay_color"/>
<solid android:color="@color/m3_popupmenu_overlay_color" />
<corners android:radius="?popupBackgroundRadius" />
</shape>
</item>

View File

@ -115,6 +115,7 @@
style="@style/DeviceInfoDialogValue"
android:id="@+id/system_abi"
android:layout_width="match_parent"
android:paddingBottom="0dp"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -31,6 +31,7 @@
style="@style/DeviceInfoDialogValue"
android:id="@+id/value"
android:gravity="center_vertical"
android:paddingBottom="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -14,20 +14,12 @@
~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_refresh"
android:title="@string/refresh" />
<item
android:id="@+id/menu_info"
android:title="@string/info" />
<item
android:id="@+id/menu_about"
android:title="@string/About" />
</menu>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/title"
style="@style/MaterialAlertDialog.Material3.Title.Text.CenterStacked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="?attr/dialogPreferredPadding"
android:paddingBottom="?attr/dialogPreferredPadding" />

View File

@ -18,8 +18,8 @@
-->
<rikka.widget.borderview.BorderNestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/container"

View File

@ -21,7 +21,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end"
@ -36,22 +35,26 @@
android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
<com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale">
<View
android:id="@+id/click_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
android:theme="@style/ThemeOverlay.MaterialComponents.ActionBar"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
<rikka.widget.switchbar.SwitchBar
android:id="@+id/master_switch"
@ -60,12 +63,27 @@
android:layout_gravity="bottom"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:layout_scrollEffect="compress"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:switchOffText="@string/enable_module"
app:switchOnText="@string/enable_module" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="?attr/dialogPreferredPadding"
android:contentDescription="@string/module_settings"
android:src="@drawable/ic_round_settings_24"
android:tooltipText="@string/module_settings"
android:visibility="gone"
app:backgroundTint="?attr/colorPrimary"
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"
app:layout_fitsSystemWindowsInsets="bottom"
app:tint="?attr/colorSurface" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -73,20 +91,13 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:ignore="MissingPrefix">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:hideAnimationBehavior="outward" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/recyclerView"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"

View File

@ -21,7 +21,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end"
@ -116,7 +115,8 @@
android:id="@+id/modules"
style="@style/HomeCard.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:tooltipText="@string/Modules">
<RelativeLayout
android:layout_width="match_parent"
@ -156,7 +156,8 @@
android:id="@+id/download"
style="@style/HomeCard.Secondary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:tooltipText="@string/module_repo">
<RelativeLayout
android:layout_width="match_parent"
@ -196,7 +197,8 @@
android:id="@+id/logs"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:tooltipText="@string/Logs">
<LinearLayout
android:layout_width="match_parent"
@ -225,7 +227,8 @@
android:id="@+id/settings"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:tooltipText="@string/Settings">
<LinearLayout
android:layout_width="match_parent"
@ -253,7 +256,8 @@
android:id="@+id/issue"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:tooltipText="@string/feedback_or_suggestion">
<LinearLayout
android:layout_width="match_parent"
@ -265,14 +269,44 @@
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/report_issue"
app:srcCompat="@drawable/ic_round_bug_report_24" />
android:contentDescription="@string/feedback_or_suggestion"
app:srcCompat="@drawable/ic_baseline_chat_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/report_issue"
android:text="@string/feedback_or_suggestion"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/about"
style="@style/HomeCard.Tertiary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:tooltipText="@string/About">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/About"
app:srcCompat="@drawable/ic_baseline_info_24" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="@string/About"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" />
</LinearLayout>

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of LSPosed.
~
~ LSPosed is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ LSPosed is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end"
app:edgeToEdge="true"
app:fitsSystemWindowsInsets="start|end">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/sliding_tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_layout_height"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:tabIndicatorAnimationMode="elastic">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_item_logs_module" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_item_logs_lsp" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fillViewport="true"
android:scrollbars="none">
<TextView
android:id="@+id/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:fontFamily="monospace"
android:textColor="?attr/colorOnSurface"
android:textIsSelectable="true"
android:textSize="12sp"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" />
</ScrollView>
</HorizontalScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -19,7 +19,6 @@
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
@ -35,22 +34,28 @@
android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
<com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
android:id="@+id/toolbar_layout"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale">
<View
android:id="@+id/click_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
android:theme="@style/ThemeOverlay.MaterialComponents.ActionBar"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
@ -58,7 +63,6 @@
android:layout_height="@dimen/tab_layout_height"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:tabGravity="center"
app:tabIndicatorAnimationMode="elastic"
app:tabMode="scrollable" />
@ -69,14 +73,6 @@
android:layout_height="match_parent"
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"
@ -89,12 +85,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:layout_margin="?attr/dialogPreferredPadding"
android:contentDescription="@string/add_module_to_user"
android:src="@drawable/ic_baseline_add_24"
android:tooltipText="@string/add_module_to_user"
android:visibility="gone"
app:backgroundTint="?attr/colorPrimary"
app:tint="@color/primary_text_material_inverse"
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior"
app:layout_fitsSystemWindowsInsets="bottom" />
app:layout_fitsSystemWindowsInsets="bottom"
app:tint="?attr/colorSurface" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -20,7 +20,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end"
@ -34,22 +33,28 @@
android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
<com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
android:id="@+id/toolbar_layout"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:forceApplySystemWindowInsetTop="true"
app:titleCollapseMode="scale"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale">
<View
android:id="@+id/click_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
android:theme="@style/ThemeOverlay.MaterialComponents.ActionBar"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
@ -58,25 +63,23 @@
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward" />
android:layout_height="match_parent">
<org.lsposed.manager.ui.widget.StatefulRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="true"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" />
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="true"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -20,7 +20,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end"
@ -34,28 +33,32 @@
android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle"
<com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale">
<View
android:id="@+id/click_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of LSPosed.
~
~ LSPosed is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ LSPosed is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2021 LSPosed Contributors
-->
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/log_item"
style="@style/TextAppearance.AppCompat.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:fontFamily="monospace"
android:textColor="?attr/colorOnSurface"
android:textSize="12sp" />

View File

@ -39,8 +39,8 @@
<ImageView
android:id="@+id/app_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -52,7 +52,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:singleLine="false"
android:ellipsize="marquee"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textIsSelectable="false"
android:textSize="16sp"
@ -67,6 +68,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="5"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@id/hint"

View File

@ -42,7 +42,9 @@
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:maxLines="1"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -19,14 +19,14 @@
-->
<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:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="true"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
app:borderTopVisibility="whenTop"
app:borderTopDrawable="@null"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" />

View File

@ -17,29 +17,22 @@
~ Copyright (C) 2021 LSPosed Contributors
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingTop="?attr/dialogPreferredPadding"
android:id="@+id/swipe_refresh_layout"
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:id="@+id/recycler_view"
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>
android:scrollbars="vertical"
app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -18,19 +18,15 @@
~ Copyright (C) 2021 LSPosed Contributors
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/menu_search"
android:actionViewClass="androidx.appcompat.widget.SearchView"
android:showAsAction="ifRoom"
android:title="" />
<item
android:id="@+id/menu_launch"
android:icon="@drawable/ic_settings"
android:showAsAction="ifRoom"
android:title="@string/module_settings" />
android:icon="@drawable/ic_baseline_search_24"
android:showAsAction="always|collapseActionView"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/use_recommended"

View File

@ -25,12 +25,6 @@
android:showAsAction="ifRoom"
android:title="@string/menuSaveToSd" />
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_refresh"
android:showAsAction="ifRoom"
android:title="@string/menuReload" />
<item
android:id="@+id/menu_scroll_top"
android:showAsAction="never"
@ -46,4 +40,9 @@
android:showAsAction="never"
android:title="@string/menuClearLog" />
<item
android:id="@+id/menu_word_wrap"
android:checkable="true"
android:checked="false"
android:title="@string/menu_enable_word_wrap" />
</menu>

View File

@ -18,18 +18,12 @@
~ Copyright (C) 2021 LSPosed Contributors
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_refresh"
android:title="@string/refresh"
android:icon="@drawable/ic_refresh"
app:showAsAction="ifRoom" />
android:actionViewClass="androidx.appcompat.widget.SearchView"
android:icon="@drawable/ic_baseline_search_24"
android:showAsAction="ifRoom|collapseActionView" />
</menu>

View File

@ -19,23 +19,19 @@
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/menu_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_refresh"
android:title="@string/refresh"
android:icon="@drawable/ic_refresh"
app:showAsAction="ifRoom" />
android:actionViewClass="androidx.appcompat.widget.SearchView"
android:icon="@drawable/ic_baseline_search_24"
android:showAsAction="always|collapseActionView"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/item_list_sort"
android:title="@string/menu_sort"
app:showAsAction="never">
android:showAsAction="never"
android:title="@string/menu_sort">
<menu>
<group android:checkableBehavior="single">
<item
@ -49,4 +45,4 @@
</group>
</menu>
</item>
</menu>
</menu>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of LSPosed.
~
~ LSPosed is free software: you can redistribute it and/or modify

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of LSPosed.
~
~ LSPosed is free software: you can redistribute it and/or modify

View File

@ -21,4 +21,65 @@
<resources>
<attr name="colorNormal" format="color" />
<attr name="colorInstall" format="color" />
<declare-styleable name="SubtitleCollapsingToolbarLayout">
<!-- Specifies extra space on the start, top, end and bottom sides of the the expanded title text.
Margin values should be positive,
subtitle will also be affected. -->
<attr name="expandedTitleMargin" />
<!-- Specifies extra space on the start side of the the expanded title text. Margin values should be positive,
subtitle will also be affected. -->
<attr name="expandedTitleMarginStart" />
<!-- Specifies extra space on the top side of the the expanded title text. Margin values should be positive,
subtitle will also be affected. -->
<attr name="expandedTitleMarginTop" />
<!-- Specifies extra space on the end side of the the expanded title text. Margin values should be positive,
subtitle will also be affected. -->
<attr name="expandedTitleMarginEnd" />
<!-- Specifies extra space on the bottom side of the the expanded title text. Margin values should be positive,
subtitle will also be affected. -->
<attr name="expandedTitleMarginBottom" />
<!-- The text appearance of the CollapsingToolbarLayout's title when it is fully 'expanded' -->
<attr name="expandedTitleTextAppearance" />
<!-- The text appearance of the CollapsingToolbarLayout's subtitle when it is fully 'expanded' -->
<attr name="expandedSubtitleTextAppearance" format="reference" />
<!-- The text appearance of the CollapsingToolbarLayouts title when it is fully 'collapsed' -->
<attr name="collapsedTitleTextAppearance" />
<!-- The text appearance of the CollapsingToolbarLayouts subtitle when it is fully 'collapsed' -->
<attr name="collapsedSubtitleTextAppearance" format="reference" />
<!-- The drawable to use as a scrim on top of the CollapsingToolbarLayouts
content when it has been scrolled sufficiently off screen. -->
<attr name="contentScrim" />
<!-- The drawable to use as a scrim for the status bar content when the
CollapsingToolbarLayout has been scrolled sufficiently off screen.
Only works on Lollipop with the correct setup. -->
<attr name="statusBarScrim" />
<!-- The id of the primary Toolbar child that you wish to use for the purpose of collapsing.
This Toolbar descendant view does not need to be a direct child of the layout.
If you do not set this, the first direct Toolbar child found will be used. -->
<attr name="toolbarId" />
<!-- Specifies the amount of visible height in pixels used to define when to trigger a
scrim visibility change. -->
<attr name="scrimVisibleHeightTrigger" />
<!-- Specifies the duration used for scrim visibility animations. -->
<attr name="scrimAnimationDuration" />
<!-- Specifies how the title should be positioned when collapsed,
subtitle will also be affected. -->
<attr name="collapsedTitleGravity" />
<!-- Specifies how the title should be positioned when expanded,
subtitle will also be affected. -->
<attr name="expandedTitleGravity" />
<!-- Whether the CollapsingToolbarLayout should draw its own shrinking/growing title. -->
<attr name="titleEnabled" />
<!-- The title to show when titleEnabled is set to true. -->
<attr name="title" />
<!-- The subtitle to show when titleEnabled is set to true. -->
<attr name="subtitle" />
</declare-styleable>
</resources>

View File

@ -14,7 +14,6 @@
~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors
-->
@ -84,6 +83,8 @@
<string name="menuReload">Reload</string>
<string name="logs_clear_failed_2">Failed to clear the log</string>
<string name="menu_enable_word_wrap">Word Wrap</string>
<string name="enabled_verbose_log">Verbose log enabled</string>
<string name="disabled_verbose_log">Verbose log disabled</string>
<!-- Notification -->
<string name="module_is_not_activated_yet">Xposed module is not activated yet</string>
@ -95,7 +96,6 @@
<!-- ModulesActivity -->
<string name="module_empty_description">(no description provided)</string>
<string name="module_no_ui">This module does not provide a user interface</string>
<string name="warning_xposed_min_version">This module requires a newer Xposed version (%d) and thus cannot be activated</string>
<string name="no_min_version_specified">This module does not specify the Xposed version it needs.</string>
<string name="warning_min_version_too_low">This module was created for Xposed version %1$d, but due to incompatible changes in version %2$d, it has been disabled</string>
@ -234,4 +234,5 @@
<string name="color_brown">Brown</string>
<string name="color_grey">Grey</string>
<string name="color_blue_grey">Blue grey</string>
<string name="feedback_or_suggestion">Feedback or suggestion</string>
</resources>

View File

@ -21,4 +21,14 @@
<resources>
<style name="AppTheme" parent="Theme.Light" />
<!-- SubtitleCollapsingToolbarLayout styles -->
<style name="Widget.Design.SubtitleCollapsingToolbar" parent="Widget.Design.CollapsingToolbar" />
<style name="TextAppearance.Design.SubtitleCollapsingToolbar.ExpandedTitle" parent="TextAppearance.Design.CollapsingToolbar.Expanded" />
<style name="TextAppearance.Design.SubtitleCollapsingToolbar.ExpandedSubtitle" parent="TextAppearance.AppCompat.Title">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
</resources>