From 52c83b2916e8c5d2ab82a7866df927edaaadfcfd Mon Sep 17 00:00:00 2001 From: Viktor De Pasquale Date: Fri, 26 Apr 2019 21:23:25 +0200 Subject: [PATCH] Updated su screen with new arch Added new Dialog for further use --- app/src/main/java/com/topjohnwu/magisk/App.kt | 12 +- .../topjohnwu/magisk/di/ApplicationModule.kt | 4 + .../com/topjohnwu/magisk/di/NamedInjection.kt | 5 + .../topjohnwu/magisk/di/ViewModelsModule.kt | 7 + .../model/entity/recycler/SpinnerRvItem.kt | 13 + .../magisk/model/events/ViewEvents.kt | 6 +- .../ui/surequest/SuRequestActivity.java | 287 ------------------ .../magisk/ui/surequest/SuRequestActivity.kt | 67 ++++ .../magisk/ui/surequest/SuRequestViewModel.kt | 251 +++++++++++++++ .../magisk/ui/surequest/_SuRequestActivity.kt | 166 ++++++++++ .../ui/surequest/_SuRequestViewModel.kt | 112 +++++++ .../magisk/utils/DataBindingAdapters.kt | 31 ++ .../com/topjohnwu/magisk/view/MagiskDialog.kt | 155 ++++++++++ app/src/main/res/layout/activity_request.xml | 229 ++++++++------ .../main/res/layout/activity_su_request.xml | 17 ++ .../main/res/layout/dialog_magisk_base.xml | 276 +++++++++++++++++ app/src/main/res/layout/item_spinner.xml | 26 ++ 17 files changed, 1274 insertions(+), 390 deletions(-) create mode 100644 app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt delete mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt create mode 100644 app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt create mode 100644 app/src/main/res/layout/activity_su_request.xml create mode 100644 app/src/main/res/layout/dialog_magisk_base.xml create mode 100644 app/src/main/res/layout/item_spinner.xml diff --git a/app/src/main/java/com/topjohnwu/magisk/App.kt b/app/src/main/java/com/topjohnwu/magisk/App.kt index 2d839a559..d9372e04e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/App.kt +++ b/app/src/main/java/com/topjohnwu/magisk/App.kt @@ -28,8 +28,12 @@ import java.util.concurrent.ThreadPoolExecutor open class App : Application(), Application.ActivityLifecycleCallbacks { // Global resources - val prefs: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(deContext) - val DB: MagiskDB by lazy { MagiskDB(deContext) } + lateinit var protectedContext: Context + val prefs: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences( + protectedContext + ) + val DB: MagiskDB by lazy { MagiskDB(protectedContext) } @Deprecated("Use dependency injection") val repoDB: RepoDatabaseHelper by inject() @Volatile @@ -49,12 +53,14 @@ open class App : Application(), Application.ActivityLifecycleCallbacks { override fun attachBaseContext(base: Context) { super.attachBaseContext(base) MultiDex.install(base) + protectedContext = baseContext self = this deContext = base registerActivityLifecycleCallbacks(this) if (Build.VERSION.SDK_INT >= 24) { - deContext = base.createDeviceProtectedStorageContext() + protectedContext = base.createDeviceProtectedStorageContext() + deContext = protectedContext deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName) } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt index f0545b75e..b185ff135 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt @@ -11,4 +11,8 @@ val applicationModule = module { single { get().resources } single { get() as App } single { get().packageManager } + single(SUTimeout) { + get().protectedContext + .getSharedPreferences("su_timeout", 0) + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt b/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt new file mode 100644 index 000000000..c6ab30319 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt @@ -0,0 +1,5 @@ +package com.topjohnwu.magisk.di + +import org.koin.core.qualifier.named + +val SUTimeout = named("su_timeout") \ No newline at end of file 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 b78e79fa1..0c59ce6cf 100644 --- a/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt +++ b/app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt @@ -1,5 +1,6 @@ package com.topjohnwu.magisk.di +import android.content.Intent import android.net.Uri import com.topjohnwu.magisk.ui.MainViewModel import com.topjohnwu.magisk.ui.flash.FlashViewModel @@ -8,6 +9,8 @@ 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 com.topjohnwu.magisk.ui.surequest.SuRequestViewModel +import com.topjohnwu.magisk.ui.surequest._SuRequestViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -20,4 +23,8 @@ val viewModelModules = module { viewModel { ModuleViewModel(get(), get()) } viewModel { LogViewModel(get(), get()) } viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) } + viewModel { (intent: Intent, action: String?) -> + _SuRequestViewModel(intent, action.orEmpty(), get(), get()) + } + viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt new file mode 100644 index 000000000..92dcbf08f --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt @@ -0,0 +1,13 @@ +package com.topjohnwu.magisk.model.entity.recycler + +import com.skoumal.teanity.databinding.ComparableRvItem +import com.topjohnwu.magisk.R + +class SpinnerRvItem(val item: String) : ComparableRvItem() { + + override val layoutRes: Int = R.layout.item_spinner + + override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other) + override fun itemSameAs(other: SpinnerRvItem) = item == other.item + +} \ 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 711ca033b..a47adff93 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt @@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.events import android.app.Activity import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.model.entity.Policy import com.topjohnwu.magisk.model.entity.Repo import io.reactivex.subjects.PublishSubject @@ -33,4 +34,7 @@ class PermissionEvent( val callback: PublishSubject ) : ViewEvent() -class BackPressEvent : ViewEvent() \ No newline at end of file +class BackPressEvent : ViewEvent() + +class SuDialogEvent(val policy: Policy) : ViewEvent() +class DieEvent : ViewEvent() \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java deleted file mode 100644 index d7726bf85..000000000 --- a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.topjohnwu.magisk.ui.surequest; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.fingerprint.FingerprintManager; -import android.os.Bundle; -import android.os.CountDownTimer; -import android.text.TextUtils; -import android.view.View; -import android.view.Window; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; - -import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; - -import com.topjohnwu.magisk.App; -import com.topjohnwu.magisk.BuildConfig; -import com.topjohnwu.magisk.Config; -import com.topjohnwu.magisk.R; -import com.topjohnwu.magisk.model.entity.Policy; -import com.topjohnwu.magisk.ui.base.BaseActivity; -import com.topjohnwu.magisk.utils.FingerprintHelper; -import com.topjohnwu.magisk.utils.SuConnector; -import com.topjohnwu.magisk.utils.SuLogger; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import butterknife.BindView; -import java9.lang.Iterables; - -public class SuRequestActivity extends BaseActivity { - - @BindView(R.id.su_popup) LinearLayout suPopup; - @BindView(R.id.timeout) Spinner timeout; - @BindView(R.id.app_icon) ImageView appIcon; - @BindView(R.id.app_name) TextView appNameView; - @BindView(R.id.package_name) TextView packageNameView; - @BindView(R.id.grant_btn) Button grant_btn; - @BindView(R.id.deny_btn) Button deny_btn; - @BindView(R.id.fingerprint) ImageView fingerprintImg; - @BindView(R.id.warning) TextView warning; - - private ActionHandler handler; - private Policy policy; - private SharedPreferences timeoutPrefs; - - public static final String REQUEST = "request"; - public static final String LOG = "log"; - public static final String NOTIFY = "notify"; - - @Override - public int getDarkTheme() { - return R.style.SuRequest_Dark; - } - - @Override - public void onBackPressed() { - handler.handleAction(Policy.DENY, -1); - } - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - lockOrientation(); - supportRequestWindowFeature(Window.FEATURE_NO_TITLE); - - timeoutPrefs = App.deContext.getSharedPreferences("su_timeout", 0); - Intent intent = getIntent(); - - String action = intent.getAction(); - - if (TextUtils.equals(action, REQUEST)) { - if (!handleRequest()) - finish(); - return; - } - - if (TextUtils.equals(action, LOG)) - SuLogger.handleLogs(intent); - else if (TextUtils.equals(action, NOTIFY)) - SuLogger.handleNotify(intent); - - finish(); - } - - private boolean handleRequest() { - String socketName = getIntent().getStringExtra("socket"); - - if (socketName == null) - return false; - - SuConnector connector; - try { - connector = new SuConnector(socketName) { - @Override - protected void onResponse() throws IOException { - out.writeInt(policy.policy); - } - }; - Bundle bundle = connector.readSocketInput(); - int uid = Integer.parseInt(bundle.getString("uid")); - app.getDB().clearOutdated(); - policy = app.getDB().getPolicy(uid); - if (policy == null) { - policy = new Policy(uid, getPackageManager()); - } - } catch (IOException | PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return false; - } - handler = new ActionHandler() { - @Override - void handleAction() { - connector.response(); - done(); - } - - @Override - void handleAction(int action) { - int pos = timeout.getSelectedItemPosition(); - timeoutPrefs.edit().putInt(policy.packageName, pos).apply(); - handleAction(action, Config.Value.TIMEOUT_LIST[pos]); - } - - @Override - void handleAction(int action, int time) { - policy.policy = action; - if (time >= 0) { - policy.until = (time == 0) ? 0 - : (System.currentTimeMillis() / 1000 + time * 60); - app.getDB().updatePolicy(policy); - } - handleAction(); - } - }; - - // Never allow com.topjohnwu.magisk (could be malware) - if (TextUtils.equals(policy.packageName, BuildConfig.APPLICATION_ID)) - return false; - - // If not interactive, response directly - if (policy.policy != Policy.INTERACTIVE) { - handler.handleAction(); - return true; - } - - switch ((int) Config.get(Config.Key.SU_AUTO_RESPONSE)) { - case Config.Value.SU_AUTO_DENY: - handler.handleAction(Policy.DENY, 0); - return true; - case Config.Value.SU_AUTO_ALLOW: - handler.handleAction(Policy.ALLOW, 0); - return true; - } - - showUI(); - return true; - } - - @SuppressLint("ClickableViewAccessibility") - private void showUI() { - setContentView(R.layout.activity_request); - new SuRequestActivity_ViewBinding(this); - - appIcon.setImageDrawable(policy.info.loadIcon(getPackageManager())); - appNameView.setText(policy.appName); - packageNameView.setText(policy.packageName); - warning.setCompoundDrawablesRelativeWithIntrinsicBounds( - AppCompatResources.getDrawable(this, R.drawable.ic_warning), null, null, null); - - ArrayAdapter adapter = ArrayAdapter.createFromResource(this, - R.array.allow_timeout, android.R.layout.simple_spinner_item); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - timeout.setAdapter(adapter); - timeout.setSelection(timeoutPrefs.getInt(policy.packageName, 0)); - - CountDownTimer timer = new CountDownTimer( - (int) Config.get(Config.Key.SU_REQUEST_TIMEOUT) * 1000, 1000) { - @Override - public void onTick(long remains) { - deny_btn.setText(getString(R.string.deny) + "(" + remains / 1000 + ")"); - } - @Override - public void onFinish() { - deny_btn.setText(getString(R.string.deny)); - handler.handleAction(Policy.DENY); - } - }; - timer.start(); - Runnable cancelTimer = () -> { - timer.cancel(); - deny_btn.setText(getString(R.string.deny)); - }; - handler.addCancel(cancelTimer); - - boolean useFP = FingerprintHelper.useFingerprint(); - - if (useFP) try { - FingerprintHelper helper = new SuFingerprint(); - helper.authenticate(); - handler.addCancel(helper::cancel); - } catch (Exception e) { - e.printStackTrace(); - useFP = false; - } - - if (!useFP) { - grant_btn.setOnClickListener(v -> { - handler.handleAction(Policy.ALLOW); - timer.cancel(); - }); - grant_btn.requestFocus(); - } - - grant_btn.setVisibility(useFP ? View.GONE : View.VISIBLE); - fingerprintImg.setVisibility(useFP ? View.VISIBLE : View.GONE); - - deny_btn.setOnClickListener(v -> { - handler.handleAction(Policy.DENY); - timer.cancel(); - }); - suPopup.setOnClickListener(v -> cancelTimer.run()); - timeout.setOnTouchListener((v, event) -> { - cancelTimer.run(); - return false; - }); - } - - private class SuFingerprint extends FingerprintHelper { - - SuFingerprint() throws Exception {} - - @Override - public void onAuthenticationError(int errorCode, CharSequence errString) { - warning.setText(errString); - } - - @Override - public void onAuthenticationHelp(int helpCode, CharSequence helpString) { - warning.setText(helpString); - } - - @Override - public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { - handler.handleAction(Policy.ALLOW); - } - - @Override - public void onAuthenticationFailed() { - warning.setText(R.string.auth_fail); - } - } - - private class ActionHandler { - private List cancelTasks = new ArrayList<>(); - - void handleAction() { - done(); - } - - void handleAction(int action) { - done(); - } - - void handleAction(int action, int time) { - done(); - } - - void addCancel(Runnable r) { - cancelTasks.add(r); - } - - void done() { - Iterables.forEach(cancelTasks, Runnable::run); - finish(); - } - } -} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt new file mode 100644 index 000000000..9ad1b2f62 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt @@ -0,0 +1,67 @@ +package com.topjohnwu.magisk.ui.surequest + +import android.content.pm.ActivityInfo +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.view.Window +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ActivityRequestBinding +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.model.events.DieEvent +import com.topjohnwu.magisk.ui.base.MagiskActivity +import com.topjohnwu.magisk.utils.SuLogger +import org.koin.androidx.viewmodel.ext.android.viewModel + +open class SuRequestActivity : MagiskActivity() { + + override val layoutRes: Int = R.layout.activity_request + override val viewModel: SuRequestViewModel by viewModel() + + override fun onBackPressed() { + viewModel.handler?.handleAction(Policy.DENY, -1) + } + + override fun onCreate(savedInstanceState: Bundle?) { + supportRequestWindowFeature(Window.FEATURE_NO_TITLE) + lockOrientation() + super.onCreate(savedInstanceState) + + val intent = intent + val action = intent.action + + if (TextUtils.equals(action, REQUEST)) { + if (!viewModel.handleRequest(intent) {}) + finish() + return + } + + if (TextUtils.equals(action, LOG)) + SuLogger.handleLogs(intent) + else if (TextUtils.equals(action, NOTIFY)) + SuLogger.handleNotify(intent) + + finish() + } + + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is DieEvent -> finish() + } + } + + private fun lockOrientation() { + requestedOrientation = if (Build.VERSION.SDK_INT < 18) + resources.configuration.orientation + else + ActivityInfo.SCREEN_ORIENTATION_LOCKED + } + + companion object { + const val REQUEST = "request" + const val LOG = "log" + const val NOTIFY = "notify" + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt new file mode 100644 index 000000000..a3f67743b --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt @@ -0,0 +1,251 @@ +package com.topjohnwu.magisk.ui.surequest + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.hardware.fingerprint.FingerprintManager +import android.os.CountDownTimer +import android.text.TextUtils +import com.skoumal.teanity.databinding.ComparableRvItem +import com.skoumal.teanity.util.DiffObservableList +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.data.database.MagiskDB +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem +import com.topjohnwu.magisk.model.events.DieEvent +import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.FingerprintHelper +import com.topjohnwu.magisk.utils.SuConnector +import com.topjohnwu.magisk.utils.now +import me.tatarka.bindingcollectionadapter2.ItemBinding +import java.io.IOException +import java.util.concurrent.TimeUnit.* + +class SuRequestViewModel( + private val packageManager: PackageManager, + private val database: MagiskDB, + private val timeoutPrefs: SharedPreferences, + private val resources: Resources +) : MagiskViewModel() { + + val icon = KObservableField(null) + val title = KObservableField("") + val packageName = KObservableField("") + + val denyText = KObservableField(resources.getString(R.string.deny)) + val warningText = KObservableField(resources.getString(R.string.su_warning)) + + val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint()) + val selectedItemPosition = KObservableField(0) + + val items = DiffObservableList(ComparableRvItem.callback) + val itemBinding = ItemBinding.of> { binding, _, item -> + item.bind(binding) + } + + + var handler: ActionHandler? = null + private var timer: CountDownTimer? = null + private var policy: Policy? = null + set(value) { + field = value + updatePolicy(value) + } + + init { + resources.getStringArray(R.array.allow_timeout) + .map { SpinnerRvItem(it) } + .let { items.update(it) } + } + + private fun updatePolicy(policy: Policy?) { + policy ?: return + + icon.value = policy.info.loadIcon(packageManager) + title.value = policy.appName + packageName.value = policy.packageName + + selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0) + } + + private fun cancelTimer() { + timer?.cancel() + denyText.value = resources.getString(R.string.deny) + } + + fun grantPressed() { + handler?.handleAction(Policy.ALLOW) + timer?.cancel() + } + + fun denyPressed() { + handler?.handleAction(Policy.DENY) + timer?.cancel() + } + + fun spinnerTouched(): Boolean { + cancelTimer() + return false + } + + fun handleRequest(intent: Intent, createUICallback: () -> Unit): Boolean { + val socketName = intent.getStringExtra("socket") ?: return false + + val connector: SuConnector + try { + connector = object : SuConnector(socketName) { + @Throws(IOException::class) + override fun onResponse() { + out.writeInt(policy?.policy ?: return) + } + } + val bundle = connector.readSocketInput() + val uid = bundle.getString("uid")?.toIntOrNull() ?: return false + database.clearOutdated() + policy = database.getPolicy(uid) ?: Policy(uid, packageManager) + } catch (e: IOException) { + e.printStackTrace() + return false + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + return false + } + + handler = object : ActionHandler() { + override fun handleAction() { + connector.response() + done() + } + + override fun handleAction(action: Int) { + val pos = selectedItemPosition.value + timeoutPrefs.edit().putInt(policy?.packageName, pos).apply() + handleAction(action, Config.Value.TIMEOUT_LIST[pos]) + } + + override fun handleAction(action: Int, time: Int) { + policy?.apply { + policy = action + if (time >= 0) { + until = if (time == 0) { + 0 + } else { + MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong()) + } + database.updatePolicy(this) + } + } + policy?.policy = action + + handleAction() + } + } + + // Never allow com.topjohnwu.magisk (could be malware) + if (TextUtils.equals(policy?.packageName, BuildConfig.APPLICATION_ID)) + return false + + // If not interactive, response directly + if (policy?.policy != Policy.INTERACTIVE) { + handler?.handleAction() + return true + } + + when (Config.get(Config.Key.SU_AUTO_RESPONSE) as Int) { + Config.Value.SU_AUTO_DENY -> { + handler?.handleAction(Policy.DENY, 0) + return true + } + Config.Value.SU_AUTO_ALLOW -> { + handler?.handleAction(Policy.ALLOW, 0) + return true + } + } + + createUICallback() + showUI() + return true + } + + @SuppressLint("ClickableViewAccessibility") + private fun showUI() { + val seconds = Config.get(Config.Key.SU_REQUEST_TIMEOUT).toLong() + val millis = SECONDS.toMillis(seconds) + timer = object : CountDownTimer(millis, 1000) { + override fun onTick(remains: Long) { + denyText.value = "%s (%d)" + .format(resources.getString(R.string.deny), remains / 1000) + } + + override fun onFinish() { + denyText.value = resources.getString(R.string.deny) + handler?.handleAction(Policy.DENY) + } + } + timer?.start() + handler?.addCancel(Runnable { cancelTimer() }) + + val useFP = canUseFingerprint.value + + if (useFP) + try { + val helper = SuFingerprint() + helper.authenticate() + handler?.addCancel(Runnable { helper.cancel() }) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private inner class SuFingerprint @Throws(Exception::class) + internal constructor() : FingerprintHelper() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + warningText.value = errString + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) { + warningText.value = helpString + } + + override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { + handler?.handleAction(Policy.ALLOW) + } + + override fun onAuthenticationFailed() { + warningText.value = resources.getString(R.string.auth_fail) + } + } + + open inner class ActionHandler { + private val cancelTasks = mutableListOf() + + internal open fun handleAction() { + done() + } + + internal open fun handleAction(action: Int) { + done() + } + + internal open fun handleAction(action: Int, time: Int) { + done() + } + + internal fun addCancel(r: Runnable) { + cancelTasks.add(r) + } + + internal fun done() { + cancelTasks.forEach { it.run() } + DieEvent().publish() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt new file mode 100644 index 000000000..4bfefa221 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt @@ -0,0 +1,166 @@ +package com.topjohnwu.magisk.ui.surequest + +import android.hardware.fingerprint.FingerprintManager +import android.os.CountDownTimer +import android.text.SpannableStringBuilder +import android.widget.Toast +import androidx.core.text.bold +import com.skoumal.teanity.viewevents.ViewEvent +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.ActivitySuRequestBinding +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.model.events.DieEvent +import com.topjohnwu.magisk.model.events.SuDialogEvent +import com.topjohnwu.magisk.ui.base.MagiskActivity +import com.topjohnwu.magisk.utils.FingerprintHelper +import com.topjohnwu.magisk.utils.feature.WIP +import com.topjohnwu.magisk.view.MagiskDialog +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import timber.log.Timber +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS + +@WIP +open class _SuRequestActivity : MagiskActivity<_SuRequestViewModel, ActivitySuRequestBinding>() { + + override val layoutRes: Int = R.layout.activity_su_request + override val viewModel: _SuRequestViewModel by viewModel { + parametersOf(intent, intent.action) + } + + //private val timeoutPrefs: SharedPreferences by inject(SUTimeout) + private val canUseFingerprint get() = FingerprintHelper.useFingerprint() + + private val countdown by lazy { + val seconds = Config.get(Config.Key.SU_REQUEST_TIMEOUT).toLong() + val millis = SECONDS.toMillis(seconds) + object : CountDownTimer(millis, 1000) { + override fun onFinish() { + viewModel.deny() + } + + override fun onTick(millisUntilFinished: Long) { + dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) { + Timber.e("Tick, tock") + title = "%s (%d)".format( + getString(R.string.deny), + MILLISECONDS.toSeconds(millisUntilFinished) + ) + } + } + } + } + + private var fingerprintHelper: SuFingerprint? = null + + private lateinit var dialog: MagiskDialog + + override fun onEventDispatched(event: ViewEvent) { + super.onEventDispatched(event) + when (event) { + is SuDialogEvent -> showDialog(event.policy) + is DieEvent -> finish() + } + } + + override fun onBackPressed() { + if (::dialog.isInitialized && dialog.isShowing) { + return + } + super.onBackPressed() + } + + override fun onDestroy() { + if (this::dialog.isInitialized && dialog.isShowing) { + dialog.dismiss() + } + fingerprintHelper?.cancel() + countdown.cancel() + super.onDestroy() + } + + private fun showDialog(policy: Policy) { + val titleText = SpannableStringBuilder("Allow ") + .bold { append(policy.appName) } + .append(" to access superuser rights?") + + val messageText = StringBuilder() + .appendln(policy.packageName) + .append(getString(R.string.su_warning)) + + dialog = MagiskDialog(this) + .applyIcon(policy.info.loadIcon(packageManager)) + .applyTitle(titleText) + .applyMessage(messageText) + //.applyView()) {} //todo add a spinner + .cancellable(false) + .applyButton(MagiskDialog.ButtonType.POSITIVE) { + titleRes = R.string.grant + onClick { viewModel.grant() } + if (canUseFingerprint) { + icon = R.drawable.ic_fingerprint + } + } + .applyButton(MagiskDialog.ButtonType.NEUTRAL) { + title = "%s %s".format(getString(R.string.grant), getString(R.string.once)) + onClick { viewModel.grant(-1) } + } + .applyButton(MagiskDialog.ButtonType.NEGATIVE) { + titleRes = R.string.deny + onClick { viewModel.deny() } + } + .onDismiss { finish() } + .onShow { + startTimer().also { Timber.e("Starting timer") } + if (canUseFingerprint) { + startFingerprintQuery() + } + } + .reveal() + } + + private fun startTimer() { + countdown.start() + } + + private fun startFingerprintQuery() { + val result = runCatching { + fingerprintHelper = SuFingerprint().apply { authenticate() } + } + + if (result.isFailure) { + dialog.applyButton(MagiskDialog.ButtonType.POSITIVE) { + icon = 0 + } + } + } + + private inner class SuFingerprint @Throws(Exception::class) + internal constructor() : FingerprintHelper() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + Toast.makeText(this@_SuRequestActivity, errString, Toast.LENGTH_LONG).show() + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) { + Toast.makeText(this@_SuRequestActivity, helpString, Toast.LENGTH_LONG).show() + } + + override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) { + viewModel.grant() + } + + override fun onAuthenticationFailed() { + Toast.makeText(this@_SuRequestActivity, R.string.auth_fail, Toast.LENGTH_LONG).show() + } + } + + companion object { + + const val REQUEST = "request" + const val LOG = "log" + const val NOTIFY = "notify" + } +} diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt new file mode 100644 index 000000000..4ac1c1ea0 --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt @@ -0,0 +1,112 @@ +package com.topjohnwu.magisk.ui.surequest + +import android.content.Intent +import android.content.pm.PackageManager +import com.skoumal.teanity.extensions.subscribeK +import com.topjohnwu.magisk.BuildConfig +import com.topjohnwu.magisk.Config +import com.topjohnwu.magisk.data.database.MagiskDB +import com.topjohnwu.magisk.model.entity.Policy +import com.topjohnwu.magisk.model.events.DieEvent +import com.topjohnwu.magisk.model.events.SuDialogEvent +import com.topjohnwu.magisk.ui.base.MagiskViewModel +import com.topjohnwu.magisk.utils.SuConnector +import com.topjohnwu.magisk.utils.SuLogger +import com.topjohnwu.magisk.utils.feature.WIP +import com.topjohnwu.magisk.utils.now +import io.reactivex.Single +import timber.log.Timber +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.MINUTES + +@WIP +class _SuRequestViewModel( + intent: Intent, + action: String, + private val packageManager: PackageManager, + private val database: MagiskDB +) : MagiskViewModel() { + + private val connector: Single = Single.fromCallable { + val socketName = intent.extras?.getString("socket") ?: let { + deny() + throw IllegalStateException("Socket is empty or null") + } + object : SuConnector(socketName) { + override fun onResponse() { + policy.subscribeK { out.writeInt(it.policy) } //this just might be incorrect, lol + } + } as SuConnector + }.cache() + + private val policy: Single = connector.map { + val bundle = it.readSocketInput() ?: throw IllegalStateException("Socket bundle is null") + val uid = bundle.getString("uid")?.toIntOrNull() ?: let { + deny() + throw IllegalStateException("UID is empty or null") + } + database.clearOutdated() + database.getPolicy(uid) ?: Policy(uid, packageManager) + }.cache() + + init { + when (action) { + SuRequestActivity.LOG -> SuLogger.handleLogs(intent).also { die() } + SuRequestActivity.NOTIFY -> SuLogger.handleNotify(intent).also { die() } + SuRequestActivity.REQUEST -> process() + else -> back() // invalid action, should ignore + } + } + + private fun process() { + policy.subscribeK(onError = ::deny) { process(it) } + } + + private fun process(policy: Policy) { + if (policy.packageName == BuildConfig.APPLICATION_ID) + deny().also { return } + + if (policy.policy != Policy.INTERACTIVE) + grant().also { return } + + when (Config.get(Config.Key.SU_AUTO_RESPONSE)) { + Config.Value.SU_AUTO_DENY -> deny().also { return } + Config.Value.SU_AUTO_ALLOW -> grant().also { return } + } + + requestDialog(policy) + } + + fun deny(e: Throwable? = null) = updatePolicy(Policy.DENY, 0).also { Timber.e(e) } + fun grant(time: Long = 0) = updatePolicy(Policy.ALLOW, time) + + private fun updatePolicy(action: Int, time: Long) { + + fun finish(e: Throwable? = null) = die().also { Timber.e(e) } + + policy + .map { it.policy = action; it } + .doOnSuccess { + if (time >= 0) { + it.until = if (time == 0L) { + 0 + } else { + MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time) + } + database.updatePolicy(it) + } + } + .flatMap { connector } + .subscribeK(onError = ::finish) { + it.response() + finish() + } + } + + private fun requestDialog(policy: Policy) { + SuDialogEvent(policy).publish() + } + + private fun die() = DieEvent().publish() + +} \ 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 f1fcb9c6c..d6cc4d32f 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,8 @@ package com.topjohnwu.magisk.utils import android.view.View +import android.widget.AdapterView +import android.widget.Spinner import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.DrawableRes @@ -110,4 +112,33 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) { } else { view.text = text } +} + +@BindingAdapter("android:selectedItemPosition") +fun setSelectedItemPosition(view: Spinner, position: Int) { + view.setSelection(position) +} + +@InverseBindingAdapter( + attribute = "android:selectedItemPosition", + event = "android:selectedItemPositionAttrChanged" +) +fun getSelectedItemPosition(view: Spinner) = view.selectedItemPosition + +@BindingAdapter("android:selectedItemPositionAttrChanged") +fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListener) { + view.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onNothingSelected(p0: AdapterView<*>?) { + listener.onChange() + } + + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + listener.onChange() + } + } +} + +@BindingAdapter("onTouch") +fun setOnTouchListener(view: View, listener: View.OnTouchListener) { + view.setOnTouchListener(listener) } \ No newline at end of file diff --git a/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt new file mode 100644 index 000000000..a9ad818ab --- /dev/null +++ b/app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt @@ -0,0 +1,155 @@ +package com.topjohnwu.magisk.view + +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import com.skoumal.teanity.util.KObservableField +import com.topjohnwu.magisk.BR +import com.topjohnwu.magisk.R +import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding + +class MagiskDialog @JvmOverloads constructor( + context: Context, theme: Int = 0 +) : AlertDialog(context, theme) { + + private val binding: DialogMagiskBaseBinding + private val data = Data() + + init { + val layoutInflater = LayoutInflater.from(context) + binding = DataBindingUtil.inflate(layoutInflater, R.layout.dialog_magisk_base, null, false) + binding.setVariable(BR.data, data) + super.setView(binding.root) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + inner class Data { + val icon = KObservableField(0) + val iconRaw = KObservableField(null) + val title = KObservableField("") + val message = KObservableField("") + + val buttonPositive = Button() + val buttonNeutral = Button() + val buttonNegative = Button() + val buttonIDGAF = Button() + } + + enum class ButtonType { + POSITIVE, NEUTRAL, NEGATIVE, IDGAF + } + + inner class Button { + val icon = KObservableField(0) + val title = KObservableField("") + val isEnabled = KObservableField(true) + + var onClickAction: OnDialogButtonClickListener = {} + + fun clicked() { + onClickAction(this@MagiskDialog) + dismiss() + } + } + + inner class ButtonBuilder(private val button: Button) { + var icon: Int + get() = button.icon.value + set(value) { + button.icon.value = value + } + var title: CharSequence + get() = button.title.value + set(value) { + button.title.value = value + } + var titleRes: Int + get() = 0 + set(value) { + button.title.value = context.getString(value) + } + var isEnabled: Boolean + get() = button.isEnabled.value + set(value) { + button.isEnabled.value = value + } + + fun onClick(listener: OnDialogButtonClickListener) { + button.onClickAction = listener + } + } + + fun applyTitle(@StringRes stringRes: Int) = + apply { data.title.value = context.getString(stringRes) } + + fun applyTitle(title: CharSequence) = + apply { data.title.value = title } + + fun applyMessage(@StringRes stringRes: Int) = + apply { data.message.value = context.getString(stringRes) } + + fun applyMessage(message: CharSequence) = + apply { data.message.value = message } + + fun applyIcon(@DrawableRes drawableRes: Int) = + apply { data.icon.value = drawableRes } + + fun applyIcon(drawable: Drawable) = + apply { data.iconRaw.value = drawable } + + fun applyButton(buttonType: ButtonType, builder: ButtonBuilder.() -> Unit) = apply { + val button = when (buttonType) { + ButtonType.POSITIVE -> data.buttonPositive + ButtonType.NEUTRAL -> data.buttonNeutral + ButtonType.NEGATIVE -> data.buttonNegative + ButtonType.IDGAF -> data.buttonIDGAF + } + ButtonBuilder(button).apply(builder) + } + + fun cancellable(isCancellable: Boolean) = apply { + setCancelable(isCancellable) + } + + fun applyView(binding: Binding, body: Binding.() -> Unit) = + apply { + this.binding.dialogBaseContainer.removeAllViews() + this.binding.dialogBaseContainer.addView(binding.root) + binding.apply(body) + } + + fun onDismiss(callback: OnDialogButtonClickListener) = + apply { setOnDismissListener(callback) } + + fun onShow(callback: OnDialogButtonClickListener) = + apply { setOnShowListener(callback) } + + fun reveal() = apply { super.show() } + + //region Deprecated Members + @Deprecated("Use applyTitle instead", ReplaceWith("applyTitle")) + override fun setTitle(title: CharSequence?) = Unit + + @Deprecated("Use applyTitle instead", ReplaceWith("applyTitle")) + override fun setTitle(titleId: Int) = Unit + + @Deprecated("Use reveal()", ReplaceWith("reveal()")) + override fun show() { + } + //endregion +} + +typealias OnDialogButtonClickListener = (DialogInterface) -> Unit \ No newline at end of file diff --git a/app/src/main/res/layout/activity_request.xml b/app/src/main/res/layout/activity_request.xml index 7d4b6eaa5..a09d5059d 100644 --- a/app/src/main/res/layout/activity_request.xml +++ b/app/src/main/res/layout/activity_request.xml @@ -1,124 +1,155 @@ - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + + android:layout_gravity="center" + android:background="?attr/colorBackgroundFloating" + android:minWidth="350dp" + android:orientation="vertical"> - + + android:layout_gravity="center_horizontal" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:orientation="horizontal" + android:paddingStart="10dp" + android:paddingEnd="10dp"> - + + + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:gravity="center_vertical" + android:orientation="vertical"> - + + + + - - - - - - - -