[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.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01") 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.github.bumptech.glide:glide:$glideVersion")
implementation("com.google.android.material:material:1.5.0-beta01") implementation("com.google.android.material:material:1.5.0-beta01")
implementation("com.google.code.gson:gson:2.8.9") 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)); }, new IntentFilter(Intent.ACTION_PACKAGE_CHANGED));
UpdateUtil.loadRemoteVersion(); UpdateUtil.loadRemoteVersion();
RepoLoader.getInstance().loadRemoteData();
executorService.submit(HTML_TEMPLATE); executorService.submit(HTML_TEMPLATE);
executorService.submit(HTML_TEMPLATE_DARK); executorService.submit(HTML_TEMPLATE_DARK);

View File

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

View File

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

View File

@ -23,6 +23,7 @@ package org.lsposed.manager.repo;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -37,10 +38,9 @@ import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@ -51,24 +51,25 @@ import okhttp3.ResponseBody;
public class RepoLoader { public class RepoLoader {
private static RepoLoader instance = null; private static RepoLoader instance = null;
private Map<String, OnlineModule> onlineModules = new HashMap<>(); private Map<String, OnlineModule> onlineModules = new HashMap<>();
private Map<String, ModuleVersion> latestVersion = new ConcurrentHashMap<>();
public static class ModuleVersion { public static class ModuleVersion {
public String versionName; public String versionName;
public long versionCode; public long versionCode;
private ModuleVersion(long versionCode, String versionName) { private ModuleVersion(long versionCode, String versionName) {
this.versionName = versionName; this.versionName = versionName;
this.versionCode = versionCode; this.versionCode = versionCode;
} }
public boolean upgradable(long versionCode, String versionName) { public boolean upgradable(long versionCode, String versionName) {
return this.versionCode > versionCode || (this.versionCode == versionCode && !versionName.equals(this.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 Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json");
private final List<Listener> listeners = new CopyOnWriteArrayList<>(); private final Set<RepoListener> listeners = ConcurrentHashMap.newKeySet();
private boolean isLoading = false;
private boolean repoLoaded = false; private boolean repoLoaded = false;
private static final String originRepoUrl = "https://modules.lsposed.org/"; 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/"; private static final String backupRepoUrl = "https://cdn.jsdelivr.net/gh/Xposed-Modules-Repo/modules@gh-pages/";
@ -81,38 +82,18 @@ public class RepoLoader {
public static synchronized RepoLoader getInstance() { public static synchronized RepoLoader getInstance() {
if (instance == null) { if (instance == null) {
instance = new RepoLoader(); instance = new RepoLoader();
instance.loadRemoteData(); App.getExecutorService().submit(instance::loadRemoteData);
} }
return instance; return instance;
} }
public void loadRemoteData() { synchronized public void loadRemoteData() {
synchronized (this) { repoLoaded = true;
if (isLoading) { try {
return; var response = App.getOkHttpClient().newCall(new Request.Builder()
}
isLoading = true;
}
App.getOkHttpClient().newCall(new Request.Builder()
.url(repoUrl + "modules.json") .url(repoUrl + "modules.json")
.build()).enqueue(new Callback() { .build()).execute();
@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();
}
}
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
ResponseBody body = response.body(); ResponseBody body = response.body();
if (body != null) { if (body != null) {
@ -123,7 +104,7 @@ public class RepoLoader {
OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class); OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class);
Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule)); Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule));
latestVersion.clear(); Map<String, ModuleVersion> versions = new ConcurrentHashMap<>();
for (var module : repoModules) { for (var module : repoModules) {
var release = module.getLatestRelease(); var release = module.getLatestRelease();
if (release == null || release.isEmpty()) continue; if (release == null || release.isEmpty()) continue;
@ -138,34 +119,41 @@ public class RepoLoader {
continue; continue;
} }
String pkgName = module.getName(); String pkgName = module.getName();
latestVersion.put(pkgName, new ModuleVersion(verCode, verName)); versions.put(pkgName, new ModuleVersion(verCode, verName));
} }
latestVersion = versions;
onlineModules = modules; onlineModules = modules;
Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8));
synchronized (this) {
repoLoaded = true; repoLoaded = true;
} for (RepoListener listener : listeners) {
for (Listener listener : listeners) { listener.onRepoLoaded();
listener.repoLoaded();
} }
} catch (Throwable t) { } catch (Throwable t) {
Log.e(App.TAG, Log.getStackTraceString(t)); Log.e(App.TAG, Log.getStackTraceString(t));
for (Listener listener : listeners) { for (RepoListener listener : listeners) {
listener.onThrowable(t); listener.onThrowable(t);
} }
} }
} }
} }
synchronized (this) { } catch (Throwable e) {
isLoading = false; 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) { public ModuleVersion getModuleLatestVersion(String packageName) {
return latestVersion.get(packageName); return repoLoaded ? latestVersion.getOrDefault(packageName, null) : null;
} }
public void loadRemoteReleases(String packageName) { public void loadRemoteReleases(String packageName) {
@ -179,7 +167,7 @@ public class RepoLoader {
repoUrl = backupRepoUrl; repoUrl = backupRepoUrl;
loadRemoteReleases(packageName); loadRemoteReleases(packageName);
} else { } else {
for (Listener listener : listeners) { for (RepoListener listener : listeners) {
listener.onThrowable(e); listener.onThrowable(e);
} }
} }
@ -196,12 +184,12 @@ public class RepoLoader {
OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); OnlineModule module = gson.fromJson(bodyString, OnlineModule.class);
module.releasesLoaded = true; module.releasesLoaded = true;
onlineModules.replace(packageName, module); onlineModules.replace(packageName, module);
for (Listener listener : listeners) { for (RepoListener listener : listeners) {
listener.moduleReleasesLoaded(module); listener.onModuleReleasesLoaded(module);
} }
} catch (Throwable t) { } catch (Throwable t) {
Log.e(App.TAG, Log.getStackTraceString(t)); Log.e(App.TAG, Log.getStackTraceString(t));
for (Listener listener : listeners) { for (RepoListener listener : listeners) {
listener.onThrowable(t); listener.onThrowable(t);
} }
} }
@ -211,28 +199,30 @@ public class RepoLoader {
}); });
} }
public void addListener(Listener listener) { public void addListener(RepoListener listener) {
if (!listeners.contains(listener)) if (!listeners.contains(listener))
listeners.add(listener); listeners.add(listener);
} }
public void removeListener(Listener listener) { public void removeListener(RepoListener listener) {
listeners.remove(listener); listeners.remove(listener);
} }
@Nullable
public OnlineModule getOnlineModule(String packageName) { 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() { public Collection<OnlineModule> getOnlineModules() {
return onlineModules.values(); return repoLoaded ? onlineModules.values() : null;
} }
public interface Listener { public interface RepoListener {
default void repoLoaded() { default void onRepoLoaded() {
} }
default void moduleReleasesLoaded(OnlineModule module) { default void onModuleReleasesLoaded(OnlineModule module) {
} }
default void onThrowable(Throwable t) { 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.App;
import org.lsposed.manager.ConfigManager; import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.databinding.DialogTitleBinding;
import org.lsposed.manager.databinding.DialogWarningBinding; import org.lsposed.manager.databinding.DialogWarningBinding;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -39,18 +40,23 @@ public class FlashDialogBuilder extends BlurBehindDialogBuilder {
var pref = App.getPreferences(); var pref = App.getPreferences();
var notes = pref.getString("release_notes", ""); var notes = pref.getString("release_notes", "");
this.zipPath = pref.getString("zip_file", null); 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); textView = new MaterialTextView(context);
var text = notes + "\n\n\n" + context.getString(R.string.update_lsposed_msg) + "\n\n"; var text = notes + "\n\n\n" + context.getString(R.string.update_lsposed_msg) + "\n\n";
textView.setText(text); textView.setText(text);
textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setTextIsSelectable(true);
LayoutInflater inflater = LayoutInflater.from(context);
DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false); DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false);
binding.container.addView(textView); binding.container.addView(textView);
rootView = binding.getRoot(); rootView = binding.getRoot();
setView(rootView); setView(rootView);
title.setOnClickListener(v -> rootView.smoothScrollTo(0, 0));
setNegativeButton(android.R.string.cancel, cancel); setNegativeButton(android.R.string.cancel, cancel);
setPositiveButton(R.string.install, null); setPositiveButton(R.string.install, null);

View File

@ -19,11 +19,14 @@
package org.lsposed.manager.ui.dialog; package org.lsposed.manager.ui.dialog;
import android.content.Context; import android.app.Dialog;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import org.lsposed.manager.BuildConfig; import org.lsposed.manager.BuildConfig;
import org.lsposed.manager.ConfigManager; import org.lsposed.manager.ConfigManager;
@ -34,12 +37,14 @@ import java.util.Locale;
import rikka.core.util.ClipboardUtils; import rikka.core.util.ClipboardUtils;
public class InfoDialogBuilder extends BlurBehindDialogBuilder { public class InfoDialogBuilder extends DialogFragment {
public InfoDialogBuilder(@NonNull Context context) { @NonNull
super(context); @Override
setTitle(R.string.info); public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
DialogInfoBinding binding = DialogInfoBinding.inflate(LayoutInflater.from(context), null, false); var activity = requireActivity();
var builder = new BlurBehindDialogBuilder(activity).setTitle(R.string.info);
DialogInfoBinding binding = DialogInfoBinding.inflate(LayoutInflater.from(activity), null, false);
if (ConfigManager.isBinderAlive()) { if (ConfigManager.isBinderAlive()) {
binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion())); binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion()));
@ -61,37 +66,38 @@ public class InfoDialogBuilder extends BlurBehindDialogBuilder {
binding.device.setText(getDevice()); binding.device.setText(getDevice());
binding.systemAbi.setText(Build.SUPPORTED_ABIS[0]); binding.systemAbi.setText(Build.SUPPORTED_ABIS[0]);
setView(binding.getRoot()); builder.setView(binding.getRoot());
setPositiveButton(android.R.string.ok, null); builder.setPositiveButton(android.R.string.ok, null);
String info = context.getString(R.string.info_api_version) + String info = activity.getString(R.string.info_api_version) +
"\n" + "\n" +
binding.apiVersion.getText() + binding.apiVersion.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_api) + activity.getString(R.string.info_api) +
"\n" + "\n" +
binding.api.getText() + binding.api.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_framework_version) + activity.getString(R.string.info_framework_version) +
"\n" + "\n" +
binding.frameworkVersion.getText() + binding.frameworkVersion.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_manager_version) + activity.getString(R.string.info_manager_version) +
"\n" + "\n" +
binding.managerVersion.getText() + binding.managerVersion.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_system_version) + activity.getString(R.string.info_system_version) +
"\n" + "\n" +
binding.systemVersion.getText() + binding.systemVersion.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_device) + activity.getString(R.string.info_device) +
"\n" + "\n" +
binding.device.getText() + binding.device.getText() +
"\n\n" + "\n\n" +
context.getString(R.string.info_system_abi) + activity.getString(R.string.info_system_abi) +
"\n" + "\n" +
binding.systemAbi.getText(); 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() { 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; package org.lsposed.manager.ui.dialog;
import android.app.Activity; import android.app.Dialog;
import android.content.Context; import android.os.Bundle;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import androidx.fragment.app.DialogFragment;
import org.lsposed.manager.ConfigManager; import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R; 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.databinding.DialogWarningBinding;
import org.lsposed.manager.util.chrome.LinkTransformationMethod; import org.lsposed.manager.util.chrome.LinkTransformationMethod;
public class WarningDialogBuilder extends BlurBehindDialogBuilder { public class WarningDialogBuilder extends DialogFragment {
@NonNull
public WarningDialogBuilder(@NonNull Context context) { @Override
super(context); public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Activity activity = (Activity) context; var activity = requireActivity();
var builder = new BlurBehindDialogBuilder(activity).
setTitle(R.string.partial_activated); setTitle(R.string.partial_activated);
LayoutInflater inflater = LayoutInflater.from(context); LayoutInflater inflater = LayoutInflater.from(activity);
DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false); DialogWarningBinding binding = DialogWarningBinding.inflate(inflater, null, false);
if (!ConfigManager.isSepolicyLoaded()) { if (!ConfigManager.isSepolicyLoaded()) {
@ -65,7 +68,9 @@ public class WarningDialogBuilder extends BlurBehindDialogBuilder {
item.value.setTransformationMethod(new LinkTransformationMethod(activity)); item.value.setTransformationMethod(new LinkTransformationMethod(activity));
} }
setView(binding.getRoot()); builder.setView(binding.getRoot());
setPositiveButton(android.R.string.ok, null); 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; package org.lsposed.manager.ui.fragment;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
@ -36,10 +36,10 @@ import androidx.appcompat.widget.SearchView;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App; import org.lsposed.manager.App;
import org.lsposed.manager.ConfigManager;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.adapters.AppHelper;
import org.lsposed.manager.adapters.ScopeAdapter; import org.lsposed.manager.adapters.ScopeAdapter;
import org.lsposed.manager.databinding.FragmentAppListBinding; import org.lsposed.manager.databinding.FragmentAppListBinding;
import org.lsposed.manager.util.BackupUtils; import org.lsposed.manager.util.BackupUtils;
@ -60,6 +60,15 @@ public class AppListFragment extends BaseFragment {
public ActivityResultLauncher<String> backupLauncher; public ActivityResultLauncher<String> backupLauncher;
public ActivityResultLauncher<String[]> restoreLauncher; 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 @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -68,7 +77,6 @@ public class AppListFragment extends BaseFragment {
return binding.getRoot(); return binding.getRoot();
} }
binding.appBar.setLiftable(true); binding.appBar.setLiftable(true);
binding.appBar.setLifted(true);
String title; String title;
if (module.userId != 0) { if (module.userId != 0) {
title = String.format(Locale.ROOT, "%s (%d)", module.getAppName(), module.userId); 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 = new ScopeAdapter(this, module);
scopeAdapter.setHasStableIds(true); scopeAdapter.setHasStableIds(true);
scopeAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { scopeAdapter.registerAdapterDataObserver(observer);
@Override
public void onChanged() {
if (binding != null && scopeAdapter != null) {
binding.progress.setVisibility(scopeAdapter.isLoaded() ? View.GONE : View.VISIBLE);
binding.swipeRefreshLayout.setRefreshing(!scopeAdapter.isLoaded());
}
}
});
binding.recyclerView.setAdapter(scopeAdapter); binding.recyclerView.setAdapter(scopeAdapter);
binding.recyclerView.setHasFixedSize(true); binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top));
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); 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(); 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(); return binding.getRoot();
} }
@ -116,6 +133,8 @@ public class AppListFragment extends BaseFragment {
int moduleUserId = args.getModuleUserId(); int moduleUserId = args.getModuleUserId();
module = ModuleUtil.getInstance().getModule(modulePackageName, moduleUserId); 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(), backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument(),
uri -> { uri -> {
@ -125,11 +144,7 @@ public class AppListFragment extends BaseFragment {
BackupUtils.backup(uri, modulePackageName); BackupUtils.backup(uri, modulePackageName);
} catch (Exception e) { } catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage()); var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
if (binding != null && isResumed()) { showHint(text, false);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
} }
}); });
}); });
@ -141,11 +156,7 @@ public class AppListFragment extends BaseFragment {
BackupUtils.restore(uri, modulePackageName); BackupUtils.restore(uri, modulePackageName);
} catch (Exception e) { } catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage()); var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
if (binding != null && isResumed()) { showHint(text, false);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
} }
}); });
}); });
@ -164,17 +175,10 @@ public class AppListFragment extends BaseFragment {
if (scopeAdapter != null) scopeAdapter.refresh(); if (scopeAdapter != null) scopeAdapter.refresh();
} }
@Override
public void onDestroy() {
if (scopeAdapter != null) scopeAdapter.onDestroy();
super.onDestroy();
}
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
scopeAdapter.unregisterAdapterDataObserver(observer);
binding = null; binding = null;
} }
@ -191,6 +195,18 @@ public class AppListFragment extends BaseFragment {
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(searchListener); 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); scopeAdapter.onPrepareOptionsMenu(menu);
} }

