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 7f779cc29..bd07733e8 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.log.LogViewModel import com.topjohnwu.magisk.ui.module.ModuleViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import org.koin.androidx.viewmodel.dsl.viewModel @@ -15,4 +16,5 @@ val viewModelModules = module { viewModel { SuperuserViewModel(get(), get(), get(), get()) } viewModel { HideViewModel(get(), get()) } viewModel { ModuleViewModel(get()) } + viewModel { LogViewModel(get(), get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt new file mode 100644 index 000000000..af4e508a8 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ConsoleRvItem.kt @@ -0,0 +1,11 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import com.skoumal.teanity.databinding.ComparableRvItem +import com.topjohnwu.magisk.R + +class ConsoleRvItem(val item: String) : ComparableRvItem() { + override val layoutRes: Int = R.layout.item_console + + override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other) + override fun itemSameAs(other: ConsoleRvItem) = item == other.item +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt new file mode 100644 index 000000000..ea2b03537 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/LogRvItem.kt @@ -0,0 +1,75 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import androidx.databinding.ObservableList +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.util.DiffObservableList +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.model.entity.SuLogEntry +import com.topjohnwu.magisk.utils.toggle + +class LogRvItem : ComparableRvItem() { + override val layoutRes: Int = R.layout.item_page_log + + val items = DiffObservableList(callback) + + fun update(list: List) { + list.firstOrNull()?.isExpanded?.value = true + items.update(list) + } + + //two of these will never be present, safe to assume it's unique + override fun contentSameAs(other: LogRvItem): Boolean = false + + override fun itemSameAs(other: LogRvItem): Boolean = false +} + +class LogItemRvItem( + val items: ObservableList> +) : ComparableRvItem() { + override val layoutRes: Int = R.layout.item_superuser_log + + val date = items.filterIsInstance().firstOrNull() + ?.item?.dateString.orEmpty() + val isExpanded = KObservableField(false) + + fun toggle() = isExpanded.toggle() + + override fun contentSameAs(other: LogItemRvItem): Boolean = items + .any { !other.items.contains(it) } + + override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date +} + +class LogItemEntryRvItem(val item: SuLogEntry) : ComparableRvItem() { + override val layoutRes: Int = R.layout.item_superuser_log_entry + + val isExpanded = KObservableField(false) + + fun toggle() = isExpanded.toggle() + + override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid && + item.toUid == other.item.toUid && + item.fromPid == other.item.fromPid && + item.packageName == other.item.packageName && + item.command == other.item.command && + item.action == other.item.action && + item.date == other.item.date + + override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName +} + +class MagiskLogRvItem : ComparableRvItem() { + override val layoutRes: Int = R.layout.item_page_magisk_log + + val items = DiffObservableList(callback) + + fun update(list: List) { + items.update(list) + } + + //two of these will never be present, safe to assume it's unique + override fun contentSameAs(other: MagiskLogRvItem): Boolean = false + + override fun itemSameAs(other: MagiskLogRvItem): Boolean = false +} \ No newline at end of file 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 bb46765ab..435bec6b3 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 @@ -23,4 +23,6 @@ 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 +class InstallModuleEvent(val item: Repo) : ViewEvent() + +class PageChangedEvent : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java deleted file mode 100644 index 538a42d8e..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.topjohnwu.magisk.ui.log; - - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.viewpager.widget.ViewPager; - -import com.google.android.material.tabs.TabLayout; -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.model.adapters.TabFragmentAdapter; -import com.topjohnwu.magisk.ui.base.BaseFragment; - -import butterknife.BindView; - -public class LogFragment extends BaseFragment { - - @BindView(R.id.container) ViewPager viewPager; - @BindView(R.id.tab) TabLayout tab; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflate the layout for this fragment - View v = inflater.inflate(R.layout.fragment_log, container, false); - unbinder = new LogFragment_ViewBinding(this, v); - - /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - ((MainActivity) requireActivity()).toolbar.setElevation(0); - }*/ - - TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager()); - - adapter.addTab(new SuLogFragment(), getString(R.string.superuser)); - adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk)); - tab.setupWithViewPager(viewPager); - tab.setVisibility(View.VISIBLE); - - viewPager.setAdapter(adapter); - - return v; - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt new file mode 100644 index 000000000..9db8f2a67 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt @@ -0,0 +1,53 @@ +package com.topjohnwu.magisk.ui.log + + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.FragmentLogBinding +import com.topjohnwu.magisk.model.events.PageChangedEvent +import com.topjohnwu.magisk.ui.base.MagiskFragment +import org.koin.androidx.viewmodel.ext.android.viewModel + +class LogFragment : MagiskFragment() { + + override val layoutRes: Int = R.layout.fragment_log + override val viewModel: LogViewModel by viewModel() + + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is PageChangedEvent -> magiskActivity.invalidateOptionsMenu() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.logTabs.setupWithViewPager(binding.logContainer, true) + } + + override fun onStart() { + super.onStart() + setHasOptionsMenu(true) + magiskActivity.setTitle(R.string.log) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.menu_log, menu) + menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1 + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_save -> viewModel.saveLog() + R.id.menu_clear -> viewModel.clearLog() + R.id.menu_refresh -> viewModel.refresh() + } + return true + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt new file mode 100644 index 000000000..0d93c32e9 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt @@ -0,0 +1,122 @@ +package com.topjohnwu.magisk.ui.log + +import android.content.res.Resources +import androidx.databinding.ObservableArrayList +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.extensions.addOnPropertyChangedCallback +import com.skoumal.teanity.extensions.doOnSuccessUi +import com.skoumal.teanity.extensions.subscribeK +import com.skoumal.teanity.util.DiffObservableList +import com.skoumal.teanity.util.KObservableField +import com.skoumal.teanity.viewevents.SnackbarEvent +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.Const +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.data.database.MagiskDB +import com.topjohnwu.magisk.model.entity.recycler.* +import com.topjohnwu.magisk.model.events.PageChangedEvent +import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.toSingle +import com.topjohnwu.magisk.utils.zip +import com.topjohnwu.superuser.Shell +import io.reactivex.Single +import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter +import me.tatarka.bindingcollectionadapter2.OnItemBind +import java.io.File +import java.io.IOException +import java.util.* + +class LogViewModel( + private val resources: Resources, + private val database: MagiskDB +) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles> { + + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = OnItemBind> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@LogViewModel) + } + val currentPage = KObservableField(0) + private val currentItem get() = items[currentPage.value] + + private val logItem get() = items[0] as LogRvItem + private val magiskLogItem get() = items[1] as MagiskLogRvItem + + init { + currentPage.addOnPropertyChangedCallback { + it ?: return@addOnPropertyChangedCallback + PageChangedEvent().publish() + } + + items.addAll(listOf(LogRvItem(), MagiskLogRvItem())) + refresh() + } + + override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) { + is LogRvItem -> resources.getString(R.string.superuser) + is MagiskLogRvItem -> resources.getString(R.string.magisk) + else -> "" + } + + fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true } + .subscribeK() + .add() + + fun saveLog() { + val now = Calendar.getInstance() + val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format( + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, + now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), + now.get(Calendar.MINUTE), now.get(Calendar.SECOND) + ) + + val logFile = File(Const.EXTERNAL_PATH, filename) + try { + logFile.createNewFile() + } catch (e: IOException) { + return + } + + Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit { + SnackbarEvent(logFile.path).publish() + } + } + + fun clearLog() = when (currentItem) { + is LogRvItem -> clearLogs { refresh() } + is MagiskLogRvItem -> clearMagiskLogs { refresh() } + else -> Unit + } + + private fun clearLogs(callback: () -> Unit) { + Single.fromCallable { database.clearLogs() } + .subscribeK { + SnackbarEvent(R.string.logs_cleared).publish() + callback() + } + .add() + } + + private fun clearMagiskLogs(callback: () -> Unit) { + Shell.su("echo -n > " + Const.MAGISK_LOG).submit { + SnackbarEvent(R.string.logs_cleared).publish() + callback() + } + } + + private fun updateLogs() = Single.fromCallable { database.logs } + .flattenAsFlowable { it } + .map { it.map { LogItemEntryRvItem(it) } } + .map { LogItemRvItem(ObservableArrayList>().apply { addAll(it) }) } + .toList() + .doOnSuccessUi { logItem.update(it) } + + private fun updateMagiskLog() = Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").toSingle() + .map { it.exec() } + .map { it.out } + .flattenAsFlowable { it } + .map { ConsoleRvItem(it) } + .toList() + .doOnSuccessUi { magiskLogItem.update(it) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt index df442e554..c8a705f55 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -6,7 +6,10 @@ import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.Toolbar import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import androidx.databinding.InverseBindingListener import androidx.drawerlayout.widget.DrawerLayout +import androidx.viewpager.widget.ViewPager import com.google.android.material.navigation.NavigationView import com.topjohnwu.magisk.R import com.topjohnwu.magisk.model.entity.state.IndeterminateState @@ -57,3 +60,24 @@ fun setChecked(view: AppCompatImageView, isChecked: IndeterminateState) { } ) } + +@BindingAdapter("position") +fun setPosition(view: ViewPager, position: Int) { + view.currentItem = position +} + +@InverseBindingAdapter(attribute = "position", event = "positionChanged") +fun getPosition(view: ViewPager) = view.currentItem + +@BindingAdapter("positionChanged") +fun setPositionChangedListener(view: ViewPager, listener: InverseBindingListener) { + view.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageSelected(position: Int) = listener.onChange() + override fun onPageScrollStateChanged(state: Int) = listener.onChange() + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int + ) = listener.onChange() + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt index 7c59a13cb..62a324c6c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XRx.kt @@ -1,5 +1,9 @@ package com.topjohnwu.magisk.utils import io.reactivex.Single +import io.reactivex.functions.BiFunction -fun T.toSingle() = Single.just(this) \ No newline at end of file +fun T.toSingle() = Single.just(this) + +fun zip(t1: Single, t2: Single, zipper: (T1, T2) -> R) = + Single.zip(t1, t2, BiFunction { rt1, rt2 -> zipper(rt1, rt2) }) \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_log.xml b/app/src/main/res/layout/fragment_log.xml index faa43477e..a07621cc6 100644 --- a/app/src/main/res/layout/fragment_log.xml +++ b/app/src/main/res/layout/fragment_log.xml @@ -1,25 +1,37 @@ - + - + - + + + + - + android:layout_height="match_parent" + android:orientation="vertical"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_console.xml b/app/src/main/res/layout/item_console.xml new file mode 100644 index 000000000..b0c4d4fb7 --- /dev/null +++ b/app/src/main/res/layout/item_console.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_page_log.xml b/app/src/main/res/layout/item_page_log.xml new file mode 100644 index 000000000..eff2e06b5 --- /dev/null +++ b/app/src/main/res/layout/item_page_log.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_page_magisk_log.xml b/app/src/main/res/layout/item_page_magisk_log.xml new file mode 100644 index 000000000..b5d830ce2 --- /dev/null +++ b/app/src/main/res/layout/item_page_magisk_log.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_superuser_log.xml b/app/src/main/res/layout/item_superuser_log.xml new file mode 100644 index 000000000..332d1d30f --- /dev/null +++ b/app/src/main/res/layout/item_superuser_log.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_superuser_log_entry.xml b/app/src/main/res/layout/item_superuser_log_entry.xml new file mode 100644 index 000000000..da660d8fc --- /dev/null +++ b/app/src/main/res/layout/item_superuser_log_entry.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file