Updated flash screen with new arch

This commit is contained in:
Viktor De Pasquale 2019-04-24 20:28:41 +02:00
parent 07eb7dda2d
commit 14ff22fbcd
21 changed files with 445 additions and 348 deletions

View File

@ -35,7 +35,7 @@
android:name="a.f"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="nosensor"
android:theme="@style/AppTheme.NoDrawer" />
android:theme="@style/MagiskTheme.Flashing" />
<!-- Superuser -->

View File

@ -35,6 +35,9 @@ public class Const {
public static final int USER_ID = Process.myUid() / 100000;
// Generic
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
public static final class MAGISK_VER {
public static final int MIN_SUPPORT = 18000;
}

View File

@ -1,6 +1,8 @@
package com.topjohnwu.magisk.di
import android.net.Uri
import com.topjohnwu.magisk.ui.MainViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
@ -17,4 +19,5 @@ val viewModelModules = module {
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get()) }
viewModel { LogViewModel(get(), get()) }
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
}

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk.model.events
import android.app.Activity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.entity.Repo
import io.reactivex.subjects.PublishSubject
data class OpenLinkEvent(val url: String) : ViewEvent()
@ -26,3 +27,10 @@ class OpenChangelogEvent(val item: Repo) : ViewEvent()
class InstallModuleEvent(val item: Repo) : ViewEvent()
class PageChangedEvent : ViewEvent()
class PermissionEvent(
val permissions: List<String>,
val callback: PublishSubject<Boolean>
) : ViewEvent()
class BackPressEvent : ViewEvent()

View File

@ -0,0 +1,7 @@
package com.topjohnwu.magisk.model.flash
interface FlashResultListener {
fun onResult(isSuccess: Boolean)
}

View File

@ -13,7 +13,7 @@ sealed class Flashing(
uri: Uri,
private val console: MutableList<String>,
log: MutableList<String>,
private val resultListener: (Result<Boolean>) -> Unit
private val resultListener: FlashResultListener
) : FlashZip(uri, console, log) {
override fun onResult(success: Boolean) {
@ -21,14 +21,14 @@ sealed class Flashing(
console.add("! Installation failed")
}
resultListener(Result.success(success))
resultListener.onResult(success)
}
class Install(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>,
resultListener: (Result<Boolean>) -> Unit = {}
resultListener: FlashResultListener
) : Flashing(uri, console, log, resultListener) {
override fun onResult(success: Boolean) {
@ -44,7 +44,7 @@ sealed class Flashing(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>,
resultListener: (Result<Boolean>) -> Unit = {}
resultListener: FlashResultListener
) : Flashing(uri, console, log, resultListener) {
private val context: Context by inject()

View File

@ -6,8 +6,8 @@ import com.topjohnwu.superuser.Shell
sealed class Patching(
private val console: MutableList<String>,
logs: List<String>,
private val resultListener: (Result<Boolean>) -> Unit
logs: MutableList<String>,
private val resultListener: FlashResultListener
) : MagiskInstaller(console, logs) {
override fun onResult(success: Boolean) {
@ -17,14 +17,14 @@ sealed class Patching(
Shell.sh("rm -rf $installDir").submit()
console.add("! Installation failed")
}
resultListener(Result.success(success))
resultListener.onResult(success)
}
class File(
private val uri: Uri,
console: MutableList<String>,
logs: List<String>,
resultListener: (Result<Boolean>) -> Unit = {}
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
@ -32,8 +32,8 @@ sealed class Patching(
class SecondSlot(
console: MutableList<String>,
logs: List<String>,
resultListener: (Result<Boolean>) -> Unit = {}
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
@ -41,8 +41,8 @@ sealed class Patching(
class Direct(
console: MutableList<String>,
logs: List<String>,
resultListener: (Result<Boolean>) -> Unit = {}
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
findImage() && extractZip() && patchBoot() && flashBoot()

View File

@ -16,6 +16,7 @@ import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
@ -36,7 +37,8 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
protected open val defaultPosition: Int = 0
protected val navigationController by lazy {
protected val navigationController get() = if (navHostId == 0) null else _navigationController
private val _navigationController by lazy {
if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?")
FragNavController(supportFragmentManager, navHostId)
}
@ -54,7 +56,7 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigationController.apply {
navigationController?.apply {
rootFragmentListener = this@MagiskActivity
initialize(defaultPosition, savedInstanceState)
}
@ -62,13 +64,14 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController.onSaveInstanceState(outState)
navigationController?.onSaveInstanceState(outState)
}
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
@ -86,15 +89,15 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
navigationController?.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
navigationController.currentStack
navigationController?.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController.popFragments(it) }
?.let { navigationController?.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
@ -127,21 +130,21 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController.pushFragment(it) }
.let { navigationController?.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController.switchTab(index)
else -> navigationController?.switchTab(index)
}
}
override fun onBackPressed() {
val fragment = navigationController.currentFrag as? MagiskFragment<*, *>
val fragment = navigationController?.currentFrag as? MagiskFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try {
navigationController.popFragment()
navigationController?.popFragment() ?: throw UnsupportedOperationException()
} catch (e: UnsupportedOperationException) {
super.onBackPressed()
}

View File

@ -5,6 +5,7 @@ import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.skoumal.teanity.view.TeanityFragment
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
@ -27,6 +28,7 @@ abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBin
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> magiskActivity.onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(requireActivity())
is PermissionEvent -> magiskActivity.withPermissions(*event.permissions.toTypedArray()) {

View File

@ -2,6 +2,7 @@ package com.topjohnwu.magisk.ui.base
import android.app.Activity
import com.skoumal.teanity.viewmodel.LoadingViewModel
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.Event
@ -23,4 +24,7 @@ abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener {
val subject = PublishSubject.create<Boolean>()
return subject.doOnSubscribe { PermissionEvent(permissions.toList(), subject).publish() }
}
fun back() = BackPressEvent().publish()
}

View File

@ -1,274 +0,0 @@
package com.topjohnwu.magisk.ui.flash;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.StringListAdapter;
import com.topjohnwu.magisk.tasks.FlashZip;
import com.topjohnwu.magisk.tasks.MagiskInstaller;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.OnClick;
public class FlashActivity extends BaseActivity {
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.button_panel) LinearLayout buttonPanel;
@BindView(R.id.reboot) Button reboot;
@BindView(R.id.recyclerView) RecyclerView rv;
@BindColor(android.R.color.white) int white;
private List<String> console, logs;
@OnClick(R.id.reboot)
void reboot() {
RootUtils.reboot();
}
@OnClick(R.id.save_logs)
void saveLogs() {
runWithExternalRW(() -> {
Calendar now = Calendar.getInstance();
String filename = String.format(Locale.US,
"magisk_install_log_%04d%02d%02d_%02d%02d%02d.log",
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));
File logFile = new File(Const.EXTERNAL_PATH, filename);
try (FileWriter writer = new FileWriter(logFile)) {
for (String s : logs) {
writer.write(s);
writer.write('\n');
}
} catch (IOException e) {
e.printStackTrace();
return;
}
Utils.toast(logFile.getPath(), Toast.LENGTH_LONG);
});
}
@OnClick(R.id.close)
public void close() {
finish();
}
@Override
public void onBackPressed() {
// Prevent user accidentally press back button
}
@Override
public int getDarkTheme() {
return R.style.AppTheme_NoDrawer_Dark;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash);
new FlashActivity_ViewBinding(this);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setTitle(R.string.flashing);
}
setFloating();
setFinishOnTouchOutside(false);
if (!Shell.rootAccess())
reboot.setVisibility(View.GONE);
logs = Collections.synchronizedList(new ArrayList<>());
console = new ConsoleList();
rv.setAdapter(new ConsoleAdapter());
Intent intent = getIntent();
Uri uri = intent.getData();
switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) {
case Const.Value.FLASH_ZIP:
new FlashModule(uri).exec();
break;
case Const.Value.UNINSTALL:
new Uninstall(uri).exec();
break;
case Const.Value.FLASH_MAGISK:
new DirectInstall().exec();
break;
case Const.Value.FLASH_INACTIVE_SLOT:
new SecondSlot().exec();
break;
case Const.Value.PATCH_FILE:
new PatchFile(uri).exec();
break;
}
}
private class ConsoleAdapter extends StringListAdapter<ConsoleAdapter.ViewHolder> {
ConsoleAdapter() {
super(console, true);
}
@Override
protected int itemLayoutRes() {
return R.layout.list_item_console;
}
@NonNull
@Override
public ViewHolder createViewHolder(@NonNull View v) {
return new ViewHolder(v);
}
class ViewHolder extends StringListAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
txt.setTextColor(white);
}
@Override
protected int textViewResId() {
return R.id.txt;
}
}
}
private class ConsoleList extends CallbackList<String> {
ConsoleList() {
super(new ArrayList<>());
}
private void updateUI() {
rv.getAdapter().notifyItemChanged(size() - 1);
rv.postDelayed(() -> rv.smoothScrollToPosition(size() - 1), 10);
}
@Override
public void onAddElement(String s) {
logs.add(s);
updateUI();
}
@Override
public String set(int i, String s) {
String ret = super.set(i, s);
UiThreadHandler.run(this::updateUI);
return ret;
}
}
private class FlashModule extends FlashZip {
FlashModule(Uri uri) {
super(uri, console, logs);
}
@Override
protected void onResult(boolean success) {
if (success) {
Utils.loadModules();
} else {
console.add("! Installation failed");
reboot.setVisibility(View.GONE);
}
buttonPanel.setVisibility(View.VISIBLE);
}
}
private class Uninstall extends FlashModule {
Uninstall(Uri uri) {
super(uri);
}
@Override
protected void onResult(boolean success) {
if (success)
UiThreadHandler.handler.postDelayed(Shell.su("pm uninstall " + getPackageName())::exec, 3000);
else
super.onResult(false);
}
}
private abstract class BaseInstaller extends MagiskInstaller {
BaseInstaller() {
super(console, logs);
}
@Override
protected void onResult(boolean success) {
if (success) {
console.add("- All done!");
} else {
Shell.sh("rm -rf " + installDir).submit();
console.add("! Installation failed");
reboot.setVisibility(View.GONE);
}
buttonPanel.setVisibility(View.VISIBLE);
}
}
private class DirectInstall extends BaseInstaller {
@Override
protected boolean operations() {
return findImage() && extractZip() && patchBoot() && flashBoot();
}
}
private class SecondSlot extends BaseInstaller {
@Override
protected boolean operations() {
return findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA();
}
}
private class PatchFile extends BaseInstaller {
private Uri uri;
PatchFile(Uri u) {
uri = u;
}
@Override
protected boolean operations() {
return extractZip() && handleFile(uri) && patchBoot() && storeBoot();
}
}
}

View File

@ -0,0 +1,24 @@
package com.topjohnwu.magisk.ui.flash
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.ui.base.MagiskActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash
override val viewModel: FlashViewModel by viewModel {
val uri = intent.data
val action = intent.getStringExtra(Const.Key.FLASH_ACTION) ?: let { finish();"" }
parametersOf(action, uri)
}
override fun onBackPressed() {
if (viewModel.loading) return
super.onBackPressed()
}
}

View File

@ -0,0 +1,104 @@
package com.topjohnwu.magisk.ui.flash
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.res.Resources
import android.net.Uri
import android.os.Handler
import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList
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.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.flash.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
class FlashViewModel(
action: String,
uri: Uri?,
private val resources: Resources
) : MagiskViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false)
val behaviorText = KObservableField(resources.getString(R.string.flashing))
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@FlashViewModel)
}
private val rawItems = ObservableArrayList<String>()
init {
rawItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
state = State.LOADING
val uri = uri ?: Uri.EMPTY
when (action) {
Const.Value.FLASH_ZIP -> Flashing
.Install(uri, rawItems, rawItems, this)
.exec()
Const.Value.UNINSTALL -> Flashing
.Uninstall(uri, rawItems, rawItems, this)
.exec()
Const.Value.FLASH_MAGISK -> Patching
.Direct(rawItems, rawItems, this)
.exec()
Const.Value.FLASH_INACTIVE_SLOT -> Patching
.SecondSlot(rawItems, rawItems, this)
.exec()
Const.Value.PATCH_FILE -> Patching
.File(uri, rawItems, rawItems, this)
.exec()
}
}
override fun onResult(isSuccess: Boolean) {
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
behaviorText.value = when {
isSuccess -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure)
}
if (isSuccess) {
Handler().postDelayed(500) {
showRestartTitle.value = true
}
}
}
fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
.map { now }
.map { it.toTime(timeFormatFull) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Const.EXTERNAL_PATH, it) }
.map { file ->
val log = items.filterIsInstance<ConsoleRvItem>()
.joinToString("\n") { it.item }
file.writeText(log)
file.path
}
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = RootUtils.reboot()
fun backPressed() = back()
}

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.utils
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
@ -9,10 +10,15 @@ import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.viewpager.widget.ViewPager
import com.google.android.material.navigation.NavigationView
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit
@BindingAdapter("onNavigationClick")
@ -81,3 +87,27 @@ fun setPositionChangedListener(view: ViewPager, listener: InverseBindingListener
) = listener.onChange()
})
}
@BindingAdapter("invisibleScale")
fun setInvisibleWithScale(view: View, isInvisible: Boolean) {
view.animate()
.scaleX(if (isInvisible) 0f else 1f)
.scaleY(if (isInvisible) 0f else 1f)
.setInterpolator(FastOutSlowInInterpolator())
.start()
}
@BindingAdapter("movieBehavior", "movieBehaviorText")
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
(view.tag as? Disposable)?.dispose()
if (isMovieBehavior) {
val observer = Observable
.interval(150, TimeUnit.MILLISECONDS)
.subscribeK {
view.text = text.replaceRandomWithSpecial()
}
view.tag = observer
} else {
view.text = text
}
}

View File

@ -1,6 +1,49 @@
package com.topjohnwu.magisk.utils
import androidx.databinding.ObservableList
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import io.reactivex.disposables.Disposable
fun <T> MutableList<T>.update(newList: List<T>) {
clear()
addAll(newList)
}
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
target: DiffObservableList<T2>,
mapper: (List<T1>) -> List<T2>
) {
addOnListChangedCallback(object :
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
override fun onChanged(sender: ObservableList<T1>?) {
updateAsync(sender ?: return)
}
override fun onItemRangeRemoved(sender: ObservableList<T1>?, p0: Int, p1: Int) {
updateAsync(sender ?: return)
}
override fun onItemRangeMoved(sender: ObservableList<T1>?, p0: Int, p1: Int, p2: Int) {
updateAsync(sender ?: return)
}
override fun onItemRangeInserted(sender: ObservableList<T1>?, p0: Int, p1: Int) {
updateAsync(sender ?: return)
}
override fun onItemRangeChanged(sender: ObservableList<T1>?, p0: Int, p1: Int) {
updateAsync(sender ?: return)
}
private var updater: Disposable? = null
private fun updateAsync(sender: List<T1>) {
updater?.dispose()
updater = sender.toSingle()
.map { mapper(it) }
.map { it to target.calculateDiff(it) }
.subscribeK { target.update(it.first, it.second) }
}
})
}

