Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
M
Magisk
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Magisk
Commits
52c83b29
Commit
52c83b29
authored
Apr 26, 2019
by
Viktor De Pasquale
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Updated su screen with new arch
Added new Dialog for further use
parent
d9cded0f
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
1276 additions
and
392 deletions
+1276
-392
App.kt
app/src/main/java/com/topjohnwu/magisk/App.kt
+9
-3
ApplicationModule.kt
...rc/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt
+4
-0
NamedInjection.kt
app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt
+5
-0
ViewModelsModule.kt
...src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt
+7
-0
SpinnerRvItem.kt
...m/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt
+13
-0
ViewEvents.kt
...main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt
+5
-1
SuRequestActivity.java
.../com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java
+0
-287
SuRequestActivity.kt
...va/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt
+67
-0
SuRequestViewModel.kt
...a/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt
+251
-0
_SuRequestActivity.kt
...a/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt
+166
-0
_SuRequestViewModel.kt
.../com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt
+112
-0
DataBindingAdapters.kt
...in/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt
+31
-0
MagiskDialog.kt
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
+155
-0
activity_request.xml
app/src/main/res/layout/activity_request.xml
+132
-101
activity_su_request.xml
app/src/main/res/layout/activity_su_request.xml
+17
-0
dialog_magisk_base.xml
app/src/main/res/layout/dialog_magisk_base.xml
+276
-0
item_spinner.xml
app/src/main/res/layout/item_spinner.xml
+26
-0
No files found.
app/src/main/java/com/topjohnwu/magisk/App.kt
View file @
52c83b29
...
@@ -28,8 +28,12 @@ import java.util.concurrent.ThreadPoolExecutor
...
@@ -28,8 +28,12 @@ import java.util.concurrent.ThreadPoolExecutor
open
class
App
:
Application
(),
Application
.
ActivityLifecycleCallbacks
{
open
class
App
:
Application
(),
Application
.
ActivityLifecycleCallbacks
{
// Global resources
// Global resources
val
prefs
:
SharedPreferences
get
()
=
PreferenceManager
.
getDefaultSharedPreferences
(
deContext
)
lateinit
var
protectedContext
:
Context
val
DB
:
MagiskDB
by
lazy
{
MagiskDB
(
deContext
)
}
val
prefs
:
SharedPreferences
get
()
=
PreferenceManager
.
getDefaultSharedPreferences
(
protectedContext
)
val
DB
:
MagiskDB
by
lazy
{
MagiskDB
(
protectedContext
)
}
@Deprecated
(
"Use dependency injection"
)
@Deprecated
(
"Use dependency injection"
)
val
repoDB
:
RepoDatabaseHelper
by
inject
()
val
repoDB
:
RepoDatabaseHelper
by
inject
()
@Volatile
@Volatile
...
@@ -49,12 +53,14 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
...
@@ -49,12 +53,14 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
override
fun
attachBaseContext
(
base
:
Context
)
{
override
fun
attachBaseContext
(
base
:
Context
)
{
super
.
attachBaseContext
(
base
)
super
.
attachBaseContext
(
base
)
MultiDex
.
install
(
base
)
MultiDex
.
install
(
base
)
protectedContext
=
baseContext
self
=
this
self
=
this
deContext
=
base
deContext
=
base
registerActivityLifecycleCallbacks
(
this
)
registerActivityLifecycleCallbacks
(
this
)
if
(
Build
.
VERSION
.
SDK_INT
>=
24
)
{
if
(
Build
.
VERSION
.
SDK_INT
>=
24
)
{
deContext
=
base
.
createDeviceProtectedStorageContext
()
protectedContext
=
base
.
createDeviceProtectedStorageContext
()
deContext
=
protectedContext
deContext
.
moveSharedPreferencesFrom
(
base
,
base
.
defaultPrefsName
)
deContext
.
moveSharedPreferencesFrom
(
base
,
base
.
defaultPrefsName
)
}
}
...
...
app/src/main/java/com/topjohnwu/magisk/di/ApplicationModule.kt
View file @
52c83b29
...
@@ -11,4 +11,8 @@ val applicationModule = module {
...
@@ -11,4 +11,8 @@ val applicationModule = module {
single
{
get
<
Context
>().
resources
}
single
{
get
<
Context
>().
resources
}
single
{
get
<
Context
>()
as
App
}
single
{
get
<
Context
>()
as
App
}
single
{
get
<
Context
>().
packageManager
}
single
{
get
<
Context
>().
packageManager
}
single
(
SUTimeout
)
{
get
<
App
>().
protectedContext
.
getSharedPreferences
(
"su_timeout"
,
0
)
}
}
}
app/src/main/java/com/topjohnwu/magisk/di/NamedInjection.kt
0 → 100644
View file @
52c83b29
package
com.topjohnwu.magisk.di
import
org.koin.core.qualifier.named
val
SUTimeout
=
named
(
"su_timeout"
)
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt
View file @
52c83b29
package
com.topjohnwu.magisk.di
package
com.topjohnwu.magisk.di
import
android.content.Intent
import
android.net.Uri
import
android.net.Uri
import
com.topjohnwu.magisk.ui.MainViewModel
import
com.topjohnwu.magisk.ui.MainViewModel
import
com.topjohnwu.magisk.ui.flash.FlashViewModel
import
com.topjohnwu.magisk.ui.flash.FlashViewModel
...
@@ -8,6 +9,8 @@ import com.topjohnwu.magisk.ui.home.HomeViewModel
...
@@ -8,6 +9,8 @@ import com.topjohnwu.magisk.ui.home.HomeViewModel
import
com.topjohnwu.magisk.ui.log.LogViewModel
import
com.topjohnwu.magisk.ui.log.LogViewModel
import
com.topjohnwu.magisk.ui.module.ModuleViewModel
import
com.topjohnwu.magisk.ui.module.ModuleViewModel
import
com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
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.androidx.viewmodel.dsl.viewModel
import
org.koin.dsl.module
import
org.koin.dsl.module
...
@@ -20,4 +23,8 @@ val viewModelModules = module {
...
@@ -20,4 +23,8 @@ val viewModelModules = module {
viewModel
{
ModuleViewModel
(
get
(),
get
())
}
viewModel
{
ModuleViewModel
(
get
(),
get
())
}
viewModel
{
LogViewModel
(
get
(),
get
())
}
viewModel
{
LogViewModel
(
get
(),
get
())
}
viewModel
{
(
action
:
String
,
uri
:
Uri
?)
->
FlashViewModel
(
action
,
uri
,
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
())
}
}
}
app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/SpinnerRvItem.kt
0 → 100644
View file @
52c83b29
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
}
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt
View file @
52c83b29
...
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.events
...
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.events
import
android.app.Activity
import
android.app.Activity
import
com.skoumal.teanity.viewevents.ViewEvent
import
com.skoumal.teanity.viewevents.ViewEvent
import
com.topjohnwu.magisk.model.entity.Policy
import
com.topjohnwu.magisk.model.entity.Repo
import
com.topjohnwu.magisk.model.entity.Repo
import
io.reactivex.subjects.PublishSubject
import
io.reactivex.subjects.PublishSubject
...
@@ -34,3 +35,6 @@ class PermissionEvent(
...
@@ -34,3 +35,6 @@ class PermissionEvent(
)
:
ViewEvent
()
)
:
ViewEvent
()
class
BackPressEvent
:
ViewEvent
()
class
BackPressEvent
:
ViewEvent
()
class
SuDialogEvent
(
val
policy
:
Policy
)
:
ViewEvent
()
class
DieEvent
:
ViewEvent
()
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java
deleted
100644 → 0
View file @
d9cded0f
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
();
}
}
}
app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt
0 → 100644
View file @
52c83b29
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"
}
}
app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt
0 → 100644
View file @
52c83b29
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
()
}
}
}
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestActivity.kt
0 → 100644
View file @
52c83b29
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"
}
}
app/src/main/java/com/topjohnwu/magisk/ui/surequest/_SuRequestViewModel.kt
0 → 100644
View file @
52c83b29
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
()
}
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt
View file @
52c83b29
package
com.topjohnwu.magisk.utils
package
com.topjohnwu.magisk.utils
import
android.view.View
import
android.view.View
import
android.widget.AdapterView
import
android.widget.Spinner
import
android.widget.TextView
import
android.widget.TextView
import
androidx.annotation.ColorInt
import
androidx.annotation.ColorInt
import
androidx.annotation.DrawableRes
import
androidx.annotation.DrawableRes
...
@@ -111,3 +113,32 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
...
@@ -111,3 +113,32 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
view
.
text
=
text
view
.
text
=
text
}
}
}
}
@BindingAdapter
(
"android:selectedItemPosition"
)
fun
setSelectedItemPosition
(
view
:
Spinner
,
position
:
Int
)
{
view
.
setSelection
(
position
)
}
@InverseBindingAdapter
(
attribute
=
"android:selectedItemPosition"
,
event
=
"android:selectedItemPositionAttrChanged"
)
fun
getSelectedItemPosition
(
view
:
Spinner
)
=
view
.
selectedItemPosition
@BindingAdapter
(
"android:selectedItemPositionAttrChanged"
)
fun
setSelectedItemPositionListener
(
view
:
Spinner
,
listener
:
InverseBindingListener
)
{
view
.
onItemSelectedListener
=
object
:
AdapterView
.
OnItemSelectedListener
{
override
fun
onNothingSelected
(
p0
:
AdapterView
<
*
>?)
{
listener
.
onChange
()
}
override
fun
onItemSelected
(
p0
:
AdapterView
<
*
>?,
p1
:
View
?,
p2
:
Int
,
p3
:
Long
)
{
listener
.
onChange
()
}
}
}
@BindingAdapter
(
"onTouch"
)
fun
setOnTouchListener
(
view
:
View
,
listener
:
View
.
OnTouchListener
)
{
view
.
setOnTouchListener
(
listener
)
}
\ No newline at end of file
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
0 → 100644
View file @
52c83b29
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
\ No newline at end of file
app/src/main/res/layout/activity_request.xml
View file @
52c83b29
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<
LinearL
ayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
<
l
ayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
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:id=
"@+id/su_popup"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
...
@@ -9,7 +20,7 @@
...
@@ -9,7 +20,7 @@
android:minWidth=
"350dp"
android:minWidth=
"350dp"
android:orientation=
"vertical"
>
android:orientation=
"vertical"
>
<
TextView
<androidx.appcompat.widget.AppCompat
TextView
android:id=
"@+id/request_title"
android:id=
"@+id/request_title"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
...
@@ -30,14 +41,16 @@
...
@@ -30,14 +41,16 @@
android:paddingStart=
"10dp"
android:paddingStart=
"10dp"
android:paddingEnd=
"10dp"
>
android:paddingEnd=
"10dp"
>
<
ImageView
<androidx.appcompat.widget.AppCompat
ImageView
android:id=
"@+id/app_icon"
android:id=
"@+id/app_icon"
android:layout_width=
"50dp"
android:layout_width=
"50dp"
android:layout_height=
"50dp"
android:layout_height=
"50dp"
android:layout_gravity=
"center_vertical"
android:layout_gravity=
"center_vertical"
android:layout_marginStart=
"5dp"
android:layout_marginStart=
"5dp"
android:layout_marginEnd=
"10dp"
android:layout_marginEnd=
"10dp"
android:layout_weight=
"0"
/>
android:layout_weight=
"0"
android:src=
"@{viewModel.icon}"
tools:src=
"@drawable/ic_delete"
/>
<LinearLayout
<LinearLayout
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
...
@@ -47,7 +60,7 @@
...
@@ -47,7 +60,7 @@
android:gravity=
"center_vertical"
android:gravity=
"center_vertical"
android:orientation=
"vertical"
>
android:orientation=
"vertical"
>
<
TextView
<androidx.appcompat.widget.AppCompat
TextView
android:id=
"@+id/app_name"
android:id=
"@+id/app_name"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
...
@@ -55,10 +68,12 @@
...
@@ -55,10 +68,12 @@
android:maxWidth=
"300dp"
android:maxWidth=
"300dp"
android:maxLines=
"1"
android:maxLines=
"1"
android:minWidth=
"200dp"
android:minWidth=
"200dp"
android:text=
"@{viewModel.title}"
android:textAppearance=
"?android:attr/textAppearanceMedium"
android:textAppearance=
"?android:attr/textAppearanceMedium"
android:textColor=
"?android:textColorPrimary"
/>
android:textColor=
"?android:textColorPrimary"
tools:text=
"Magisk"
/>
<
TextView
<androidx.appcompat.widget.AppCompat
TextView
android:id=
"@+id/package_name"
android:id=
"@+id/package_name"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
...
@@ -66,25 +81,34 @@
...
@@ -66,25 +81,34 @@
android:maxWidth=
"300dp"
android:maxWidth=
"300dp"
android:maxLines=
"1"
android:maxLines=
"1"
android:minWidth=
"200dp"
android:minWidth=
"200dp"
android:textColor=
"?android:textColorSecondary"
/>
android:text=
"@{viewModel.packageName}"
android:textColor=
"?android:textColorSecondary"
tools:text=
"com.topjohnwu.magisk"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<
Spinner
<androidx.appcompat.widget.AppCompat
Spinner
android:id=
"@+id/timeout"
android:id=
"@+id/timeout"
itemBinding=
"@{viewModel.itemBinding}"
items=
"@{viewModel.items}"
onTouch=
"@{() -> viewModel.spinnerTouched()}"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"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.AppCompat
TextView
android:id=
"@+id/warning"
android:id=
"@+id/warning"
android:layout_width=
"wrap_content"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_gravity=
"center_horizontal"
android:layout_gravity=
"center_horizontal"
android:layout_margin=
"5dp"
android:layout_margin=
"5dp"
android:drawablePadding=
"10dp"
android:drawablePadding=
"10dp"
android:text=
"@string/su_warning"
android:text=
"@{viewModel.warningText}"
android:textColor=
"?android:textColorSecondary"
/>
android:textColor=
"?android:textColorSecondary"
tools:text=
"@string/su_warning"
/>
<LinearLayout
<LinearLayout
style=
"?android:buttonBarStyle"
style=
"?android:buttonBarStyle"
...
@@ -95,24 +119,29 @@
...
@@ -95,24 +119,29 @@
android:paddingStart=
"30dp"
android:paddingStart=
"30dp"
android:paddingEnd=
"30dp"
>
android:paddingEnd=
"30dp"
>
<
Button
<com.google.android.material.button.Material
Button
android:id=
"@+id/deny_btn"
android:id=
"@+id/deny_btn"
style=
"?android:buttonBarButtonStyle
"
style=
"@style/Widget.Button.Text
"
android:layout_width=
"0dp"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_weight=
"1"
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.Material
Button
android:id=
"@+id/grant_btn"
android:id=
"@+id/grant_btn"
style=
"?android:buttonBarButtonStyle"
style=
"@style/Widget.Button.Text"
gone=
"@{viewModel.canUseFingerprint}"
android:layout_width=
"0dp"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_weight=
"1"
android:layout_weight=
"1"
android:onClick=
"@{() -> viewModel.grantPressed()}"
android:text=
"@string/grant"
/>
android:text=
"@string/grant"
/>
<
ImageView
<androidx.appcompat.widget.AppCompat
ImageView
android:id=
"@+id/fingerprint"
android:id=
"@+id/fingerprint"
gone=
"@{!viewModel.canUseFingerprint}"
android:layout_width=
"0dp"
android:layout_width=
"0dp"
android:layout_height=
"match_parent"
android:layout_height=
"match_parent"
android:layout_weight=
"1"
android:layout_weight=
"1"
...
@@ -121,4 +150,6 @@
...
@@ -121,4 +150,6 @@
app:srcCompat=
"@drawable/ic_fingerprint"
/>
app:srcCompat=
"@drawable/ic_fingerprint"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</layout>
\ No newline at end of file
app/src/main/res/layout/activity_su_request.xml
0 → 100644
View file @
52c83b29
<?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>
\ No newline at end of file
app/src/main/res/layout/dialog_magisk_base.xml
0 → 100644
View file @
52c83b29
<?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>
\ No newline at end of file
app/src/main/res/layout/item_spinner.xml
0 → 100644
View file @
52c83b29
<?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>
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment