Updated su screen with new arch

Added new Dialog for further use
This commit is contained in:
Viktor De Pasquale 2019-04-26 21:23:25 +02:00
parent d9cded0fc9
commit 52c83b2916
17 changed files with 1274 additions and 390 deletions

View File

@ -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)
}

View File

@ -11,4 +11,8 @@ val applicationModule = module {
single { get<Context>().resources }
single { get<Context>() as App }
single { get<Context>().packageManager }
single(SUTimeout) {
get<App>().protectedContext
.getSharedPreferences("su_timeout", 0)
}
}

View File

@ -0,0 +1,5 @@
package com.topjohnwu.magisk.di
import org.koin.core.qualifier.named
val SUTimeout = named("su_timeout")

View File

@ -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()) }
}

View File

@ -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<SpinnerRvItem>() {
override val layoutRes: Int = R.layout.item_spinner
override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other)
override fun itemSameAs(other: SpinnerRvItem) = item == other.item
}

View File

@ -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
@ -34,3 +35,6 @@ class PermissionEvent(
) : ViewEvent()
class BackPressEvent : ViewEvent()
class SuDialogEvent(val policy: Policy) : ViewEvent()
class DieEvent : ViewEvent()

View File

@ -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<CharSequence> 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<Runnable> 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();
}
}
}

View File

@ -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<SuRequestViewModel, ActivityRequestBinding>() {
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"
}
}

View File

@ -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<Drawable?>(null)
val title = KObservableField("")
val packageName = KObservableField("")
val denyText = KObservableField(resources.getString(R.string.deny))
val warningText = KObservableField<CharSequence>(resources.getString(R.string.su_warning))
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
val selectedItemPosition = KObservableField(0)
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { 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<Any>(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<Int>(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<Runnable>()
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()
}
}
}

View File

@ -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<Int>(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"
}
}

View File

@ -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<SuConnector> = 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<Policy> = 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<Int>(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()
}

View File

@ -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
@ -111,3 +113,32 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
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)
}

View File

@ -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<Drawable?>(null)
val title = KObservableField<CharSequence>("")
val message = KObservableField<CharSequence>("")
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<CharSequence>("")
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 <Binding : ViewDataBinding> 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

View File