View File

@ -0,0 +1,11 @@
package com.topjohnwu.magisk.utils
val specialChars = arrayOf('!', '@', '#', '$', '%', '&', '?')
fun String.replaceRandomWithSpecial(): String {
var random: Char
do {
random = random()
} while (random == '.')
return replace(random, specialChars.random())
}

View File

@ -0,0 +1,18 @@
package com.topjohnwu.magisk.utils
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
val now get() = System.currentTimeMillis()
fun Long.toTime(format: SimpleDateFormat) = format.format(this).orEmpty()
fun String.toTime(format: SimpleDateFormat) = try {
format.parse(this)?.time ?: -1
} catch (e: ParseException) {
-1L
}
private val locale get() = Locale.getDefault()
val timeFormatFull by lazy { SimpleDateFormat("YYYY/MM/DD_HH:mm:ss", locale) }
val timeFormatStandard by lazy { SimpleDateFormat("YYYY-MM-DD'T'HH:mm:ss'Z'", locale) }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/colorText"
android:pathData="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,4C14.1,4 16.1,4.8 17.6,6.3C20.7,9.4 20.7,14.5 17.6,17.6C15.8,19.5 13.3,20.2 10.9,19.9L11.4,17.9C13.1,18.1 14.9,17.5 16.2,16.2C18.5,13.9 18.5,10.1 16.2,7.7C15.1,6.6 13.5,6 12,6V10.6L7,5.6L12,0.6V4M6.3,17.6C3.7,15 3.3,11 5.1,7.9L6.6,9.4C5.5,11.6 5.9,14.4 7.8,16.2C8.3,16.7 8.9,17.1 9.6,17.4L9,19.4C8,19 7.1,18.4 6.3,17.6Z" />
</vector>

View File

@ -1,61 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/flashing_background_color"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.topjohnwu.magisk.ui.flash.FlashViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</HorizontalScrollView>
<LinearLayout
android:id="@+id/button_panel"
style="?android:buttonStyle"
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:visibility="gone">
android:theme="@style/AppBarLayoutTheme.Flashing">
<Button
android:id="@+id/close"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:text="@string/close" />
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:popupTheme="@style/ToolbarPopupTheme.Flashing">
<Button
android:id="@+id/save_logs"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:text="@string/menuSaveLog" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<Button
android:id="@+id/reboot"
style="?android:borderlessButtonStyle"
<FrameLayout
invisibleScale="@{viewModel.loading}"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:onClick="@{() -> viewModel.backPressed()}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintLeft_toLeftOf="parent">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon"
android:layout_gravity="center"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_back"
app:tint="@android:color/white" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.Emphasize"
movieBehavior="@{viewModel.loading}"
movieBehaviorText="@{viewModel.behaviorText}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Flashing..." />
<FrameLayout
invisibleScale="@{viewModel.loading}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:onClick="@{() -> viewModel.savePressed()}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintRight_toRightOf="parent">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon"
android:layout_gravity="center"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_save"
app:tint="@android:color/white" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_console" />
<com.google.android.material.card.MaterialCardView
invisibleScale="@{!viewModel.loaded || !viewModel.canShowReboot}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_generic"
app:cardBackgroundColor="@color/colorSecondary"
app:cardCornerRadius="26dp"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:onClick="@{() -> viewModel.restartPressed()}"
android:padding="@dimen/margin_generic_half">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_restart"
app:tint="@color/colorTextTinted" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.Emphasize.Tinted"
gone="@{!viewModel.showRestartTitle}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_generic_half"
android:paddingRight="@dimen/margin_generic_half"
android:text="@string/reboot" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -99,7 +99,9 @@
<string name="dtbo_patched_title">DTBO was patched!</string>
<string name="dtbo_patched_reboot">Magisk Manager has patched dtbo.img. Please reboot.</string>
<string name="flashing">Flashing</string>
<string name="flashing">Flashing…</string>
<string name="done">Done!</string>
<string name="failure">Failed</string>
<string name="hide_manager_title">Hiding Magisk Manager…</string>
<string name="hide_manager_fail_toast">Hide Magisk Manager failed.</string>
<string name="open_link_failed_toast">No application found to open the link.</string>