diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppManageAdapter.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppManageAdapter.kt new file mode 100644 index 0000000..75f1713 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppManageAdapter.kt @@ -0,0 +1,34 @@ +package icu.nullptr.hidemyapplist.ui.adapter + +import android.view.ViewGroup +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.view.AppItemView + +class AppManageAdapter( + private val onItemClickListener: (String) -> Unit +) : AppSelectAdapter() { + + inner class ViewHolder(view: AppItemView) : AppSelectAdapter.ViewHolder(view) { + init { + view.setOnClickListener { + onItemClickListener.invoke(filteredList[absoluteAdapterPosition]) + } + } + + override fun bind(packageName: String) { + (itemView as AppItemView).let { + it.load(packageName) + it.showEnabled = ConfigManager.isHideEnabled(packageName) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = AppItemView(parent.context, false) + view.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + return ViewHolder(view) + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppScopeAdapter.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppScopeAdapter.kt new file mode 100644 index 0000000..5e1facf --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppScopeAdapter.kt @@ -0,0 +1,42 @@ +package icu.nullptr.hidemyapplist.ui.adapter + +import android.view.ViewGroup +import icu.nullptr.hidemyapplist.ui.view.AppItemView + +class AppScopeAdapter( + private val checked: MutableSet, + firstFilter: ((String) -> Boolean)?, +) : AppSelectAdapter(firstFilter) { + + private inline var String.isChecked + get() = checked.contains(this) + set(value) { + if (value) checked.add(this) else checked.remove(this) + } + + inner class ViewHolder(view: AppItemView) : AppSelectAdapter.ViewHolder(view) { + init { + view.setOnClickListener { + val packageName = filteredList[absoluteAdapterPosition] + packageName.isChecked = !packageName.isChecked + view.isChecked = packageName.isChecked + } + } + + override fun bind(packageName: String) { + (itemView as AppItemView).let { + it.load(packageName) + it.isChecked = packageName.isChecked + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = AppItemView(parent.context, true) + view.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + return ViewHolder(view) + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppSelectAdapter.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppSelectAdapter.kt new file mode 100644 index 0000000..3474bc5 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/AppSelectAdapter.kt @@ -0,0 +1,54 @@ +package icu.nullptr.hidemyapplist.ui.adapter + +import android.widget.Filter +import android.widget.Filterable +import androidx.recyclerview.widget.RecyclerView +import icu.nullptr.hidemyapplist.service.PrefManager +import icu.nullptr.hidemyapplist.ui.view.AppItemView +import icu.nullptr.hidemyapplist.util.PackageHelper +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +abstract class AppSelectAdapter( + private val firstFilter: ((String) -> Boolean)? = null +) : RecyclerView.Adapter(), Filterable { + + abstract class ViewHolder(view: AppItemView) : RecyclerView.ViewHolder(view) { + abstract fun bind(packageName: String) + } + + private inner class AppFilter : Filter() { + override fun performFiltering(constraint: CharSequence): FilterResults { + return runBlocking { + val constraintLowered = constraint.toString().lowercase() + val filteredList = PackageHelper.appList.first().filter { + if (firstFilter?.invoke(it) == false) return@filter false + if (!PrefManager.appFilter_showSystem && PackageHelper.isSystem(it)) return@filter false + val label = PackageHelper.loadAppLabel(it) + val packageInfo = PackageHelper.loadPackageInfo(it) + label.lowercase().contains(constraintLowered) || packageInfo.packageName.lowercase().contains(constraintLowered) + } + + FilterResults().also { it.values = filteredList } + } + } + + @Suppress("UNCHECKED_CAST", "NotifyDataSetChanged") + override fun publishResults(constraint: CharSequence, results: FilterResults) { + filteredList = results.values as List + notifyDataSetChanged() + } + } + + private val mFilter = AppFilter() + + protected var filteredList: List = listOf() + + override fun getItemCount() = filteredList.size + + override fun getItemId(position: Int) = filteredList[position].hashCode().toLong() + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(filteredList[position]) + + override fun getFilter(): Filter = mFilter +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/TemplateAdapter.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/TemplateAdapter.kt new file mode 100644 index 0000000..b0881d2 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/adapter/TemplateAdapter.kt @@ -0,0 +1,68 @@ +package icu.nullptr.hidemyapplist.ui.adapter + +import android.annotation.SuppressLint +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.view.ListItemView +import org.frknkrc44.hma_oss.R +import java.text.Collator +import java.util.Locale + +class TemplateAdapter( + private val onClickListener: ((ConfigManager.TemplateInfo) -> Unit)? +) : RecyclerView.Adapter() { + + private lateinit var list: List + + init { + updateList() + } + + inner class ViewHolder(view: ListItemView) : RecyclerView.ViewHolder(view) { + init { + view.setOnClickListener { + onClickListener?.invoke(list[absoluteAdapterPosition]) + } + } + + fun bind(info: ConfigManager.TemplateInfo) { + with(itemView as ListItemView) { + setIcon( + if (info.isWhiteList) R.drawable.outline_assignment_24 + else R.drawable.baseline_assignment_24 + ) + text = info.name + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = ListItemView(parent.context) + view.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + return ViewHolder(view) + } + + override fun getItemCount() = list.size + + override fun getItemId(position: Int) = list[position].name.hashCode().toLong() + + override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(list[position]) + + @SuppressLint("NotifyDataSetChanged") + fun updateList() { + list = ConfigManager.getTemplateList().apply { + sortWith { o1, o2 -> + if (o1.isWhiteList != o2.isWhiteList) { + o1.isWhiteList.compareTo(o2.isWhiteList) + } else { + Collator.getInstance(Locale.getDefault()).compare(o1.name, o2.name) + } + } + } + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppManageFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppManageFragment.kt new file mode 100644 index 0000000..c9f27e7 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppManageFragment.kt @@ -0,0 +1,24 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.os.Bundle +import com.google.android.material.transition.MaterialSharedAxis +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.adapter.AppManageAdapter +import icu.nullptr.hidemyapplist.ui.util.navController +import org.frknkrc44.hma_oss.R + +class AppManageFragment : AppSelectFragment() { + + override val firstComparator: Comparator = Comparator.comparing(ConfigManager::isHideEnabled).reversed() + + override val adapter = AppManageAdapter { + val args = AppSettingsFragmentArgs(it) + navController.navigate(R.id.nav_app_settings, args.toBundle()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + // returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSelectFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSelectFragment.kt new file mode 100644 index 0000000..fcb19a1 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSelectFragment.kt @@ -0,0 +1,124 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.activity.addCallback +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import icu.nullptr.hidemyapplist.service.PrefManager +import icu.nullptr.hidemyapplist.ui.adapter.AppSelectAdapter +import icu.nullptr.hidemyapplist.ui.util.navController +import icu.nullptr.hidemyapplist.ui.util.setupToolbar +import icu.nullptr.hidemyapplist.util.PackageHelper +import kotlinx.coroutines.launch +import org.frknkrc44.hma_oss.R +import org.frknkrc44.hma_oss.databinding.FragmentAppSelectBinding + +abstract class AppSelectFragment : Fragment(R.layout.fragment_app_select) { + + private val binding by viewBinding() + + protected abstract val firstComparator: Comparator + protected abstract val adapter: AppSelectAdapter + + private var search = "" + + protected open fun onBack() { + navController.navigateUp() + } + + private fun applyFilter() { + adapter.filter.filter(search) + } + + private fun sortList() { + lifecycleScope.launch { + PackageHelper.sortList(firstComparator) + applyFilter() + } + } + + private fun onMenuOptionSelected(item: MenuItem) { + when (item.itemId) { + R.id.menu_show_system -> { + item.isChecked = !item.isChecked + PrefManager.appFilter_showSystem = item.isChecked + } + R.id.menu_sort_by_label -> { + item.isChecked = true + PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_LABEL + } + R.id.menu_sort_by_package_name -> { + item.isChecked = true + PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_PACKAGE_NAME + } + R.id.menu_sort_by_install_time -> { + item.isChecked = true + PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_INSTALL_TIME + } + R.id.menu_sort_by_update_time -> { + item.isChecked = true + PrefManager.appFilter_sortMethod = PrefManager.SortMethod.BY_UPDATE_TIME + } + R.id.menu_reverse_order -> { + item.isChecked = !item.isChecked + PrefManager.appFilter_reverseOrder = item.isChecked + } + } + sortList() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack() } + setupToolbar( + toolbar = binding.toolbar, + title = getString(R.string.title_app_select), + navigationIcon = R.drawable.baseline_arrow_back_24, + navigationOnClick = { onBack() }, + menuRes = R.menu.menu_app_list, + onMenuOptionSelected = this::onMenuOptionSelected + ) + + with(binding.toolbar.menu) { + val searchView = findItem(R.id.menu_search).actionView as SearchView + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String) = false + + override fun onQueryTextChange(newText: String): Boolean { + search = newText + applyFilter() + return true + } + }) + + findItem(R.id.menu_show_system).isChecked = PrefManager.appFilter_showSystem + when (PrefManager.appFilter_sortMethod) { + PrefManager.SortMethod.BY_LABEL -> findItem(R.id.menu_sort_by_label).isChecked = true + PrefManager.SortMethod.BY_PACKAGE_NAME -> findItem(R.id.menu_sort_by_package_name).isChecked = true + PrefManager.SortMethod.BY_INSTALL_TIME -> findItem(R.id.menu_sort_by_install_time).isChecked = true + PrefManager.SortMethod.BY_UPDATE_TIME -> findItem(R.id.menu_sort_by_update_time).isChecked = true + } + findItem(R.id.menu_reverse_order).isChecked = PrefManager.appFilter_reverseOrder + } + binding.list.layoutManager = LinearLayoutManager(context) + binding.list.adapter = adapter + binding.swipeRefresh.setOnRefreshListener { + PackageHelper.invalidateCache() + } + + lifecycleScope.launch { + PackageHelper.isRefreshing + .flowWithLifecycle(lifecycle) + .collect { + binding.swipeRefresh.isRefreshing = it + } + } + + sortList() + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt new file mode 100644 index 0000000..63f8c9d --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/AppSettingsFragment.kt @@ -0,0 +1,210 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.activity.addCallback +import androidx.core.graphics.drawable.toDrawable +import androidx.fragment.app.Fragment +import androidx.fragment.app.clearFragmentResultListener +import androidx.fragment.app.setFragmentResultListener +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.navArgs +import androidx.preference.Preference +import androidx.preference.PreferenceDataStore +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference +import androidx.preference.SwitchPreferenceCompat +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import icu.nullptr.hidemyapplist.common.JsonConfig +import icu.nullptr.hidemyapplist.common.Presets +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.util.navController +import icu.nullptr.hidemyapplist.ui.util.setupToolbar +import icu.nullptr.hidemyapplist.ui.viewmodel.AppSettingsViewModel +import icu.nullptr.hidemyapplist.util.PackageHelper +import org.frknkrc44.hma_oss.BuildConfig +import org.frknkrc44.hma_oss.R +import org.frknkrc44.hma_oss.databinding.FragmentSettingsBinding + +class AppSettingsFragment : Fragment(R.layout.fragment_settings) { + + private val binding by viewBinding() + private val viewModel by viewModels() { + val args by navArgs() + val cfg = ConfigManager.getAppConfig(args.packageName) + val pack = if (cfg != null) AppSettingsViewModel.Pack(args.packageName, true, cfg) + else AppSettingsViewModel.Pack(args.packageName, false, JsonConfig.AppConfig()) + AppSettingsViewModel.Factory(pack) + } + + private fun saveConfig() { + if (!viewModel.pack.enabled) ConfigManager.setAppConfig(viewModel.pack.app, null) + else ConfigManager.setAppConfig(viewModel.pack.app, viewModel.pack.config) + } + + private fun onBack() { + saveConfig() + navController.navigateUp() + } + + override fun onPause() { + super.onPause() + saveConfig() + } + + override fun onDestroyView() { + super.onDestroyView() + saveConfig() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack() } + setupToolbar( + toolbar = binding.toolbar, + title = getString(R.string.title_app_settings), + navigationIcon = R.drawable.baseline_arrow_back_24, + navigationOnClick = { onBack() } + ) + + if (childFragmentManager.findFragmentById(R.id.settings_container) == null) { + childFragmentManager.beginTransaction() + .replace(R.id.settings_container, AppPreferenceFragment()) + .commit() + } + } + + class AppPreferenceDataStore(private val pack: AppSettingsViewModel.Pack) : PreferenceDataStore() { + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + return when (key) { + "enableHide" -> pack.enabled + "useWhiteList" -> pack.config.useWhitelist + "excludeSystemApps" -> pack.config.excludeSystemApps + else -> throw IllegalArgumentException("Invalid key: $key") + } + } + + override fun putBoolean(key: String, value: Boolean) { + when (key) { + "enableHide" -> pack.enabled = value + "useWhiteList" -> pack.config.useWhitelist = value + "excludeSystemApps" -> pack.config.excludeSystemApps = value + else -> throw IllegalArgumentException("Invalid key: $key") + } + } + } + + class AppPreferenceFragment : PreferenceFragmentCompat() { + + private val parent get() = requireParentFragment() as AppSettingsFragment + private val pack get() = parent.viewModel.pack + + private fun updateApplyTemplates() { + findPreference("applyTemplates")?.title = + getString(R.string.app_template_using, pack.config.applyTemplates.size) + } + + private fun updateApplyPresets() { + findPreference("applyPresets")?.title = + getString(R.string.app_preset_using, pack.config.applyPresets.size) + } + + private fun updateExtraAppList(useWhiteList: Boolean) { + findPreference("extraAppList")?.title = + if (useWhiteList) getString(R.string.app_extra_apps_visible_count, pack.config.extraAppList.size) + else getString(R.string.app_extra_apps_invisible_count, pack.config.extraAppList.size) + } + + @SuppressLint("DiscouragedApi") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.preferenceDataStore = AppPreferenceDataStore(pack) + setPreferencesFromResource(R.xml.app_settings, rootKey) + findPreference("appInfo")?.let { + it.icon = PackageHelper.loadAppIcon(pack.app).toDrawable(resources) + it.title = PackageHelper.loadAppLabel(pack.app) + it.summary = PackageHelper.loadPackageInfo(pack.app).packageName + } + findPreference("useWhiteList")?.setOnPreferenceChangeListener { _, newValue -> + pack.config.applyTemplates.clear() + pack.config.extraAppList.clear() + updateApplyTemplates() + updateApplyPresets() + updateExtraAppList(newValue as Boolean) + true + } + findPreference("applyTemplates")?.setOnPreferenceClickListener { + val templates = ConfigManager.getTemplateList().mapNotNull { + if (it.isWhiteList == pack.config.useWhitelist) it.name else null + }.toTypedArray() + val checked = templates.map { + pack.config.applyTemplates.contains(it) + }.toBooleanArray() + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.app_choose_template) + .setMultiChoiceItems(templates, checked) { _, i, value -> checked[i] = value } + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + pack.config.applyTemplates = templates.mapIndexedNotNullTo(mutableSetOf()) { i, name -> + if (checked[i]) name else null + } + updateApplyTemplates() + } + .show() + true + } + findPreference("applyPresets")?.setOnPreferenceClickListener { + val presetNames = Presets.instance.getAllPresetNames() + val presetTranslations = presetNames.map { name -> + try { + val id = resources.getIdentifier( + "preset_${name}", + "string", + BuildConfig.APPLICATION_ID + ) + + return@map if (id != 0) { getString(id) } else { name } + } catch (ignore: Throwable) {} + + name + } + + val presets = presetNames.zip(presetTranslations).toMap().toSortedMap() + val checked = presets.keys.map { + pack.config.applyPresets.contains(it) + }.toBooleanArray() + val presetValues = presets.values.toTypedArray() + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.app_choose_preset) + .setMultiChoiceItems(presetValues, checked) { _, i, value -> checked[i] = value } + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + pack.config.applyPresets = presetValues.mapIndexedNotNullTo(mutableSetOf()) { i, name -> + if (checked[i]) presets.filterValues { v -> v == name }.keys.first() else null + } + updateApplyPresets() + } + .show() + true + } + findPreference("extraAppList")?.setOnPreferenceClickListener { + parent.setFragmentResultListener("app_select") { _, bundle -> + pack.config.extraAppList = bundle.getStringArrayList("checked")!!.toMutableSet() + updateExtraAppList(pack.config.useWhitelist) + parent.clearFragmentResultListener("app_select") + } + + val args = ScopeFragmentArgs( + filterOnlyEnabled = false, + checked = pack.config.extraAppList.toTypedArray() + ) + parent.navController.navigate(R.id.nav_scope, args.toBundle()) + true + } + updateApplyTemplates() + updateApplyPresets() + updateExtraAppList(pack.config.useWhitelist) + } + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/ScopeFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/ScopeFragment.kt new file mode 100644 index 0000000..cde1a5d --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/ScopeFragment.kt @@ -0,0 +1,29 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.os.Bundle +import androidx.fragment.app.setFragmentResult +import androidx.navigation.fragment.navArgs +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.adapter.AppScopeAdapter +import icu.nullptr.hidemyapplist.ui.util.navController + +class ScopeFragment : AppSelectFragment() { + + private lateinit var checked: MutableSet + + override val firstComparator: Comparator = Comparator.comparing { !checked.contains(it) } + + override val adapter by lazy { + val args by navArgs() + checked = args.checked.toMutableSet() + if (!args.filterOnlyEnabled) AppScopeAdapter(checked, null) + else AppScopeAdapter(checked) { ConfigManager.getAppConfig(it)?.useWhitelist == args.isWhiteList } + } + + override fun onBack() { + setFragmentResult("app_select", Bundle().apply { + putStringArrayList("checked", ArrayList(checked)) + }) + navController.navigateUp() + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateManageFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateManageFragment.kt new file mode 100644 index 0000000..a6e3a66 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateManageFragment.kt @@ -0,0 +1,85 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import androidx.core.view.doOnPreDraw +import androidx.fragment.app.Fragment +import androidx.fragment.app.clearFragmentResultListener +import androidx.fragment.app.setFragmentResultListener +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.recyclerview.widget.LinearLayoutManager +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.transition.MaterialContainerTransform +import icu.nullptr.hidemyapplist.common.JsonConfig +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.adapter.TemplateAdapter +import icu.nullptr.hidemyapplist.ui.util.navController +import icu.nullptr.hidemyapplist.ui.util.setupToolbar +import org.frknkrc44.hma_oss.R +import org.frknkrc44.hma_oss.databinding.FragmentTemplateManageBinding + +class TemplateManageFragment : Fragment(R.layout.fragment_template_manage) { + + private val binding by viewBinding() + private val adapter = TemplateAdapter(this::navigateToSettings) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedElementEnterTransition = MaterialContainerTransform().apply { + drawingViewId = R.id.nav_host_fragment + scrimColor = Color.TRANSPARENT + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupToolbar( + toolbar = binding.toolbar, + title = getString(R.string.title_template_manage), + navigationIcon = R.drawable.baseline_arrow_back_24, + navigationOnClick = { navController.navigateUp() } + ) + postponeEnterTransition() + view.doOnPreDraw { startPostponedEnterTransition() } + + binding.newBlacklistTemplate.setOnClickListener { + navigateToSettings(ConfigManager.TemplateInfo(null, false)) + } + binding.newWhitelistTemplate.setOnClickListener { + navigateToSettings(ConfigManager.TemplateInfo(null, true)) + } + binding.templateList.layoutManager = LinearLayoutManager(context) + binding.templateList.adapter = adapter + } + + private fun navigateToSettings(info: ConfigManager.TemplateInfo) { + setFragmentResultListener("template_settings") { _, bundle -> + fun deal() { + var name = bundle.getString("name") + val appliedList = bundle.getStringArrayList("appliedList")!! + val targetList = bundle.getStringArrayList("targetList")!! + if (info.name == null) { // New template + if (name.isNullOrEmpty()) return + ConfigManager.updateTemplate(name, JsonConfig.Template(info.isWhiteList, targetList.toSet())) + ConfigManager.updateTemplateAppliedApps(name, appliedList) + } else { // Existing template + if (name == null) { + ConfigManager.deleteTemplate(info.name) + } else { + if (name.isEmpty()) name = info.name + if (name != info.name) ConfigManager.renameTemplate(info.name, name) + ConfigManager.updateTemplate(name, JsonConfig.Template(info.isWhiteList, targetList.toSet())) + ConfigManager.updateTemplateAppliedApps(name, appliedList) + } + } + } + deal() + adapter.updateList() + clearFragmentResultListener("template_settings") + } + + val args = TemplateSettingsFragmentArgs(info.name, info.isWhiteList) + val extras = FragmentNavigatorExtras(binding.hintCard to "transition_manage") + navController.navigate(R.id.nav_template_settings, args.toBundle(), null, extras) + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateSettingsFragment.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateSettingsFragment.kt new file mode 100644 index 0000000..d22a823 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/fragment/TemplateSettingsFragment.kt @@ -0,0 +1,122 @@ +package icu.nullptr.hidemyapplist.ui.fragment + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import androidx.activity.addCallback +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.clearFragmentResultListener +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.setFragmentResultListener +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.navArgs +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.transition.MaterialContainerTransform +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.util.navController +import icu.nullptr.hidemyapplist.ui.util.setupToolbar +import icu.nullptr.hidemyapplist.ui.viewmodel.TemplateSettingsViewModel +import kotlinx.coroutines.launch +import org.frknkrc44.hma_oss.R +import org.frknkrc44.hma_oss.databinding.FragmentTemplateSettingsBinding + +class TemplateSettingsFragment : Fragment(R.layout.fragment_template_settings) { + + private val binding by viewBinding() + private val viewModel by viewModels { + val args by navArgs() + TemplateSettingsViewModel.Factory(args) + } + + private fun onBack(delete: Boolean) { + viewModel.name = viewModel.name?.trim() + if (viewModel.name != viewModel.originalName && (ConfigManager.hasTemplate(viewModel.name) || viewModel.name == null) || delete) { + val builder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(if (delete) R.string.template_delete_title else R.string.template_name_invalid) + .setMessage(if (delete) R.string.template_delete else R.string.template_name_already_exist) + .setPositiveButton(android.R.string.ok) { _, _ -> + saveResult(delete) + } + if (delete) builder.setNegativeButton(android.R.string.cancel, null) + builder.show() + } else { + saveResult(false) + } + } + + private fun saveResult(delete: Boolean) { + setFragmentResult("template_settings", Bundle().apply { + putString("name",if (delete) null else viewModel.name) + putStringArrayList("appliedList", viewModel.appliedAppList.value) + putStringArrayList("targetList", viewModel.targetAppList.value) + }) + navController.navigateUp() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + /* + sharedElementEnterTransition = MaterialContainerTransform().apply { + drawingViewId = R.id.nav_host_fragment + scrimColor = Color.TRANSPARENT + } + */ + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { onBack(false) } + setupToolbar( + toolbar = binding.toolbar, + title = getString(R.string.title_template_settings), + navigationIcon = R.drawable.baseline_arrow_back_24, + navigationOnClick = { onBack(false) }, + menuRes = R.menu.menu_delete, + onMenuOptionSelected = { + onBack(true) + } + ) + + binding.templateName.setText(viewModel.name) + binding.templateName.addTextChangedListener { viewModel.name = it.toString() } + binding.targetApps.setOnClickListener { + setFragmentResultListener("app_select") { _, bundle -> + viewModel.targetAppList.value = bundle.getStringArrayList("checked")!! + clearFragmentResultListener("app_select") + } + val args = ScopeFragmentArgs( + filterOnlyEnabled = false, + checked = viewModel.targetAppList.value.toTypedArray() + ) + navController.navigate(R.id.nav_scope, args.toBundle()) + } + binding.appliedApps.setOnClickListener { + setFragmentResultListener("app_select") { _, bundle -> + viewModel.appliedAppList.value = bundle.getStringArrayList("checked")!! + clearFragmentResultListener("app_select") + } + val args = ScopeFragmentArgs( + filterOnlyEnabled = true, + isWhiteList = viewModel.isWhiteList, + checked = viewModel.appliedAppList.value.toTypedArray() + ) + navController.navigate(R.id.nav_scope, args.toBundle()) + } + + lifecycleScope.launch { + viewModel.targetAppList.collect { + val fmt = + if (viewModel.isWhiteList) R.string.template_apps_visible_count + else R.string.template_apps_invisible_count + binding.targetApps.text = String.format(getString(fmt), it.size) + } + } + lifecycleScope.launch { + viewModel.appliedAppList.collect { + binding.appliedApps.text = String.format(getString(R.string.template_applied_count), it.size) + } + } + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/AppItemView.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/AppItemView.kt new file mode 100644 index 0000000..48e2af7 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/AppItemView.kt @@ -0,0 +1,41 @@ +package icu.nullptr.hidemyapplist.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import icu.nullptr.hidemyapplist.util.PackageHelper +import org.frknkrc44.hma_oss.databinding.AppItemViewBinding + +class AppItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val binding by viewBinding(createMethod = CreateMethod.INFLATE) + + var showEnabled: Boolean + get() = binding.enabled.visibility == VISIBLE + set(value) { + binding.enabled.visibility = if (value) VISIBLE else GONE + } + + var isChecked: Boolean + get() = binding.checkbox.isChecked + set(value) { + binding.checkbox.isChecked = value + } + + constructor(context: Context, isCheckable: Boolean) : this(context) { + binding.checkbox.visibility = if (isCheckable) VISIBLE else GONE + } + + fun load(packageName: String) { + binding.packageName.text = packageName + binding.label.text = PackageHelper.loadAppLabel(packageName) + binding.icon.setImageBitmap(PackageHelper.loadAppIcon(packageName)) + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/ListItemView.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/ListItemView.kt new file mode 100644 index 0000000..244e0f6 --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/view/ListItemView.kt @@ -0,0 +1,52 @@ +package icu.nullptr.hidemyapplist.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import androidx.annotation.DrawableRes +import by.kirich1409.viewbindingdelegate.CreateMethod +import by.kirich1409.viewbindingdelegate.viewBinding +import org.frknkrc44.hma_oss.R +import org.frknkrc44.hma_oss.databinding.ListItemViewBinding + +class ListItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val binding by viewBinding(createMethod = CreateMethod.INFLATE) + + init { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ListItemView, defStyleAttr, defStyleRes) + val icon = typedArray.getResourceId(R.styleable.ListItemView_icon, 0) + val text = typedArray.getString(R.styleable.ListItemView_text) + val buttonText = typedArray.getText(R.styleable.ListItemView_buttonText) + typedArray.recycle() + binding.icon.setImageResource(icon) + binding.text.text = text + if (buttonText != null) { + binding.button.visibility = VISIBLE + binding.button.text = buttonText + } + } + + var text: CharSequence? + get() = binding.text.text + set(value) { + binding.text.text = value + } + + fun setIcon(@DrawableRes icon: Int) { + binding.icon.setImageResource(icon) + } + + override fun setOnClickListener(l: OnClickListener?) { + if (binding.button.visibility == VISIBLE) { + binding.button.setOnClickListener(l) + } else { + super.setOnClickListener(l) + } + } +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/AppSettingsViewModel.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/AppSettingsViewModel.kt new file mode 100644 index 0000000..e27e0af --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/AppSettingsViewModel.kt @@ -0,0 +1,23 @@ +package icu.nullptr.hidemyapplist.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import icu.nullptr.hidemyapplist.common.JsonConfig + +class AppSettingsViewModel(val pack: Pack) : ViewModel() { + + class Factory(private val pack: Pack) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AppSettingsViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return AppSettingsViewModel(pack) as T + } else throw IllegalArgumentException("Unknown ViewModel class") + } + } + + class Pack( + val app: String, + var enabled: Boolean, + val config: JsonConfig.AppConfig + ) +} diff --git a/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/TemplateSettingsViewModel.kt b/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/TemplateSettingsViewModel.kt new file mode 100644 index 0000000..b93f8fb --- /dev/null +++ b/app/src/main/java/icu/nullptr/hidemyapplist/ui/viewmodel/TemplateSettingsViewModel.kt @@ -0,0 +1,31 @@ +package icu.nullptr.hidemyapplist.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import icu.nullptr.hidemyapplist.service.ConfigManager +import icu.nullptr.hidemyapplist.ui.fragment.TemplateSettingsFragmentArgs +import kotlinx.coroutines.flow.MutableStateFlow + +class TemplateSettingsViewModel( + val originalName: String?, + val isWhiteList: Boolean, + var name: String? +) : ViewModel() { + + class Factory(private val args: TemplateSettingsFragmentArgs) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(TemplateSettingsViewModel::class.java)) { + val viewModel = TemplateSettingsViewModel(args.name, args.isWhiteList, args.name) + args.name?.let { + viewModel.appliedAppList.value = ConfigManager.getTemplateAppliedAppList(it) + viewModel.targetAppList.value = ConfigManager.getTemplateTargetAppList(it) + } + @Suppress("UNCHECKED_CAST") + return viewModel as T + } else throw IllegalArgumentException("Unknown ViewModel class") + } + } + + val appliedAppList = MutableStateFlow>(ArrayList()) + val targetAppList = MutableStateFlow>(ArrayList()) +} diff --git a/app/src/main/java/org/frknkrc44/hma_oss/ui/adapter/AppListAdapter.kt b/app/src/main/java/org/frknkrc44/hma_oss/ui/adapter/AppListAdapter.kt deleted file mode 100644 index 43cf64c..0000000 --- a/app/src/main/java/org/frknkrc44/hma_oss/ui/adapter/AppListAdapter.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.frknkrc44.hma_oss.ui.adapter - -class AppListAdapter { -} \ No newline at end of file diff --git a/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/HomeFragment.kt b/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/HomeFragment.kt index 0028f31..ccc6c29 100644 --- a/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/HomeFragment.kt +++ b/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/HomeFragment.kt @@ -87,7 +87,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) { } } - @SuppressLint("StringFormatInvalid") + @SuppressLint("StringFormatInvalid", "StringFormatMatches") override fun onStart() { super.onStart() @@ -223,7 +223,6 @@ class HomeFragment : Fragment(R.layout.fragment_home) { with(binding.howToUse) { text1.text = getString(R.string.about_how_to_use_title) icon.setImageResource(R.drawable.outline_info_24) - // itemIcon.setCircleBackground(getColor(R.color.info)) root.setOnClickListener { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.about_how_to_use_title) @@ -239,19 +238,22 @@ class HomeFragment : Fragment(R.layout.fragment_home) { with(binding.manageApps) { text1.text = getString(R.string.title_app_manage) icon.setImageResource(R.drawable.outline_android_24) - // itemIcon.setCircleBackground(getColor(R.color.warn)) + root.setOnClickListener { + navigate(R.id.nav_app_manage) + } } with(binding.manageTemplates) { text1.text = getString(R.string.title_template_manage) icon.setImageResource(R.drawable.ic_outline_layers_24) - // itemIcon.setCircleBackground(getColor(R.color.invalid)) + root.setOnClickListener { + navigate(R.id.nav_template_manage) + } } with(binding.navLogs) { text1.text = getString(R.string.title_logs) icon.setImageResource(R.drawable.outline_assignment_24) - // itemIcon.setCircleBackground(getColor(R.color.error)) root.setOnClickListener { navigate(R.id.nav_logs) } diff --git a/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/ManageAppsFragment.kt b/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/ManageAppsFragment.kt deleted file mode 100644 index 5c5bbe3..0000000 --- a/app/src/main/java/org/frknkrc44/hma_oss/ui/fragment/ManageAppsFragment.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.frknkrc44.hma_oss.ui.fragment - -import android.os.Bundle -import androidx.fragment.app.Fragment -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import by.kirich1409.viewbindingdelegate.viewBinding -import icu.nullptr.hidemyapplist.service.ConfigManager -import icu.nullptr.hidemyapplist.ui.util.navController -import icu.nullptr.hidemyapplist.util.PackageHelper -import kotlinx.coroutines.launch -import org.frknkrc44.hma_oss.R -import org.frknkrc44.hma_oss.databinding.FragmentManageAppsBinding -import org.frknkrc44.hma_oss.ui.adapter.AppListAdapter - -/** - * A simple [Fragment] subclass. - * Use the [ManageAppsFragment.newInstance] factory method to - * create an instance of this fragment. - */ -class ManageAppsFragment : Fragment(R.layout.fragment_manage_apps) { - protected val firstComparator: Comparator = Comparator.comparing(ConfigManager::isHideEnabled).reversed() - - private val binding by viewBinding() - val adapter: AppListAdapter - - private var search = "" - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - } - - protected open fun onBack() { - navController.navigateUp() - } - - private fun applyFilter() { - adapter.filter.filter(search) - } - - private fun sortList() { - lifecycleScope.launch { - PackageHelper.sortList(firstComparator) - applyFilter() - } - } - - companion object { - @JvmStatic - fun newInstance(param1: String, param2: String) = ManageAppsFragment() - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/app_item_view.xml b/app/src/main/res/layout/app_item_view.xml new file mode 100644 index 0000000..04f8484 --- /dev/null +++ b/app/src/main/res/layout/app_item_view.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_app_select.xml b/app/src/main/res/layout/fragment_app_select.xml new file mode 100644 index 0000000..ce8c52f --- /dev/null +++ b/app/src/main/res/layout/fragment_app_select.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_manage_apps.xml b/app/src/main/res/layout/fragment_manage_apps.xml deleted file mode 100644 index b99e213..0000000 --- a/app/src/main/res/layout/fragment_manage_apps.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_template_manage.xml b/app/src/main/res/layout/fragment_template_manage.xml new file mode 100644 index 0000000..ae0d416 --- /dev/null +++ b/app/src/main/res/layout/fragment_template_manage.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_template_settings.xml b/app/src/main/res/layout/fragment_template_settings.xml new file mode 100644 index 0000000..3c85d83 --- /dev/null +++ b/app/src/main/res/layout/fragment_template_settings.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/line.xml b/app/src/main/res/layout/line.xml new file mode 100644 index 0000000..99ef1a2 --- /dev/null +++ b/app/src/main/res/layout/line.xml @@ -0,0 +1,5 @@ + + diff --git a/app/src/main/res/layout/list_item_view.xml b/app/src/main/res/layout/list_item_view.xml new file mode 100644 index 0000000..72e500d --- /dev/null +++ b/app/src/main/res/layout/list_item_view.xml @@ -0,0 +1,35 @@ + + + + + + + +