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
6348d0a6
Commit
6348d0a6
authored
Jul 10, 2020
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove more code using RxJava
parent
f7a650b9
Show whitespace changes
Inline
Side-by-side
Showing
31 changed files
with
300 additions
and
420 deletions
+300
-420
DownloadService.kt
...ava/com/topjohnwu/magisk/core/download/DownloadService.kt
+21
-34
ManagerUpgrade.kt
...java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
+3
-3
NotificationService.kt
...com/topjohnwu/magisk/core/download/NotificationService.kt
+10
-0
RemoteFileService.kt
...a/com/topjohnwu/magisk/core/download/RemoteFileService.kt
+24
-31
Repo.kt
.../main/java/com/topjohnwu/magisk/core/model/module/Repo.kt
+1
-1
MagiskInstaller.kt
...n/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt
+12
-10
Locales.kt
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
+8
-6
PatchAPK.kt
...src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
+13
-9
RepoDao.kt
...c/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt
+3
-1
GithubServices.kt
.../java/com/topjohnwu/magisk/data/network/GithubServices.kt
+5
-9
MagiskRepository.kt
.../com/topjohnwu/magisk/data/repository/MagiskRepository.kt
+0
-2
StringRepository.kt
.../com/topjohnwu/magisk/data/repository/StringRepository.kt
+3
-2
AdaptersGeneric.kt
.../java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt
+16
-6
NetworkingModule.kt
...src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt
+2
-0
ViewModelsModule.kt
...src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt
+1
-1
RxJava.kt
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
+2
-151
XList.kt
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
+13
-9
XSU.kt
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
+0
-8
ViewEvents.kt
...main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt
+65
-56
ManagerInstallDialog.kt
...johnwu/magisk/model/events/dialog/ManagerInstallDialog.kt
+8
-1
BaseViewModel.kt
...c/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt
+6
-0
FlashViewModel.kt
...main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
+1
-1
InstallFragment.kt
...n/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt
+4
-0
InstallViewModel.kt
.../java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
+7
-12
SafetynetViewModel.kt
...a/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt
+1
-1
SettingsItems.kt
...in/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
+11
-8
SettingsViewModel.kt
...ava/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt
+10
-15
DataBindingAdapters.kt
...in/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt
+7
-9
MagiskDialog.kt
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
+1
-1
MarkDownWindow.kt
...src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt
+40
-32
fragment_install_md2.xml
app/src/main/res/layout/fragment_install_md2.xml
+2
-1
No files found.
app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt
View file @
6348d0a6
...
...
@@ -13,16 +13,12 @@ import com.topjohnwu.magisk.core.tasks.EnvFixTask
import
com.topjohnwu.magisk.extensions.chooser
import
com.topjohnwu.magisk.extensions.exists
import
com.topjohnwu.magisk.extensions.provide
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.model.entity.internal.Configuration.*
import
com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import
com.topjohnwu.magisk.ui.flash.FlashFragment
import
com.topjohnwu.magisk.utils.APKInstall
import
io.reactivex.Completable
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.launch
import
org.koin.core.get
import
java.io.File
import
kotlin.random.Random.Default.nextInt
...
...
@@ -37,20 +33,20 @@ open class DownloadService : RemoteFileService() {
.
getMimeTypeFromExtension
(
extension
)
?:
"resource/folder"
override
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
=
when
(
subject
)
{
is
Magisk
->
onFinished
Internal
(
subject
,
id
)
is
Module
->
onFinished
Internal
(
subject
,
id
)
is
Manager
->
onFinished
Internal
(
subject
,
id
)
override
suspend
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
=
when
(
subject
)
{
is
Magisk
->
onFinished
(
subject
,
id
)
is
Module
->
onFinished
(
subject
,
id
)
is
Manager
->
onFinished
(
subject
,
id
)
}
private
fun
onFinishedInternal
(
private
suspend
fun
onFinished
(
subject
:
Magisk
,
id
:
Int
)
=
when
(
val
conf
=
subject
.
configuration
)
{
Uninstall
->
FlashFragment
.
uninstall
(
subject
.
file
,
id
)
EnvFix
->
{
remove
(
id
)
GlobalScope
.
launch
{
EnvFixTask
(
subject
.
file
).
exec
()
}
EnvFixTask
(
subject
.
file
).
exec
()
Unit
}
is
Patch
->
FlashFragment
.
patch
(
subject
.
file
,
conf
.
fileUri
,
id
)
...
...
@@ -58,7 +54,7 @@ open class DownloadService : RemoteFileService() {
else
->
Unit
}
private
fun
onFinished
Internal
(
private
fun
onFinished
(
subject
:
Module
,
id
:
Int
)
=
when
(
subject
.
configuration
)
{
...
...
@@ -66,33 +62,30 @@ open class DownloadService : RemoteFileService() {
else
->
Unit
}
private
fun
onFinishedInternal
(
private
suspend
fun
onFinished
(
subject
:
Manager
,
id
:
Int
)
{
Completable
.
fromAction
{
handleAPK
(
subject
)
}.
subscribeK
{
remove
(
id
)
when
(
subject
.
configuration
)
{
is
APK
.
Upgrade
->
APKInstall
.
install
(
this
,
subject
.
file
)
is
APK
.
Restore
->
Unit
}
}
}
// ---
override
fun
Notification
.
Builder
.
addActions
(
subject
:
DownloadSubject
)
=
when
(
subject
)
{
is
Magisk
->
addActions
Internal
(
subject
)
is
Module
->
addActions
Internal
(
subject
)
is
Manager
->
addActions
Internal
(
subject
)
is
Magisk
->
addActions
(
subject
)
is
Module
->
addActions
(
subject
)
is
Manager
->
addActions
(
subject
)
}
private
fun
Notification
.
Builder
.
addActions
Internal
(
subject
:
Magisk
)
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Magisk
)
=
when
(
val
conf
=
subject
.
configuration
)
{
Download
->
this
.
apply
{
Download
->
apply
{
fileIntent
(
subject
.
file
.
parentFile
!!
)
.
takeIf
{
it
.
exists
(
get
())
}
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_parent
,
it
.
chooser
())
}
...
...
@@ -101,18 +94,12 @@ open class DownloadService : RemoteFileService() {
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_self
,
it
.
chooser
())
}
}
Uninstall
->
setContentIntent
(
FlashFragment
.
uninstallIntent
(
context
,
subject
.
file
))
is
Flash
->
setContentIntent
(
FlashFragment
.
flashIntent
(
context
,
subject
.
file
,
conf
is
Secondary
)
)
is
Flash
->
setContentIntent
(
FlashFragment
.
flashIntent
(
context
,
subject
.
file
,
conf
is
Secondary
))
is
Patch
->
setContentIntent
(
FlashFragment
.
patchIntent
(
context
,
subject
.
file
,
conf
.
fileUri
))
else
->
this
}
private
fun
Notification
.
Builder
.
addActions
Internal
(
subject
:
Module
)
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Module
)
=
when
(
subject
.
configuration
)
{
Download
->
this
.
apply
{
fileIntent
(
subject
.
file
.
parentFile
!!
)
...
...
@@ -126,7 +113,7 @@ open class DownloadService : RemoteFileService() {
else
->
this
}
private
fun
Notification
.
Builder
.
addActions
Internal
(
subject
:
Manager
)
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Manager
)
=
when
(
subject
.
configuration
)
{
APK
.
Upgrade
->
setContentIntent
(
APKInstall
.
installIntent
(
context
,
subject
.
file
))
else
->
this
...
...
app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
View file @
6348d0a6
...
...
@@ -32,14 +32,14 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
patched
.
renameTo
(
apk
)
}
private
fun
RemoteFileService
.
upgrade
(
apk
:
File
,
id
:
Int
)
{
private
suspend
fun
RemoteFileService
.
upgrade
(
apk
:
File
,
id
:
Int
)
{
if
(
isRunningAsStub
)
{
// Move to upgrade location
apk
.
copyTo
(
DynAPK
.
update
(
this
),
overwrite
=
true
)
apk
.
delete
()
if
(
Info
.
stub
!!
.
version
<
Info
.
remote
.
stub
.
versionCode
)
{
// We also want to upgrade stub
service
.
fetchFile
(
Info
.
remote
.
stub
.
link
).
b
lockingGet
().
b
yteStream
().
use
{
service
.
fetchFile
(
Info
.
remote
.
stub
.
link
).
byteStream
().
use
{
it
.
writeTo
(
apk
)
}
patch
(
apk
,
id
)
...
...
@@ -65,7 +65,7 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
Shell
.
su
(
"pm install $apk && pm uninstall $packageName"
).
exec
()
}
fun
RemoteFileService
.
handleAPK
(
subject
:
DownloadSubject
.
Manager
)
=
suspend
fun
RemoteFileService
.
handleAPK
(
subject
:
DownloadSubject
.
Manager
)
=
when
(
subject
.
configuration
)
{
is
Upgrade
->
upgrade
(
subject
.
file
,
subject
.
hashCode
())
is
Restore
->
restore
(
subject
.
file
,
subject
.
hashCode
())
...
...
app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt
View file @
6348d0a6
...
...
@@ -5,6 +5,9 @@ import android.content.Intent
import
android.os.IBinder
import
com.topjohnwu.magisk.core.base.BaseService
import
com.topjohnwu.magisk.core.view.Notifications
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.cancel
import
org.koin.core.KoinComponent
import
java.util.*
import
kotlin.collections.HashMap
...
...
@@ -16,12 +19,19 @@ abstract class NotificationService : BaseService(), KoinComponent {
private
val
notifications
=
Collections
.
synchronizedMap
(
HashMap
<
Int
,
Notification
.
Builder
>())
val
coroutineScope
=
CoroutineScope
(
Dispatchers
.
IO
)
override
fun
onTaskRemoved
(
rootIntent
:
Intent
?)
{
super
.
onTaskRemoved
(
rootIntent
)
notifications
.
forEach
{
cancel
(
it
.
key
)
}
notifications
.
clear
()
}
override
fun
onDestroy
()
{
super
.
onDestroy
()
coroutineScope
.
cancel
()
}
abstract
fun
createNotification
():
Notification
.
Builder
// --
...
...
app/src/main/java/com/topjohnwu/magisk/core/download/RemoteFileService.kt
View file @
6348d0a6
...
...
@@ -9,17 +9,17 @@ import com.topjohnwu.magisk.core.ForegroundTracker
import
com.topjohnwu.magisk.core.utils.ProgressInputStream
import
com.topjohnwu.magisk.core.view.Notifications
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.extensions.writeTo
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Magisk
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject.Module
import
com.topjohnwu.superuser.ShellUtils
import
io.reactivex.Completable
import
kotlinx.coroutines.launch
import
okhttp3.ResponseBody
import
org.koin.android.ext.android.inject
import
org.koin.core.KoinComponent
import
timber.log.Timber
import
java.io.IOException
import
java.io.InputStream
abstract
class
RemoteFileService
:
NotificationService
()
{
...
...
@@ -29,7 +29,14 @@ abstract class RemoteFileService : NotificationService() {
override
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
intent
?.
getParcelableExtra
<
DownloadSubject
>(
ARG_URL
)
?.
let
{
update
(
it
.
hashCode
())
coroutineScope
.
launch
{
try
{
start
(
it
)
}
catch
(
e
:
IOException
)
{
Timber
.
e
(
e
)
failNotify
(
it
)
}
}
}
return
START_REDELIVER_INTENT
}
...
...
@@ -38,37 +45,24 @@ abstract class RemoteFileService : NotificationService() {
// ---
private
fun
start
(
subject
:
DownloadSubject
)
=
checkExisting
(
subject
)
.
onErrorResumeNext
{
download
(
subject
)
}
.
subscribeK
(
onError
=
{
Timber
.
e
(
it
)
failNotify
(
subject
)
})
{
private
suspend
fun
start
(
subject
:
DownloadSubject
)
{
if
(
subject
!
is
Magisk
||
!
subject
.
file
.
exists
()
||
!
ShellUtils
.
checkSum
(
"MD5"
,
subject
.
file
,
subject
.
magisk
.
md5
))
{
val
stream
=
service
.
fetchFile
(
subject
.
url
).
toProgressStream
(
subject
)
when
(
subject
)
{
is
Module
->
stream
.
toModule
(
subject
.
file
,
service
.
fetchInstaller
().
byteStream
())
else
->
stream
.
writeTo
(
subject
.
file
)
}
}
val
newId
=
finishNotify
(
subject
)
if
(
ForegroundTracker
.
hasForeground
)
{
onFinished
(
subject
,
newId
)
}
}
private
fun
checkExisting
(
subject
:
DownloadSubject
)
=
Completable
.
fromAction
{
check
(
subject
is
Magisk
)
{
"Download cache is disabled"
}
check
(
subject
.
file
.
exists
()
&&
ShellUtils
.
checkSum
(
"MD5"
,
subject
.
file
,
subject
.
magisk
.
md5
))
{
"The given file does not match checksum"
}
}
private
fun
download
(
subject
:
DownloadSubject
)
=
service
.
fetchFile
(
subject
.
url
)
.
map
{
it
.
toProgressStream
(
subject
)
}
.
flatMapCompletable
{
stream
->
when
(
subject
)
{
is
Module
->
service
.
fetchInstaller
()
.
doOnSuccess
{
stream
.
toModule
(
subject
.
file
,
it
.
byteStream
())
}
.
ignoreElement
()
else
->
Completable
.
fromAction
{
stream
.
writeTo
(
subject
.
file
)
}
}
}
private
fun
ResponseBody
.
toProgressStream
(
subject
:
DownloadSubject
):
InputStream
{
val
maxRaw
=
contentLength
()
val
max
=
maxRaw
/
1
_000_000f
...
...
@@ -112,8 +106,7 @@ abstract class RemoteFileService : NotificationService() {
// ---
@Throws
(
Throwable
::
class
)
protected
abstract
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
protected
abstract
suspend
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
protected
abstract
fun
Notification
.
Builder
.
addActions
(
subject
:
DownloadSubject
)
:
Notification
.
Builder
...
...
app/src/main/java/com/topjohnwu/magisk/core/model/module/Repo.kt
View file @
6348d0a6
...
...
@@ -31,7 +31,7 @@ data class Repo(
val
downloadFilename
:
String
get
()
=
"$name-$version($versionCode).zip"
.
legalFilename
()
val
readme
get
()
=
stringRepo
.
getReadme
(
this
)
suspend
fun
readme
()
=
stringRepo
.
getReadme
(
this
)
val
zipUrl
:
String
get
()
=
Const
.
Url
.
ZIP_URL
.
format
(
id
)
...
...
app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt
View file @
6348d0a6
...
...
@@ -15,7 +15,9 @@ import com.topjohnwu.magisk.core.Info
import
com.topjohnwu.magisk.core.utils.Utils
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.di.Protected
import
com.topjohnwu.magisk.extensions.*
import
com.topjohnwu.magisk.extensions.readUri
import
com.topjohnwu.magisk.extensions.reboot
import
com.topjohnwu.magisk.extensions.withStreams
import
com.topjohnwu.magisk.model.events.dialog.EnvFixDialog
import
com.topjohnwu.signing.SignBoot
import
com.topjohnwu.superuser.Shell
...
...
@@ -322,7 +324,7 @@ abstract class MagiskInstallImpl : KoinComponent {
tarOut
=
null
it
}
?:
destFile
.
outputStream
()
patched
.
suInputStream
(
).
use
{
it
.
copyTo
(
os
);
os
.
close
()
}
SuFileInputStream
(
patched
).
use
{
it
.
copyTo
(
os
);
os
.
close
()
}
}
catch
(
e
:
IOException
)
{
console
.
add
(
"! Failed to output to $destFile"
)
Timber
.
e
(
e
)
...
...
@@ -338,10 +340,10 @@ abstract class MagiskInstallImpl : KoinComponent {
return
true
}
private
fun
postOTA
():
Boolean
{
private
suspend
fun
postOTA
():
Boolean
{
val
bootctl
=
SuFile
(
"/data/adb/bootctl"
)
try
{
withStreams
(
service
.
fetchBootctl
().
b
lockingGet
().
byteStream
(),
bootctl
.
suOutputStream
(
))
{
withStreams
(
service
.
fetchBootctl
().
b
yteStream
(),
SuFileOutputStream
(
bootctl
))
{
input
,
out
->
input
.
copyTo
(
out
)
}
}
catch
(
e
:
IOException
)
{
...
...
@@ -368,7 +370,7 @@ abstract class MagiskInstallImpl : KoinComponent {
protected
fun
direct
()
=
findImage
()
&&
extractZip
()
&&
patchBoot
()
&&
flashBoot
()
protected
fun
secondSlot
()
=
protected
suspend
fun
secondSlot
()
=
findSecondaryImage
()
&&
extractZip
()
&&
patchBoot
()
&&
flashBoot
()
&&
postOTA
()
protected
fun
fixEnv
(
zip
:
File
):
Boolean
{
...
...
@@ -379,7 +381,7 @@ abstract class MagiskInstallImpl : KoinComponent {
}
@WorkerThread
protected
abstract
fun
operations
():
Boolean
protected
abstract
suspend
fun
operations
():
Boolean
open
suspend
fun
exec
()
=
withContext
(
Dispatchers
.
IO
)
{
operations
()
}
}
...
...
@@ -407,7 +409,7 @@ sealed class MagiskInstaller(
console
:
MutableList
<
String
>,
logs
:
MutableList
<
String
>
)
:
MagiskInstaller
(
file
,
console
,
logs
)
{
override
fun
operations
()
=
doPatchFile
(
uri
)
override
suspend
fun
operations
()
=
doPatchFile
(
uri
)
}
class
SecondSlot
(
...
...
@@ -415,7 +417,7 @@ sealed class MagiskInstaller(
console
:
MutableList
<
String
>,
logs
:
MutableList
<
String
>
)
:
MagiskInstaller
(
file
,
console
,
logs
)
{
override
fun
operations
()
=
secondSlot
()
override
suspend
fun
operations
()
=
secondSlot
()
}
class
Direct
(
...
...
@@ -423,7 +425,7 @@ sealed class MagiskInstaller(
console
:
MutableList
<
String
>,
logs
:
MutableList
<
String
>
)
:
MagiskInstaller
(
file
,
console
,
logs
)
{
override
fun
operations
()
=
direct
()
override
suspend
fun
operations
()
=
direct
()
}
}
...
...
@@ -431,7 +433,7 @@ sealed class MagiskInstaller(
class
EnvFixTask
(
private
val
zip
:
File
)
:
MagiskInstallImpl
()
{
override
fun
operations
()
=
fixEnv
(
zip
)
override
suspend
fun
operations
()
=
fixEnv
(
zip
)
override
suspend
fun
exec
():
Boolean
{
val
success
=
super
.
exec
()
...
...
app/src/main/java/com/topjohnwu/magisk/core/utils/Locales.kt
View file @
6348d0a6
...
...
@@ -13,7 +13,8 @@ import com.topjohnwu.magisk.core.ResMgr
import
com.topjohnwu.magisk.core.addAssetPath
import
com.topjohnwu.magisk.extensions.langTagToLocale
import
com.topjohnwu.magisk.extensions.toLangTag
import
io.reactivex.Single
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.withContext
import
java.util.*
import
kotlin.Comparator
import
kotlin.collections.ArrayList
...
...
@@ -23,7 +24,10 @@ var currentLocale: Locale = Locale.getDefault()
@SuppressLint
(
"ConstantLocale"
)
val
defaultLocale
:
Locale
=
Locale
.
getDefault
()
val
availableLocales
=
Single
.
fromCallable
{
private
var
cachedLocales
:
Pair
<
Array
<
String
>,
Array
<
String
>>?
=
null
suspend
fun
availableLocales
()
=
cachedLocales
?:
withContext
(
Dispatchers
.
Default
)
{
val
compareId
=
R
.
string
.
app_changelog
// Create a completely new resource to prevent cross talk over app's configs
...
...
@@ -56,8 +60,6 @@ val availableLocales = Single.fromCallable {
res
.
updateConfiguration
(
config
,
metrics
)
val
defName
=
res
.
getString
(
R
.
string
.
system_default
)
Pair
(
locales
,
defName
)
}.
map
{
(
locales
,
defName
)
->
val
names
=
ArrayList
<
String
>(
locales
.
size
+
1
)
val
values
=
ArrayList
<
String
>(
locales
.
size
+
1
)
...
...
@@ -69,8 +71,8 @@ val availableLocales = Single.fromCallable {
values
.
add
(
locale
.
toLangTag
())
}
Pair
(
names
.
toTypedArray
(),
values
.
toTypedArray
())
}
.
cache
()
!!
(
names
.
toTypedArray
()
to
values
.
toTypedArray
()).
also
{
cachedLocales
=
it
}
}
fun
Resources
.
updateConfig
(
config
:
Configuration
=
configuration
)
{
config
.
setLocale
(
currentLocale
)
...
...
app/src/main/java/com/topjohnwu/magisk/core/utils/PatchAPK.kt
View file @
6348d0a6
...
...
@@ -11,15 +11,18 @@ import com.topjohnwu.magisk.core.isRunningAsStub
import
com.topjohnwu.magisk.core.view.Notifications
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.extensions.get
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.extensions.writeTo
import
com.topjohnwu.signing.JarMap
import
com.topjohnwu.signing.SignAPK
import
com.topjohnwu.superuser.Shell
import
io.reactivex.Single
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.launch
import
kotlinx.coroutines.withContext
import
timber.log.Timber
import
java.io.File
import
java.io.FileOutputStream
import
java.io.IOException
import
java.nio.ByteBuffer
import
java.nio.ByteOrder
import
java.security.SecureRandom
...
...
@@ -102,16 +105,16 @@ object PatchAPK {
return
true
}
private
fun
patchAndHide
(
context
:
Context
,
label
:
String
):
Boolean
{
private
suspend
fun
patchAndHide
(
context
:
Context
,
label
:
String
):
Boolean
{
val
dlStub
=
!
isRunningAsStub
&&
SDK_INT
>=
28
&&
Const
.
Version
.
atLeast_20_2
()
val
src
=
if
(
dlStub
)
{
val
stub
=
File
(
context
.
cacheDir
,
"stub.apk"
)
val
svc
=
get
<
GithubRawServices
>()
try
{
svc
.
fetchFile
(
Info
.
remote
.
stub
.
link
).
b
lockingGet
().
b
yteStream
().
use
{
svc
.
fetchFile
(
Info
.
remote
.
stub
.
link
).
byteStream
().
use
{
it
.
writeTo
(
stub
)
}
}
catch
(
e
:
Exception
)
{
}
catch
(
e
:
IO
Exception
)
{
Timber
.
e
(
e
)
return
false
}
...
...
@@ -143,10 +146,11 @@ object PatchAPK {
fun
hideManager
(
context
:
Context
,
label
:
String
)
{
val
progress
=
Notifications
.
progress
(
context
,
context
.
getString
(
R
.
string
.
hide_manager_title
))
Notifications
.
mgr
.
notify
(
Const
.
ID
.
HIDE_MANAGER_NOTIFICATION_ID
,
progress
.
build
())
Single
.
fromCallable
{
GlobalScope
.
launch
{
val
result
=
withContext
(
Dispatchers
.
IO
)
{
patchAndHide
(
context
,
label
)
}.
subscribeK
{
if
(!
i
t
)
}
if
(!
resul
t
)
Utils
.
toast
(
R
.
string
.
hide_manager_fail_toast
,
Toast
.
LENGTH_LONG
)
Notifications
.
mgr
.
cancel
(
Const
.
ID
.
HIDE_MANAGER_NOTIFICATION_ID
)
}
...
...
app/src/main/java/com/topjohnwu/magisk/data/database/RepoDao.kt
View file @
6348d0a6
...
...
@@ -3,6 +3,8 @@ package com.topjohnwu.magisk.data.database
import
androidx.room.*
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.model.module.Repo
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.withContext
@Database
(
version
=
6
,
entities
=
[
Repo
::
class
,
RepoEtag
::
class
],
exportSchema
=
false
)
abstract
class
RepoDatabase
:
RoomDatabase
()
{
...
...
@@ -26,7 +28,7 @@ abstract class RepoDao(private val db: RepoDatabase) {
set
(
value
)
=
addEtagRaw
(
RepoEtag
(
0
,
value
))
get
()
=
etagRaw
()
?.
key
.
orEmpty
()
fun
clear
()
=
db
.
clearAllTables
()
suspend
fun
clear
()
=
withContext
(
Dispatchers
.
IO
)
{
db
.
clearAllTables
()
}
@Query
(
"SELECT * FROM repos ORDER BY last_update DESC"
)
protected
abstract
fun
getReposDateOrder
():
List
<
Repo
>
...
...
app/src/main/java/com/topjohnwu/magisk/data/network/GithubServices.kt
View file @
6348d0a6
...
...
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.data.network
import
com.topjohnwu.magisk.core.Const
import
com.topjohnwu.magisk.core.model.UpdateInfo
import
com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import
io.reactivex.Single
import
okhttp3.ResponseBody
import
retrofit2.Response
import
retrofit2.http.*
...
...
@@ -26,18 +25,15 @@ interface GithubRawServices {
@GET
(
"$MAGISK_FILES/{$REVISION}/snet.jar"
)
@Streaming
fun
fetchSafetynet
(
@Path
(
REVISION
)
revision
:
String
=
Const
.
SNET_REVISION
):
Single
<
ResponseBody
>
suspend
fun
fetchSafetynet
(
@Path
(
REVISION
)
revision
:
String
=
Const
.
SNET_REVISION
):
ResponseBody
@GET
(
"$MAGISK_FILES/{$REVISION}/bootctl"
)
@Streaming
fun
fetchBootctl
(
@Path
(
REVISION
)
revision
:
String
=
Const
.
BOOTCTL_REVISION
):
Single
<
ResponseBody
>
suspend
fun
fetchBootctl
(
@Path
(
REVISION
)
revision
:
String
=
Const
.
BOOTCTL_REVISION
):
ResponseBody
@GET
(
"$MAGISK_MASTER/scripts/module_installer.sh"
)
@Streaming
fun
fetchInstaller
():
Single
<
ResponseBody
>
@GET
(
"$MAGISK_MODULES/{$MODULE}/master/{$FILE}"
)
fun
fetchModuleInfo
(
@Path
(
MODULE
)
id
:
String
,
@Path
(
FILE
)
file
:
String
):
Single
<
String
>
suspend
fun
fetchInstaller
():
ResponseBody
@GET
(
"$MAGISK_MODULES/{$MODULE}/master/{$FILE}"
)
suspend
fun
fetchModuleFile
(
@Path
(
MODULE
)
id
:
String
,
@Path
(
FILE
)
file
:
String
):
String
...
...
@@ -50,10 +46,10 @@ interface GithubRawServices {
* */
@GET
@Streaming
fun
fetchFile
(
@Url
url
:
String
):
Single
<
ResponseBody
>
suspend
fun
fetchFile
(
@Url
url
:
String
):
ResponseBody
@GET
fun
fetchString
(
@Url
url
:
String
):
Single
<
String
>
suspend
fun
fetchString
(
@Url
url
:
String
):
String
companion
object
{
...
...
app/src/main/java/com/topjohnwu/magisk/data/repository/MagiskRepository.kt
View file @
6348d0a6
...
...
@@ -20,8 +20,6 @@ class MagiskRepository(
private
val
packageManager
:
PackageManager
)
{
fun
fetchSafetynet
()
=
apiRaw
.
fetchSafetynet
()
suspend
fun
fetchUpdate
()
=
try
{
var
info
=
when
(
Config
.
updateChannel
)
{
Config
.
Value
.
DEFAULT_CHANNEL
,
Config
.
Value
.
STABLE_CHANNEL
->
apiRaw
.
fetchStableUpdate
()
...
...
app/src/main/java/com/topjohnwu/magisk/data/repository/StringRepository.kt
View file @
6348d0a6
...
...
@@ -7,9 +7,10 @@ class StringRepository(
private
val
api
:
GithubRawServices
)
{
fun
getString
(
url
:
String
)
=
api
.
fetchString
(
url
)
suspend
fun
getString
(
url
:
String
)
=
api
.
fetchString
(
url
)
suspend
fun
getMetadata
(
repo
:
Repo
)
=
api
.
fetchModuleFile
(
repo
.
id
,
"module.prop"
)
fun
getReadme
(
repo
:
Repo
)
=
api
.
fetchModuleInfo
(
repo
.
id
,
"README.md"
)
suspend
fun
getReadme
(
repo
:
Repo
)
=
api
.
fetchModuleFile
(
repo
.
id
,
"README.md"
)
}
app/src/main/java/com/topjohnwu/magisk/databinding/AdaptersGeneric.kt
View file @
6348d0a6
...
...
@@ -7,8 +7,11 @@ import androidx.core.view.isGone
import
androidx.core.view.isInvisible
import
androidx.core.widget.TextViewCompat
import
androidx.databinding.BindingAdapter
import
com.topjohnwu.magisk.extensions.subscribeK
import
io.reactivex.Single
import
com.topjohnwu.magisk.extensions.get
import
io.noties.markwon.Markwon
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.launch
@BindingAdapter
(
"gone"
)
fun
setGone
(
view
:
View
,
gone
:
Boolean
)
{
...
...
@@ -32,9 +35,16 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
@BindingAdapter
(
"precomputedText"
)
fun
setPrecomputedText
(
tv
:
TextView
,
text
:
CharSequence
)
{
Single
.
fromCallable
{
PrecomputedTextCompat
.
create
(
text
,
TextViewCompat
.
getTextMetricsParams
(
tv
))
}.
subscribeK
{
TextViewCompat
.
setPrecomputedText
(
tv
,
it
);
GlobalScope
.
launch
(
Dispatchers
.
Default
)
{
val
pre
=
PrecomputedTextCompat
.
create
(
text
,
TextViewCompat
.
getTextMetricsParams
(
tv
))
tv
.
post
{
TextViewCompat
.
setPrecomputedText
(
tv
,
pre
);
}
}
}
@BindingAdapter
(
"markdownText"
)
fun
setMarkdownText
(
tv
:
TextView
,
text
:
CharSequence
)
{
val
markwon
=
get
<
Markwon
>()
markwon
.
setMarkdown
(
tv
,
text
.
toString
())
}
app/src/main/java/com/topjohnwu/magisk/di/NetworkingModule.kt
View file @
6348d0a6
...
...
@@ -7,6 +7,7 @@ import com.topjohnwu.magisk.data.network.GithubApiServices
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.net.Networking
import
com.topjohnwu.magisk.net.NoSSLv3SocketFactory
import
com.topjohnwu.magisk.view.PrecomputedTextSetter
import
io.noties.markwon.Markwon
import
io.noties.markwon.html.HtmlPlugin
import
io.noties.markwon.image.ImagesPlugin
...
...
@@ -93,6 +94,7 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
fun
createMarkwon
(
context
:
Context
,
okHttpClient
:
OkHttpClient
):
Markwon
{
return
Markwon
.
builder
(
context
)
.
textSetter
(
PrecomputedTextSetter
())
.
usePlugin
(
HtmlPlugin
.
create
())
.
usePlugin
(
ImagesPlugin
.
create
{
it
.
addSchemeHandler
(
OkHttpNetworkSchemeHandler
.
create
(
okHttpClient
))
...
...
app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt
View file @
6348d0a6
...
...
@@ -25,7 +25,7 @@ val viewModelModules = module {
viewModel
{
SettingsViewModel
(
get
())
}
viewModel
{
SuperuserViewModel
(
get
(),
get
(),
get
())
}
viewModel
{
ThemeViewModel
()
}
viewModel
{
InstallViewModel
(
get
()
,
get
()
)
}
viewModel
{
InstallViewModel
(
get
())
}
viewModel
{
MainViewModel
()
}
// Legacy
...
...
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
View file @
6348d0a6
package
com.topjohnwu.magisk.extensions
import
androidx.databinding.ObservableField
import
com.topjohnwu.superuser.internal.UiThreadHandler
import
io.reactivex.*
import
io.reactivex.Observable
import
io.reactivex.Scheduler
import
io.reactivex.android.schedulers.AndroidSchedulers
import
io.reactivex.disposables.Disposables
import
io.reactivex.functions.BiFunction
import
io.reactivex.schedulers.Schedulers
import
androidx.databinding.Observable
as
BindingObservable
fun
<
T
>
Observable
<
T
>.
applySchedulers
(
subscribeOn
:
Scheduler
=
Schedulers
.
io
(),
observeOn
:
Scheduler
=
AndroidSchedulers
.
mainThread
()
):
Observable
<
T
>
=
this
.
subscribeOn
(
subscribeOn
).
observeOn
(
observeOn
)
fun
<
T
>
Flowable
<
T
>.
applySchedulers
(
subscribeOn
:
Scheduler
=
Schedulers
.
io
(),
observeOn
:
Scheduler
=
AndroidSchedulers
.
mainThread
()
):
Flowable
<
T
>
=
this
.
subscribeOn
(
subscribeOn
).
observeOn
(
observeOn
)
fun
<
T
>
Single
<
T
>.
applySchedulers
(
subscribeOn
:
Scheduler
=
Schedulers
.
io
(),
observeOn
:
Scheduler
=
AndroidSchedulers
.
mainThread
()
):
Single
<
T
>
=
this
.
subscribeOn
(
subscribeOn
).
observeOn
(
observeOn
)
fun
<
T
>
Maybe
<
T
>.
applySchedulers
(
subscribeOn
:
Scheduler
=
Schedulers
.
io
(),
observeOn
:
Scheduler
=
AndroidSchedulers
.
mainThread
()
):
Maybe
<
T
>
=
this
.
subscribeOn
(
subscribeOn
).
observeOn
(
observeOn
)
fun
Completable
.
applySchedulers
(
subscribeOn
:
Scheduler
=
Schedulers
.
io
(),
observeOn
:
Scheduler
=
AndroidSchedulers
.
mainThread
()
):
Completable
=
this
.
subscribeOn
(
subscribeOn
).
observeOn
(
observeOn
)
/*=== ALIASES FOR OBSERVABLES ===*/
typealias
OnCompleteListener
=
()
->
Unit
...
...
@@ -49,128 +25,3 @@ fun <T> Observable<T>.subscribeK(
)
=
applySchedulers
()
.
subscribe
(
onNext
,
onError
,
onComplete
)
fun
<
T
>
Single
<
T
>.
subscribeK
(
onError
:
OnErrorListener
=
{
it
.
printStackTrace
()
},
onNext
:
OnSuccessListener
<
T
>
=
{}
)
=
applySchedulers
()
.
subscribe
(
onNext
,
onError
)
fun
<
T
>
Maybe
<
T
>.
subscribeK
(
onError
:
OnErrorListener
=
{
it
.
printStackTrace
()
},
onComplete
:
OnCompleteListener
=
{},
onSuccess
:
OnSuccessListener
<
T
>
=
{}
)
=
applySchedulers
()
.
subscribe
(
onSuccess
,
onError
,
onComplete
)
fun
<
T
>
Flowable
<
T
>.
subscribeK
(
onError
:
OnErrorListener
=
{
it
.
printStackTrace
()
},
onComplete
:
OnCompleteListener
=
{},
onNext
:
OnSuccessListener
<
T
>
=
{}
)
=
applySchedulers
()
.
subscribe
(
onNext
,
onError
,
onComplete
)
fun
Completable
.
subscribeK
(
onError
:
OnErrorListener
=
{
it
.
printStackTrace
()
},
onComplete
:
OnCompleteListener
=
{}
)
=
applySchedulers
()
.
subscribe
(
onComplete
,
onError
)
fun
<
T
>
Observable
<
T
>.
doOnSubscribeUi
(
body
:
()
->
Unit
)
=
doOnSubscribe
{
UiThreadHandler
.
run
{
body
()
}
}
fun
<
T
>
Single
<
T
>.
doOnSubscribeUi
(
body
:
()
->
Unit
)
=
doOnSubscribe
{
UiThreadHandler
.
run
{
body
()
}
}
fun
<
T
>
Maybe
<
T
>.
doOnSubscribeUi
(
body
:
()
->
Unit
)
=
doOnSubscribe
{
UiThreadHandler
.
run
{
body
()
}
}
fun
<
T
>
Flowable
<
T
>.
doOnSubscribeUi
(
body
:
()
->
Unit
)
=
doOnSubscribe
{
UiThreadHandler
.
run
{
body
()
}
}
fun
Completable
.
doOnSubscribeUi
(
body
:
()
->
Unit
)
=
doOnSubscribe
{
UiThreadHandler
.
run
{
body
()
}
}
fun
<
T
>
Observable
<
T
>.
doOnErrorUi
(
body
:
(
Throwable
)
->
Unit
)
=
doOnError
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Single
<
T
>.
doOnErrorUi
(
body
:
(
Throwable
)
->
Unit
)
=
doOnError
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Maybe
<
T
>.
doOnErrorUi
(
body
:
(
Throwable
)
->
Unit
)
=
doOnError
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Flowable
<
T
>.
doOnErrorUi
(
body
:
(
Throwable
)
->
Unit
)
=
doOnError
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
Completable
.
doOnErrorUi
(
body
:
(
Throwable
)
->
Unit
)
=
doOnError
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Observable
<
T
>.
doOnNextUi
(
body
:
(
T
)
->
Unit
)
=
doOnNext
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Flowable
<
T
>.
doOnNextUi
(
body
:
(
T
)
->
Unit
)
=
doOnNext
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Single
<
T
>.
doOnSuccessUi
(
body
:
(
T
)
->
Unit
)
=
doOnSuccess
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Maybe
<
T
>.
doOnSuccessUi
(
body
:
(
T
)
->
Unit
)
=
doOnSuccess
{
UiThreadHandler
.
run
{
body
(
it
)
}
}
fun
<
T
>
Maybe
<
T
>.
doOnCompleteUi
(
body
:
()
->
Unit
)
=
doOnComplete
{
UiThreadHandler
.
run
{
body
()
}
}
fun
Completable
.
doOnCompleteUi
(
body
:
()
->
Unit
)
=
doOnComplete
{
UiThreadHandler
.
run
{
body
()
}
}
fun
<
T
,
R
>
Observable
<
List
<
T
>>.
mapList
(
transformer
:
(
T
)
->
R
)
=
flatMapIterable
{
it
}
.
map
(
transformer
)
.
toList
()
fun
<
T
,
R
>
Single
<
List
<
T
>>.
mapList
(
transformer
:
(
T
)
->
R
)
=
flattenAsFlowable
{
it
}
.
map
(
transformer
)
.
toList
()
fun
<
T
,
R
>
Maybe
<
List
<
T
>>.
mapList
(
transformer
:
(
T
)
->
R
)
=
flattenAsFlowable
{
it
}
.
map
(
transformer
)
.
toList
()
fun
<
T
,
R
>
Flowable
<
List
<
T
>>.
mapList
(
transformer
:
(
T
)
->
R
)
=
flatMapIterable
{
it
}
.
map
(
transformer
)
.
toList
()
fun
<
T
>
ObservableField
<
T
>.
toObservable
():
Observable
<
T
>
{
val
observableField
=
this
return
Observable
.
create
{
emitter
->
observableField
.
get
()
?.
let
{
emitter
.
onNext
(
it
)
}
val
callback
=
object
:
BindingObservable
.
OnPropertyChangedCallback
()
{
override
fun
onPropertyChanged
(
sender
:
BindingObservable
?,
propertyId
:
Int
)
{
observableField
.
get
()
?.
let
{
emitter
.
onNext
(
it
)
}
}
}
observableField
.
addOnPropertyChangedCallback
(
callback
)
emitter
.
setDisposable
(
Disposables
.
fromAction
{
observableField
.
removeOnPropertyChangedCallback
(
callback
)
})
}
}
fun
<
T
:
Any
>
T
.
toSingle
()
=
Single
.
just
(
this
)
inline
fun
<
T1
,
T2
,
R
>
zip
(
t1
:
Single
<
T1
>,
t2
:
Single
<
T2
>,
crossinline
zipper
:
(
T1
,
T2
)
->
R
)
=
Single
.
zip
(
t1
,
t2
,
BiFunction
<
T1
,
T2
,
R
>
{
rt1
,
rt2
->
zipper
(
rt1
,
rt2
)
})
app/src/main/java/com/topjohnwu/magisk/extensions/XList.kt
View file @
6348d0a6
...
...
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.extensions
import
androidx.collection.SparseArrayCompat
import
androidx.databinding.ObservableList
import
com.topjohnwu.magisk.utils.DiffObservableList
import
io.reactivex.disposables.Disposable
import
kotlinx.coroutines.*
fun
<
T
>
MutableList
<
T
>.
update
(
newList
:
List
<
T
>)
{
clear
()
...
...
@@ -26,6 +26,7 @@ fun List<String>.toShellCmd(): String {
fun
<
T1
,
T2
>
ObservableList
<
T1
>.
sendUpdatesTo
(
target
:
DiffObservableList
<
T2
>,
scope
:
CoroutineScope
,
mapper
:
(
List
<
T1
>)
->
List
<
T2
>
)
=
addOnListChangedCallback
(
object
:
ObservableList
.
OnListChangedCallback
<
ObservableList
<
T1
>>()
{
...
...
@@ -49,14 +50,17 @@ fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
updateAsync
(
sender
?:
return
)
}
private
var
updater
:
Disposable
?
=
null
private
var
updater
:
Job
?
=
null
private
fun
updateAsync
(
sender
:
List
<
T1
>)
{
updater
?.
dispose
()
updater
=
sender
.
toSingle
()
.
map
{
mapper
(
it
)
}
.
map
{
it
to
target
.
calculateDiff
(
it
)
}
.
subscribeK
{
target
.
update
(
it
.
first
,
it
.
second
)
}
updater
?.
cancel
()
updater
=
scope
.
launch
{
val
(
list
,
diff
)
=
withContext
(
Dispatchers
.
Default
)
{
val
list
=
mapper
(
sender
)
list
to
target
.
calculateDiff
(
list
)
}
target
.
update
(
list
,
diff
)
}
}
})
...
...
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
View file @
6348d0a6
...
...
@@ -2,19 +2,11 @@ package com.topjohnwu.magisk.extensions
import
com.topjohnwu.magisk.core.Info
import
com.topjohnwu.superuser.Shell
import
com.topjohnwu.superuser.io.SuFileInputStream
import
com.topjohnwu.superuser.io.SuFileOutputStream
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.withContext
import
java.io.File
fun
reboot
(
reason
:
String
=
if
(
Info
.
recovery
)
"recovery"
else
""
)
{
Shell
.
su
(
"/system/bin/svc power reboot $reason || /system/bin/reboot $reason"
).
submit
()
}
fun
File
.
suOutputStream
()
=
SuFileOutputStream
(
this
)
fun
File
.
suInputStream
()
=
SuFileInputStream
(
this
)
val
hasRoot
get
()
=
Shell
.
rootAccess
()
suspend
fun
Shell
.
Job
.
await
()
=
withContext
(
Dispatchers
.
IO
)
{
exec
()
}
app/src/main/java/com/topjohnwu/magisk/model/events/ViewEvents.kt
View file @
6348d0a6
...
...
@@ -8,23 +8,22 @@ import com.topjohnwu.magisk.core.Const
import
com.topjohnwu.magisk.core.base.BaseActivity
import
com.topjohnwu.magisk.core.model.module.Repo
import
com.topjohnwu.magisk.core.utils.SafetyNetHelper
import
com.topjohnwu.magisk.data.
repository.MagiskRepository
import
com.topjohnwu.magisk.data.
network.GithubRawServices
import
com.topjohnwu.magisk.extensions.DynamicClassLoader
import
com.topjohnwu.magisk.extensions.OnErrorListener
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.extensions.writeTo
import
com.topjohnwu.magisk.ui.safetynet.SafetyNetResult
import
com.topjohnwu.magisk.view.MagiskDialog
import
com.topjohnwu.magisk.view.MarkDownWindow
import
com.topjohnwu.superuser.Shell
import
dalvik.system.DexFile
import
io.reactivex.Completable
import
io.reactivex.subjects.PublishSubject
import
kotlinx.coroutines.*
import
org.json.JSONObject
import
org.koin.core.KoinComponent
import
org.koin.core.inject
import
timber.log.Timber
import
java.io.File
import
java.io.IOException
import
java.lang.reflect.InvocationHandler
/**
...
...
@@ -38,11 +37,15 @@ abstract class ViewEvent {
var
handled
=
false
}
abstract
class
ViewEventsWithScope
:
ViewEvent
()
{
lateinit
var
scope
:
CoroutineScope
}
class
CheckSafetyNetEvent
(
private
val
callback
:
(
SafetyNetResult
)
->
Unit
=
{}
)
:
ViewEvent
(),
ContextExecutor
,
KoinComponent
,
SafetyNetHelper
.
Callback
{
)
:
ViewEvent
sWithScope
(),
ContextExecutor
,
KoinComponent
,
SafetyNetHelper
.
Callback
{
private
val
magiskRepo
by
inject
<
MagiskRepository
>()
private
val
svc
by
inject
<
GithubRawServices
>()
private
lateinit
var
apk
:
File
private
lateinit
var
dex
:
File
...
...
@@ -51,16 +54,21 @@ class CheckSafetyNetEvent(
apk
=
File
(
"${context.filesDir.parent}/snet"
,
"snet.jar"
)
dex
=
File
(
apk
.
parent
,
"snet.dex"
)
scope
.
launch
{
attest
(
context
)
{
// Download and retry
withContext
(
Dispatchers
.
IO
)
{
Shell
.
sh
(
"rm -rf "
+
apk
.
parent
).
exec
()
apk
.
parentFile
?.
mkdir
()
}
download
(
context
,
true
)
}
}
}
private
fun
attest
(
context
:
Context
,
onError
:
OnErrorListener
)
{
Completable
.
fromAction
{
private
suspend
fun
attest
(
context
:
Context
,
onError
:
suspend
(
Exception
)
->
Unit
)
{
try
{
val
helper
=
withContext
(
Dispatchers
.
IO
)
{
val
loader
=
DynamicClassLoader
(
apk
)
val
dex
=
DexFile
.
loadDex
(
apk
.
path
,
dex
.
path
,
0
)
...
...
@@ -79,23 +87,39 @@ class CheckSafetyNetEvent(
val
helper
=
helperClass
.
getMethod
(
"get"
,
Class
::
class
.
java
,
Context
::
class
.
java
,
Any
::
class
.
java
)
.
invoke
(
null
,
SafetyNetHelper
::
class
.
java
,
context
,
this
)
as
SafetyNetHelper
.
invoke
(
null
,
SafetyNetHelper
::
class
.
java
,
context
,
this
@CheckSafetyNetEvent
)
as
SafetyNetHelper
if
(
helper
.
version
<
Const
.
SNET_EXT_VER
)
throw
Exception
()
helper
}
helper
.
attest
()
}.
subscribeK
(
onError
=
onError
)
}
catch
(
e
:
Exception
)
{
if
(
e
is
CancellationException
)
throw
e
onError
(
e
)
}
}
@Suppress
(
"SameParameterValue"
)
private
fun
download
(
context
:
Context
,
askUser
:
Boolean
)
{
fun
downloadInternal
()
=
magiskRepo
.
fetchSafetynet
()
.
map
{
it
.
byteStream
().
writeTo
(
apk
)
}
.
subscribeK
{
attest
(
context
)
{
fun
downloadInternal
()
=
scope
.
launch
{
val
abort
:
suspend
(
Exception
)
->
Unit
=
{
Timber
.
e
(
it
)
callback
(
SafetyNetResult
())
}
}
}
try
{
withContext
(
Dispatchers
.
IO
)
{
svc
.
fetchSafetynet
().
byteStream
().
writeTo
(
apk
)
}
attest
(
context
,
abort
)
}
catch
(
e
:
IOException
)
{
if
(
e
is
CancellationException
)
throw
e
abort
(
e
)
}
}
if
(!
askUser
)
{
downloadInternal
()
...
...
@@ -126,25 +150,10 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
override
fun
invoke
(
activity
:
BaseActivity
)
=
activity
.
run
(
action
)
}
class
OpenChangelogEvent
(
val
item
:
Repo
)
:
ViewEvent
(),
ContextExecutor
{
class
OpenChangelogEvent
(
val
item
:
Repo
)
:
ViewEvent
sWithScope
(),
ContextExecutor
{
override
fun
invoke
(
context
:
Context
)
{
MarkDownWindow
.
show
(
context
,
null
,
item
.
readme
)
}
}
class
RxPermissionEvent
(
val
permissions
:
List
<
String
>,
val
callback
:
PublishSubject
<
Boolean
>
)
:
ViewEvent
(),
ActivityExecutor
{
override
fun
invoke
(
activity
:
BaseActivity
)
=
activity
.
withPermissions
(*
permissions
.
toTypedArray
())
{
onSuccess
{
callback
.
onNext
(
true
)
}
onFailure
{
callback
.
onNext
(
false
)
callback
.
onError
(
SecurityException
(
"User refused permissions"
))
scope
.
launch
{
MarkDownWindow
.
show
(
context
,
null
,
item
::
readme
)
}
}
}
...
...
app/src/main/java/com/topjohnwu/magisk/model/events/dialog/ManagerInstallDialog.kt
View file @
6348d0a6
...
...
@@ -8,6 +8,9 @@ import com.topjohnwu.magisk.model.entity.internal.Configuration
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import
com.topjohnwu.magisk.view.MagiskDialog
import
com.topjohnwu.magisk.view.MarkDownWindow
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.GlobalScope
import
kotlinx.coroutines.launch
class
ManagerInstallDialog
:
DialogEvent
()
{
...
...
@@ -28,7 +31,11 @@ class ManagerInstallDialog : DialogEvent() {
if
(
Info
.
remote
.
app
.
note
.
isEmpty
())
return
applyButton
(
MagiskDialog
.
ButtonType
.
NEGATIVE
)
{
titleRes
=
R
.
string
.
app_changelog
onClick
{
MarkDownWindow
.
show
(
context
,
null
,
Info
.
remote
.
app
.
note
)
}
onClick
{
GlobalScope
.
launch
(
Dispatchers
.
Main
.
immediate
)
{
MarkDownWindow
.
show
(
context
,
null
,
Info
.
remote
.
app
.
note
)
}
}
}
}
}
...
...
app/src/main/java/com/topjohnwu/magisk/ui/base/BaseViewModel.kt
View file @
6348d0a6
...
...
@@ -10,6 +10,7 @@ import androidx.databinding.PropertyChangeRegistry
import
androidx.lifecycle.LiveData
import
androidx.lifecycle.MutableLiveData
import
androidx.lifecycle.ViewModel
import
androidx.lifecycle.viewModelScope
import
androidx.navigation.NavDirections
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.core.Info
...
...
@@ -96,6 +97,11 @@ abstract class BaseViewModel(
_viewEvents
.
postValue
(
this
)
}
fun
<
Event
:
ViewEventsWithScope
>
Event
.
publish
()
{
scope
=
viewModelScope
_viewEvents
.
postValue
(
this
)
}
fun
Int
.
publish
()
{
_viewEvents
.
postValue
(
SimpleViewEvent
(
this
))
}
...
...
app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
View file @
6348d0a6
...
...
@@ -42,7 +42,7 @@ class FlashViewModel(
private
val
logItems
=
Collections
.
synchronizedList
(
mutableListOf
<
String
>())
init
{
outItems
.
sendUpdatesTo
(
items
)
{
it
.
map
{
ConsoleItem
(
it
)
}
}
outItems
.
sendUpdatesTo
(
items
,
viewModelScope
)
{
it
.
map
{
ConsoleItem
(
it
)
}
}
outItems
.
copyNewInputInto
(
logItems
)
args
.
dismissId
.
takeIf
{
it
!=
-
1
}
?.
also
{
...
...
app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt
View file @
6348d0a6
package
com.topjohnwu.magisk.ui.install
import
android.content.Intent
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import
com.topjohnwu.magisk.extensions.value
...
...
@@ -21,6 +22,9 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
override
fun
onStart
()
{
super
.
onStart
()
requireActivity
().
setTitle
(
R
.
string
.
install
)
// Allow markwon to run in viewmodel scope
binding
.
releaseNotes
.
tag
=
viewModel
.
viewModelScope
}
}
app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
View file @
6348d0a6
package
com.topjohnwu.magisk.ui.install
import
android.net.Uri
import
android.text.SpannableString
import
android.text.Spanned
import
android.widget.Toast
import
androidx.databinding.ObservableField
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.Info
import
com.topjohnwu.magisk.core.download.DownloadService
...
...
@@ -12,7 +11,6 @@ import com.topjohnwu.magisk.core.download.RemoteFileService
import
com.topjohnwu.magisk.core.utils.Utils
import
com.topjohnwu.magisk.data.repository.StringRepository
import
com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.extensions.value
import
com.topjohnwu.magisk.model.entity.internal.Configuration
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
...
...
@@ -20,13 +18,12 @@ import com.topjohnwu.magisk.model.events.RequestFileEvent
import
com.topjohnwu.magisk.model.events.dialog.SecondSlotWarningDialog
import
com.topjohnwu.magisk.ui.base.BaseViewModel
import
com.topjohnwu.superuser.Shell
import
io.noties.markwon.Markwon
import
kotlinx.coroutines.launch
import
org.koin.core.get
import
kotlin.math.roundToInt
class
InstallViewModel
(
stringRepo
:
StringRepository
,
markwon
:
Markwon
stringRepo
:
StringRepository
)
:
BaseViewModel
(
State
.
LOADED
)
{
val
isRooted
get
()
=
Shell
.
rootAccess
()
...
...
@@ -35,8 +32,8 @@ class InstallViewModel(
val
step
=
ObservableField
(
0
)
val
method
=
ObservableField
(-
1
)
val
progress
=
ObservableField
(
0
)
val
data
=
ObservableField
<
Uri
?>(
null
)
val
notes
=
ObservableField
<
Spanned
>(
SpannableString
(
""
)
)
val
data
=
ObservableField
(
null
as
Uri
?
)
val
notes
=
ObservableField
(
""
)
init
{
RemoteFileService
.
reset
()
...
...
@@ -50,10 +47,8 @@ class InstallViewModel(
state
=
State
.
LOADED
}
}
stringRepo
.
getString
(
Info
.
remote
.
magisk
.
note
).
map
{
markwon
.
toMarkdown
(
it
)
}.
subscribeK
{
notes
.
value
=
it
viewModelScope
.
launch
{
notes
.
value
=
stringRepo
.
getString
(
Info
.
remote
.
magisk
.
note
)
}
method
.
addOnPropertyChangedCallback
{
when
(
it
!!
)
{
...
...
app/src/main/java/com/topjohnwu/magisk/ui/safetynet/SafetynetViewModel.kt
View file @
6348d0a6
...
...
@@ -50,7 +50,7 @@ class SafetynetViewModel : BaseViewModel() {
private
fun
attest
()
{
currentState
=
LOADING
CheckSafetyNetEvent
{
CheckSafetyNetEvent
()
{
resolveResponse
(
it
)
}.
publish
()
}
...
...
app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
View file @
6348d0a6
...
...
@@ -16,10 +16,11 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import
com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import
com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import
com.topjohnwu.magisk.extensions.get
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import
com.topjohnwu.magisk.utils.asTransitive
import
com.topjohnwu.superuser.Shell
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.launch
import
java.io.File
// --- Customization
...
...
@@ -38,8 +39,9 @@ object Language : SettingsItem.Selector() {
override
var
entries
=
emptyArray
<
String
>()
override
var
entryValues
=
emptyArray
<
String
>()
init
{
availableLocales
.
subscribeK
{
(
names
,
values
)
->
suspend
fun
loadLanguages
(
scope
:
CoroutineScope
)
{
scope
.
launch
{
availableLocales
().
let
{
(
names
,
values
)
->
entries
=
names
entryValues
=
values
val
selectedLocale
=
currentLocale
.
getDisplayName
(
currentLocale
)
...
...
@@ -47,6 +49,7 @@ object Language : SettingsItem.Selector() {
notifyChange
(
BR
.
selectedEntry
)
}
}
}
}
object
Theme
:
SettingsItem
.
Blank
()
{
...
...
app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt
View file @
6348d0a6
package
com.topjohnwu.magisk.ui.settings
import
android.Manifest
import
android.os.Build
import
android.view.View
import
android.widget.Toast
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.Const
...
...
@@ -12,21 +12,18 @@ import com.topjohnwu.magisk.core.download.DownloadService
import
com.topjohnwu.magisk.core.utils.PatchAPK
import
com.topjohnwu.magisk.core.utils.Utils
import
com.topjohnwu.magisk.data.database.RepoDao
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.magisk.extensions.value
import
com.topjohnwu.magisk.model.entity.internal.Configuration
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import
com.topjohnwu.magisk.model.entity.recycler.SettingsItem
import
com.topjohnwu.magisk.model.events.RecreateEvent
import
com.topjohnwu.magisk.model.events.RxPermissionEvent
import
com.topjohnwu.magisk.model.events.dialog.BiometricDialog
import
com.topjohnwu.magisk.ui.base.BaseViewModel
import
com.topjohnwu.magisk.ui.base.adapterOf
import
com.topjohnwu.magisk.ui.base.diffListOf
import
com.topjohnwu.magisk.ui.base.itemBindingOf
import
com.topjohnwu.superuser.Shell
import
io.reactivex.Completable
import
io.reactivex.subjects.PublishSubject
import
kotlinx.coroutines.launch
import
org.koin.core.get
class
SettingsViewModel
(
...
...
@@ -48,6 +45,9 @@ class SettingsViewModel(
// making theming a pain in the ass. Just forget about it
list
.
remove
(
Theme
)
}
viewModelScope
.
launch
{
Language
.
loadLanguages
(
this
)
}
// Manager
list
.
addAll
(
listOf
(
...
...
@@ -125,8 +125,10 @@ class SettingsViewModel(
}
private
fun
clearRepoCache
()
{
Completable
.
fromAction
{
repositoryDao
.
clear
()
}
.
subscribeK
{
Utils
.
toast
(
R
.
string
.
repo_cache_cleared
,
Toast
.
LENGTH_SHORT
)
}
viewModelScope
.
launch
{
repositoryDao
.
clear
()
Utils
.
toast
(
R
.
string
.
repo_cache_cleared
,
Toast
.
LENGTH_SHORT
)
}
}
private
fun
createHosts
()
{
...
...
@@ -136,14 +138,7 @@ class SettingsViewModel(
}
private
fun
requireRWPermission
()
{
val
callback
=
PublishSubject
.
create
<
Boolean
>()
callback
.
subscribeK
{
if
(!
it
)
requireRWPermission
()
}
RxPermissionEvent
(
listOf
(
Manifest
.
permission
.
READ_EXTERNAL_STORAGE
,
Manifest
.
permission
.
WRITE_EXTERNAL_STORAGE
),
callback
).
publish
()
withExternalRW
{
if
(!
it
)
requireRWPermission
()
}
}
private
fun
updateManager
(
hide
:
Boolean
)
{
...
...
app/src/main/java/com/topjohnwu/magisk/utils/DataBindingAdapters.kt
View file @
6348d0a6
...
...
@@ -22,11 +22,8 @@ import com.google.android.material.chip.Chip
import
com.google.android.material.textfield.TextInputLayout
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
import
com.topjohnwu.magisk.extensions.subscribeK
import
com.topjohnwu.superuser.internal.UiThreadHandler
import
io.reactivex.Observable
import
io.reactivex.disposables.Disposable
import
java.util.concurrent.TimeUnit
import
kotlinx.coroutines.*
import
kotlin.math.roundToInt
...
...
@@ -42,14 +39,15 @@ fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) {
@BindingAdapter
(
"movieBehavior"
,
"movieBehaviorText"
)
fun
setMovieBehavior
(
view
:
TextView
,
isMovieBehavior
:
Boolean
,
text
:
String
)
{
(
view
.
tag
as
?
Disposable
)
?.
dispose
()
(
view
.
tag
as
?
Job
)
?.
cancel
()
view
.
tag
=
null
if
(
isMovieBehavior
)
{
v
al
observer
=
Observable
.
interval
(
150
,
TimeUnit
.
MILLISECONDS
)
.
subscribeK
{
v
iew
.
tag
=
GlobalScope
.
launch
(
Dispatchers
.
Main
.
immediate
)
{
while
(
true
)
{
delay
(
150
)
view
.
text
=
text
.
replaceRandomWithSpecial
()
}
view
.
tag
=
observer
}
}
else
{
view
.
text
=
text
}
...
...
app/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
View file @
6348d0a6
...
...
@@ -28,7 +28,7 @@ import com.topjohnwu.magisk.ui.base.itemBindingOf
import
me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapters
import
me.tatarka.bindingcollectionadapter2.ItemBinding
class
MagiskDialog
@JvmOverloads
constructor
(
class
MagiskDialog
(
context
:
Context
,
theme
:
Int
=
0
)
:
AppCompatDialog
(
context
,
theme
)
{
...
...
app/src/main/java/com/topjohnwu/magisk/view/MarkDownWindow.kt
View file @
6348d0a6
package
com.topjohnwu.magisk.view
import
android.content.Context
import
android.text.Spanned
import
android.view.LayoutInflater
import
android.widget.TextView
import
androidx.core.text.PrecomputedTextCompat
import
androidx.core.widget.TextViewCompat
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.data.repository.StringRepository
import
com.topjohnwu.magisk.extensions.subscribeK
import
io.noties.markwon.Markwon
import
io.reactivex.Completable
import
io.reactivex.Single
import
kotlinx.coroutines.*
import
org.koin.core.KoinComponent
import
org.koin.core.inject
import
timber.log.Timber
import
java.io.InputStream
import
java.util.*
import
kotlin.coroutines.coroutineContext
class
PrecomputedTextSetter
:
Markwon
.
TextSetter
{
override
fun
setText
(
tv
:
TextView
,
text
:
Spanned
,
bufferType
:
TextView
.
BufferType
,
onComplete
:
Runnable
)
{
val
scope
=
tv
.
tag
as
?
CoroutineScope
?:
GlobalScope
scope
.
launch
(
Dispatchers
.
Default
)
{
val
pre
=
PrecomputedTextCompat
.
create
(
text
,
TextViewCompat
.
getTextMetricsParams
(
tv
))
tv
.
post
{
TextViewCompat
.
setPrecomputedText
(
tv
,
pre
)
onComplete
.
run
()
}
}
}
}
object
MarkDownWindow
:
KoinComponent
{
private
val
stringR
epo
:
StringRepository
by
inject
()
private
val
r
epo
:
StringRepository
by
inject
()
private
val
markwon
:
Markwon
by
inject
()
fun
show
(
activity
:
Context
,
title
:
String
?,
url
:
String
)
{
show
(
activity
,
title
,
stringRepo
.
getString
(
url
))
}
fun
show
(
activity
:
Context
,
title
:
String
?,
input
:
InputStream
)
{
Single
.
just
(
Scanner
(
input
,
"UTF-8"
).
apply
{
useDelimiter
(
"\\A"
)
})
.
map
{
it
.
next
()
}
.
also
{
show
(
activity
,
title
,
it
)
suspend
fun
show
(
activity
:
Context
,
title
:
String
?,
url
:
String
)
{
show
(
activity
,
title
)
{
repo
.
getString
(
url
)
}
}
fun
show
(
activity
:
Context
,
title
:
String
?,
content
:
Single
<
String
>
)
{
suspend
fun
show
(
activity
:
Context
,
title
:
String
?,
input
:
suspend
()
->
String
)
{
val
mdRes
=
R
.
layout
.
markdown_window_md2
val
mv
=
LayoutInflater
.
from
(
activity
).
inflate
(
mdRes
,
null
)
val
tv
=
mv
.
findViewById
<
TextView
>(
R
.
id
.
md_txt
)
tv
.
tag
=
CoroutineScope
(
coroutineContext
)
content
.
map
{
markwon
.
setMarkdown
(
tv
,
it
)
}.
ignoreElement
().
onErrorResumeNext
{
// Nothing we can actually do other than show error message
Timber
.
e
(
it
)
try
{
markwon
.
setMarkdown
(
tv
,
input
())
}
catch
(
e
:
Exception
)
{
if
(
e
is
CancellationException
)
throw
e
Timber
.
e
(
e
)
tv
.
setText
(
R
.
string
.
download_file_error
)
Completable
.
complete
()
}.
subscribeK
{
}
MagiskDialog
(
activity
)
.
applyTitle
(
title
?:
""
)
.
applyView
(
mv
)
...
...
@@ -52,7 +62,5 @@ object MarkDownWindow : KoinComponent {
titleRes
=
android
.
R
.
string
.
cancel
}
.
reveal
()
return
@subscribeK
}
}
}
app/src/main/res/layout/fragment_install_md2.xml
View file @
6348d0a6
...
...
@@ -228,11 +228,12 @@
app:strokeWidth=
"@{viewModel.step != 0 ? 0f : @dimen/l_125}"
>
<TextView
android:id=
"@+id/release_notes"
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:layout_margin=
"15dp"
android:textAppearance=
"@style/AppearanceFoundation.Caption"
android:t
ext=
"@{viewModel.notes}"
/>
markdownT
ext=
"@{viewModel.notes}"
/>
</com.google.android.material.card.MaterialCardView>
...
...
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