mirror of
https://github.com/topjohnwu/Magisk.git
synced 2024-12-26 00:47:43 +00:00
Updated su screen with new arch
Added new Dialog for further use
This commit is contained in:
parent
d9cded0fc9
commit
52c83b2916
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
@ -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()) }
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -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<Boolean>
|
||||
) : ViewEvent()
|
||||
|
||||
class BackPressEvent : ViewEvent()
|
||||
class BackPressEvent : ViewEvent()
|
||||
|
||||
class SuDialogEvent(val policy: Policy) : ViewEvent()
|
||||
class DieEvent : ViewEvent()
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
155
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
Normal file
155
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
Normal 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
|
@ -1,124 +1,155 @@
|
||||
<?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"
|
||||
android:id="@+id/su_popup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/colorBackgroundFloating"
|
||||
android:minWidth="350dp"
|
||||
android:orientation="vertical">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/request_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/su_request_title"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge" />
|
||||
<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"
|
||||
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"
|
||||
android:background="?attr/colorBackgroundFloating"
|
||||
android:minWidth="350dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
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" />
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/request_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/su_request_title"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/app_name"
|
||||
<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:src="@{viewModel.icon}"
|
||||
tools:src="@drawable/ic_delete" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
android:maxLines="1"
|
||||
android:minWidth="200dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
android:maxLines="1"
|
||||
android:minWidth="200dp"
|
||||
android:textColor="?android:textColorSecondary" />
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/app_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
android:maxLines="1"
|
||||
android:minWidth="200dp"
|
||||
android:text="@{viewModel.title}"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="Magisk" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="300dp"
|
||||
android:maxLines="1"
|
||||
android:minWidth="200dp"
|
||||
android:text="@{viewModel.packageName}"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="com.topjohnwu.magisk" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/timeout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<TextView
|
||||
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" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/deny_btn"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
<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_weight="1"
|
||||
android:text="@string/deny_with_str" />
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:selectedItemPosition="@={viewModel.selectedItemPosition}" />
|
||||
itemDropDownLayout="@{android.R.layout.simple_spinner_dropdown_item}"
|
||||
android:onClick="@{() -> viewModel.spinnerPressed()}"
|
||||
|
||||
<Button
|
||||
android:id="@+id/grant_btn"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/warning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/grant" />
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_margin="5dp"
|
||||
android:drawablePadding="10dp"
|
||||
android:text="@{viewModel.warningText}"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
tools:text="@string/su_warning" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/fingerprint"
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
style="?android:buttonBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:padding="7dp"
|
||||
android:tint="?attr/colorAccent"
|
||||
app:srcCompat="@drawable/ic_fingerprint" />
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="30dp"
|
||||
android:paddingEnd="30dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/deny_btn"
|
||||
style="@style/Widget.Button.Text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:onClick="@{() -> viewModel.denyPressed()}"
|
||||
android:text="@{viewModel.denyText}"
|
||||
tools:text="@string/deny" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/grant_btn"
|
||||
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" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/fingerprint"
|
||||
gone="@{!viewModel.canUseFingerprint}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:padding="7dp"
|
||||
android:tint="?attr/colorAccent"
|
||||
app:srcCompat="@drawable/ic_fingerprint" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
17
app/src/main/res/layout/activity_su_request.xml
Normal file
17
app/src/main/res/layout/activity_su_request.xml
Normal 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>
|
276
app/src/main/res/layout/dialog_magisk_base.xml
Normal file
276
app/src/main/res/layout/dialog_magisk_base.xml
Normal 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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 && 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>
|
26
app/src/main/res/layout/item_spinner.xml
Normal file
26
app/src/main/res/layout/item_spinner.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user