View File

@ -21,12 +21,16 @@ package org.lsposed.manager.ui.fragment;
import android.app.Activity; import android.app.Activity;
import android.view.View; import android.view.View;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App; import org.lsposed.manager.App;
import org.lsposed.manager.R; import org.lsposed.manager.R;
@ -41,22 +45,24 @@ public class BaseFragment extends Fragment {
return NavHostFragment.findNavController(this); return NavHostFragment.findNavController(this);
} }
public void setupToolbar(Toolbar toolbar, int title) { public void setupToolbar(Toolbar toolbar, View tipsView, int title) {
setupToolbar(toolbar, getString(title), -1); setupToolbar(toolbar, tipsView, getString(title), -1);
} }
public void setupToolbar(Toolbar toolbar, int title, int menu) { public void setupToolbar(Toolbar toolbar, View tipsView, int title, int menu) {
setupToolbar(toolbar, getString(title), menu, null); setupToolbar(toolbar, tipsView, getString(title), menu, null);
} }
public void setupToolbar(Toolbar toolbar, String title, int menu) { public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu) {
setupToolbar(toolbar, title, menu, null); 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.setNavigationOnClickListener(navigationOnClickListener == null ? (v -> navigateUp()) : navigationOnClickListener);
toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24); toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
toolbar.setTitle(title); toolbar.setTitle(title);
toolbar.setTooltipText(title);
if (tipsView != null) tipsView.setTooltipText(title);
if (menu != -1) { if (menu != -1) {
toolbar.inflateMenu(menu); toolbar.inflateMenu(menu);
toolbar.setOnMenuItemClickListener(this::onOptionsItemSelected); toolbar.setOnMenuItemClickListener(this::onOptionsItemSelected);
@ -74,4 +80,29 @@ public class BaseFragment extends Fragment {
activity.runOnUiThread(runnable); 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 android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDialogFragment; import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App; import org.lsposed.manager.App;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentCompileDialogBinding; import org.lsposed.manager.databinding.FragmentCompileDialogBinding;
@ -47,19 +46,21 @@ import java.lang.ref.WeakReference;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class CompileDialogFragment extends AppCompatDialogFragment { public class CompileDialogFragment extends AppCompatDialogFragment {
private ApplicationInfo appInfo; 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(); CompileDialogFragment fragment = new CompileDialogFragment();
fragment.setCancelable(false); fragment.setCancelable(false);
fragment.appInfo = info; var bundle = new Bundle();
fragment.snackBar = snackBar; bundle.putParcelable("appInfo", info);
fragment.setArguments(bundle);
fragment.show(fragmentManager, "compile_dialog"); fragment.show(fragmentManager, "compile_dialog");
} }
@Override @Override
@NonNull @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
var arguments = getArguments();
appInfo = arguments != null ? arguments.getParcelable("appInfo") : null;
if (appInfo == null) { if (appInfo == null) {
throw new IllegalStateException("appInfo should not be 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); FragmentCompileDialogBinding binding = FragmentCompileDialogBinding.inflate(LayoutInflater.from(requireActivity()), null, false);
final PackageManager pm = requireContext().getPackageManager(); final PackageManager pm = requireContext().getPackageManager();
var builder = new BlurBehindDialogBuilder(requireActivity()) var builder = new BlurBehindDialogBuilder(requireActivity())
.setIcon(appInfo.loadIcon(pm))
.setTitle(appInfo.loadLabel(pm)) .setTitle(appInfo.loadLabel(pm))
.setView(binding.getRoot()); .setView(binding.getRoot());
@ -75,8 +75,8 @@ public class CompileDialogFragment extends AppCompatDialogFragment {
} }
@Override @Override
public void onAttach(@NonNull Context context) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onAttach(context); super.onViewCreated(view, savedInstanceState);
new CompileTask(this).executeOnExecutor(App.getExecutorService(), appInfo.packageName); new CompileTask(this).executeOnExecutor(App.getExecutorService(), appInfo.packageName);
} }
@ -118,9 +118,8 @@ public class CompileDialogFragment extends AppCompatDialogFragment {
if (fragment != null) { if (fragment != null) {
fragment.dismissAllowingStateLoss(); fragment.dismissAllowingStateLoss();
var parent = fragment.getParentFragment(); var parent = fragment.getParentFragment();
if (fragment.snackBar != null && parent != null && parent.isResumed()) { if (parent instanceof BaseFragment) {
Snackbar.make(fragment.snackBar, text, Snackbar.LENGTH_LONG).show(); ((BaseFragment) parent).showHint(text, true);
return;
} }
} }
Toast.makeText(context, text, Toast.LENGTH_LONG).show(); Toast.makeText(context, text, Toast.LENGTH_LONG).show();

View File

@ -20,17 +20,18 @@
package org.lsposed.manager.ui.fragment; package org.lsposed.manager.ui.fragment;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.color.MaterialColors; import com.google.android.material.color.MaterialColors;
import com.google.android.material.snackbar.Snackbar; 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.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.dialog.FlashDialogBuilder; import org.lsposed.manager.ui.dialog.FlashDialogBuilder;
import org.lsposed.manager.ui.dialog.InfoDialogBuilder; 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.ui.dialog.WarningDialogBuilder;
import org.lsposed.manager.util.ModuleUtil; import org.lsposed.manager.util.ModuleUtil;
import org.lsposed.manager.util.NavUtil; import org.lsposed.manager.util.NavUtil;
@ -56,7 +57,7 @@ import java.util.Locale;
import rikka.core.util.ResourceUtils; 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; private FragmentHomeBinding binding;
@ -66,15 +67,16 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ShortcutDialogBuilder.showIfNeed(requireContext()); ShortcutDialog.showIfNeed(getChildFragmentManager());
} }
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentHomeBinding.inflate(inflater, container, false); 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.setNavigationIcon(null);
binding.toolbar.setOnClickListener(v -> showAbout());
binding.appBar.setLiftable(true); binding.appBar.setLiftable(true);
binding.nestedScrollView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top)); 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 -> { binding.status.setOnClickListener(v -> {
if (ConfigManager.isBinderAlive() && !UpdateUtil.needUpdate()) { if (ConfigManager.isBinderAlive() && !UpdateUtil.needUpdate()) {
if (!ConfigManager.isSepolicyLoaded() || !ConfigManager.systemServerRequested() || !ConfigManager.dex2oatFlagsLoaded()) { if (!ConfigManager.isSepolicyLoaded() || !ConfigManager.systemServerRequested() || !ConfigManager.dex2oatFlagsLoaded()) {
new WarningDialogBuilder(activity).show(); new WarningDialogBuilder().show(getChildFragmentManager(), "warning");
} else { } else {
new InfoDialogBuilder(activity).show(); new InfoDialogBuilder().show(getChildFragmentManager(), "info");
} }
} else { } else {
if (UpdateUtil.canInstall()) { 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.download.setOnClickListener(new StartFragmentListener(R.id.action_repo_fragment, false));
binding.logs.setOnClickListener(new StartFragmentListener(R.id.action_logs_fragment, true)); binding.logs.setOnClickListener(new StartFragmentListener(R.id.action_logs_fragment, true));
binding.settings.setOnClickListener(new StartFragmentListener(R.id.action_settings_fragment, false)); 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()); updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate());
repoLoader.addListener(this); repoLoader.addListener(this);
moduleUtil.addListener(this); moduleUtil.addListener(this);
if (repoLoader.isRepoLoaded()) { onModulesReloaded();
repoLoaded();
}
return binding.getRoot(); 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) { private void updateStates(Activity activity, boolean binderAlive, boolean needUpdate) {
int cardBackgroundColor; int cardBackgroundColor;
if (binderAlive) { if (binderAlive) {
@ -186,7 +161,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.download.setVisibility(View.GONE); binding.download.setVisibility(View.GONE);
} }
binding.statusIcon.setImageResource(R.drawable.ic_round_error_outline_24); 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); cardBackgroundColor = MaterialColors.harmonizeWithPrimary(activity, cardBackgroundColor);
binding.status.setCardBackgroundColor(cardBackgroundColor); binding.status.setCardBackgroundColor(cardBackgroundColor);
@ -194,10 +169,33 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
binding.status.setOutlineSpotShadowColor(cardBackgroundColor); binding.status.setOutlineSpotShadowColor(cardBackgroundColor);
binding.status.setOutlineAmbientShadowColor(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 @Override
public void repoLoaded() { public void onRepoLoaded() {
final int[] count = new int[]{0}; final int[] count = new int[]{0};
HashSet<String> processedModules = new HashSet<>(); HashSet<String> processedModules = new HashSet<>();
var modules = moduleUtil.getModules(); var modules = moduleUtil.getModules();
@ -228,7 +226,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override @Override
public void onModulesReloaded() { public void onModulesReloaded() {
if (repoLoader.isRepoLoaded()) repoLoaded(); onRepoLoaded();
setModulesSummary(moduleUtil.getEnabledModulesCount()); setModulesSummary(moduleUtil.getEnabledModulesCount());
} }
@ -244,7 +242,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (requireInstalled && !ConfigManager.isBinderAlive()) { 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 { } else {
getNavController().navigate(fragment); getNavController().navigate(fragment);
} }
@ -260,7 +258,7 @@ public class HomeFragment extends BaseFragment implements RepoLoader.Listener, M
} }
private void setModulesSummary(int moduleCount) { 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 @Override

View File

@ -14,57 +14,74 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with LSPosed. If not, see <https://www.gnu.org/licenses/>. * along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
* *
* Copyright (C) 2020 EdXposed Contributors
* Copyright (C) 2021 LSPosed Contributors * Copyright (C) 2021 LSPosed Contributors
*/ */
package org.lsposed.manager.ui.fragment; package org.lsposed.manager.ui.fragment;
import static org.lsposed.manager.App.TAG;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ScrollView; import android.widget.HorizontalScrollView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.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.ConfigManager;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentLogsBinding; import org.lsposed.manager.databinding.FragmentPagerBinding;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; 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.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import rikka.core.os.FileUtils; import rikka.core.os.FileUtils;
import rikka.recyclerview.RecyclerViewKt;
public class LogsFragment extends BaseFragment { public class LogsFragment extends BaseFragment {
private boolean verbose = false;
private final Handler handler = new Handler(Looper.getMainLooper()); 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( private final ActivityResultLauncher<String> saveLogsLauncher = registerForActivityResult(
new ActivityResultContracts.CreateDocument(), new ActivityResultContracts.CreateDocument(),
uri -> { uri -> {
@ -78,11 +95,7 @@ public class LogsFragment extends BaseFragment {
os.finish(); os.finish();
} catch (IOException e) { } catch (IOException e) {
var text = context.getString(R.string.logs_save_failed2, e.getMessage()); var text = context.getString(R.string.logs_save_failed2, e.getMessage());
if (binding != null && isResumed()) { showHint(text, false);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(context, text, Toast.LENGTH_LONG).show();
}
} }
}); });
}); });
@ -90,56 +103,59 @@ public class LogsFragment extends BaseFragment {
@Nullable @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentLogsBinding.inflate(inflater, container, false); binding = FragmentPagerBinding.inflate(inflater, container, false);
setupToolbar(binding.toolbar, R.string.Logs, R.menu.menu_logs); binding.appBar.setLiftable(true);
setupToolbar(binding.toolbar, binding.clickView, R.string.Logs, R.menu.menu_logs);
binding.slidingTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { binding.toolbar.setSubtitle(ConfigManager.isVerboseLogEnabled() ? R.string.enabled_verbose_log : R.string.disabled_verbose_log);
@Override adapter = new LogPageAdapter(this);
public void onTabSelected(TabLayout.Tab tab) { binding.viewPager.setAdapter(adapter);
verbose = tab.getPosition() == 1; new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText((int) adapter.getItemId(position))).attach();
reloadLogs();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
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(); return binding.getRoot();
} }
@Override public void setOptionsItemSelectListener(OptionsItemSelectListener optionsItemSelectListener) {
public void onResume() { this.optionsItemSelectListener = optionsItemSelectListener;
super.onResume();
reloadLogs();
} }
@SuppressLint("NotifyDataSetChanged")
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId(); var itemId = item.getItemId();
if (itemId == R.id.menu_scroll_top) { if (itemId == R.id.menu_save) {
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(); save();
return true; return true;
} else if (itemId == R.id.menu_clear) { } else if (itemId == R.id.menu_word_wrap) {
clear(); 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 true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
@ -147,21 +163,6 @@ public class LogsFragment extends BaseFragment {
binding = null; 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() { private void save() {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
String filename = String.format(Locale.ROOT, "LSPosed_%s.zip", now.toString()); String filename = String.format(Locale.ROOT, "LSPosed_%s.zip", now.toString());
@ -176,7 +177,7 @@ public class LogsFragment extends BaseFragment {
FileUtils.copy(is, os); FileUtils.copy(is, os);
os.closeEntry(); os.closeEntry();
} catch (IOException e) { } 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); FileUtils.copy(is, os);
os.closeEntry(); os.closeEntry();
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, name, e); Log.w(App.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();
}
} }
} }
@ -243,19 +198,245 @@ public class LogsFragment extends BaseFragment {
super.onDestroy(); super.onDestroy();
} }
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 @Override
public void onSaveInstanceState(@NonNull Bundle outState) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
super.onSaveInstanceState(outState); return new ViewHolder(ItemLogTextviewBinding.inflate(getLayoutInflater(), parent, false));
outState.putInt(LogsFragment.class.getName() + "." + "tab", binding.slidingTabs.getSelectedTabPosition());
} }
@Override @Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) { public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
if (savedInstanceState != null) { holder.item.setText(log.get(position));
var tabPosition = savedInstanceState.getInt(LogsFragment.class.getName() + "." + "tab", 0); }
if (tabPosition < binding.slidingTabs.getTabCount())
binding.slidingTabs.selectTab(binding.slidingTabs.getTabAt(tabPosition)); @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();
}
}
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.Filterable;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.floatingactionbutton.FloatingActionButton; 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.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator; 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.ConfigManager;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.adapters.AppHelper; 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.FragmentPagerBinding;
import org.lsposed.manager.databinding.ItemModuleBinding; import org.lsposed.manager.databinding.ItemModuleBinding;
import org.lsposed.manager.databinding.ItemRepoRecyclerviewBinding;
import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
@ -94,7 +91,7 @@ import java.util.stream.IntStream;
import rikka.core.util.ResourceUtils; import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt; 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 PackageManager pm = App.getInstance().getPackageManager();
private static final ModuleUtil moduleUtil = ModuleUtil.getInstance(); private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
private static final RepoLoader repoLoader = RepoLoader.getInstance(); private static final RepoLoader repoLoader = RepoLoader.getInstance();
@ -103,14 +100,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected SearchView searchView; protected SearchView searchView;
private SearchView.OnQueryTextListener searchListener; private SearchView.OnQueryTextListener searchListener;
private final ArrayList<ModuleAdapter> adapters = new ArrayList<>(); final ArrayList<ModuleAdapter> adapters = new ArrayList<>();
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
updateProgress();
}
};
private ModuleUtil.InstalledModule selectedModule; private ModuleUtil.InstalledModule selectedModule;
@ -125,8 +115,8 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
@Override @Override
public boolean onQueryTextChange(String newText) { public boolean onQueryTextChange(String query) {
adapters.forEach(adapter -> adapter.getFilter().filter(newText)); adapters.forEach(adapter -> adapter.getFilter().filter(query));
return false; return false;
} }
}; };
@ -137,7 +127,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
adapter.setHasStableIds(true); adapter.setHasStableIds(true);
adapter.setStateRestorationPolicy(PREVENT_WHEN_EMPTY); adapter.setStateRestorationPolicy(PREVENT_WHEN_EMPTY);
adapters.add(adapter); 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 @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentPagerBinding.inflate(inflater, container, false); 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.setAdapter(new PagerAdapter(this));
binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
updateProgress();
showFab(); showFab();
} }
}); });
@ -204,31 +180,18 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
binding.tabLayout.setVisibility(View.GONE); binding.tabLayout.setVisibility(View.GONE);
} }
binding.fab.setOnClickListener(v -> { binding.fab.setOnClickListener(v -> {
var pickAdaptor = new ModuleAdapter(adapters.get(binding.viewPager.getCurrentItem()).getUser(), true); var bundle = new Bundle();
var position = binding.viewPager.getCurrentItem(); var user = adapters.get(binding.viewPager.getCurrentItem()).getUser();
var user = adapters.get(position).getUser(); bundle.putParcelable("userInfo", user);
var binding = DialogRecyclerviewBinding.inflate(getLayoutInflater()); var f = new RecyclerViewDialogFragment();
binding.list.setAdapter(pickAdaptor); f.setArguments(bundle);
binding.list.setLayoutManager(new LinearLayoutManager(requireActivity())); f.show(getChildFragmentManager(), "install_to_user" + user.id);
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();
});
}); });
moduleUtil.addListener(this);
repoLoader.addListener(this);
updateModuleSummary();
return binding.getRoot(); return binding.getRoot();
} }
@ -236,6 +199,16 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
public void onPrepareOptionsMenu(Menu menu) { public void onPrepareOptionsMenu(Menu menu) {
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(searchListener); 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 @Override
@ -245,31 +218,30 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
@Override @Override
public void onDestroy() { public void onSingleModuleReloaded(ModuleUtil.InstalledModule module) {
super.onDestroy();
moduleUtil.removeListener(this);
}
@Override
public void onSingleInstalledModuleReloaded(ModuleUtil.InstalledModule module) {
adapters.forEach(ModuleAdapter::refresh); adapters.forEach(ModuleAdapter::refresh);
} }
@Override @Override
public void onModulesReloaded() { public void onModulesReloaded() {
adapters.forEach(ModuleAdapter::refresh); adapters.forEach(ModuleAdapter::refresh);
updateModuleSummary();
} }
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public void onRepoLoaded() {
int itemId = item.getItemId(); adapters.forEach(ModuleAdapter::refresh);
if (itemId == R.id.menu_refresh) {
adapters.forEach(adapter -> adapter.refresh(true));
}
return super.onOptionsItemSelected(item);
} }
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()) new BlurBehindDialogBuilder(requireActivity())
.setTitle(getString(R.string.install_to_user, user.name)) .setTitle(getString(R.string.install_to_user, user.name))
.setMessage(getString(R.string.install_to_user_message, module.getAppName(), 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 ? String text = success ?
getString(R.string.module_installed, module.getAppName(), user.name) : getString(R.string.module_installed, module.getAppName(), user.name) :
getString(R.string.module_install_failed); getString(R.string.module_install_failed);
if (binding != null && isResumed()) { showHint(text, false);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
if (success) if (success)
moduleUtil.reloadSingleModule(module.packageName, user.id); 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); Intent intent = AppHelper.getSettingsIntent(packageName, selectedModule.userId);
if (intent != null) { if (intent != null) {
ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId); ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
} else {
Snackbar.make(binding.snackbar, R.string.module_no_ui, Snackbar.LENGTH_LONG).show();
} }
return true; return true;
} else if (itemId == R.id.menu_other_app) { } else if (itemId == R.id.menu_other_app) {
@ -326,11 +292,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
runAsync(() -> { runAsync(() -> {
boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId); boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId);
String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed); String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed);
if (binding != null && isResumed()) { showHint(text, false);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
if (success) if (success)
moduleUtil.reloadSingleModule(selectedModule.packageName, selectedModule.userId); 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())); getNavController().navigate(ModulesFragmentDirections.actionModulesFragmentToRepoItemFragment(selectedModule.packageName, selectedModule.getAppName()));
return true; return true;
} else if (itemId == R.id.menu_compile_speed) { } 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); return super.onContextItemSelected(item);
} }
@ -349,12 +311,33 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
moduleUtil.removeListener(this); moduleUtil.removeListener(this);
repoLoader.removeListener(this);
binding = null; binding = null;
} }
public static class ModuleListFragment extends Fragment { 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 @Nullable
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 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; return null;
} }
int position = arguments.getInt("position"); int position = arguments.getInt("position");
ItemRepoRecyclerviewBinding binding = ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), container, false); binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
binding.recyclerView.setAdapter(fragment.adapters.get(position)); adapter = fragment.adapters.get(position);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireActivity()); binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setLayoutManager(layoutManager); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
adapter.registerAdapterDataObserver(observer);
return binding.getRoot(); 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 { 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> searchList = new ArrayList<>();
private List<ModuleUtil.InstalledModule> showList = new ArrayList<>(); private List<ModuleUtil.InstalledModule> showList = new ArrayList<>();
private final UserInfo user; private final UserInfo user;
@ -487,7 +536,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} }
if (repoLoader.isRepoLoaded()) {
var ver = repoLoader.getModuleLatestVersion(item.packageName); var ver = repoLoader.getModuleLatestVersion(item.packageName);
if (ver != null && ver.upgradable(item.versionCode, item.versionName)) { if (ver != null && ver.upgradable(item.versionCode, item.versionName)) {
if (warningText != null) sb.append("\n"); if (warningText != null) sb.append("\n");
@ -503,7 +551,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
sb.setSpan(foregroundColorSpan, 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) { if (sb.length() == 0) {
holder.hint.setVisibility(View.GONE); holder.hint.setVisibility(View.GONE);
} else { } else {
@ -515,7 +562,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f); holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f);
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
searchView.clearFocus(); searchView.clearFocus();
searchView.onActionViewCollapsed();
getNavController().navigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId)); getNavController().navigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId));
}); });
holder.itemView.setOnLongClickListener(v -> { holder.itemView.setOnLongClickListener(v -> {
@ -585,12 +631,15 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
public void refresh() { public void refresh() {
refresh(false); runAsync(reloadModules);
} }
public void refresh(boolean force) { public void fullRefresh() {
if (force) runAsync(moduleUtil::reloadInstalledModules); runAsync(() -> {
runAsync(reloadModules); setLoaded(false);
moduleUtil.reloadInstalledModules();
refresh();
});
} }
private final Runnable reloadModules = () -> { private final Runnable reloadModules = () -> {
@ -634,7 +683,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
}); });
String queryStr = searchView != null ? searchView.getQuery().toString() : ""; String queryStr = searchView != null ? searchView.getQuery().toString() : "";
searchList = tmpList; searchList = tmpList;
runOnUiThread(() -> getFilter().filter(queryStr, count -> setLoaded(true))); runOnUiThread(() -> getFilter().filter(queryStr));
}; };
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
@ -647,7 +696,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
@Override @Override
public boolean isLoaded() { public boolean isLoaded() {
return isLoaded; return isLoaded && moduleUtil.isModulesLoaded();
} }
class ViewHolder extends RecyclerView.ViewHolder { class ViewHolder extends RecyclerView.ViewHolder {
@ -681,9 +730,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected FilterResults performFiltering(CharSequence constraint) { protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults(); FilterResults filterResults = new FilterResults();
List<ModuleUtil.InstalledModule> filtered = new ArrayList<>(); List<ModuleUtil.InstalledModule> filtered = new ArrayList<>();
if (constraint.toString().isEmpty()) {
filtered.addAll(searchList);
} else {
String filter = constraint.toString().toLowerCase(); String filter = constraint.toString().toLowerCase();
for (ModuleUtil.InstalledModule info : searchList) { for (ModuleUtil.InstalledModule info : searchList) {
if (lowercaseContains(info.getAppName(), filter) || if (lowercaseContains(info.getAppName(), filter) ||
@ -692,7 +738,6 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
filtered.add(info); filtered.add(info);
} }
} }
}
filterResults.values = filtered; filterResults.values = filtered;
filterResults.count = filtered.size(); filterResults.count = filtered.size();
return filterResults; return filterResults;
@ -702,6 +747,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
protected void publishResults(CharSequence constraint, FilterResults results) { protected void publishResults(CharSequence constraint, FilterResults results) {
//noinspection unchecked //noinspection unchecked
showList = (List<ModuleUtil.InstalledModule>) results.values; 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.annotation.Nullable;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.lifecycle.Lifecycle;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App; import org.lsposed.manager.App;
import org.lsposed.manager.R; import org.lsposed.manager.R;
import org.lsposed.manager.databinding.FragmentRepoBinding; import org.lsposed.manager.databinding.FragmentRepoBinding;
import org.lsposed.manager.databinding.ItemOnlinemoduleBinding; import org.lsposed.manager.databinding.ItemOnlinemoduleBinding;
import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.repo.RepoLoader;
import org.lsposed.manager.repo.model.OnlineModule; 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.ModuleUtil;
import org.lsposed.manager.util.SimpleStatefulAdaptor;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -71,7 +68,7 @@ import rikka.core.util.LabelComparator;
import rikka.core.util.ResourceUtils; import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt; 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 FragmentRepoBinding binding;
protected SearchView searchView; protected SearchView searchView;
private SearchView.OnQueryTextListener mSearchListener; private SearchView.OnQueryTextListener mSearchListener;
@ -79,7 +76,14 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
private boolean preLoadWebview = true; private boolean preLoadWebview = true;
private final RepoLoader repoLoader = RepoLoader.getInstance(); private final RepoLoader repoLoader = RepoLoader.getInstance();
private final ModuleUtil moduleUtil = ModuleUtil.getInstance();
private RepoAdapter adapter; private RepoAdapter adapter;
private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
}
};
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
@ -103,33 +107,78 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentRepoBinding.inflate(getLayoutInflater(), container, false); 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 = new RepoAdapter();
adapter.setHasStableIds(true); adapter.setHasStableIds(true);
adapter.registerAdapterDataObserver(observer);
binding.recyclerView.setAdapter(adapter); binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setHasFixedSize(true); binding.recyclerView.setHasFixedSize(true);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); 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); repoLoader.addListener(this);
moduleUtil.addListener(this);
/* updateRepoSummary();
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);
return binding.getRoot(); 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 @Override
public void onPrepareOptionsMenu(Menu menu) { public void onPrepareOptionsMenu(Menu menu) {
searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
searchView.setOnQueryTextListener(mSearchListener); 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); int sort = App.getPreferences().getInt("repo_sort", 0);
if (sort == 0) { if (sort == 0) {
menu.findItem(R.id.item_sort_by_name).setChecked(true); 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); mHandler.removeCallbacksAndMessages(null);
repoLoader.removeListener(this); repoLoader.removeListener(this);
moduleUtil.removeListener(this);
adapter.unregisterAdapterDataObserver(observer);
binding = null; binding = null;
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
adapter.initData(); adapter.refresh();
if (preLoadWebview) { if (preLoadWebview) {
mHandler.postDelayed(() -> new WebView(requireContext()), 500); mHandler.postDelayed(() -> new WebView(requireContext()), 500);
preLoadWebview = false; preLoadWebview = false;
@ -158,41 +209,41 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
} }
@Override @Override
public void repoLoaded() { public void onRepoLoaded() {
runOnUiThread(() -> { adapter.refresh();
binding.progress.hide(); updateRepoSummary();
adapter.setData(repoLoader.getOnlineModules());
});
} }
@Override @Override
public void onThrowable(Throwable t) { public void onThrowable(Throwable t) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show(); updateRepoSummary();
} }
@Override
public void onModulesReloaded() {
updateRepoSummary();
} }
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId(); int itemId = item.getItemId();
if (itemId == R.id.menu_refresh) { if (itemId == R.id.item_sort_by_name) {
binding.progress.show();
repoLoader.loadRemoteData();
} else if (itemId == R.id.item_sort_by_name) {
item.setChecked(true); item.setChecked(true);
App.getPreferences().edit().putInt("repo_sort", 0).apply(); App.getPreferences().edit().putInt("repo_sort", 0).apply();
adapter.setData(repoLoader.getOnlineModules()); adapter.refresh();
} else if (itemId == R.id.item_sort_by_update_time) { } else if (itemId == R.id.item_sort_by_update_time) {
item.setChecked(true); item.setChecked(true);
App.getPreferences().edit().putInt("repo_sort", 1).apply(); App.getPreferences().edit().putInt("repo_sort", 1).apply();
adapter.setData(repoLoader.getOnlineModules()); adapter.refresh();
} }
return super.onOptionsItemSelected(item); 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 List<OnlineModule> fullList, showList;
private final LabelComparator labelComparator = new LabelComparator(); private final LabelComparator labelComparator = new LabelComparator();
private boolean isLoaded = false;
RepoAdapter() { RepoAdapter() {
fullList = showList = Collections.emptyList(); fullList = showList = Collections.emptyList();
@ -219,7 +270,7 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
holder.appDescription.setVisibility(View.VISIBLE); holder.appDescription.setVisibility(View.VISIBLE);
holder.appDescription.setText(sb); holder.appDescription.setText(sb);
sb = new SpannableStringBuilder(); sb = new SpannableStringBuilder();
ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(module.getName()); ModuleUtil.InstalledModule installedModule = moduleUtil.getModule(module.getName());
if (installedModule != null) { if (installedModule != null) {
var ver = repoLoader.getModuleLatestVersion(installedModule.packageName); var ver = repoLoader.getModuleLatestVersion(installedModule.packageName);
if (ver != null && ver.upgradable(installedModule.versionCode, installedModule.versionName)) { 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 -> { holder.itemView.setOnClickListener(v -> {
searchView.clearFocus(); searchView.clearFocus();
searchView.onActionViewCollapsed();
getNavController().navigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName(), module.getDescription())); getNavController().navigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName(), module.getDescription()));
}); });
holder.itemView.setTooltipText(module.getDescription());
} }
@Override @Override
@ -255,27 +306,37 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
return showList.size(); return showList.size();
} }
public void setData(Collection<OnlineModule> modules) { private void setLoaded(boolean isLoaded) {
fullList = new ArrayList<>(modules); this.isLoaded = isLoaded;
fullList = fullList.stream().filter((onlineModule -> !onlineModule.isHide() && !onlineModule.getReleases().isEmpty())).collect(Collectors.toList()); runOnUiThread(this::notifyDataSetChanged);
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));
} }
public void initData() { public void setData(Collection<OnlineModule> modules) {
Collection<OnlineModule> modules = repoLoader.getOnlineModules(); if (modules == null) return;
if (!repoLoader.isRepoLoaded()) { setLoaded(false);
binding.progress.show(); int sort = App.getPreferences().getInt("repo_sort", 0);
repoLoader.loadRemoteData(); fullList = modules.parallelStream().filter((onlineModule -> !onlineModule.isHide() && !onlineModule.getReleases().isEmpty()))
.sorted((a, b) -> {
if (sort == 0) {
return labelComparator.compare(a.getDescription(), b.getDescription());
} else { } else {
adapter.setData(modules); 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();
refresh();
});
}
public void refresh() {
runAsync(() -> adapter.setData(repoLoader.getOnlineModules()));
} }
@Override @Override
@ -288,6 +349,11 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
return new RepoAdapter.ModuleFilter(); return new RepoAdapter.ModuleFilter();
} }
@Override
public boolean isLoaded() {
return isLoaded && repoLoader.isRepoLoaded();
}
class ViewHolder extends RecyclerView.ViewHolder { class ViewHolder extends RecyclerView.ViewHolder {
ConstraintLayout root; ConstraintLayout root;
TextView appName; TextView appName;
@ -311,9 +377,7 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
@Override @Override
protected FilterResults performFiltering(CharSequence constraint) { protected FilterResults performFiltering(CharSequence constraint) {
if (constraint.toString().isEmpty()) { FilterResults filterResults = new FilterResults();
showList = fullList;
} else {
ArrayList<OnlineModule> filtered = new ArrayList<>(); ArrayList<OnlineModule> filtered = new ArrayList<>();
String filter = constraint.toString().toLowerCase(); String filter = constraint.toString().toLowerCase();
for (OnlineModule info : fullList) { for (OnlineModule info : fullList) {
@ -323,14 +387,16 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
filtered.add(info); filtered.add(info);
} }
} }
showList = filtered; filterResults.values = filtered;
} filterResults.count = filtered.size();
return null; return filterResults;
} }
@Override @Override
protected void publishResults(CharSequence constraint, FilterResults results) { 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; package org.lsposed.manager.ui.fragment;
import android.app.Dialog;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
@ -36,16 +37,20 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.CircularProgressIndicator; 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.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.tabs.TabLayoutMediator;
@ -83,13 +88,13 @@ import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import rikka.core.util.ResourceUtils; import rikka.core.util.ResourceUtils;
import rikka.recyclerview.RecyclerViewKt; import rikka.recyclerview.RecyclerViewKt;
import rikka.widget.borderview.BorderNestedScrollView; import rikka.widget.borderview.BorderView;
import rikka.widget.borderview.BorderRecyclerView;
public class RepoItemFragment extends BaseFragment implements RepoLoader.Listener { public class RepoItemFragment extends BaseFragment implements RepoLoader.RepoListener {
FragmentPagerBinding binding; FragmentPagerBinding binding;
private OnlineModule module; OnlineModule module;
private ReleaseAdapter releaseAdapter; private ReleaseAdapter releaseAdapter;
private InformationAdapter informationAdapter;
@Nullable @Nullable
@Override @Override
@ -98,9 +103,11 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
if (module == null) return binding.getRoot(); if (module == null) return binding.getRoot();
String modulePackageName = module.getName(); String modulePackageName = module.getName();
String moduleName = module.getDescription(); 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.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}; 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(); 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.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(); return binding.getRoot();
} }
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
RepoLoader.getInstance().addListener(this); RepoLoader.getInstance().addListener(this);
@ -208,44 +213,35 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
} }
@Override @Override
public void moduleReleasesLoaded(OnlineModule module) { public void onDestroyView() {
this.module = module; super.onDestroyView();
if (releaseAdapter != null) { RepoLoader.getInstance().removeListener(this);
runAsync(releaseAdapter::loadItems); binding = null;
if (isResumed() && module.getReleases().size() == 1) {
Snackbar.make(binding.snackbar, R.string.module_release_no_more, Snackbar.LENGTH_SHORT).show();
} }
@Override
public void onModuleReleasesLoaded(OnlineModule module) {
this.module = module;
runAsync(releaseAdapter::loadItems);
if (module.getReleases().size() == 1) {
showHint(R.string.module_release_no_more, true);
} }
} }
@Override @Override
public void onThrowable(Throwable t) { public void onThrowable(Throwable t) {
if (releaseAdapter != null) {
runAsync(releaseAdapter::loadItems); runAsync(releaseAdapter::loadItems);
if (isResumed()) { showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true);
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;
} }
private class InformationAdapter extends SimpleStatefulAdaptor<InformationAdapter.ViewHolder> { private class InformationAdapter extends SimpleStatefulAdaptor<InformationAdapter.ViewHolder> {
private final OnlineModule module;
private int rowCount = 0; private int rowCount = 0;
private int homepageRow = -1; private int homepageRow = -1;
private int collaboratorsRow = -1; private int collaboratorsRow = -1;
private int sourceUrlRow = -1; private int sourceUrlRow = -1;
public InformationAdapter(OnlineModule module) { public InformationAdapter() {
this.module = module;
if (!TextUtils.isEmpty(module.getHomepageUrl())) { if (!TextUtils.isEmpty(module.getHomepageUrl())) {
homepageRow = rowCount++; homepageRow = rowCount++;
} }
@ -260,7 +256,7 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@NonNull @NonNull
@Override @Override
public InformationAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 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 @Override
@ -302,7 +298,6 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
NavUtil.startURL(requireActivity(), module.getSourceUrl()); NavUtil.startURL(requireActivity(), module.getSourceUrl());
} }
}); });
} }
@Override @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 class ReleaseAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ReleaseAdapter.ViewHolder> {
private List<Release> items = new ArrayList<>(); private List<Release> items = new ArrayList<>();
private final Resources resources = App.getInstance().getResources(); private final Resources resources = App.getInstance().getResources();
@ -353,9 +369,9 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@Override @Override
public ReleaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ReleaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == 0) { if (viewType == 0) {
return new ReleaseAdapter.ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false)); return new ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false));
} else { } 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 -> { holder.viewAssets.setOnClickListener(v -> {
ArrayList<String> names = new ArrayList<>(); ArrayList<String> names = new ArrayList<>();
assets.forEach(releaseAsset -> names.add(releaseAsset.getName())); assets.forEach(releaseAsset -> names.add(releaseAsset.getName()));
new BlurBehindDialogBuilder(requireActivity()) DownloadDialog.create(getChildFragmentManager(), names.toArray(new String[0]), assets.stream().map(ReleaseAsset::getDownloadUrl).collect(Collectors.toCollection(ArrayList::new)));
.setItems(names.toArray(new String[0]), (dialog, which) -> NavUtil.startURL(requireActivity(), assets.get(which).getDownloadUrl()))
.show();
}); });
} else { } else {
holder.viewAssets.setVisibility(View.GONE); 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 @NonNull
@Override @Override
public PagerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public Fragment createFragment(int position) {
if (viewType == 0) { Bundle bundle = new Bundle();
return new PagerAdapter.ReadmeViewHolder(ItemRepoReadmeBinding.inflate(getLayoutInflater(), parent, false)); bundle.putInt("position", position);
Fragment f;
if (position == 0) {
f = new ReadmeFragment();
} else if (position == 1) {
f = new RecyclerviewFragment();
} else { } else {
return new PagerAdapter.RecyclerviewBinding(ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), parent, false)); f = new RecyclerviewFragment();
}
}
@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.setArguments(bundle);
return f;
} }
@Override @Override
@ -481,29 +484,112 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
return position == 0 ? 0 : 1; return position == 0 ? 0 : 1;
} }
class ViewHolder extends RecyclerView.ViewHolder { @Override
WebView webView; public long getItemId(int position) {
BorderNestedScrollView scrollView; return position;
BorderRecyclerView recyclerView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
} }
} }
class ReadmeViewHolder extends PagerAdapter.ViewHolder { public static abstract class BorderFragment extends BaseFragment {
public ReadmeViewHolder(ItemRepoReadmeBinding binding) { BorderView borderView;
super(binding.getRoot());
webView = binding.readme; void attachListeners() {
scrollView = binding.scrollView; 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 RecyclerviewBinding extends PagerAdapter.ViewHolder { abstract void scrollToTop();
public RecyclerviewBinding(ItemRepoRecyclerviewBinding binding) {
super(binding.getRoot()); void detachListeners() {
recyclerView = binding.recyclerView; borderView.getBorderViewDelegate().setBorderVisibilityChangedListener(null);
} }
@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.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
@ -42,7 +41,6 @@ import androidx.preference.SwitchPreference;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.DynamicColors; import com.google.android.material.color.DynamicColors;
import com.google.android.material.snackbar.Snackbar;
import org.lsposed.manager.App; import org.lsposed.manager.App;
import org.lsposed.manager.BuildConfig; import org.lsposed.manager.BuildConfig;
@ -72,22 +70,19 @@ public class SettingsFragment extends BaseFragment {
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentSettingsBinding.inflate(inflater, container, false); 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) { if (savedInstanceState == null) {
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.add(R.id.container, new PreferenceFragment()).commitNow(); .add(R.id.container, new PreferenceFragment()).commitNow();
} }
if (ConfigManager.isBinderAlive()) {
/* binding.toolbar.setSubtitle(String.format(Locale.ROOT, "%s (%d) - %s",
CollapsingToolbarLayout consumes window insets, causing child views not ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi()));
receiving window insets. } else {
See https://github.com/material-components/material-components-android/issues/1310 binding.toolbar.setSubtitle(String.format(Locale.ROOT, "%s (%d) - %s",
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed)));
Insets can be handled by RikkaX Insets, so we can manually set }
OnApplyWindowInsetsListener to null.
*/
binding.collapsingToolbarLayout.setOnApplyWindowInsetsListener(null);
return binding.getRoot(); return binding.getRoot();
} }
@ -109,11 +104,7 @@ public class SettingsFragment extends BaseFragment {
BackupUtils.backup(uri); BackupUtils.backup(uri);
} catch (Exception e) { } catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage()); var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage());
if (parentFragment != null && parentFragment.binding != null && isResumed()) { parentFragment.showHint(text, false);
Snackbar.make(parentFragment.binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
} }
}); });
}); });
@ -125,11 +116,7 @@ public class SettingsFragment extends BaseFragment {
BackupUtils.restore(uri); BackupUtils.restore(uri);
} catch (Exception e) { } catch (Exception e) {
var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage()); var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage());
if (parentFragment != null && parentFragment.binding != null && isResumed()) { parentFragment.showHint(text, false);
Snackbar.make(parentFragment.binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else {
Toast.makeText(App.getInstance(), text, Toast.LENGTH_LONG).show();
}
} }
}); });
}); });
@ -311,6 +298,17 @@ public class SettingsFragment extends BaseFragment {
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
BorderRecyclerView recyclerView = (BorderRecyclerView) super.onCreateRecyclerView(inflater, parent, savedInstanceState); BorderRecyclerView recyclerView = (BorderRecyclerView) super.onCreateRecyclerView(inflater, parent, savedInstanceState);
RecyclerViewKt.fixEdgeEffect(recyclerView, false, true); 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; return recyclerView;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -18,8 +18,8 @@
--> -->
<rikka.widget.borderview.BorderNestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <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 <LinearLayout
android:id="@+id/container" android:id="@+id/container"

View File

@ -21,7 +21,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end" app:consumeSystemWindowsInsets="start|end"
@ -36,22 +35,26 @@
android:fitsSystemWindows="false" android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top"> app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout" style="?attr/collapsingToolbarLayoutLargeStyle"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize" android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed" app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale"> 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="0dp" android:elevation="0dp"
android:theme="@style/ThemeOverlay.MaterialComponents.ActionBar"
app:layout_collapseMode="pin" /> app:layout_collapseMode="pin" />
</com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
<rikka.widget.switchbar.SwitchBar <rikka.widget.switchbar.SwitchBar
android:id="@+id/master_switch" android:id="@+id/master_switch"
@ -60,12 +63,27 @@
android:layout_gravity="bottom" android:layout_gravity="bottom"
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="1" app:layout_collapseParallaxMultiplier="1"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:layout_scrollEffect="compress" app:layout_scrollEffect="compress"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:switchOffText="@string/enable_module" app:switchOffText="@string/enable_module"
app:switchOnText="@string/enable_module" /> app:switchOnText="@string/enable_module" />
</com.google.android.material.appbar.AppBarLayout> </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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -73,20 +91,13 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:ignore="MissingPrefix"> 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 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.lsposed.manager.ui.widget.EmptyStateRecyclerView <org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"

View File

@ -21,7 +21,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/snackbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end" app:consumeSystemWindowsInsets="start|end"
@ -116,7 +115,8 @@
android:id="@+id/modules" android:id="@+id/modules"
style="@style/HomeCard.Secondary" style="@style/HomeCard.Secondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:tooltipText="@string/Modules">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -156,7 +156,8 @@
android:id="@+id/download" android:id="@+id/download"
style="@style/HomeCard.Secondary" style="@style/HomeCard.Secondary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:tooltipText="@string/module_repo">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -196,7 +197,8 @@
android:id="@+id/logs" android:id="@+id/logs"
style="@style/HomeCard.Tertiary" style="@style/HomeCard.Tertiary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:tooltipText="@string/Logs">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -225,7 +227,8 @@
android:id="@+id/settings" android:id="@+id/settings"
style="@style/HomeCard.Tertiary" style="@style/HomeCard.Tertiary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:tooltipText="@string/Settings">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -253,7 +256,8 @@
android:id="@+id/issue" android:id="@+id/issue"
style="@style/HomeCard.Tertiary" style="@style/HomeCard.Tertiary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:tooltipText="@string/feedback_or_suggestion">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -265,14 +269,44 @@
<ImageView <ImageView
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:contentDescription="@string/report_issue" android:contentDescription="@string/feedback_or_suggestion"
app:srcCompat="@drawable/ic_round_bug_report_24" /> app:srcCompat="@drawable/ic_baseline_chat_24" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" 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:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" /> android:textSize="16sp" />
</LinearLayout> </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" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
@ -35,22 +34,28 @@
android:fitsSystemWindows="false" android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top"> app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout" android:id="@+id/toolbar_layout"
style="?attr/collapsingToolbarLayoutMediumStyle" style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize" android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed" app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale"> 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="0dp" android:elevation="0dp"
android:theme="@style/ThemeOverlay.MaterialComponents.ActionBar"
app:layout_collapseMode="pin" /> app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
@ -58,7 +63,6 @@
android:layout_height="@dimen/tab_layout_height" android:layout_height="@dimen/tab_layout_height"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:layout_scrollFlags="scroll|enterAlways"
app:tabGravity="center" app:tabGravity="center"
app:tabIndicatorAnimationMode="elastic" app:tabIndicatorAnimationMode="elastic"
app:tabMode="scrollable" /> app:tabMode="scrollable" />
@ -69,14 +73,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:hideAnimationBehavior="outward" />
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/view_pager"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -89,12 +85,13 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom" android:layout_gravity="end|bottom"
android:layout_margin="16dp" android:layout_margin="?attr/dialogPreferredPadding"
android:contentDescription="@string/add_module_to_user" android:contentDescription="@string/add_module_to_user"
android:src="@drawable/ic_baseline_add_24" android:src="@drawable/ic_baseline_add_24"
android:tooltipText="@string/add_module_to_user"
android:visibility="gone" android:visibility="gone"
app:backgroundTint="?attr/colorPrimary" app:backgroundTint="?attr/colorPrimary"
app:tint="@color/primary_text_material_inverse"
app:layout_behavior="@string/hide_bottom_view_on_scroll_behavior" 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> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

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

View File

@ -20,7 +20,6 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/snackbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:consumeSystemWindowsInsets="start|end" app:consumeSystemWindowsInsets="start|end"
@ -34,28 +33,32 @@
android:fitsSystemWindows="false" android:fitsSystemWindows="false"
app:fitsSystemWindowsInsets="top"> app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.SubtitleCollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout" style="?attr/collapsingToolbarLayoutLargeStyle"
style="?attr/collapsingToolbarLayoutMediumStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize" android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:titleCollapseMode="scale"> 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 <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:elevation="0dp" android:elevation="0dp"
app:layout_collapseMode="pin" /> app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.SubtitleCollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </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 <ImageView
android:id="@+id/app_icon" android:id="@+id/app_icon"
android:layout_width="36dp" android:layout_width="@dimen/app_icon_size"
android:layout_height="36dp" android:layout_height="@dimen/app_icon_size"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -52,7 +52,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:singleLine="false" android:ellipsize="marquee"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItem" android:textAppearance="?android:attr/textAppearanceListItem"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textSize="16sp" android:textSize="16sp"
@ -67,6 +68,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:maxLines="5" android:maxLines="5"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItemSecondary" android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
app:layout_constraintBottom_toBottomOf="@id/hint" app:layout_constraintBottom_toBottomOf="@id/hint"

View File

@ -42,7 +42,9 @@
android:id="@+id/app_name" android:id="@+id/app_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="marquee"
android:maxLines="1" android:maxLines="1"
android:scrollbars="none"
android:textAppearance="?android:attr/textAppearanceListItem" android:textAppearance="?android:attr/textAppearanceListItem"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" 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" <org.lsposed.manager.ui.widget.EmptyStateRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:fadeScrollbars="true" android:fadeScrollbars="true"
android:scrollbarStyle="insideOverlay" android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical" android:scrollbars="vertical"
app:borderTopVisibility="whenTop"
app:borderTopDrawable="@null"
app:borderBottomVisibility="never" app:borderBottomVisibility="never"
app:borderTopDrawable="@null"
app:borderTopVisibility="whenTop"
app:fitsSystemWindowsInsets="bottom" /> app:fitsSystemWindowsInsets="bottom" />

View File

@ -17,29 +17,22 @@
~ Copyright (C) 2021 LSPosed Contributors ~ 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" 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_width="match_parent"
android:layout_height="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 <org.lsposed.manager.ui.widget.EmptyStateRecyclerView
android:id="@+id/list" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:fadeScrollbars="true" android:fadeScrollbars="true"
android:minHeight="?attr/listPreferredItemHeight"
android:scrollbarStyle="insideOverlay" android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical" /> android:scrollbars="vertical"
</FrameLayout> 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 ~ 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 <item
android:id="@+id/menu_search" android:id="@+id/menu_search"
android:actionViewClass="androidx.appcompat.widget.SearchView" android:actionViewClass="androidx.appcompat.widget.SearchView"
android:showAsAction="ifRoom" android:icon="@drawable/ic_baseline_search_24"
android:title="" /> android:showAsAction="always|collapseActionView"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/menu_launch"
android:icon="@drawable/ic_settings"
android:showAsAction="ifRoom"
android:title="@string/module_settings" />
<item <item
android:id="@+id/use_recommended" android:id="@+id/use_recommended"

View File

@ -25,12 +25,6 @@
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/menuSaveToSd" /> android:title="@string/menuSaveToSd" />
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_refresh"
android:showAsAction="ifRoom"
android:title="@string/menuReload" />
<item <item
android:id="@+id/menu_scroll_top" android:id="@+id/menu_scroll_top"
android:showAsAction="never" android:showAsAction="never"
@ -46,4 +40,9 @@
android:showAsAction="never" android:showAsAction="never"
android:title="@string/menuClearLog" /> 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> </menu>

View File

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

View File

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

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. ~ This file is part of LSPosed.
~ ~
~ LSPosed is free software: you can redistribute it and/or modify ~ 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. ~ This file is part of LSPosed.
~ ~
~ LSPosed is free software: you can redistribute it and/or modify ~ LSPosed is free software: you can redistribute it and/or modify

View File

@ -21,4 +21,65 @@
<resources> <resources>
<attr name="colorNormal" format="color" /> <attr name="colorNormal" format="color" />
<attr name="colorInstall" 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> </resources>

View File

@ -14,7 +14,6 @@
~ You should have received a copy of the GNU General Public License ~ You should have received a copy of the GNU General Public License
~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>. ~ along with LSPosed. If not, see <https://www.gnu.org/licenses/>.
~ ~
~ Copyright (C) 2020 EdXposed Contributors
~ Copyright (C) 2021 LSPosed Contributors ~ Copyright (C) 2021 LSPosed Contributors
--> -->
@ -84,6 +83,8 @@
<string name="menuReload">Reload</string> <string name="menuReload">Reload</string>
<string name="logs_clear_failed_2">Failed to clear the log</string> <string name="logs_clear_failed_2">Failed to clear the log</string>
<string name="menu_enable_word_wrap">Word Wrap</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 --> <!-- Notification -->
<string name="module_is_not_activated_yet">Xposed module is not activated yet</string> <string name="module_is_not_activated_yet">Xposed module is not activated yet</string>
@ -95,7 +96,6 @@
<!-- ModulesActivity --> <!-- ModulesActivity -->
<string name="module_empty_description">(no description provided)</string> <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="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="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> <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_brown">Brown</string>
<string name="color_grey">Grey</string> <string name="color_grey">Grey</string>
<string name="color_blue_grey">Blue grey</string> <string name="color_blue_grey">Blue grey</string>
<string name="feedback_or_suggestion">Feedback or suggestion</string>
</resources> </resources>

View File

@ -21,4 +21,14 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.Light" /> <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> </resources>