[app] Add update available to repo list (#1011)

* [app] Fix possible crashes

* [app] Add update available to repo list

* [app] Fix scrollbar
This commit is contained in:
tehcneko 2021-08-26 15:39:16 +08:00 committed by GitHub
parent 48c642e778
commit 5cf522b656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 148 deletions

View File

@ -35,14 +35,12 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Message;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -91,12 +89,13 @@ import rikka.core.res.ResourcesKt;
import rikka.widget.switchbar.SwitchBar; import rikka.widget.switchbar.SwitchBar;
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder> implements Filterable, Handler.Callback { public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder> implements Filterable {
private final Activity activity; private final Activity activity;
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 Handler loadAppListHandler;
private final ModuleUtil moduleUtil; private final ModuleUtil moduleUtil;
@ -143,9 +142,8 @@ public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder>
this.activity = fragment.requireActivity(); this.activity = fragment.requireActivity();
this.module = module; this.module = module;
moduleUtil = ModuleUtil.getInstance(); moduleUtil = ModuleUtil.getInstance();
HandlerThread handlerThread = new HandlerThread("appList");
handlerThread.start(); handlerThread.start();
loadAppListHandler = new Handler(handlerThread.getLooper(), this); loadAppListHandler = new Handler(handlerThread.getLooper());
preferences = App.getPreferences(); preferences = App.getPreferences();
pm = activity.getPackageManager(); pm = activity.getPackageManager();
} }
@ -269,7 +267,7 @@ public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder>
} else if (!AppHelper.onOptionsItemSelected(item, preferences)) { } else if (!AppHelper.onOptionsItemSelected(item, preferences)) {
return false; return false;
} }
refresh(false); refresh();
return true; return true;
} }
@ -444,52 +442,27 @@ public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder>
return showList.size(); return showList.size();
} }
public void refresh(boolean force) { public void onDestroy() {
loadAppListHandler.removeCallbacksAndMessages(null);
handlerThread.quit();
}
public void refresh() {
synchronized (this) { synchronized (this) {
if (refreshing) { if (refreshing) {
return; return;
} }
refreshing = true; refreshing = true;
} }
loadAppListHandler.removeMessages(0); loadAppListHandler.removeCallbacksAndMessages(null);
if (!force) { boolean force = fragment.binding.swipeRefreshLayout.isRefreshing();
fragment.binding.progress.setIndeterminate(true); 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.sendMessage(Message.obtain(loadAppListHandler, 0, force)); loadAppListHandler.post(() -> {
} List<PackageInfo> appList = AppHelper.getAppList(force);
protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, AppInfo appInfo) {
if (isChecked) {
checkedList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
}
if (!ConfigManager.setModuleScope(module.packageName, checkedList)) {
Snackbar.make(fragment.binding.snackbar, R.string.failed_to_save_scope_list, Snackbar.LENGTH_SHORT).show();
if (!isChecked) {
checkedList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
}
buttonView.setChecked(!isChecked);
} else if (appInfo.packageName.equals("android")) {
Snackbar.make(fragment.binding.snackbar, R.string.reboot_required, Snackbar.LENGTH_SHORT)
.setAction(R.string.reboot, v -> ConfigManager.reboot(false))
.show();
}
}
@Override
public boolean handleMessage(@NonNull Message msg) {
if (msg.what != 0) {
return false;
}
try {
List<PackageInfo> appList = AppHelper.getAppList((Boolean) msg.obj);
checkedList.clear(); checkedList.clear();
recommendedList.clear(); recommendedList.clear();
var tmpList = new ArrayList<AppInfo>(); var tmpList = new ArrayList<AppInfo>();
@ -543,12 +516,33 @@ public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder>
refreshing = false; refreshing = false;
} }
activity.runOnUiThread(dataReadyRunnable); activity.runOnUiThread(dataReadyRunnable);
try {
dataReadyRunnable.wait(); dataReadyRunnable.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
return true; }
} catch (Exception e) { });
Log.e(App.TAG, Log.getStackTraceString(e)); }
return false;
protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, AppInfo appInfo) {
if (isChecked) {
checkedList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
}
if (!ConfigManager.setModuleScope(module.packageName, checkedList)) {
Snackbar.make(fragment.binding.snackbar, R.string.failed_to_save_scope_list, Snackbar.LENGTH_SHORT).show();
if (!isChecked) {
checkedList.add(appInfo.application);
} else {
checkedList.remove(appInfo.application);
}
buttonView.setChecked(!isChecked);
} else if (appInfo.packageName.equals("android")) {
Snackbar.make(fragment.binding.snackbar, R.string.reboot_required, Snackbar.LENGTH_SHORT)
.setAction(R.string.reboot, v -> ConfigManager.reboot(false))
.show();
} }
} }
@ -609,13 +603,13 @@ public class ScopeAdapter extends RecyclerView.Adapter<ScopeAdapter.ViewHolder>
return new SearchView.OnQueryTextListener() { return new SearchView.OnQueryTextListener() {
@Override @Override
public boolean onQueryTextSubmit(String query) { public boolean onQueryTextSubmit(String query) {
refresh(false); refresh();
return true; return true;
} }
@Override @Override
public boolean onQueryTextChange(String newText) { public boolean onQueryTextChange(String newText) {
refresh(false); refresh();
return true; return true;
} }
}; };

View File

@ -21,6 +21,7 @@
package org.lsposed.manager.repo; package org.lsposed.manager.repo;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -39,6 +40,7 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import okhttp3.Call; import okhttp3.Call;
@ -50,6 +52,7 @@ 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 final Map<String, Pair<Integer, String>> 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 List<Listener> listeners = new CopyOnWriteArrayList<>();
private boolean isLoading = false; private boolean isLoading = false;
@ -106,6 +109,25 @@ public class RepoLoader {
Map<String, OnlineModule> modules = new HashMap<>(); Map<String, OnlineModule> modules = new HashMap<>();
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();
for (var module : repoModules) {
var release = module.getLatestRelease();
if (release == null || release.isEmpty()) continue;
var splits = release.split("-", 2);
if (splits.length < 2) continue;
int verCode;
String verName;
try {
verCode = Integer.parseInt(splits[0]);
verName = splits[1];
} catch (NumberFormatException ignored) {
continue;
}
String pkgName = module.getName();
latestVersion.put(pkgName, new Pair<>(verCode, verName));
}
onlineModules = modules; onlineModules = modules;
Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8));
for (Listener listener : listeners) { for (Listener listener : listeners) {
@ -129,6 +151,10 @@ public class RepoLoader {
}); });
} }
public Pair<Integer, String> getModuleLatestVersion(String packageName) {
return latestVersion.get(packageName);
}
public void loadRemoteReleases(String packageName) { public void loadRemoteReleases(String packageName) {
App.getOkHttpClient().newCall(new Request.Builder() App.getOkHttpClient().newCall(new Request.Builder()
.url(String.format(repoUrl + "module/%s.json", packageName)) .url(String.format(repoUrl + "module/%s.json", packageName))

View File

@ -81,7 +81,7 @@ public class AppListFragment extends BaseFragment {
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.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh(true)); binding.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh());
searchListener = scopeAdapter.getSearchListener(); searchListener = scopeAdapter.getSearchListener();
@ -150,7 +150,14 @@ public class AppListFragment extends BaseFragment {
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
scopeAdapter.refresh(false); scopeAdapter.refresh();
}
@Override
public void onDestroy() {
scopeAdapter.onDestroy();
super.onDestroy();
} }
@Override @Override

View File

@ -19,6 +19,7 @@
package org.lsposed.manager.ui.fragment; package org.lsposed.manager.ui.fragment;
import android.app.Activity;
import android.view.View; import android.view.View;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
@ -66,4 +67,11 @@ public class BaseFragment extends Fragment {
public Future<?> runAsync(Runnable runnable) { public Future<?> runAsync(Runnable runnable) {
return App.getExecutorService().submit(runnable); return App.getExecutorService().submit(runnable);
} }
public void runOnUiThread(Runnable runnable) {
Activity activity = getActivity();
if (activity != null && !activity.isFinishing()) {
activity.runOnUiThread(runnable);
}
}
} }

View File

@ -280,13 +280,19 @@ public class LogsFragment extends BaseFragment {
protected void onPostExecute(List<String> logs) { protected void onPostExecute(List<String> logs) {
adapter.setLogs(logs); adapter.setLogs(logs);
handler.removeCallbacks(mRunnable);//It loaded so fast that no need to show progress handler.removeCallbacks(mRunnable);
if (mProgressDialog.isShowing()) { if (mProgressDialog.isShowing()) {
mProgressDialog.dismiss(); mProgressDialog.dismiss();
} }
} }
} }
@Override
public void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
private class LogsAdapter extends RecyclerView.Adapter<LogsAdapter.ViewHolder> { private class LogsAdapter extends RecyclerView.Adapter<LogsAdapter.ViewHolder> {
ArrayList<String> logs = new ArrayList<>(); ArrayList<String> logs = new ArrayList<>();

View File

@ -32,15 +32,12 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
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;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -59,7 +56,6 @@ import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
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 androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.adapter.FragmentStateAdapter;
@ -89,8 +85,6 @@ import org.lsposed.manager.util.ModuleUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -101,8 +95,7 @@ import rikka.insets.WindowInsetsHelperKt;
import rikka.recyclerview.RecyclerViewKt; import rikka.recyclerview.RecyclerViewKt;
import rikka.widget.borderview.BorderRecyclerView; import rikka.widget.borderview.BorderRecyclerView;
public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener, RepoLoader.Listener { public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener {
private static final Handler workHandler;
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();
@ -113,16 +106,8 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
private final ArrayList<ModuleAdapter> adapters = new ArrayList<>(); private final ArrayList<ModuleAdapter> adapters = new ArrayList<>();
private final ArrayList<String> tabTitles = new ArrayList<>(); private final ArrayList<String> tabTitles = new ArrayList<>();
private final Map<String, Pair<Integer, String>> latestVersion = new ConcurrentHashMap<>();
private ModuleUtil.InstalledModule selectedModule; private ModuleUtil.InstalledModule selectedModule;
static {
HandlerThread workThread = new HandlerThread("ModulesActivity WorkHandler");
workThread.start();
workHandler = new Handler(workThread.getLooper());
}
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -142,18 +127,9 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
} }
@Override @Override
public void onAttach(@NonNull Context context) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onAttach(context); super.onViewCreated(view, savedInstanceState);
moduleUtil.addListener(this); moduleUtil.addListener(this);
repoLoader.addListener(this);
repoLoaded();
}
@Override
public void onDetach() {
moduleUtil.removeListener(this);
repoLoader.removeListener(this);
super.onDetach();
} }
@Nullable @Nullable
@ -281,16 +257,16 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
.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))
.setPositiveButton(android.R.string.ok, (dialog, which) -> .setPositiveButton(android.R.string.ok, (dialog, which) ->
workHandler.post(() -> { runAsync(() -> {
var success = ConfigManager.installExistingPackageAsUser(module.packageName, user.id); var success = ConfigManager.installExistingPackageAsUser(module.packageName, user.id);
requireActivity().runOnUiThread(() -> { String text = success ?
String text = success ? getString(R.string.module_installed, module.getAppName(), user.name) : getString(R.string.module_install_failed); getString(R.string.module_installed, module.getAppName(), user.name) :
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { getString(R.string.module_install_failed);
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_SHORT).show(); if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else { } else {
Toast.makeText(requireActivity(), text, Toast.LENGTH_SHORT).show(); 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);
})) }))
@ -330,16 +306,14 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
.setTitle(selectedModule.getAppName()) .setTitle(selectedModule.getAppName())
.setMessage(R.string.module_uninstall_message) .setMessage(R.string.module_uninstall_message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> .setPositiveButton(android.R.string.ok, (dialog, which) ->
workHandler.post(() -> { runAsync(() -> {
boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId); boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId);
requireActivity().runOnUiThread(() -> {
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 (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { if (binding != null && isResumed()) {
Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_SHORT).show(); Snackbar.make(binding.snackbar, text, Snackbar.LENGTH_LONG).show();
} else { } else {
Toast.makeText(requireActivity(), text, Toast.LENGTH_SHORT).show(); 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);
})) }))
@ -357,31 +331,10 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
moduleUtil.removeListener(this);
binding = null; binding = null;
} }
@Override
synchronized public void repoLoaded() {
latestVersion.clear();
for (var module : repoLoader.getOnlineModules()) {
var release = module.getLatestRelease();
if (release == null || release.isEmpty()) continue;
var splits = release.split("-", 2);
if (splits.length < 2) continue;
int verCode;
String verName;
try {
verCode = Integer.parseInt(splits[0]);
verName = splits[1];
} catch (NumberFormatException ignored) {
continue;
}
String pkgName = module.getName();
latestVersion.put(pkgName, new Pair<>(verCode, verName));
}
requireActivity().runOnUiThread(() -> adapters.forEach(ModuleAdapter::notifyDataSetChanged));
}
public static class ModuleListFragment extends Fragment { public static class ModuleListFragment extends Fragment {
@Nullable @Nullable
@Override @Override
@ -529,9 +482,8 @@ 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()) {
if (latestVersion.containsKey(item.packageName)) { var ver = repoLoader.getModuleLatestVersion(item.packageName);
var ver = latestVersion.get(item.packageName);
if (ver != null && ver.first > item.versionCode) { if (ver != null && ver.first > item.versionCode) {
sb.append("\n"); sb.append("\n");
String recommended = getString(R.string.update_available, ver.second); String recommended = getString(R.string.update_available, ver.second);
@ -630,7 +582,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
public void refresh(boolean force) { public void refresh(boolean force) {
if (force) moduleUtil.reloadInstalledModules(); if (force) moduleUtil.reloadInstalledModules();
requireActivity().runOnUiThread(reloadModules); runOnUiThread(reloadModules);
} }
private final Runnable reloadModules = new Runnable() { private final Runnable reloadModules = new Runnable() {
@ -651,7 +603,7 @@ public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleLi
searchList.clear(); searchList.clear();
searchList.addAll(tmpList); searchList.addAll(tmpList);
String queryStr = searchView != null ? searchView.getQuery().toString() : ""; String queryStr = searchView != null ? searchView.getQuery().toString() : "";
requireActivity().runOnUiThread(() -> getFilter().filter(queryStr)); runOnUiThread(() -> getFilter().filter(queryStr));
} }
}; };

View File

@ -19,11 +19,17 @@
package org.lsposed.manager.ui.fragment; package org.lsposed.manager.ui.fragment;
import android.graphics.Typeface;
import android.os.Build;
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.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -50,6 +56,7 @@ 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.util.ModuleUtil;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,6 +66,7 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import rikka.core.res.ResourcesKt;
import rikka.core.util.LabelComparator; import rikka.core.util.LabelComparator;
import rikka.recyclerview.RecyclerViewKt; import rikka.recyclerview.RecyclerViewKt;
@ -66,7 +74,7 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
protected FragmentRepoBinding binding; protected FragmentRepoBinding binding;
protected SearchView searchView; protected SearchView searchView;
private SearchView.OnQueryTextListener mSearchListener; private SearchView.OnQueryTextListener mSearchListener;
private Handler mHandler = new Handler(Looper.getMainLooper()); private final Handler mHandler = new Handler(Looper.getMainLooper());
private boolean preLoadWebview = true; private boolean preLoadWebview = true;
private final RepoLoader repoLoader = RepoLoader.getInstance(); private final RepoLoader repoLoader = RepoLoader.getInstance();
@ -121,9 +129,12 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
} }
@Override @Override
public void onDestroy() { public void onDestroyView() {
super.onDestroy(); super.onDestroyView();
mHandler.removeCallbacksAndMessages(null);
repoLoader.removeListener(this); repoLoader.removeListener(this);
binding = null;
} }
@Override @Override
@ -138,15 +149,9 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
} }
} }
@Override
public void onDetach() {
mHandler.removeCallbacksAndMessages(null);
super.onDetach();
}
@Override @Override
public void repoLoaded() { public void repoLoaded() {
requireActivity().runOnUiThread(() -> { runOnUiThread(() -> {
binding.progress.hide(); binding.progress.hide();
adapter.setData(repoLoader.getOnlineModules()); adapter.setData(repoLoader.getOnlineModules());
}); });
@ -201,6 +206,24 @@ public class RepoFragment extends BaseFragment implements RepoLoader.Listener {
sb.append("\n"); sb.append("\n");
sb.append(summary); sb.append(summary);
} }
ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(module.getName());
if (installedModule != null) {
var ver = repoLoader.getModuleLatestVersion(installedModule.packageName);
if (ver != null && ver.first > installedModule.versionCode) {
sb.append("\n");
String recommended = getString(R.string.update_available, ver.second);
sb.append(recommended);
final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourcesKt.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorAccent));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
holder.appDescription.setText(sb); holder.appDescription.setText(sb);
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
searchView.clearFocus(); searchView.clearFocus();

View File

@ -204,8 +204,8 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
public void moduleReleasesLoaded(OnlineModule module) { public void moduleReleasesLoaded(OnlineModule module) {
this.module = module; this.module = module;
if (releaseAdapter != null) { if (releaseAdapter != null) {
requireActivity().runOnUiThread(() -> releaseAdapter.loadItems()); runOnUiThread(() -> releaseAdapter.loadItems());
if (module.getReleases().size() == 1) { if (isResumed() && module.getReleases().size() == 1) {
Snackbar.make(binding.snackbar, R.string.module_release_no_more, Snackbar.LENGTH_SHORT).show(); Snackbar.make(binding.snackbar, R.string.module_release_no_more, Snackbar.LENGTH_SHORT).show();
} }
} }
@ -214,23 +214,18 @@ public class RepoItemFragment extends BaseFragment implements RepoLoader.Listene
@Override @Override
public void onThrowable(Throwable t) { public void onThrowable(Throwable t) {
if (releaseAdapter != null) { if (releaseAdapter != null) {
requireActivity().runOnUiThread(() -> releaseAdapter.loadItems()); runOnUiThread(() -> releaseAdapter.loadItems());
} if (isResumed()) {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show(); Snackbar.make(binding.snackbar, getString(R.string.repo_load_failed, t.getLocalizedMessage()), Snackbar.LENGTH_SHORT).show();
} }
} }
@Override
public void onDestroy() {
super.onDestroy();
RepoLoader.getInstance().removeListener(this);
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();
RepoLoader.getInstance().removeListener(this);
binding = null; binding = null;
} }

View File

@ -82,7 +82,7 @@
android:clipToPadding="false" android:clipToPadding="false"
android:fadeScrollbars="true" android:fadeScrollbars="true"
android:paddingTop="104dp" android:paddingTop="104dp"
android:scrollbarStyle="outsideOverlay" android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical" android:scrollbars="vertical"
app:borderTopVisibility="whenTop" app:borderTopVisibility="whenTop"
app:borderTopDrawable="@null" app:borderTopDrawable="@null"

View File

@ -25,7 +25,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.1.0-alpha09") classpath("com.android.tools.build:gradle:7.1.0-alpha10")
classpath("org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r") classpath("org.eclipse.jgit:org.eclipse.jgit:5.12.0.202106070339-r")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha07") classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha07")
} }