From adbd47a36c7536f85def39b17ba0edd44497586a Mon Sep 17 00:00:00 2001 From: Viktor De Pasquale Date: Sat, 20 Apr 2019 23:44:08 +0200 Subject: [PATCH] Updated modules and repos screen Screens are merged via common viewModel, all data are immediately accessible to both of them --- .../com/topjohnwu/magisk/di/DatabaseModule.kt | 1 + .../topjohnwu/magisk/di/ViewModelsModule.kt | 2 + .../model/entity/recycler/ModuleRvItem.kt | 66 ++++++++ .../topjohnwu/magisk/model/events/RxEvents.kt | 3 + .../magisk/model/events/ViewEvents.kt | 8 +- .../magisk/ui/base/MagiskLeanbackActivity.kt | 3 + .../magisk/ui/module/ModuleViewModel.kt | 81 ++++++++++ .../magisk/ui/module/ModulesFragment.java | 141 ------------------ .../magisk/ui/module/ModulesFragment.kt | 115 ++++++++++++++ .../magisk/ui/module/ReposFragment.java | 101 ------------- .../magisk/ui/module/ReposFragment.kt | 104 +++++++++++++ app/src/main/res/layout/fragment_modules.xml | 65 +++++--- app/src/main/res/layout/fragment_repos.xml | 58 ++++--- app/src/main/res/layout/item_module.xml | 136 +++++++++++++++++ app/src/main/res/layout/item_repo.xml | 126 ++++++++++++++++ 15 files changed, 725 insertions(+), 285 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt create mode 100644 app/src/main/res/layout/item_module.xml create mode 100644 app/src/main/res/layout/item_repo.xml diff --git a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt index 483f9e3ef..4d8869d82 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt @@ -6,4 +6,5 @@ import org.koin.dsl.module val databaseModule = module { single { get().DB } + single { get().repoDB } } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt index 57ec12564..7f779cc29 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -3,6 +3,7 @@ package com.topjohnwu.magisk.di import com.topjohnwu.magisk.ui.MainViewModel import com.topjohnwu.magisk.ui.hide.HideViewModel import com.topjohnwu.magisk.ui.home.HomeViewModel +import com.topjohnwu.magisk.ui.module.ModuleViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -13,4 +14,5 @@ val viewModelModules = module { viewModel { HomeViewModel(get(), get()) } viewModel { SuperuserViewModel(get(), get(), get(), get()) } viewModel { HideViewModel(get(), get()) } + viewModel { ModuleViewModel(get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt new file mode 100644 index 000000000..74c90a814 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt @@ -0,0 +1,66 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import android.content.res.Resources +import androidx.annotation.StringRes +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.extensions.addOnPropertyChangedCallback +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.model.entity.Module +import com.topjohnwu.magisk.model.entity.Repo +import com.topjohnwu.magisk.utils.get +import com.topjohnwu.magisk.utils.toggle + +class ModuleRvItem(val item: Module) : ComparableRvItem() { + + override val layoutRes: Int = R.layout.item_module + + val lastActionNotice = KObservableField("") + val isChecked = KObservableField(item.isEnabled) + val isDeletable = KObservableField(item.willBeRemoved()) + + init { + isChecked.addOnPropertyChangedCallback { + when (it) { + true -> item.removeDisableFile().notice(R.string.disable_file_removed) + false -> item.createDisableFile().notice(R.string.disable_file_created) + } + } + isDeletable.addOnPropertyChangedCallback { + when (it) { + true -> item.createRemoveFile().notice(R.string.remove_file_created) + false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted) + } + } + when { + item.isUpdated -> notice(R.string.update_file_created) + item.willBeRemoved() -> notice(R.string.remove_file_created) + } + } + + fun toggle() = isChecked.toggle() + fun toggleDelete() = isDeletable.toggle() + + @Suppress("unused") + private fun Any.notice(@StringRes info: Int) { + lastActionNotice.value = get().getString(info) + } + + override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version + && item.versionCode == other.item.versionCode + && item.description == other.item.description + + override fun itemSameAs(other: ModuleRvItem): Boolean = item.name == other.item.name +} + +class RepoRvItem(val item: Repo) : ComparableRvItem() { + + override val layoutRes: Int = R.layout.item_repo + + override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version + && item.lastUpdate == other.item.lastUpdate + && item.versionCode == other.item.versionCode + && item.description == other.item.description + + override fun itemSameAs(other: RepoRvItem): Boolean = item.detailUrl == other.item.detailUrl +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt index 62a8df4cb..e713d4ece 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/RxEvents.kt @@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.events import com.skoumal.teanity.rxbus.RxBus import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem +import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event @@ -11,3 +12,5 @@ sealed class PolicyUpdateEvent(val item: PolicyRvItem) : RxBus.Event { class Notification(item: PolicyRvItem) : PolicyUpdateEvent(item) class Log(item: PolicyRvItem) : PolicyUpdateEvent(item) } + +class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event diff --git a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt index f4b5fbf5b..bb46765ab 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.events import android.app.Activity import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.entity.Repo data class OpenLinkEvent(val url: String) : ViewEvent() @@ -17,4 +18,9 @@ class EnvFixEvent : ViewEvent() class UpdateSafetyNetEvent : ViewEvent() -class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent() \ No newline at end of file +class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent() + +class OpenFilePickerEvent : ViewEvent() + +class OpenChangelogEvent(val item: Repo) : ViewEvent() +class InstallModuleEvent(val item: Repo) : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt index 8ad249173..365785e42 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskLeanbackActivity.kt @@ -17,6 +17,9 @@ abstract class MagiskLeanbackActivity() + @Deprecated("Permissions will be checked in a different streamlined way") + fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() }) + @Deprecated("Permissions will be checked in a different streamlined way") override fun runWithExternalRW(callback: Runnable) { runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback) diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt new file mode 100644 index 000000000..3110eaead --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -0,0 +1,81 @@ +package com.topjohnwu.magisk.ui.module + +import android.database.Cursor +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.extensions.subscribeK +import com.skoumal.teanity.util.DiffObservableList +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.data.database.RepoDatabaseHelper +import com.topjohnwu.magisk.model.entity.Module +import com.topjohnwu.magisk.model.entity.Repo +import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem +import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem +import com.topjohnwu.magisk.model.events.InstallModuleEvent +import com.topjohnwu.magisk.model.events.OpenChangelogEvent +import com.topjohnwu.magisk.model.events.OpenFilePickerEvent +import com.topjohnwu.magisk.tasks.UpdateRepos +import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.Event +import com.topjohnwu.magisk.utils.Utils +import io.reactivex.Single +import me.tatarka.bindingcollectionadapter2.OnItemBind + +class ModuleViewModel( + private val repoDatabase: RepoDatabaseHelper +) : MagiskViewModel() { + + val query = KObservableField("") + + val itemsInstalled = DiffObservableList(ComparableRvItem.callback) + val itemsRemote = DiffObservableList(ComparableRvItem.callback) + val itemBinding = OnItemBind> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel) + } + + init { + Event.register(this) + refresh() + } + + override fun getListeningEvents(): IntArray { + return intArrayOf(Event.MODULE_LOAD_DONE, Event.REPO_LOAD_DONE) + } + + override fun onEvent(event: Int) = when (event) { + Event.MODULE_LOAD_DONE -> updateModules(Event.getResult(event)) + Event.REPO_LOAD_DONE -> updateRepos() + else -> Unit + } + + fun fabPressed() = OpenFilePickerEvent().publish() + fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish() + fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish() + + fun refresh() { + state = State.LOADING + Utils.loadModules(true) + UpdateRepos().exec(true) + } + + private fun updateModules(result: Map) = result.values + .map { ModuleRvItem(it) } + .let { itemsInstalled.update(it) } + + internal fun updateRepos() { + Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } } + .flattenAsFlowable { it } + .map { RepoRvItem(it) } + .toList() + .applyViewModel(this) + .subscribeK { itemsRemote.update(it) } + } + + private fun Cursor.toList(transformer: (Cursor) -> Result): List { + val out = mutableListOf() + while (moveToNext()) out.add(transformer(this)) + return out + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java deleted file mode 100644 index 78d5989a7..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.topjohnwu.magisk.ui.module; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -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.TextView; - -import com.topjohnwu.magisk.ClassMap; -import com.topjohnwu.magisk.Const; -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.model.adapters.ModulesAdapter; -import com.topjohnwu.magisk.model.entity.Module; -import com.topjohnwu.magisk.ui.base.BaseFragment; -import com.topjohnwu.magisk.ui.flash.FlashActivity; -import com.topjohnwu.magisk.utils.Event; -import com.topjohnwu.magisk.utils.RootUtils; -import com.topjohnwu.magisk.utils.Utils; -import com.topjohnwu.superuser.Shell; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import butterknife.BindView; -import butterknife.OnClick; - -public class ModulesFragment extends BaseFragment { - - @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; - @BindView(R.id.recyclerView) RecyclerView recyclerView; - @BindView(R.id.empty_rv) TextView emptyRv; - - @OnClick(R.id.fab) - void selectFile() { - runWithExternalRW(() -> { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("application/zip"); - startActivityForResult(intent, Const.ID.FETCH_ZIP); - }); - } - - private List listModules = new ArrayList<>(); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_modules, container, false); - unbinder = new ModulesFragment_ViewBinding(this, view); - setHasOptionsMenu(true); - - mSwipeRefreshLayout.setOnRefreshListener(() -> { - recyclerView.setVisibility(View.GONE); - Utils.loadModules(); - }); - - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - mSwipeRefreshLayout.setEnabled(recyclerView.getChildAt(0).getTop() >= 0); - } - - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - super.onScrollStateChanged(recyclerView, newState); - } - }); - - requireActivity().setTitle(R.string.modules); - - return view; - } - - @Override - public int[] getListeningEvents() { - return new int[] {Event.MODULE_LOAD_DONE}; - } - - @Override - public void onEvent(int event) { - updateUI(Event.getResult(event)); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) { - // Get the URI of the selected file - Intent intent = new Intent(getActivity(), ClassMap.get(FlashActivity.class)); - intent.setData(data.getData()).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP); - startActivity(intent); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_reboot, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.reboot: - RootUtils.reboot(); - return true; - case R.id.reboot_recovery: - Shell.su("/system/bin/reboot recovery").submit(); - return true; - case R.id.reboot_bootloader: - Shell.su("/system/bin/reboot bootloader").submit(); - return true; - case R.id.reboot_download: - Shell.su("/system/bin/reboot download").submit(); - return true; - default: - return false; - } - } - - private void updateUI(Map moduleMap) { - listModules.clear(); - listModules.addAll(moduleMap.values()); - if (listModules.size() == 0) { - emptyRv.setVisibility(View.VISIBLE); - recyclerView.setVisibility(View.GONE); - } else { - emptyRv.setVisibility(View.GONE); - recyclerView.setVisibility(View.VISIBLE); - recyclerView.setAdapter(new ModulesAdapter(listModules)); - } - mSwipeRefreshLayout.setRefreshing(false); - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt new file mode 100644 index 000000000..9aff75076 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModulesFragment.kt @@ -0,0 +1,115 @@ +package com.topjohnwu.magisk.ui.module + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.ClassMap +import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.FragmentModulesBinding +import com.topjohnwu.magisk.model.events.OpenFilePickerEvent +import com.topjohnwu.magisk.ui.base.MagiskFragment +import com.topjohnwu.magisk.ui.flash.FlashActivity +import com.topjohnwu.magisk.utils.RootUtils +import com.topjohnwu.superuser.Shell +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class ModulesFragment : MagiskFragment() { + + override val layoutRes: Int = R.layout.fragment_modules + override val viewModel: ModuleViewModel by sharedViewModel() + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) { + // Get the URI of the selected file + val intent = Intent(activity, ClassMap.get(FlashActivity::class.java)) + intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP) + startActivity(intent) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.modulesContent.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + binding.modulesRefreshLayout.isEnabled = recyclerView.getChildAt(0).top >= 0 + } + }) + } + + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is OpenFilePickerEvent -> selectFile() + } + } + + override fun onStart() { + super.onStart() + setHasOptionsMenu(true) + requireActivity().setTitle(R.string.modules) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_reboot, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.reboot -> { + RootUtils.reboot() + return true + } + R.id.reboot_recovery -> { + Shell.su("/system/bin/reboot recovery").submit() + return true + } + R.id.reboot_bootloader -> { + Shell.su("/system/bin/reboot bootloader").submit() + return true + } + R.id.reboot_download -> { + Shell.su("/system/bin/reboot download").submit() + return true + } + else -> return false + } + } + + private fun selectFile() { + magiskActivity.runWithExternalRW { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "application/zip" + startActivityForResult(intent, Const.ID.FETCH_ZIP) + } + } + + /*override fun getListeningEvents(): IntArray { + return intArrayOf(Event.MODULE_LOAD_DONE) + } + + override fun onEvent(event: Int) { + updateUI(Event.getResult(event)) + }*/ + + /*private fun updateUI(moduleMap: Map) { + listModules.clear() + listModules.addAll(moduleMap.values) + if (listModules.size == 0) { + emptyRv!!.visibility = View.VISIBLE + recyclerView!!.visibility = View.GONE + } else { + emptyRv!!.visibility = View.GONE + recyclerView!!.visibility = View.VISIBLE + recyclerView!!.adapter = ModulesAdapter(listModules) + } + mSwipeRefreshLayout!!.isRefreshing = false + }*/ +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java deleted file mode 100644 index 4c12fee70..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.topjohnwu.magisk.ui.module; - -import android.app.AlertDialog; -import android.os.Bundle; -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.SearchView; -import android.widget.TextView; - -import com.topjohnwu.magisk.Config; -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.model.adapters.ReposAdapter; -import com.topjohnwu.magisk.tasks.UpdateRepos; -import com.topjohnwu.magisk.ui.base.BaseFragment; -import com.topjohnwu.magisk.utils.Event; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import butterknife.BindView; - -public class ReposFragment extends BaseFragment { - - @BindView(R.id.recyclerView) RecyclerView recyclerView; - @BindView(R.id.empty_rv) TextView emptyRv; - @BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout; - - private ReposAdapter adapter; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_repos, container, false); - unbinder = new ReposFragment_ViewBinding(this, view); - - mSwipeRefreshLayout.setRefreshing(true); - mSwipeRefreshLayout.setOnRefreshListener(() -> new UpdateRepos().exec(true)); - - adapter = new ReposAdapter(); - recyclerView.setAdapter(adapter); - recyclerView.setVisibility(View.GONE); - - requireActivity().setTitle(R.string.downloads); - - return view; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - Event.unregister(adapter); - } - - @Override - public int[] getListeningEvents() { - return new int[] {Event.REPO_LOAD_DONE}; - } - - @Override - public void onEvent(int event) { - adapter.notifyDBChanged(false); - Event.register(adapter); - mSwipeRefreshLayout.setRefreshing(false); - boolean empty = adapter.getItemCount() == 0; - recyclerView.setVisibility(empty ? View.GONE : View.VISIBLE); - emptyRv.setVisibility(empty ? View.VISIBLE : View.GONE); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.menu_repo, menu); - SearchView search = (SearchView) menu.findItem(R.id.repo_search).getActionView(); - adapter.setSearchView(search); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.repo_sort) { - new AlertDialog.Builder(getActivity()) - .setTitle(R.string.sorting_order) - .setSingleChoiceItems(R.array.sorting_orders, - Config.get(Config.Key.REPO_ORDER), (d, which) -> { - Config.set(Config.Key.REPO_ORDER, which); - adapter.notifyDBChanged(true); - d.dismiss(); - }).show(); - } - return true; - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt new file mode 100644 index 000000000..51b6d8830 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ReposFragment.kt @@ -0,0 +1,104 @@ +package com.topjohnwu.magisk.ui.module + +import android.app.AlertDialog +import android.content.Intent +import android.os.Build +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.widget.SearchView +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.ClassMap +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.FragmentReposBinding +import com.topjohnwu.magisk.model.download.DownloadModuleService +import com.topjohnwu.magisk.model.entity.Repo +import com.topjohnwu.magisk.model.events.InstallModuleEvent +import com.topjohnwu.magisk.model.events.OpenChangelogEvent +import com.topjohnwu.magisk.ui.base.MagiskFragment +import com.topjohnwu.magisk.view.MarkDownWindow +import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class ReposFragment : MagiskFragment(), + SearchView.OnQueryTextListener { + + override val layoutRes: Int = R.layout.fragment_repos + override val viewModel: ModuleViewModel by sharedViewModel() + + override fun onStart() { + super.onStart() + setHasOptionsMenu(true) + requireActivity().setTitle(R.string.downloads) + } + + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is OpenChangelogEvent -> openChangelog(event.item) + is InstallModuleEvent -> installModule(event.item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_repo, menu) + (menu.findItem(R.id.repo_search).actionView as? SearchView) + ?.setOnQueryTextListener(this) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.repo_sort) { + AlertDialog.Builder(activity) + .setTitle(R.string.sorting_order) + .setSingleChoiceItems( + R.array.sorting_orders, + Config.get(Config.Key.REPO_ORDER)!! + ) { d, which -> + Config.set(Config.Key.REPO_ORDER, which) + viewModel.updateRepos() + d.dismiss() + }.show() + } + return true + } + + override fun onQueryTextSubmit(p0: String?): Boolean { + viewModel.query.value = p0.orEmpty() + return false + } + + override fun onQueryTextChange(p0: String?): Boolean { + viewModel.query.value = p0.orEmpty() + return false + } + + private fun openChangelog(item: Repo) { + MarkDownWindow.show(context, null, item.detailUrl) + } + + private fun installModule(item: Repo) { + val context = magiskActivity + + fun download(install: Boolean) { + context.runWithExternalRW { + val intent = Intent(activity, ClassMap.get(DownloadModuleService::class.java)) + .putExtra("repo", item).putExtra("install", install) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary + } else { + context.startService(intent) + } + } + } + + CustomAlertDialog(context) + .setTitle(context.getString(R.string.repo_install_title, item.name)) + .setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename)) + .setCancelable(true) + .setPositiveButton(R.string.install) { _, _ -> download(true) } + .setNeutralButton(R.string.download) { _, _ -> download(false) } + .setNegativeButton(R.string.no_thanks, null) + .show() + } +} diff --git a/app/src/main/res/layout/fragment_modules.xml b/app/src/main/res/layout/fragment_modules.xml index df11725f3..d8492a4d0 100644 --- a/app/src/main/res/layout/fragment_modules.xml +++ b/app/src/main/res/layout/fragment_modules.xml @@ -1,30 +1,47 @@ - + xmlns:tools="http://schemas.android.com/tools"> + + + + + + android:layout_height="match_parent"> - + android:orientation="vertical" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:onRefreshListener="@{() -> viewModel.refresh()}" + app:refreshing="@{viewModel.loading}"> - + + + + + android:visibility="gone" + tools:visibility="visible" /> + android:onClick="@{() -> viewModel.fabPressed()}" + app:fabSize="normal" + app:layout_behavior="com.google.android.material.floatingactionbutton.FloatingActionButton$Behavior" + app:srcCompat="@drawable/ic_add" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_repos.xml b/app/src/main/res/layout/fragment_repos.xml index a00d6c9f6..e74219325 100644 --- a/app/src/main/res/layout/fragment_repos.xml +++ b/app/src/main/res/layout/fragment_repos.xml @@ -1,18 +1,48 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + - + + + + + + + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_module.xml b/app/src/main/res/layout/item_module.xml new file mode 100644 index 000000000..6f3283144 --- /dev/null +++ b/app/src/main/res/layout/item_module.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml new file mode 100644 index 000000000..cc0ec5035 --- /dev/null +++ b/app/src/main/res/layout/item_repo.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +