diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8ff18386..b0c8bb51f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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" /> diff --git a/app/src/main/java/com/topjohnwu/magisk/Const.java b/app/src/main/java/com/topjohnwu/magisk/Const.java index 311283b70..5189cda47 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Const.java +++ b/app/src/main/java/com/topjohnwu/magisk/Const.java @@ -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; } 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 0d9e7866e..b78e79fa1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -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()) } } 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 435bec6b3..711ca033b 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 @@ -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() @@ -25,4 +26,11 @@ class OpenFilePickerEvent : ViewEvent() class OpenChangelogEvent(val item: Repo) : ViewEvent() class InstallModuleEvent(val item: Repo) : ViewEvent() -class PageChangedEvent : ViewEvent() \ No newline at end of file +class PageChangedEvent : ViewEvent() + +class PermissionEvent( + val permissions: List, + val callback: PublishSubject +) : ViewEvent() + +class BackPressEvent : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt new file mode 100644 index 000000000..f89d87f12 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/FlashResultListener.kt @@ -0,0 +1,7 @@ +package com.topjohnwu.magisk.model.flash + +interface FlashResultListener { + + fun onResult(isSuccess: Boolean) + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt index 7ab6a6f8d..858ddcc8c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Flashing.kt @@ -13,7 +13,7 @@ sealed class Flashing( uri: Uri, private val console: MutableList, log: MutableList, - private val resultListener: (Result) -> 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, log: MutableList, - resultListener: (Result) -> Unit = {} + resultListener: FlashResultListener ) : Flashing(uri, console, log, resultListener) { override fun onResult(success: Boolean) { @@ -44,7 +44,7 @@ sealed class Flashing( uri: Uri, console: MutableList, log: MutableList, - resultListener: (Result) -> Unit = {} + resultListener: FlashResultListener ) : Flashing(uri, console, log, resultListener) { private val context: Context by inject() diff --git a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt index 7f6a98920..ebb21128e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/flash/Patching.kt @@ -6,8 +6,8 @@ import com.topjohnwu.superuser.Shell sealed class Patching( private val console: MutableList, - logs: List, - private val resultListener: (Result) -> Unit + logs: MutableList, + 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, - logs: List, - resultListener: (Result) -> Unit = {} + logs: MutableList, + 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, - logs: List, - resultListener: (Result) -> Unit = {} + logs: MutableList, + 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, - logs: List, - resultListener: (Result) -> Unit = {} + logs: MutableList, + resultListener: FlashResultListener ) : Patching(console, logs, resultListener) { override fun operations() = findImage() && extractZip() && patchBoot() && flashBoot() diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt index e79d31941..c12de4a3e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskActivity.kt @@ -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 onBackPressed() is MagiskNavigationEvent -> navigateTo(event) is ViewActionEvent -> event.action(this) is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) { @@ -86,15 +89,15 @@ abstract class MagiskActivity navigateToActivity(event) @@ -127,21 +130,21 @@ abstract class MagiskActivity 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() } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt index cc398e0d9..a05134d16 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskFragment.kt @@ -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 magiskActivity.onBackPressed() is MagiskNavigationEvent -> navigateTo(event) is ViewActionEvent -> event.action(requireActivity()) is PermissionEvent -> magiskActivity.withPermissions(*event.permissions.toTypedArray()) { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt index b3b6c60a2..3b1663958 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/MagiskViewModel.kt @@ -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() return subject.doOnSubscribe { PermissionEvent(permissions.toList(), subject).publish() } } + + fun back() = BackPressEvent().publish() + } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java deleted file mode 100644 index ba1c54aa1..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java +++ /dev/null @@ -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 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() { - 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 { - - 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(); - } - } - -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt new file mode 100644 index 000000000..48579b832 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.kt @@ -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() { + + 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() + } + +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt new file mode 100644 index 000000000..e0413d233 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -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> { itemBinding, _, item -> + item.bind(itemBinding) + itemBinding.bindExtra(BR.viewModel, this@FlashViewModel) + } + + private val rawItems = ObservableArrayList() + + 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() + .joinToString("\n") { it.item } + file.writeText(log) + file.path + } + .subscribeK { SnackbarEvent(it).publish() } + .add() + + fun restartPressed() = RootUtils.reboot() + + fun backPressed() = back() + +} \ 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 c8a705f55..f1fcb9c6c 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt @@ -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") @@ -80,4 +86,28 @@ fun setPositionChangedListener(view: ViewPager, listener: InverseBindingListener positionOffsetPixels: Int ) = 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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt index f5f8a41d5..705ee9bb3 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XList.kt @@ -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 MutableList.update(newList: List) { clear() addAll(newList) +} + +fun ObservableList.sendUpdatesTo( + target: DiffObservableList, + mapper: (List) -> List +) { + addOnListChangedCallback(object : + ObservableList.OnListChangedCallback>() { + override fun onChanged(sender: ObservableList?) { + updateAsync(sender ?: return) + } + + override fun onItemRangeRemoved(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeMoved(sender: ObservableList?, p0: Int, p1: Int, p2: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeInserted(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + override fun onItemRangeChanged(sender: ObservableList?, p0: Int, p1: Int) { + updateAsync(sender ?: return) + } + + private var updater: Disposable? = null + + private fun updateAsync(sender: List) { + updater?.dispose() + updater = sender.toSingle() + .map { mapper(it) } + .map { it to target.calculateDiff(it) } + .subscribeK { target.update(it.first, it.second) } + } + }) } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt new file mode 100644 index 000000000..5f41b8693 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XString.kt @@ -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()) +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt new file mode 100644 index 000000000..b2588a540 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XTime.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..928252fff --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_restart.xml b/app/src/main/res/drawable/ic_restart.xml new file mode 100644 index 000000000..5d7c2e1f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_restart.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_flash.xml b/app/src/main/res/layout/activity_flash.xml index 0bc141964..194303896 100644 --- a/app/src/main/res/layout/activity_flash.xml +++ b/app/src/main/res/layout/activity_flash.xml @@ -1,61 +1,150 @@ - + - + - + + + + + android:layout_height="match_parent"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + - + -