content: /*
   * <!--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 static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
  
  import android.annotation.SuppressLint;
  import android.content.Intent;
  import android.content.pm.PackageInfo;
  import android.content.pm.PackageManager;
  import android.graphics.Typeface;
  import android.graphics.drawable.Drawable;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.text.Spannable;
  import android.text.SpannableStringBuilder;
  import android.text.TextUtils;
  import android.text.style.ForegroundColorSpan;
  import android.text.style.StyleSpan;
  import android.text.style.TypefaceSpan;
  import android.util.SparseArray;
  import android.view.LayoutInflater;
  import android.view.Menu;
  import android.view.MenuInflater;
  import android.view.MenuItem;
  import android.view.View;
  import android.view.ViewGroup;
  import android.widget.Filter;
  import android.widget.Filterable;
  import android.widget.ImageView;
  import android.widget.TextView;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.appcompat.widget.SearchView;
  import androidx.constraintlayout.widget.ConstraintLayout;
  import androidx.coordinatorlayout.widget.CoordinatorLayout;
  import androidx.core.view.MenuProvider;
  import androidx.fragment.app.Fragment;
  import androidx.navigation.NavOptions;
  import androidx.recyclerview.widget.LinearLayoutManager;
  import androidx.recyclerview.widget.RecyclerView;
  import androidx.viewpager2.adapter.FragmentStateAdapter;
  import androidx.viewpager2.widget.ViewPager2;
  
  import com.bumptech.glide.request.target.CustomTarget;
  import com.bumptech.glide.request.transition.Transition;
  import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
  import com.google.android.material.checkbox.MaterialCheckBox;
  import com.google.android.material.floatingactionbutton.FloatingActionButton;
  import com.google.android.material.tabs.TabLayout;
  import com.google.android.material.tabs.TabLayoutMediator;
  
  import org.lsposed.lspd.models.UserInfo;
  import org.lsposed.manager.App;
  import org.lsposed.manager.ConfigManager;
  import org.lsposed.manager.R;
  import org.lsposed.manager.adapters.AppHelper;
  import org.lsposed.manager.databinding.FragmentPagerBinding;
  import org.lsposed.manager.databinding.ItemModuleBinding;
  import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding;
  import org.lsposed.manager.repo.RepoLoader;
  import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder;
  import org.lsposed.manager.ui.widget.EmptyStateRecyclerView;
  import org.lsposed.manager.util.GlideApp;
  import org.lsposed.manager.util.ModuleUtil;
  
  import java.util.ArrayList;
  import java.util.Comparator;
  import java.util.HashSet;
  import java.util.List;
  import java.util.function.Consumer;
  import java.util.stream.IntStream;
  
  import rikka.core.util.ResourceUtils;
  import rikka.material.app.LocaleDelegate;
  import rikka.recyclerview.RecyclerViewKt;
  
  public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener, RepoLoader.RepoListener, MenuProvider {
      private static final PackageManager pm = App.getInstance().getPackageManager();
      private static final ModuleUtil moduleUtil = ModuleUtil.getInstance();
      private static final RepoLoader repoLoader = RepoLoader.getInstance();
      protected FragmentPagerBinding binding;
      protected SearchView searchView;
      private SearchView.OnQueryTextListener searchListener;
  
      SparseArray<ModuleAdapter> adapters = new SparseArray<>();
      PagerAdapter pagerAdapter = null;
  
      private ModuleUtil.InstalledModule selectedModule;
  
      @Override
      public void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          searchListener = new SearchView.OnQueryTextListener() {
              @Override
              public boolean onQueryTextSubmit(String query) {
                  forEachAdaptor(adapter -> adapter.getFilter().filter(query));
                  return false;
              }
  
              @Override
              public boolean onQueryTextChange(String query) {
                  forEachAdaptor(adapter -> adapter.getFilter().filter(query));
                  return false;
              }
          };
      }
  
      private void forEachAdaptor(Consumer<? super ModuleAdapter> action) {
          var snapshot = adapters;
          for (var i = 0; i < snapshot.size(); ++i) {
              action.accept(snapshot.valueAt(i));
          }
      }
  
      private void showFab() {
          var layoutParams = binding.fab.getLayoutParams();
          if (layoutParams instanceof CoordinatorLayout.LayoutParams) {
              var coordinatorLayoutBehavior =
                      ((CoordinatorLayout.LayoutParams) layoutParams).getBehavior();
              if (coordinatorLayoutBehavior instanceof HideBottomViewOnScrollBehavior) {
                  //noinspection unchecked
                  ((HideBottomViewOnScrollBehavior<FloatingActionButton>) coordinatorLayoutBehavior).slideUp(binding.fab);
              }
          }
      }
  
      @Nullable
      @Override
      public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
          binding = FragmentPagerBinding.inflate(inflater, container, false);
          binding.appBar.setLiftable(true);
          setupToolbar(binding.toolbar, binding.clickView, R.string.Modules, R.menu.menu_modules);
          binding.toolbar.setNavigationIcon(null);
          pagerAdapter = new PagerAdapter(this);
          binding.viewPager.setAdapter(pagerAdapter);
          binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
              @Override
              public void onPageSelected(int position) {
                  showFab();
              }
          });
  
          new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> {
              if (position < adapters.size()) {
                  tab.setText(adapters.valueAt(position).getUser().name);
              }
          }).attach();
  
          binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
              ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0);
              int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum();
              if (tabLayoutWidth <= binding.getRoot().getWidth()) {
                  binding.tabLayout.setTabMode(TabLayout.MODE_FIXED);
                  binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
              }
          });
  
          binding.fab.setOnClickListener(v -> {
              var bundle = new Bundle();
              var user = adapters.valueAt(binding.viewPager.getCurrentItem()).getUser();
              bundle.putParcelable("userInfo", user);
              var f = new RecyclerViewDialogFragment();
              f.setArguments(bundle);
              f.show(getChildFragmentManager(), "install_to_user" + user.id);
          });
  
          moduleUtil.addListener(this);
          repoLoader.addListener(this);
          onModulesReloaded();
  
          return binding.getRoot();
      }
  
      @Override
      public void onPrepareMenu(Menu menu) {
          searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView();
          if (searchView != null) {
              searchView.setOnQueryTextListener(searchListener);
              searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                  @Override
                  public void onViewAttachedToWindow(@NonNull View arg0) {
                      binding.appBar.setExpanded(false, true);
                  }
  
                  @Override
                  public void onViewDetachedFromWindow(@NonNull View v) {
                  }
              });
              searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
          }
      }
  
      @Override
      public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
  
      }
  
      @Override
      public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
          return false;
      }
  
      @Override
      public void onResume() {
          super.onResume();
          forEachAdaptor(ModuleAdapter::refresh);
      }
  
      @Override
      public void onSingleModuleReloaded(ModuleUtil.InstalledModule module) {
          forEachAdaptor(ModuleAdapter::refresh);
      }
  
      @Override
      public void onModulesReloaded() {
          var users = moduleUtil.getUsers();
          if (users == null) return;
  
          if (users.size() != 1) {
              binding.viewPager.setUserInputEnabled(true);
              binding.tabLayout.setVisibility(View.VISIBLE);
              binding.fab.show();
          } else {
              binding.viewPager.setUserInputEnabled(false);
              binding.tabLayout.setVisibility(View.GONE);
          }
  
          var tmp = new SparseArray<ModuleAdapter>(users.size());
          var snapshot = adapters;
          for (var user : users) {
              if (snapshot.indexOfKey(user.id) >= 0) {
                  tmp.put(user.id, snapshot.get(user.id));
              } else {
                  var adapter = new ModuleAdapter(user);
                  adapter.setHasStableIds(true);
                  tmp.put(user.id, adapter);
              }
          }
          adapters = tmp;
          forEachAdaptor(ModuleAdapter::refresh);
          runOnUiThread(pagerAdapter::notifyDataSetChanged);
          updateModuleSummary();
      }
  
      @Override
      public void onRepoLoaded() {
          forEachAdaptor(ModuleAdapter::refresh);
      }
  
      private void updateModuleSummary() {
          var moduleCount = moduleUtil.getEnabledModulesCount();
          runOnUiThread(() -> {
              if (binding != null) {
                  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(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons)
                  .setTitle(getString(R.string.install_to_user, user.name))
                  .setMessage(getString(R.string.install_to_user_message, module.getAppName(), user.name))
                  .setPositiveButton(android.R.string.ok, (dialog, which) ->
                          runAsync(() -> {
                              var success = ConfigManager.installExistingPackageAsUser(module.packageName, user.id);
                              String text = success ?
                                      getString(R.string.module_installed, module.getAppName(), user.name) :
                                      getString(R.string.module_install_failed);
                              showHint(text, false);
                              if (success)
                                  moduleUtil.reloadSingleModule(module.packageName, user.id);
                          }))
                  .setNegativeButton(android.R.string.cancel, null)
                  .show();
      }
  
      @SuppressLint("WrongConstant")
      @Override
      public boolean onContextItemSelected(@NonNull MenuItem item) {
          if (selectedModule == null) {
              return false;
          }
          int itemId = item.getItemId();
          if (itemId == R.id.menu_launch) {
              String packageName = selectedModule.packageName;
              if (packageName == null) {
                  return false;
              }
              Intent intent = AppHelper.getSettingsIntent(packageName, selectedModule.userId);
              if (intent != null) {
                  ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
              }
              return true;
          } else if (itemId == R.id.menu_other_app) {
              var intent = new Intent(Intent.ACTION_SHOW_APP_INFO);
              intent.putExtra(Intent.EXTRA_PACKAGE_NAME, selectedModule.packageName);
              intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
              ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId);
              return true;
          } else if (itemId == R.id.menu_app_info) {
              ConfigManager.startActivityAsUserWithFeature(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", selectedModule.packageName, null)), selectedModule.userId);
              return true;
          } else if (itemId == R.id.menu_uninstall) {
              new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_FullWidthButtons)
                      .setIcon(selectedModule.app.loadIcon(pm))
                      .setTitle(selectedModule.getAppName())
                      .setMessage(R.string.module_uninstall_message)
                      .setPositiveButton(android.R.string.ok, (dialog, which) ->
                              runAsync(() -> {
                                  boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId);
                                  String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed);
                                  showHint(text, false);
                                  if (success)
                                      moduleUtil.reloadSingleModule(selectedModule.packageName, selectedModule.userId);
                              }))
                      .setNegativeButton(android.R.string.cancel, null)
                      .show();
              return true;
          } else if (itemId == R.id.menu_repo) {
              var navController = getNavController();
              navController.navigate(
                      new Uri.Builder().scheme("lsposed").authority("repo").appendQueryParameter("modulePackageName", selectedModule.packageName).build(),
                      new NavOptions.Builder().setEnterAnim(R.anim.fragment_enter).setExitAnim(R.anim.fragment_exit).setPopEnterAnim(R.anim.fragment_enter_pop).setPopExitAnim(R.anim.fragment_exit_pop).setLaunchSingleTop(true).setPopUpTo(getNavController().getGraph().getStartDestinationId(), false, true).build());
              return true;
          } else if (itemId == R.id.menu_compile_speed) {
              CompileDialogFragment.speed(getChildFragmentManager(), selectedModule.pkg.applicationInfo);
          }
          return super.onContextItemSelected(item);
      }
  
      @Override
      public void onDestroyView() {
          super.onDestroyView();
          moduleUtil.removeListener(this);
          repoLoader.removeListener(this);
          binding = null;
      }
  
      public static class ModuleListFragment extends Fragment {
          public SwiperefreshRecyclerviewBinding binding;
          private ModuleAdapter adapter;
          private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() {
              @Override
              public void onChanged() {
                  binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded());
              }
          };
  
          private final View.OnAttachStateChangeListener searchViewLocker = new View.OnAttachStateChangeListener() {
              @Override
              public void onViewAttachedToWindow(@NonNull View v) {
                  binding.recyclerView.setNestedScrollingEnabled(false);
              }
  
              @Override
              public void onViewDetachedFromWindow(@NonNull View v) {
                  binding.recyclerView.setNestedScrollingEnabled(true);
              }
          };
  
          @Nullable
          @Override
          public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
              ModulesFragment fragment = (ModulesFragment) getParentFragment();
              Bundle arguments = getArguments();
              if (fragment == null || arguments == null) {
                  return null;
              }
              int userId = arguments.getInt("user_id");
              binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false);
              adapter = fragment.adapters.get(userId);
              binding.recyclerView.setAdapter(adapter);
              binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));
              binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh);
              binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset());
              RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true);
              adapter.registerAdapterDataObserver(observer);
              return binding.getRoot();
          }
  
          void attachListeners() {
              var parent = getParentFragment();
              if (parent instanceof ModulesFragment moduleFragment) {
                  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 moduleFragment) {
                  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 {
  
          public PagerAdapter(@NonNull Fragment fragment) {
              super(fragment);
          }
  
          @NonNull
          @Override
          public Fragment createFragment(int position) {
              Bundle bundle = new Bundle();
              bundle.putInt("user_id", adapters.keyAt(position));
              Fragment fragment = new ModuleListFragment();
              fragment.setArguments(bundle);
              return fragment;
          }
  
          @Override
          public int getItemCount() {
              return adapters.size();
          }
  
          @Override
          public long getItemId(int position) {
              return adapters.keyAt(position);
          }
  
          @Override
          public boolean containsItem(long itemId) {
              return adapters.indexOfKey((int) itemId) >= 0;
          }
      }
  
      ModuleAdapter createPickModuleAdapter(UserInfo userInfo) {
          return new ModuleAdapter(userInfo, true);
      }
  
      class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter<ModuleAdapter.ViewHolder> implements Filterable {
          private List<ModuleUtil.InstalledModule> searchList = new ArrayList<>();
          private List<ModuleUtil.InstalledModule> showList = new ArrayList<>();
          private final UserInfo user;
          private final boolean isPick;
          private boolean isLoaded;
          private View.OnClickListener onPickListener;
  
          ModuleAdapter(UserInfo user) {
              this(user, false);
          }
  
          ModuleAdapter(UserInfo user, boolean isPick) {
              this.user = user;
              this.isPick = isPick;
          }
  
          public UserInfo getUser() {
              return user;
          }
  
          @NonNull
          @Override
          public ModuleAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
              return new ViewHolder(ItemModuleBinding.inflate(getLayoutInflater(), parent, false));
          }
  
          public boolean isPick() {
              return isPick;
          }
  
          @Override
          public void onBindViewHolder(@NonNull ModuleAdapter.ViewHolder holder, int position) {
              ModuleUtil.InstalledModule item = showList.get(position);
              String appName;
              if (item.userId != 0) {
                  appName = String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", item.getAppName(), item.userId);
              } else {
                  appName = item.getAppName();
              }
              holder.appName.setText(appName);
              GlideApp.with(holder.appIcon)
                      .load(item.getPackageInfo())
                      .into(new CustomTarget<Drawable>() {
                          @Override
                          public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
                              holder.appIcon.setImageDrawable(resource);
                          }
  
                          @Override
                          public void onLoadCleared(@Nullable Drawable placeholder) {
  
                          }
                      });
              SpannableStringBuilder sb = new SpannableStringBuilder();
              if (!item.getDescription().isEmpty()) {
                  sb.append(item.getDescription());
              } else {
                  sb.append(getString(R.string.module_empty_description));
              }
              holder.appDescription.setText(sb);
              holder.appDescription.setVisibility(View.VISIBLE);
              sb = new SpannableStringBuilder();
  
              int installXposedVersion = ConfigManager.getXposedApiVersion();
              String warningText = null;
              if (item.minVersion == 0) {
                  warningText = getString(R.string.no_min_version_specified);
              } else if (installXposedVersion > 0 && item.minVersion > installXposedVersion) {
                  warningText = getString(R.string.warning_xposed_min_version, item.minVersion);
              } else if (item.targetVersion > installXposedVersion) {
                  warningText = getString(R.string.warning_target_version_higher, item.targetVersion);
              } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) {
                  warningText = getString(R.string.warning_min_version_too_low, item.minVersion, ModuleUtil.MIN_MODULE_VERSION);
              } else if (item.isInstalledOnExternalStorage()) {
                  warningText = getString(R.string.warning_installed_on_external_storage);
              }
              if (warningText != null) {
                  sb.append(warningText);
                  final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorError));
                  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() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                  } else {
                      final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
                      sb.setSpan(styleSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                  }
                  sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
              }
              var ver = repoLoader.getModuleLatestVersion(item.packageName);
              if (ver != null && ver.upgradable(item.versionCode, item.versionName)) {
                  if (warningText != null) sb.append("\n");
                  String recommended = getString(R.string.update_available, ver.versionName);
                  sb.append(recommended);
                  final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorPrimary));
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                      final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL));
                      sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                  } else {
                      final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD);
                      sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
                  }
                  sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
              }
              if (sb.length() == 0) {
                  holder.hint.setVisibility(View.GONE);
              } else {
                  holder.hint.setVisibility(View.VISIBLE);
                  holder.hint.setText(sb);
              }
  
              if (!isPick) {
                  holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f);
                  holder.itemView.setOnClickListener(v -> {
                      searchView.clearFocus();
                      safeNavigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId));
                  });
                  holder.itemView.setOnLongClickListener(v -> {
                      searchView.clearFocus();
                      selectedModule = item;
                      return false;
                  });
                  holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
                      requireActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu);
                      menu.setHeaderTitle(item.getAppName());
                      Intent intent = AppHelper.getSettingsIntent(item.packageName, item.userId);
                      if (intent == null) {
                          menu.removeItem(R.id.menu_launch);
                      }
                      if (repoLoader.getOnlineModule(item.packageName) == null) {
                          menu.removeItem(R.id.menu_repo);
                      }
                      if (item.userId == 0) {
                          var users = ConfigManager.getUsers();
                          if (users != null) {
                              for (var user : users) {
                                  if (moduleUtil.getModule(item.packageName, user.id) == null) {
                                      menu.add(1, user.id, 0, getString(R.string.install_to_user, user.name)).setOnMenuItemClickListener(i -> {
                                          installModuleToUser(selectedModule, user);
                                          return true;
                                      });
                                  }
                              }
                          }
                      }
                  });
                  holder.appVersion.setVisibility(View.VISIBLE);
                  holder.appVersion.setText(item.versionName);
                  holder.appVersion.setSelected(true);
              } else {
                  holder.itemView.setTag(item);
                  holder.itemView.setOnClickListener(v -> {
                      if (onPickListener != null) onPickListener.onClick(v);
                  });
              }
          }
  
          @Override
          public void onViewRecycled(@NonNull ViewHolder holder) {
              holder.itemView.setTag(null);
              super.onViewRecycled(holder);
          }
  
          @Override
          public int getItemCount() {
              return showList.size();
          }
  
          @Override
          public long getItemId(int position) {
              var module = showList.get(position);
              return (module.packageName + "!" + module.userId).hashCode();
          }
  
          @Override
          public Filter getFilter() {
              return new ModuleAdapter.ApplicationFilter();
          }
  
          public void setOnPickListener(View.OnClickListener onPickListener) {
              this.onPickListener = onPickListener;
          }
  
          public void refresh() {
              runAsync(reloadModules);
          }
  
          public void fullRefresh() {
              runAsync(() -> {
                  setLoaded(null, false);
                  moduleUtil.reloadInstalledModules();
                  refresh();
              });
          }
  
          private final Runnable reloadModules = () -> {
              var modules = moduleUtil.getModules();
              if (modules == null) return;
              Comparator<PackageInfo> cmp = AppHelper.getAppListComparator(0, pm);
              setLoaded(null, false);
              var tmpList = new ArrayList<ModuleUtil.InstalledModule>();
              modules.values().parallelStream()
                      .sorted((a, b) -> {
                          boolean aChecked = moduleUtil.isModuleEnabled(a.packageName);
                          boolean bChecked = moduleUtil.isModuleEnabled(b.packageName);
                          if (aChecked == bChecked) {
                              var c = cmp.compare(a.pkg, b.pkg);
                              if (c == 0) {
                                  if (a.userId == getUser().id) return -1;
                                  if (b.userId == getUser().id) return 1;
                                  else return Integer.compare(a.userId, b.userId);
                              }
                              return c;
                          } else if (aChecked) {
                              return -1;
                          } else {
                              return 1;
                          }
                      }).forEachOrdered(new Consumer<>() {
                          private final HashSet<String> uniquer = new HashSet<>();
  
                          @Override
                          public void accept(ModuleUtil.InstalledModule module) {
                              if (isPick()) {
                                  if (!uniquer.contains(module.packageName)) {
                                      uniquer.add(module.packageName);
                                      if (module.userId != getUser().id)
                                          tmpList.add(module);
                                  }
                              } else if (module.userId == getUser().id) {
                                  tmpList.add(module);
                              }
                          }
                      });
              String queryStr = searchView != null ? searchView.getQuery().toString() : "";
              searchList = tmpList;
              runOnUiThread(() -> getFilter().filter(queryStr));
          };
  
          @SuppressLint("NotifyDataSetChanged")
          private void setLoaded(List<ModuleUtil.InstalledModule> list, boolean loaded) {
              runOnUiThread(() -> {
                  if (list != null) showList = list;
                  isLoaded = loaded;
                  notifyDataSetChanged();
              });
          }
  
          @Override
          public boolean isLoaded() {
              return isLoaded && moduleUtil.isModulesLoaded();
          }
  
          static class ViewHolder extends RecyclerView.ViewHolder {
              ConstraintLayout root;
              ImageView appIcon;
              TextView appName;
              TextView appDescription;
              TextView appVersion;
              TextView hint;
              MaterialCheckBox checkBox;
  
              ViewHolder(ItemModuleBinding binding) {
                  super(binding.getRoot());
                  root = binding.itemRoot;
                  appIcon = binding.appIcon;
                  appName = binding.appName;
                  appDescription = binding.description;
                  appVersion = binding.versionName;
                  hint = binding.hint;
                  checkBox = binding.checkbox;
              }
          }
  
          class ApplicationFilter extends Filter {
  
              private boolean lowercaseContains(String s, String filter) {
                  return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter);
              }
  
              @Override
              protected FilterResults performFiltering(CharSequence constraint) {
                  FilterResults filterResults = new FilterResults();
                  List<ModuleUtil.InstalledModule> filtered = new ArrayList<>();
                  String filter = constraint.toString().toLowerCase();
                  for (ModuleUtil.InstalledModule info : searchList) {
                      if (lowercaseContains(info.getAppName(), filter) ||
                              lowercaseContains(info.packageName, filter) ||
                              lowercaseContains(info.getDescription(), filter)) {
                          filtered.add(info);
                      }
                  }
                  filterResults.values = filtered;
                  filterResults.count = filtered.size();
                  return filterResults;
              }
  
              @Override
              protected void publishResults(CharSequence constraint, FilterResults results) {
                  //noinspection unchecked
                  setLoaded((List<ModuleUtil.InstalledModule>) results.values, true);
              }
          }
      }
  }
  