@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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.surequest.SuRequestViewModel" />
</data>
<LinearLayout
android:id="@+id/su_popup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -9,7 +20,7 @@
android:minWidth="350dp"
android:orientation="vertical">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/request_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -30,14 +41,16 @@
android:paddingStart="10dp"
android:paddingEnd="10dp">
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/app_icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="5dp"
android:layout_marginEnd="10dp"
android:layout_weight="0" />
android:layout_weight="0"
android:src="@{viewModel.icon}"
tools:src="@drawable/ic_delete" />
<LinearLayout
android:layout_width="wrap_content"
@ -47,7 +60,7 @@
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/app_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -55,10 +68,12 @@
android:maxWidth="300dp"
android:maxLines="1"
android:minWidth="200dp"
android:text="@{viewModel.title}"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:textColorPrimary" />
android:textColor="?android:textColorPrimary"
tools:text="Magisk" />
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/package_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -66,25 +81,34 @@
android:maxWidth="300dp"
android:maxLines="1"
android:minWidth="200dp"
android:textColor="?android:textColorSecondary" />
android:text="@{viewModel.packageName}"
android:textColor="?android:textColorSecondary"
tools:text="com.topjohnwu.magisk" />
</LinearLayout>
</LinearLayout>
<Spinner
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/timeout"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
onTouch="@{() -> viewModel.spinnerTouched()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
android:layout_gravity="center_horizontal"
android:selectedItemPosition="@={viewModel.selectedItemPosition}" />
itemDropDownLayout="@{android.R.layout.simple_spinner_dropdown_item}"
android:onClick="@{() -> viewModel.spinnerPressed()}"
<TextView
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="5dp"
android:drawablePadding="10dp"
android:text="@string/su_warning"
android:textColor="?android:textColorSecondary" />
android:text="@{viewModel.warningText}"
android:textColor="?android:textColorSecondary"
tools:text="@string/su_warning" />
<LinearLayout
style="?android:buttonBarStyle"
@ -95,24 +119,29 @@
android:paddingStart="30dp"
android:paddingEnd="30dp">
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/deny_btn"
style="?android:buttonBarButtonStyle"
style="@style/Widget.Button.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/deny_with_str" />
android:onClick="@{() -> viewModel.denyPressed()}"
android:text="@{viewModel.denyText}"
tools:text="@string/deny" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/grant_btn"
style="?android:buttonBarButtonStyle"
style="@style/Widget.Button.Text"
gone="@{viewModel.canUseFingerprint}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="@{() -> viewModel.grantPressed()}"
android:text="@string/grant" />
<ImageView
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/fingerprint"
gone="@{!viewModel.canUseFingerprint}"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
@ -122,3 +151,5 @@
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="Object" />
</data>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent" />
</layout>

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data"
type="com.topjohnwu.magisk.view.MagiskDialog.Data" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Card"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:cardElevation="@dimen/margin_generic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_max="400dp"
app:layout_constraintWidth_percent=".9">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<FrameLayout
android:id="@+id/dialog_base_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon"
gone="@{data.icon == 0}"
srcCompat="@{data.icon}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/margin_generic"
app:tint="@color/colorSecondary"
tools:src="@drawable/ic_delete" />
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon.Large"
gone="@{data.iconRaw == null}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/margin_generic"
android:src="@{data.iconRaw}"
tools:src="@drawable/ic_delete" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_base_title"
style="@style/Widget.Text.Title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic_half"
android:gravity="center"
android:text="@{data.title}"
app:layout_constraintLeft_toLeftOf="@+id/dialog_base_start"
app:layout_constraintRight_toRightOf="@+id/dialog_base_end"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.core.widget.NestedScrollView
android:id="@+id/dialog_base_scroll"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="@+id/dialog_base_start"
app:layout_constraintRight_toRightOf="@+id/dialog_base_end"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_base_message"
style="@style/Widget.Text"
gone="@{data.message.length == 0}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_generic_half"
android:gravity="center"
android:text="@{data.message}"
tools:lines="3"
tools:text="@tools:sample/lorem/random" />
<FrameLayout
android:id="@+id/dialog_base_container"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<Space
android:id="@+id/dialog_base_space"
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_generic"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_scroll" />
<View
android:id="@+id/dialog_base_button_0_divider"
style="@style/Widget.Divider.Horizontal"
gone="@{data.buttonPositive.icon == 0 &amp;&amp; data.buttonPositive.title.length == 0}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_space" />
<LinearLayout
android:id="@+id/dialog_base_button_1"
style="@style/Widget.DialogButton"
gone="@{data.buttonPositive.icon == 0 &amp;&amp; data.buttonPositive.title.length == 0}"
android:clickable="@{data.buttonPositive.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonPositive.isEnabled()}"
android:onClick="@{() -> data.buttonPositive.clicked()}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_0_divider">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon.DialogButton"
gone="@{data.buttonPositive.icon == 0}"
srcCompat="@{data.buttonPositive.icon}"
tools:src="@drawable/ic_delete" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.DialogButton"
gone="@{data.buttonPositive.title.length == 0}"
android:text="@{data.buttonPositive.title}"
tools:text="Button 1" />
</LinearLayout>
<View
android:id="@+id/dialog_base_button_1_divider"
style="@style/Widget.Divider.Horizontal"
gone="@{data.buttonNeutral.icon == 0 &amp;&amp; data.buttonNeutral.title.length == 0}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_1" />
<LinearLayout
android:id="@+id/dialog_base_button_2"
style="@style/Widget.DialogButton"
gone="@{data.buttonNeutral.icon == 0 &amp;&amp; data.buttonNeutral.title.length == 0}"
android:clickable="@{data.buttonNeutral.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonNeutral.isEnabled()}"
android:onClick="@{() -> data.buttonNeutral.clicked()}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_1_divider">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon.DialogButton"
gone="@{data.buttonNeutral.icon == 0}"
srcCompat="@{data.buttonNeutral.icon}"
tools:src="@drawable/ic_delete" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.DialogButton"
gone="@{data.buttonNeutral.title.length == 0}"
android:text="@{data.buttonNeutral.title}"
tools:text="Button 2" />
</LinearLayout>
<View
android:id="@+id/dialog_base_button_2_divider"
style="@style/Widget.Divider.Horizontal"
gone="@{data.buttonNegative.icon == 0 &amp;&amp; data.buttonNegative.title.length == 0}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_2" />
<LinearLayout
android:id="@+id/dialog_base_button_3"
style="@style/Widget.DialogButton"
gone="@{data.buttonNegative.icon == 0 &amp;&amp; data.buttonNegative.title.length == 0}"
android:clickable="@{data.buttonNegative.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonNegative.isEnabled()}"
android:onClick="@{() -> data.buttonNegative.clicked()}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_2_divider">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon.DialogButton"
gone="@{data.buttonNegative.icon == 0}"
srcCompat="@{data.buttonNegative.icon}"
tools:src="@drawable/ic_delete" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.DialogButton"
gone="@{data.buttonNegative.title.length == 0}"
android:text="@{data.buttonNegative.title}"
tools:text="Button 3" />
</LinearLayout>
<View
android:id="@+id/dialog_base_button_3_divider"
style="@style/Widget.Divider.Horizontal"
gone="@{data.buttonIDGAF.icon == 0 &amp;&amp; data.buttonIDGAF.title.length == 0}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_3" />
<LinearLayout
android:id="@+id/dialog_base_button_4"
style="@style/Widget.DialogButton"
gone="@{data.buttonIDGAF.icon == 0 &amp;&amp; data.buttonIDGAF.title.length == 0}"
android:clickable="@{data.buttonIDGAF.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonIDGAF.isEnabled()}"
android:onClick="@{() -> data.buttonIDGAF.clicked()}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_button_3_divider">
<androidx.appcompat.widget.AppCompatImageView
style="@style/Widget.Icon.DialogButton"
gone="@{data.buttonIDGAF.icon == 0}"
srcCompat="@{data.buttonIDGAF.icon}"
tools:src="@drawable/ic_delete" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/Widget.Text.DialogButton"
gone="@{data.buttonIDGAF.title.length == 0}"
android:text="@{data.buttonIDGAF.title}"
tools:text="Button 4" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem" />
</data>
<androidx.appcompat.widget.AppCompatTextView
android:id="@android:id/text1"
style="?android:attr/spinnerItemStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:singleLine="true"
android:text="@{item.item}"
android:textAlignment="inherit"
tools:text="Forever" />
</layout>