Updated modules and repos screen

Screens are merged via common viewModel, all data are immediately accessible to both of them
This commit is contained in:
Viktor De Pasquale
2019-04-20 23:44:08 +02:00
parent ce693aa5e9
commit adbd47a36c
15 changed files with 725 additions and 285 deletions
@@ -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<ComparableRvItem<*>> { 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<String, Module>) = 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 <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
val out = mutableListOf<Result>()
while (moveToNext()) out.add(transformer(this))
return out
}
}
@@ -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<Module> 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<String, Module> 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);
}
}
@@ -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<ModuleViewModel, FragmentModulesBinding>() {
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<Any>(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<String, Module>) {
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
}*/
}
@@ -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;
}
}
@@ -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<ModuleViewModel, FragmentReposBinding>(),
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<Int>(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<Any>(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()
}
}