package com.fox2code.mmm; import android.app.Activity; import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.repo.RepoModule; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; public class ModuleViewListBuilder { private static final String TAG = "ModuleViewListBuilder"; private final EnumSet notifications = EnumSet.noneOf(NotificationType.class); private final HashMap mappedModuleHolders = new HashMap<>(); private final Object updateLock = new Object(); private final Object queryLock = new Object(); private final Activity activity; @NonNull private String query = ""; private int footerPx; private boolean noUpdate; public ModuleViewListBuilder(Activity activity) { this.activity = activity; } public void addNotification(NotificationType notificationType) { synchronized (this.updateLock) { this.notifications.add(notificationType); } } public void appendInstalledModules() { synchronized (this.updateLock) { for (ModuleHolder moduleHolder : this.mappedModuleHolders.values()) { moduleHolder.moduleInfo = null; } ModuleManager moduleManager = ModuleManager.getINSTANCE(); moduleManager.runAfterScan(() -> { Log.i(TAG, "A1: " + moduleManager.getModules().size()); for (ModuleInfo moduleInfo : moduleManager.getModules().values()) { ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id); if (moduleHolder == null) { this.mappedModuleHolders.put(moduleInfo.id, moduleHolder = new ModuleHolder(moduleInfo.id)); } moduleHolder.moduleInfo = moduleInfo; } }); } } public void appendRemoteModules() { synchronized (this.updateLock) { boolean showIncompatible = MainApplication.isShowIncompatibleModules(); for (ModuleHolder moduleHolder : this.mappedModuleHolders.values()) { moduleHolder.repoModule = null; } RepoManager repoManager = RepoManager.getINSTANCE(); repoManager.runAfterUpdate(() -> { Log.i(TAG, "A2: " + repoManager.getModules().size()); for (RepoModule repoModule : repoManager.getModules().values()) { if (!showIncompatible && (repoModule.moduleInfo.minApi > Build.VERSION.SDK_INT || // Only check Magisk compatibility if root is present (InstallerInitializer.peekMagiskPath() != null && repoModule.moduleInfo.minMagisk > InstallerInitializer.peekMagiskVersion() ))) continue; // Skip adding incompatible modules ModuleHolder moduleHolder = this.mappedModuleHolders.get(repoModule.id); if (moduleHolder == null) { this.mappedModuleHolders.put(repoModule.id, moduleHolder = new ModuleHolder(repoModule.id)); } moduleHolder.repoModule = repoModule; } }); } } public void applyTo(RecyclerView moduleList, ModuleViewAdapter moduleViewAdapter) { if (this.noUpdate) return; this.noUpdate = true; final ArrayList moduleHolders; final int newNotificationsLen; try { synchronized (this.updateLock) { // Build start moduleHolders = new ArrayList<>(); int special = 0; Iterator notificationTypeIterator = this.notifications.iterator(); while (notificationTypeIterator.hasNext()) { NotificationType notificationType = notificationTypeIterator.next(); if (notificationType.shouldRemove()) { notificationTypeIterator.remove(); } else { if (notificationType.special) special++; moduleHolders.add(new ModuleHolder(notificationType)); } } newNotificationsLen = this.notifications.size() - special; EnumSet headerTypes = EnumSet.of( ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.SEPARATOR); Iterator moduleHolderIterator = this.mappedModuleHolders.values().iterator(); synchronized (this.queryLock) { while (moduleHolderIterator.hasNext()) { ModuleHolder moduleHolder = moduleHolderIterator.next(); if (moduleHolder.shouldRemove()) { moduleHolderIterator.remove(); } else { ModuleHolder.Type type = moduleHolder.getType(); if (matchFilter(moduleHolder)) { if (headerTypes.add(type)) { moduleHolders.add(new ModuleHolder(type)); } moduleHolders.add(moduleHolder); } } } } Collections.sort(moduleHolders, ModuleHolder::compareTo); Log.i(TAG, "Got " + moduleHolders.size() + " entries!"); // Build end } } finally { this.noUpdate = false; } this.activity.runOnUiThread(() -> { final EnumSet oldNotifications = EnumSet.noneOf(NotificationType.class); if (this.footerPx != 0) { moduleHolders.add(new ModuleHolder(this.footerPx)); } boolean isTop = !moduleList.canScrollVertically(-1); boolean isBottom = !isTop && !moduleList.canScrollVertically(1); int oldNotificationsLen = 0; int oldOfflineModulesLen = 0; for (ModuleHolder moduleHolder : moduleViewAdapter.moduleHolders) { NotificationType notificationType = moduleHolder.notificationType; if (notificationType != null) { oldNotifications.add(notificationType); if (!notificationType.special) oldNotificationsLen++; } if (moduleHolder.separator == ModuleHolder.Type.INSTALLABLE) break; oldOfflineModulesLen++; } oldOfflineModulesLen -= oldNotificationsLen; int newOfflineModulesLen = 0; for (ModuleHolder moduleHolder : moduleHolders) { if (moduleHolder.separator == ModuleHolder.Type.INSTALLABLE) break; newOfflineModulesLen++; } newOfflineModulesLen -= newNotificationsLen; moduleViewAdapter.moduleHolders.size(); int newLen = moduleHolders.size(); int oldLen = moduleViewAdapter.moduleHolders.size(); moduleViewAdapter.moduleHolders.clear(); moduleViewAdapter.moduleHolders.addAll(moduleHolders); if (oldNotificationsLen != newNotificationsLen || !oldNotifications.equals(this.notifications)) { notifySizeChanged(moduleViewAdapter, 0, oldNotificationsLen, newNotificationsLen); } if (newLen - newNotificationsLen == 0) { notifySizeChanged(moduleViewAdapter, newNotificationsLen, oldLen - oldNotificationsLen, 0); } else { notifySizeChanged(moduleViewAdapter, newNotificationsLen, oldOfflineModulesLen, newOfflineModulesLen); notifySizeChanged(moduleViewAdapter, newNotificationsLen + newOfflineModulesLen, oldLen - oldNotificationsLen - oldOfflineModulesLen, newLen - newNotificationsLen - newOfflineModulesLen); } if (isTop) moduleList.scrollToPosition(0); if (isBottom) moduleList.scrollToPosition(newLen); }); } public void setFooterPx(int footerPx) { this.footerPx = Math.max(footerPx, 0); } private boolean matchFilter(ModuleHolder moduleHolder) { if (this.query.isEmpty()) return true; ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); return moduleInfo.id.toLowerCase(Locale.ROOT).contains(this.query) || moduleInfo.name.toLowerCase(Locale.ROOT).contains(this.query); } private static void notifySizeChanged(ModuleViewAdapter moduleViewAdapter, int index, int oldLen, int newLen) { // Log.i(TAG, "A: " + index + " " + oldLen + " " + newLen); if (oldLen == newLen) { if (newLen != 0) moduleViewAdapter.notifyItemRangeChanged(index, newLen); } else if (oldLen < newLen) { if (oldLen != 0) moduleViewAdapter.notifyItemRangeChanged(index, oldLen); moduleViewAdapter.notifyItemRangeInserted( index + oldLen, newLen - oldLen); } else { if (newLen != 0) moduleViewAdapter.notifyItemRangeChanged(index, newLen); moduleViewAdapter.notifyItemRangeRemoved( index + newLen, oldLen - newLen); } } public void setQuery(String query) { synchronized (this.queryLock) { Log.i(TAG, "Query " + this.query + " -> " + query); this.query = query == null ? "" : query.trim().toLowerCase(Locale.ROOT); } } public boolean setQueryChange(String query) { synchronized (this.queryLock) { String newQuery = query == null ? "" : query.trim().toLowerCase(Locale.ROOT); Log.i(TAG, "Query change " + this.query + " -> " + newQuery); if (this.query.equals(newQuery)) return false; this.query = newQuery; } return true; } }