Compare commits

...

126 Commits

Author SHA1 Message Date
topjohnwu
6fe03d2795 Fix stub strings 2019-05-20 01:33:08 -07:00
topjohnwu
c595a87ccf Update Magisk Manager changelog 2019-05-20 01:05:27 -07:00
topjohnwu
fac07c3913 Update R8 version 2019-05-19 17:39:19 -07:00
topjohnwu
c63fdbbc6b Add traditional Chinese translations 2019-05-19 17:39:05 -07:00
osm0sis
2ff5d9606b magiskboot: add support for remaining Nook HD pre-image loaders 2019-05-19 17:38:41 -07:00
zertyuiop
ed43452c1a Added missing strings 2019-05-19 13:50:08 -07:00
Oliver Cervera
8f28d4028f Update Italian structure 2019-05-19 13:49:51 -07:00
Cristian Silaghi
b54543b18c Update RO language 2019-05-19 13:49:25 -07:00
topjohnwu
966d6593ca Fix strings 2019-05-13 23:21:01 -07:00
JoanVC100
ad95b1c9d1 Addition, reorganisation and fixing Catalan strings 2019-05-13 23:13:48 -07:00
Ingan121
3bfa38c60a Update strings.xml 2019-05-13 23:13:34 -07:00
topjohnwu
0bdbcad8be Don't specify Provider 2019-05-13 22:39:28 -07:00
topjohnwu
80cd85b061 Try to use broadcast for su logging and notify
In commit 8d4c407, native Magisk always launches an activity for
communicating with Magisk Manager. While this works extremely well,
since it also workaround stupid OEMs that blocks broadcasts, it has a
problem: launching an activity will claim the focus of the device,
which could be super annoying in some circumstances.

This commit adds a new feature to run a broadcast test on boot complete.
If Magisk Manager successfully receives the broadcast, it will toggle
a setting in magiskd so all future su loggings and notifies will always
use broadcasts instead of launching activities.

Fix #1412
2019-05-13 02:01:10 -07:00
topjohnwu
89275270f3 Fix code to install GMS_Conscrypt 2019-05-12 16:19:27 -07:00
topjohnwu
e7339ba619 We don't need BouncyCastle provider on Android 2019-05-12 16:06:22 -07:00
topjohnwu
d9ad7d522c Update dependencies 2019-05-12 13:42:53 -07:00
topjohnwu
ef0e22cc41 Slightly update scripts 2019-05-11 02:29:13 -07:00
topjohnwu
62db65bf18 Reset SafetyNet status on refresh 2019-05-11 01:55:44 -07:00
topjohnwu
d5371f752c Sort hide targets by app name 2019-05-11 01:53:37 -07:00
topjohnwu
a5f5e94115 Always reload string from resource 2019-05-11 01:50:01 -07:00
Viktor De Pasquale
67c3f40adb Fixed language won't change in certain views unless app restarts 2019-05-10 16:22:03 +02:00
topjohnwu
ff7a0ba599 Force apply preferred locale in applyOverrideConfiguration
Close #1442
2019-05-10 00:19:28 -07:00
topjohnwu
b152c63102 Upgrade AS 2019-05-09 23:16:21 -07:00
Shaka Huang
415ff23be5 Fix error mounting /data partition
For devices come with two /data mount points, magisk will bind the one in tmpfs and failed to load modules since this partition is empty.

Signed-off-by: Shaka Huang <shakalaca@gmail.com>
2019-05-09 20:29:10 -07:00
osm0sis
b0d6de783e Correct magiskboot help 2019-05-09 20:28:48 -07:00
osm0sis
ac28e6e5ca Fix uninstaller missing recent changes
- group unsupported formats into the same code (86f778c0aa (diff-93690a8d9f50c177ef97416af3be8726))
- support A only system-as-root devices (e72c6685ed (diff-93690a8d9f50c177ef97416af3be8726))
- remove unnecessary '--' from magiskboot actions (7f08c06943 (diff-93690a8d9f50c177ef97416af3be8726))
- get_flags need to be before find_boot_image (a4f5d47e72)

closes #1371, closes #1431, closes #1439
2019-05-09 20:28:48 -07:00
dark-basic
a9350f50c9 Update strings.xml
New Lines Added.
2019-05-05 12:28:57 -07:00
Andrea Cioccarelli
ed7babcbf1 Translation fixes 2019-05-05 12:24:37 -07:00
Alexander Pohl
61ebc335c4 Add hi6250 support
not only hi3660 and kirin970,980 need this, also kirin 659 does
2019-05-05 11:45:21 -07:00
Viktor De Pasquale
0167bd76f1 Removed unnecessary overriding of observable list and replaced it copy function within observable changed callback 2019-05-05 11:33:17 -07:00
Viktor De Pasquale
79d704008b Fixed rewritten java code being java-styled in kotlin
Fixed accessing kotlin code illegally via companion helper
2019-05-05 11:33:17 -07:00
Viktor De Pasquale
0a703585b0 Fixed items in navView not being checked 2019-05-05 11:33:17 -07:00
topjohnwu
b27801a27c Remove unused dependencies 2019-05-04 17:56:02 -07:00
topjohnwu
a0cfce7cbc Rewrite FlashZip in Kotlin 2019-05-03 04:42:57 -04:00
topjohnwu
8b7144c986 Rewrite ZipUtils in Kotlin 2019-05-03 04:10:27 -04:00
topjohnwu
d3f5f5ee59 Rewrite RootUtils in Kotlin 2019-05-03 03:36:39 -04:00
topjohnwu
a2a3c7f438 Collect both STDOUT and STDERR for logs 2019-05-03 02:05:51 -04:00
Viktor De Pasquale
4496f82d5b Added scrolling to latest items while flashing
Since the adapter might be set _after_ the request, as there is no guaranteed order, it's done with waiting recursion yuck
2019-05-03 00:50:46 -04:00
Viktor De Pasquale
09d531557d Fixed requesting permissions off main thread 2019-05-03 00:50:46 -04:00
Viktor De Pasquale
7fee82f731 Fixed shell long dumping to UI 2019-05-03 00:50:46 -04:00
Viktor De Pasquale
475054c48a Fixed backpress not working 2019-05-03 00:50:46 -04:00
Viktor De Pasquale
a743d05751 Fixed icon not being tintable resulting in transparent block 2019-05-03 00:50:46 -04:00
topjohnwu
d1ed502e03 Multidex debug only 2019-05-02 14:06:08 -04:00
vvb2060
37744c7ab6 exclude useless files 2019-05-02 13:43:45 -04:00
topjohnwu
b25c49725f Sort hidden items on the top 2019-05-02 06:38:42 -04:00
topjohnwu
b245782c7e Always show hidden apps 2019-05-02 06:16:58 -04:00
topjohnwu
a9f32baae0 Fix links 2019-05-02 04:42:54 -04:00
topjohnwu
e7ef71865d Update doc index 2019-05-02 04:41:59 -04:00
topjohnwu
88c4f72b37 Remove Butterknife 2019-05-02 04:06:59 -04:00
topjohnwu
abbcdf91a5 Remove SafetyNet.java 2019-05-02 03:45:15 -04:00
topjohnwu
b876df6e21 Fix Czech strings 2019-05-02 03:22:14 -04:00
topjohnwu
4bb81f35d7 Rename MagiskFragment to HomeFragment 2019-05-02 03:21:46 -04:00
topjohnwu
ff20267b3f Remove redundent classes 2019-05-02 02:42:00 -04:00
topjohnwu
2c9586d811 Update dependencies 2019-05-02 02:12:29 -04:00
topjohnwu
2813d2031a Merge branch 'WIP' 2019-05-02 02:03:20 -04:00
Viktor De Pasquale
db218407b0 Fixed wrong link for github source 2019-04-27 12:13:30 +02:00
Viktor De Pasquale
d52210dd90 (Re)Added animations and shortcut endpoints
Fixed first backpress closing the app instead of showing default fragment
2019-04-27 12:09:49 +02:00
Viktor De Pasquale
f3cd9a096a Removed old Base[Activity/Fragment] 2019-04-27 11:49:25 +02:00
Viktor De Pasquale
e426090a18 Fixed checkboxes on homescreen not writing values to static fields 2019-04-27 11:43:55 +02:00
Viktor De Pasquale
cbe64fd559 Removed unnecessary assets 2019-04-27 11:38:31 +02:00
Viktor De Pasquale
63ea7a70bd Removed duplicated background from policy item 2019-04-27 11:34:26 +02:00
Viktor De Pasquale
fb0998f7a2 Fixed section titles that looked odd due to replicating paddings 2019-04-27 11:32:57 +02:00
Viktor De Pasquale
a9b00dd537 Updated deprecation statements and moved components init after attaching base context
This needed to be done in order to get the Koin working as it requires injection before calling onCreate
2019-04-27 11:27:42 +02:00
Viktor De Pasquale
52eb059515 Fixed items in superuser not disappearing when deleted 2019-04-26 21:29:13 +02:00
Viktor De Pasquale
7640246255 Fixed delete button size for policy items 2019-04-26 21:28:13 +02:00
Viktor De Pasquale
52c83b2916 Updated su screen with new arch
Added new Dialog for further use
2019-04-26 21:23:58 +02:00
Viktor De Pasquale
d9cded0fc9 Fixed styles for SU screen 2019-04-26 19:34:22 +02:00
Viktor De Pasquale
750c42caf1 Added annotations for marking code with it's current state 2019-04-26 19:33:42 +02:00
Viktor De Pasquale
bbf650c6cf Updated gradle & AS 2019-04-26 19:32:53 +02:00
Viktor De Pasquale
a25dace7e0 Merge remote-tracking branch 'john/master' into development 2019-04-24 20:39:27 +02:00
Viktor De Pasquale
14ff22fbcd Updated flash screen with new arch 2019-04-24 20:28:41 +02:00
Viktor De Pasquale
07eb7dda2d Added permission request event 2019-04-24 19:34:40 +02:00
Viktor De Pasquale
d4058175b4 Fixed list query not being disposed so it could occasionally crash due to several changes rewriting each other 2019-04-22 18:28:40 +02:00
Viktor De Pasquale
2de984ae24 Added division of the modules section to updatable, installed and not installed 2019-04-22 18:20:23 +02:00
Viktor De Pasquale
761a8bf2a9 Merge remote-tracking branch 'john/master' into development 2019-04-22 17:04:08 +02:00
Viktor De Pasquale
6df7006b36 Cleaned up unnecessary classes 2019-04-22 17:03:21 +02:00
Viktor De Pasquale
aceb3ee863 Rewritten flashing internal modules to model
This is done in an effort to separate flash activity to smaller pieces.
2019-04-22 16:59:59 +02:00
Viktor De Pasquale
11d716a3c8 Updated splash screen with new arch 2019-04-22 16:00:48 +02:00
Viktor De Pasquale
7cc8c014eb Updated log screen with new arch 2019-04-22 14:11:41 +02:00
Viktor De Pasquale
f21241d944 Added divider to module actions 2019-04-22 10:47:38 +02:00
Viktor De Pasquale
a181fa0652 Fixed updating lists being to heavy for the UI thread
Moved list diff recalculation to the computing thread instead
2019-04-22 09:30:38 +02:00
Viktor De Pasquale
3f748b4d2a Fixed search in magisk hide not being case insensitive 2019-04-22 08:58:23 +02:00
Viktor De Pasquale
683450f9c6 Added search functionality to repos (downloads) 2019-04-22 08:57:32 +02:00
Viktor De Pasquale
adbd47a36c Updated modules and repos screen
Screens are merged via common viewModel, all data are immediately accessible to both of them
2019-04-20 23:44:08 +02:00
Viktor De Pasquale
ce693aa5e9 Updated policy items so listeners are not indirectly set to them and kept out of the instance of the parent object 2019-04-19 19:22:18 +02:00
Viktor De Pasquale
ad80804461 Cleaned up usage of rx subscribers 2019-04-19 16:43:44 +02:00
Viktor De Pasquale
2d55632430 Merge remote-tracking branch 'john/WIP' into development
# Conflicts:
#	gradle/wrapper/gradle-wrapper.properties
2019-04-19 16:34:15 +02:00
Viktor De Pasquale
e81f00ef1a Updated Hide screen with new arch 2019-04-19 16:32:01 +02:00
topjohnwu
93fb0e3d74 Fix release builds 2019-04-19 03:26:33 -04:00
topjohnwu
71ce0de606 Make debug buildable 2019-04-19 02:11:22 -04:00
topjohnwu
0407062c1d Merge branch 'master' into pull request #1342 2019-04-19 01:28:45 -04:00
Viktor De Pasquale
cda14af208 Fixed log tabbar titles having wrong color 2019-04-18 16:13:59 +02:00
Viktor De Pasquale
258f170cd7 Fixed elevation causing log screen look odd 2019-04-18 16:13:31 +02:00
Viktor De Pasquale
f76015d714 Fixed options menus appearing on screens that they shouldn't 2019-04-18 16:00:54 +02:00
Viktor De Pasquale
7e5e14163c Fixed titles not setting to activity toolbar 2019-04-18 15:51:02 +02:00
Viktor De Pasquale
bcd1064e94 Updated superuser fragment to new arch
Fixed theme issues along the way
2019-04-17 18:27:03 +02:00
Viktor De Pasquale
8a8441c875 Added failure callback to fingerprint dialog 2019-04-17 18:20:53 +02:00
Viktor De Pasquale
15aa813416 Migrated to compat shared prefs and fixed it not reacting to changes
Added back dark theme
2019-04-17 14:03:25 +02:00
Viktor De Pasquale
605faccffd Merge remote-tracking branch 'john/master' into development
# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/topjohnwu/magisk/App.java
#	app/src/main/java/com/topjohnwu/magisk/model/adapters/ReposAdapter.java
#	app/src/main/java/com/topjohnwu/magisk/model/update/UpdateCheckService.java
#	app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.java
#	app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.java
#	app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashActivity.java
#	app/src/main/java/com/topjohnwu/magisk/ui/home/MagiskFragment.java
#	app/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.java
#	app/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.java
#	app/src/main/java/com/topjohnwu/magisk/utils/ValueSortedMap.java
#	app/src/main/java/com/topjohnwu/magisk/view/dialogs/InstallMethodDialog.java
#	app/src/main/java/com/topjohnwu/magisk/view/dialogs/MagiskInstallDialog.java
#	app/src/main/java/com/topjohnwu/magisk/view/dialogs/ManagerInstallDialog.java
#	build.gradle
2019-04-16 19:40:34 +02:00
Viktor De Pasquale
79f2d08c81 Fixed new fragment not clearing menu in toolbar 2019-04-16 19:26:53 +02:00
Viktor De Pasquale
0568ae5391 Fixed dependencies on old base 2019-04-16 19:21:20 +02:00
Viktor De Pasquale
5330dda9f8 Removed redundant casts 2019-04-16 19:03:52 +02:00
Viktor De Pasquale
ebab126579 Replaced xml navigation with self-handled 2019-04-16 19:00:32 +02:00
Viktor De Pasquale
0e5417a13e Updated progress style to match app theme and paddings to advanced settings 2019-04-16 16:21:53 +02:00
Viktor De Pasquale
9a968e0584 Added leanback activity that implements several functions which custom dialogs depend on 2019-04-15 20:26:22 +02:00
Viktor De Pasquale
ffec64d209 Added safetynet to the rewritten home fragment 2019-04-15 19:48:07 +02:00
Viktor De Pasquale
f332746188 Fixed current version showing null when magisk is not installed 2019-04-15 15:57:23 +02:00
Viktor De Pasquale
b2fa5b551e Added hiding of UI elements when no root access is detected 2019-04-14 13:17:51 +02:00
Viktor De Pasquale
36e83edddc Fixed dialog buttons after a theme change 2019-04-14 12:59:00 +02:00
Viktor De Pasquale
6b045eadef Added env fix prompt 2019-04-14 12:55:03 +02:00
Viktor De Pasquale
147264822c Fixed leaking base instance to the event listener 2019-04-14 12:29:07 +02:00
Viktor De Pasquale
36e4ccd800 Fixed touch events on includes not being propagated due to missing viewModel 2019-04-14 12:21:23 +02:00
Viktor De Pasquale
796c16237d Fixed same events not being able to propagate consecutively 2019-04-14 12:21:04 +02:00
Viktor De Pasquale
861ad9881c Updated design of the front page (with removed cards and added dividers)
Also updated material library and injected backported styles which were incompatible with the current UI for the most part and as it was over-carded all cards were removed and replaced with flat UI components.
This change is temporary and *will* be redone to the final redesign, in other words this is sufficient for the transition period.

All themers should refrain from trying to theme the app until the redesign is done. It will break your efforts with every other release.
2019-04-14 11:51:47 +02:00
Viktor De Pasquale
3101c538e9 Added (backported) styles from design concept 2019-04-14 11:28:45 +02:00
Viktor De Pasquale
42adc7382f Updated kotlin 2019-04-14 11:07:13 +02:00
Viktor De Pasquale
9bb4dfad13 Added back version checking (and version boxes) after transitioning homepage to MVVM
Fixed several errors caused along the way
2019-04-14 11:00:29 +02:00
Viktor De Pasquale
bd00ae8ede Updated Magisk fragment to Kotlin
Exported old update card to special xml include where binding takes care of everything that had to be done in code beforehand.
Added several easing functions and enums.
Backported some classes and functions from the old fork

Expect major breakage. Literally nothing works as the functionality needs to be implemented
2019-04-13 00:14:37 +02:00
Viktor De Pasquale
f309522268 Added (backported) values and styles for views 2019-04-12 22:06:57 +02:00
Viktor De Pasquale
0efaddff23 Added binding between navigation view and navigation components
Removed bunch of code focusing on the hamburger not being stationary
2019-04-11 21:17:54 +02:00
Viktor De Pasquale
94ba7cb0c5 Added navigation endpoints 2019-04-11 20:10:14 +02:00
Viktor De Pasquale
2d58c725e0 Added koin, databinding and navigation components
Converted App class and Main activity to Kotlin. With that refactored fields within App class to allow lazy initialization

BEWARE: at this point the navigation is very much broken, won't let you anywhere beyond home screen
2019-04-11 20:01:49 +02:00
Viktor De Pasquale
e035523eb8 Added base framework 2019-04-11 18:52:30 +02:00
Viktor De Pasquale
bea5308ab7 Updated locations of nearly all files
This has been done in preparations for rewrite to kotlin and upcoming design changes.
Nothing should be broken but use caution.
2019-04-11 18:03:23 +02:00
Viktor De Pasquale
f006a85fec Fixed butter knife not building with kotlin 2019-04-11 15:32:36 +02:00
Viktor De Pasquale
ea93013ebc Added kotlin support 2019-04-11 14:49:52 +02:00
220 changed files with 7430 additions and 5823 deletions

View File

@@ -1,6 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
kapt {
@@ -16,6 +16,7 @@ android {
defaultConfig {
applicationId 'com.topjohnwu.magisk'
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
versionName configProps['appVersion']
versionCode configProps['appVersionCode'] as Integer
javaCompileOptions {
@@ -30,6 +31,20 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled = true
}
packagingOptions {
exclude '/META-INF/*.version'
exclude '/META-INF/*.kotlin_module'
exclude '/META-INF/rxkotlin.properties'
exclude '/androidsupportmultidexversion.txt'
exclude '/org/**'
exclude '/kotlin/**'
exclude '/kotlinx/**'
}
}
dependencies {
@@ -37,30 +52,32 @@ dependencies {
implementation project(':net')
implementation project(':shared')
implementation project(':signing')
implementation 'com.github.topjohnwu:jtar:1.0.0'
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
def markwonVersion = '3.0.0'
implementation 'com.github.topjohnwu:jtar:1.0.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.github.skoumalcz:teanity:0.3.3'
implementation 'com.ncapdevi:frag-nav:3.2.0'
def markwonVersion = '3.0.1'
implementation "ru.noties.markwon:core:${markwonVersion}"
implementation "ru.noties.markwon:html:${markwonVersion}"
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
def androidXVersion = "1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation "androidx.preference:preference:${androidXVersion}"
implementation "androidx.recyclerview:recyclerview:${androidXVersion}"
implementation "androidx.cardview:cardview:${androidXVersion}"
implementation "com.google.android.material:material:${androidXVersion}"
implementation 'androidx.work:work-runtime:2.0.1'
implementation 'androidx.transition:transition:1.1.0-beta01'
def libsuVersion = '2.5.0'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
def butterKnifeVersion = '10.1.0'
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
def koin = "2.0.0-rc-2"
implementation "org.koin:koin-core:${koin}"
implementation "org.koin:koin-android:${koin}"
implementation "org.koin:koin-androidx-viewmodel:${koin}"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha06'
implementation 'androidx.work:work-runtime:2.0.1'
implementation 'androidx.transition:transition:1.2.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1'
}

View File

@@ -16,12 +16,6 @@
# public *;
#}
# BouncyCastle
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
-keep,allowoptimization class org.bouncycastle.jcajce.provider.digest.SHA1** { *; }
-dontwarn javax.naming.**
# Snet
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
@@ -29,18 +23,17 @@
void onResponse(int);
}
# Keep all fragment constructors
-keepclassmembers class * extends androidx.fragment.app.Fragment {
public <init>(...);
}
# DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
# BootSigner
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
# SVG
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
# RetroStreams
-dontwarn java9.**
# Strip logging
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
public *** debug(...);

View File

@@ -11,7 +11,7 @@
<application
android:allowBackup="true"
android:name="a.e"
android:theme="@style/AppTheme"
android:theme="@style/MagiskTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
@@ -35,7 +35,7 @@
android:name="a.f"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="nosensor"
android:theme="@style/AppTheme.NoDrawer" />
android:theme="@style/MagiskTheme.Flashing" />
<!-- Superuser -->
@@ -44,7 +44,7 @@
android:exported="false"
android:directBootAware="true"
android:excludeFromRecents="true"
android:theme="@style/SuRequest" />
android:theme="@style/MagiskTheme.SU" />
<!-- Receiver -->
@@ -74,4 +74,4 @@
</application>
</manifest>
</manifest>

View File

@@ -1,101 +0,0 @@
package com.topjohnwu.magisk;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import com.topjohnwu.magisk.data.database.MagiskDB;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import java.util.concurrent.ThreadPoolExecutor;
public class App extends Application implements Application.ActivityLifecycleCallbacks {
public static App self;
public static Context deContext;
public static ThreadPoolExecutor THREAD_POOL;
// Global resources
public SharedPreferences prefs;
public MagiskDB mDB;
public RepoDatabaseHelper repoDB;
private volatile BaseActivity foreground;
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
Shell.Config.verboseLogging(BuildConfig.DEBUG);
Shell.Config.addInitializers(RootUtils.class);
Shell.Config.setTimeout(2);
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
self = this;
deContext = base;
registerActivityLifecycleCallbacks(this);
if (Build.VERSION.SDK_INT >= 24) {
deContext = base.createDeviceProtectedStorageContext();
deContext.moveSharedPreferencesFrom(base,
PreferenceManager.getDefaultSharedPreferencesName(base));
}
prefs = PreferenceManager.getDefaultSharedPreferences(deContext);
mDB = new MagiskDB(base);
Networking.init(base);
LocaleManager.setLocale(this);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LocaleManager.setLocale(this);
}
public static BaseActivity foreground() {
return self.foreground;
}
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {}
@Override
public void onActivityStarted(@NonNull Activity activity) {}
@Override
public synchronized void onActivityResumed(@NonNull Activity activity) {
foreground = (BaseActivity) activity;
}
@Override
public synchronized void onActivityPaused(@NonNull Activity activity) {
foreground = null;
}
@Override
public void onActivityStopped(@NonNull Activity activity) {}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {}
}

View File

@@ -0,0 +1,130 @@
package com.topjohnwu.magisk
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
import java.util.concurrent.ThreadPoolExecutor
open class App : Application(), Application.ActivityLifecycleCallbacks {
lateinit var protectedContext: Context
@Deprecated("Use dependency injection")
val prefs: SharedPreferences by inject()
@Deprecated("Use dependency injection")
val DB: MagiskDB by inject()
@Deprecated("Use dependency injection")
val repoDB: RepoDatabaseHelper by inject()
@Volatile
private var foreground: Activity? = null
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (BuildConfig.DEBUG)
MultiDex.install(base)
Timber.plant(Timber.DebugTree())
startKoin {
androidContext(this@App)
modules(koinModules)
}
protectedContext = baseContext
self = this
deContext = base
if (Build.VERSION.SDK_INT >= 24) {
protectedContext = base.createDeviceProtectedStorageContext()
deContext = protectedContext
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
}
registerActivityLifecycleCallbacks(this)
Networking.init(base)
LocaleManager.setLocale(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LocaleManager.setLocale(this)
}
//region ActivityLifecycleCallbacks
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
@Synchronized
override fun onActivityResumed(activity: Activity) {
foreground = activity
}
@Synchronized
override fun onActivityPaused(activity: Activity) {
foreground = null
}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
//endregion
private val Context.defaultPrefsName get() = "${packageName}_preferences"
companion object {
@SuppressLint("StaticFieldLeak")
@Deprecated("Use dependency injection")
@JvmStatic
lateinit var self: App
@SuppressLint("StaticFieldLeak")
@Deprecated("Use dependency injection; replace with protectedContext")
@JvmStatic
lateinit var deContext: Context
@Deprecated("Use Rx or similar")
@JvmField
var THREAD_POOL: ThreadPoolExecutor
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootUtils::class.java)
Shell.Config.setTimeout(2)
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
}
@Deprecated("")
@JvmStatic
fun foreground(): Activity? {
val app: App by inject()
return app.foreground
}
}
}

View File

@@ -3,8 +3,6 @@ package com.topjohnwu.magisk;
import android.content.SharedPreferences;
import android.util.Xml;
import androidx.collection.ArrayMap;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
@@ -17,6 +15,8 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import androidx.collection.ArrayMap;
public class Config {
// Current status
@@ -109,14 +109,14 @@ public class Config {
public static void export() {
// Flush prefs to disk
App app = App.self;
app.prefs.edit().commit();
app.getPrefs().edit().commit();
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
app.getPackageName() + "_preferences.xml");
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
}
public static void initialize() {
SharedPreferences pref = App.self.prefs;
SharedPreferences pref = App.self.getPrefs();
SharedPreferences.Editor editor = pref.edit();
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
if (config.exists()) {
@@ -238,19 +238,19 @@ public class Config {
App app = App.self;
switch (getConfigType(key)) {
case PREF_INT:
return (T) (Integer) app.prefs.getInt(key, getDef(key));
return (T) (Integer) app.getPrefs().getInt(key, getDef(key));
case PREF_STR_INT:
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key));
return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key));
case PREF_BOOL:
return (T) (Boolean) app.prefs.getBoolean(key, getDef(key));
return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key));
case PREF_STR:
return (T) app.prefs.getString(key, getDef(key));
return (T) app.getPrefs().getString(key, getDef(key));
case DB_INT:
return (T) (Integer) app.mDB.getSettings(key, getDef(key));
return (T) (Integer) app.getDB().getSettings(key, getDef(key));
case DB_BOOL:
return (T) (Boolean) (app.mDB.getSettings(key, getDef(key) ? 1 : 0) != 0);
return (T) (Boolean) (app.getDB().getSettings(key, getDef(key) ? 1 : 0) != 0);
case DB_STR:
return (T) app.mDB.getStrings(key, getDef(key));
return (T) app.getDB().getStrings(key, getDef(key));
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return null;
@@ -260,25 +260,25 @@ public class Config {
App app = App.self;
switch (getConfigType(key)) {
case PREF_INT:
app.prefs.edit().putInt(key, (int) val).apply();
app.getPrefs().edit().putInt(key, (int) val).apply();
break;
case PREF_STR_INT:
app.prefs.edit().putString(key, String.valueOf(val)).apply();
app.getPrefs().edit().putString(key, String.valueOf(val)).apply();
break;
case PREF_BOOL:
app.prefs.edit().putBoolean(key, (boolean) val).apply();
app.getPrefs().edit().putBoolean(key, (boolean) val).apply();
break;
case PREF_STR:
app.prefs.edit().putString(key, (String) val).apply();
app.getPrefs().edit().putString(key, (String) val).apply();
break;
case DB_INT:
app.mDB.setSettings(key, (int) val);
app.getDB().setSettings(key, (int) val);
break;
case DB_BOOL:
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
app.getDB().setSettings(key, (boolean) val ? 1 : 0);
break;
case DB_STR:
app.mDB.setStrings(key, (String) val);
app.getDB().setStrings(key, (String) val);
break;
}
}
@@ -290,14 +290,14 @@ public class Config {
case PREF_STR_INT:
case PREF_BOOL:
case PREF_STR:
app.prefs.edit().remove(key).apply();
app.getPrefs().edit().remove(key).apply();
break;
case DB_BOOL:
case DB_INT:
app.mDB.rmSettings(key);
app.getDB().rmSettings(key);
break;
case DB_STR:
app.mDB.setStrings(key, null);
app.getDB().setStrings(key, null);
break;
}
}
@@ -365,13 +365,13 @@ public class Config {
switch (type) {
case DB_INT:
editor.putString(key, String.valueOf(
app.mDB.getSettings(key, (Integer) defs.get(key))));
app.getDB().getSettings(key, (Integer) defs.get(key))));
continue;
case DB_STR:
editor.putString(key, app.mDB.getStrings(key, (String) defs.get(key)));
editor.putString(key, app.getDB().getStrings(key, (String) defs.get(key)));
continue;
case DB_BOOL:
int bs = app.mDB.getSettings(key, -1);
int bs = app.getDB().getSettings(key, -1);
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
continue;
}

View File

@@ -35,6 +35,9 @@ public class Const {
public static final int USER_ID = Process.myUid() / 100000;
// Generic
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
public static final class MAGISK_VER {
public static final int MIN_SUPPORT = 18000;
}

View File

@@ -0,0 +1,19 @@
package com.topjohnwu.magisk.di
import android.content.Context
import androidx.preference.PreferenceManager
import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.App
import org.koin.dsl.module
val applicationModule = module {
single { RxBus() }
factory { get<Context>().resources }
factory { get<Context>() as App }
factory { get<Context>().packageManager }
single(SUTimeout) {
get<App>().protectedContext.getSharedPreferences("su_timeout", 0)
}
single { PreferenceManager.getDefaultSharedPreferences(get<App>().protectedContext) }
}

View File

@@ -0,0 +1,12 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import org.koin.dsl.module
val databaseModule = module {
single { MagiskDB(get<App>().protectedContext) }
single { RepoDatabaseHelper(get()) }
}

View File

@@ -0,0 +1,10 @@
package com.topjohnwu.magisk.di
import org.koin.dsl.module
val miscModule = module {
// define miscs here
}

View File

@@ -0,0 +1,10 @@
package com.topjohnwu.magisk.di
val koinModules = listOf(
applicationModule,
networkingModule,
databaseModule,
repositoryModule,
viewModelModules,
miscModule
)

View File

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

View File

@@ -0,0 +1,6 @@
package com.topjohnwu.magisk.di
import org.koin.dsl.module
val networkingModule = module {}

View File

@@ -0,0 +1,6 @@
package com.topjohnwu.magisk.di
import org.koin.dsl.module
val repositoryModule = module {}

View File

@@ -0,0 +1,25 @@
package com.topjohnwu.magisk.di
import android.net.Uri
import com.topjohnwu.magisk.ui.MainViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.hide.HideViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModules = module {
viewModel { MainViewModel() }
viewModel { HomeViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get()) }
viewModel { LogViewModel(get(), get()) }
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
}

View File

@@ -1,427 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.collection.ArraySet;
import androidx.recyclerview.widget.RecyclerView;
import com.buildware.widget.indeterm.IndeterminateCheckBox;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.ArrowExpandable;
import com.topjohnwu.magisk.view.Expandable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import butterknife.BindView;
import java9.util.Comparators;
import java9.util.Lists;
import java9.util.Objects;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
import java9.util.stream.StreamSupport;
public class ApplicationAdapter extends SectionedAdapter
<ApplicationAdapter.AppViewHolder, ApplicationAdapter.ProcessViewHolder> {
private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable";
private static final String GMS_PACKAGE = "com.google.android.gms";
private static boolean old_hide = false;
/* A list of apps that should not be shown as hide-able */
private static final List<String> HIDE_BLACKLIST = Lists.of(
App.self.getPackageName(),
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
);
private static final List<String> DEFAULT_HIDELIST = Lists.of(
SAFETYNET_PROCESS
);
private static int BOTTOM_MARGIN = -1;
private List<HideAppInfo> fullList, showList;
private List<HideTarget> hideList;
private PackageManager pm;
private boolean showSystem;
public ApplicationAdapter(Context context) {
fullList = showList = Collections.emptyList();
hideList = Collections.emptyList();
pm = context.getPackageManager();
showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP);
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
}
private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) {
return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams();
}
@Override
public int getSectionCount() {
return showList.size();
}
@Override
public int getItemCount(int section) {
HideAppInfo app = showList.get(section);
return app.expanded ? app.processList.size() : 0;
}
@Override
public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false);
AppViewHolder vh = new AppViewHolder(v);
if (BOTTOM_MARGIN < 0)
BOTTOM_MARGIN = getMargins(vh).bottomMargin;
return vh;
}
@Override
public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false);
return new ProcessViewHolder(v);
}
@Override
public void onBindSectionViewHolder(AppViewHolder holder, int section) {
HideAppInfo app = showList.get(section);
holder.app_name.setText(app.name);
holder.app_icon.setImageDrawable(app.info.loadIcon(pm));
holder.package_name.setText(app.info.packageName);
holder.checkBox.setOnStateChangedListener(null);
holder.checkBox.setState(app.getState());
holder.ex.setExpanded(app.expanded);
int index = getItemPosition(section, 0);
holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> {
if (status != null) {
setHide(status, app);
if (app.expanded)
notifyItemRangeChanged(index, app.processList.size());
}
});
if (app.processList.size() > 1) {
holder.arrow.setVisibility(View.VISIBLE);
holder.trigger.setOnClickListener((v) -> {
if (app.expanded) {
app.expanded = false;
notifyItemRangeRemoved(index, app.processList.size());
holder.ex.collapse();
} else {
app.expanded = true;
notifyItemRangeInserted(index, app.processList.size());
holder.ex.expand();
}
});
} else {
holder.arrow.setVisibility(View.GONE);
holder.trigger.setOnClickListener(null);
}
}
@Override
public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) {
HideAppInfo app = showList.get(section);
HideProcessInfo target = app.processList.get(position);
holder.process.setText(target.name);
holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(target.hidden);
holder.checkbox.setOnCheckedChangeListener((v, checked) -> {
setHide(checked, app, target);
notifyItemChanged(getSectionPosition(section));
});
getMargins(holder).bottomMargin =
position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0;
}
public void filter(String constraint) {
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
Stream<HideAppInfo> s = StreamSupport.stream(fullList)
.filter(this::systemFilter)
.filter(t -> nameFilter(t, constraint));
UiThreadHandler.run(() -> {
showList = s.collect(Collectors.toList());
notifyDataSetChanged();
});
});
}
public void setShowSystem(boolean b) {
showSystem = b;
}
public void refresh() {
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
}
private void setHide(boolean add, HideAppInfo app) {
if (add) {
StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p));
} else {
if (StreamSupport.stream(app.processList)
.anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) {
StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p));
} else {
// Quick removal
Shell.su("magiskhide --rm " + app.info.packageName).submit();
StreamSupport.stream(app.processList).forEach(p -> p.hidden = false);
}
}
}
private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) {
// Don't remove SafetyNet
if (!add && DEFAULT_HIDELIST.contains(process.name))
return;
Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm",
app.info.packageName, process.name)).submit();
process.hidden = add;
}
private void addProcesses(Set<String> set, ComponentInfo[] infos) {
if (infos != null)
for (ComponentInfo info : infos)
set.add(info.processName);
}
private PackageInfo getPackageInfo(String pkg) {
// Try super hard to get as much info as possible
try {
return pm.getPackageInfo(pkg,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES |
PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS);
} catch (Exception e1) {
try {
PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services;
info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers;
info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers;
return info;
} catch (Exception e2) {
return null;
}
}
}
@WorkerThread
private void loadApps() {
hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut())
.map(HideTarget::new)
.collect(Collectors.toList());
fullList = StreamSupport.stream(pm.getInstalledApplications(0))
.filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled)
.map(info -> {
if (old_hide) {
return new HideAppInfo(info, Sets.of(info.packageName));
} else {
Set<String> set = new ArraySet<>();
PackageInfo pkg = getPackageInfo(info.packageName);
if (pkg != null) {
addProcesses(set, pkg.activities);
addProcesses(set, pkg.services);
addProcesses(set, pkg.receivers);
addProcesses(set, pkg.providers);
}
if (set.isEmpty())
return null;
return new HideAppInfo(info, set);
}
}).filter(Objects::nonNull).sorted()
.collect(Collectors.toList());
Event.trigger(false, Event.MAGISK_HIDE_DONE);
}
// True if not system app or user already hidden it
private boolean systemFilter(HideAppInfo target) {
return showSystem || target.haveHidden() ||
(target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
}
private boolean contains(String s, String filter) {
return s.toLowerCase().contains(filter);
}
private boolean nameFilter(HideAppInfo target, String filter) {
if (filter == null || filter.isEmpty())
return true;
filter = filter.toLowerCase();
if (contains(target.name, filter))
return true;
for (HideProcessInfo p : target.processList) {
if (contains(p.name, filter))
return true;
}
return contains(target.info.packageName, filter);
}
class HideAppInfo implements Comparable<HideAppInfo> {
String name;
ApplicationInfo info;
List<HideProcessInfo> processList;
boolean expanded;
IndeterminateCheckBox.OnStateChangedListener listener;
HideAppInfo(ApplicationInfo appInfo, Set<String> set) {
info = appInfo;
name = Utils.getAppLabel(info, pm);
expanded = false;
processList = StreamSupport.stream(set)
.map(process -> new HideProcessInfo(info.packageName, process))
.sorted().collect(Collectors.toList());
listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> {
if (status != null) {
for (HideProcessInfo p : processList) {
String cmd = Utils.fmt("magiskhide --%s %s %s",
status ? "add" : "rm", info.packageName, p.name);
Shell.su(cmd).submit();
p.hidden = status;
}
}
};
}
@Override
public int compareTo(HideAppInfo o) {
Comparator<HideAppInfo> c;
c = Comparators.comparing(HideAppInfo::haveHidden);
c = Comparators.reversed(c);
c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase);
c = Comparators.thenComparing(c, t -> t.info.packageName);
return c.compare(this, o);
}
Boolean getState() {
boolean all = true;
boolean hidden = false;
for (HideProcessInfo p : processList) {
if (!p.hidden)
all = false;
else
hidden = true;
}
if (all)
return true;
return hidden ? null : false;
}
boolean haveHidden() {
Boolean c = getState();
return c == null ? true : c;
}
}
class HideProcessInfo implements Comparable<HideProcessInfo> {
String name;
boolean hidden;
HideProcessInfo(String pkg, String process) {
this.name = process;
for (HideTarget t : hideList) {
if (t.pkg.equals(pkg) && t.process.equals(process)) {
hidden = true;
break;
}
}
}
@Override
public int compareTo(HideProcessInfo o) {
Comparator<HideProcessInfo> c;
c = Comparators.comparing((HideProcessInfo t) -> t.hidden);
c = Comparators.reversed(c);
c = Comparators.thenComparing(c, t -> t.name);
return c.compare(this, o);
}
}
class HideTarget {
String pkg;
String process;
HideTarget(String line) {
String[] split = line.split("\\|", 2);
pkg = split[0];
if (split.length == 2) {
process = split[1];
} else {
// Backwards compatibility
old_hide = true;
process = pkg;
}
}
}
class AppViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_icon) ImageView app_icon;
@BindView(R.id.app_name) TextView app_name;
@BindView(R.id.package_name) TextView package_name;
@BindView(R.id.checkbox) IndeterminateCheckBox checkBox;
@BindView(R.id.trigger) View trigger;
@BindView(R.id.arrow) ImageView arrow;
Expandable ex;
AppViewHolder(@NonNull View itemView) {
super(itemView);
new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView);
ex = new ArrowExpandable(new Expandable() {
@Override
protected void onExpand() {
getMargins(AppViewHolder.this).bottomMargin = 0;
}
@Override
protected void onCollapse() {
getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN;
}
}, arrow);
}
}
class ProcessViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.process) TextView process;
@BindView(R.id.checkbox) CheckBox checkbox;
ProcessViewHolder(@NonNull View itemView) {
super(itemView);
new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView);
}
}
}

View File

@@ -1,127 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.entity.Module;
import com.topjohnwu.magisk.view.SnackbarMaker;
import com.topjohnwu.superuser.Shell;
import java.util.List;
import butterknife.BindView;
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
private final List<Module> mList;
public ModulesAdapter(List<Module> list) {
mList = list;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Context context = holder.itemView.getContext();
final Module module = mList.get(position);
String version = module.getVersion();
String author = module.getAuthor();
String description = module.getDescription();
String noInfo = context.getString(R.string.no_info_provided);
holder.title.setText(module.getName());
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
holder.checkBox.setOnCheckedChangeListener(null);
holder.checkBox.setChecked(module.isEnabled());
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
int snack;
if (isChecked) {
module.removeDisableFile();
snack = R.string.disable_file_removed;
} else {
module.createDisableFile();
snack = R.string.disable_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
});
holder.delete.setOnClickListener(v -> {
boolean removed = module.willBeRemoved();
int snack;
if (removed) {
module.deleteRemoveFile();
snack = R.string.remove_file_deleted;
} else {
module.createRemoveFile();
snack = R.string.remove_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
updateDeleteButton(holder, module);
});
if (module.isUpdated()) {
holder.notice.setVisibility(View.VISIBLE);
holder.notice.setText(R.string.update_file_created);
holder.delete.setEnabled(false);
} else {
updateDeleteButton(holder, module);
}
}
private void updateDeleteButton(ViewHolder holder, Module module) {
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
if (module.willBeRemoved()) {
holder.delete.setImageResource(R.drawable.ic_undelete);
} else {
holder.delete.setImageResource(R.drawable.ic_delete);
}
}
@Override
public int getItemCount() {
return mList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@BindView(R.id.description) TextView description;
@BindView(R.id.notice) TextView notice;
@BindView(R.id.checkbox) CheckBox checkBox;
@BindView(R.id.author) TextView author;
@BindView(R.id.delete) ImageView delete;
ViewHolder(View itemView) {
super(itemView);
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
if (!Shell.rootAccess()) {
checkBox.setEnabled(false);
delete.setEnabled(false);
}
}
}
}

View File

@@ -1,172 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.MagiskDB;
import com.topjohnwu.magisk.model.entity.Policy;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.view.ArrowExpandable;
import com.topjohnwu.magisk.view.Expandable;
import com.topjohnwu.magisk.view.ExpandableViewHolder;
import com.topjohnwu.magisk.view.SnackbarMaker;
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
import java.util.List;
import butterknife.BindView;
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
private List<Policy> policyList;
private MagiskDB dbHelper;
private PackageManager pm;
private boolean[] expandList;
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
policyList = list;
expandList = new boolean[policyList.size()];
dbHelper = db;
this.pm = pm;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Policy policy = policyList.get(position);
holder.settings.setExpanded(expandList[position]);
holder.trigger.setOnClickListener(view -> {
if (holder.settings.isExpanded()) {
holder.settings.collapse();
expandList[position] = false;
} else {
holder.settings.expand();
expandList[position] = true;
}
});
holder.appName.setText(policy.appName);
holder.packageName.setText(policy.packageName);
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
holder.notificationSwitch.setOnCheckedChangeListener(null);
holder.loggingSwitch.setOnCheckedChangeListener(null);
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
holder.notificationSwitch.setChecked(policy.notification);
holder.loggingSwitch.setChecked(policy.logging);
holder.masterSwitch.setOnClickListener(v -> {
boolean isChecked = holder.masterSwitch.isChecked();
Runnable r = () -> {
if ((isChecked && policy.policy == Policy.DENY) ||
(!isChecked && policy.policy == Policy.ALLOW)) {
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
};
if (FingerprintHelper.useFingerprint()) {
holder.masterSwitch.setChecked(!isChecked);
new FingerprintAuthDialog((Activity) v.getContext(), () -> {
holder.masterSwitch.setChecked(isChecked);
r.run();
}).show();
} else {
r.run();
}
});
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.notification) ||
(!isChecked && policy.notification)) {
policy.notification = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.logging) ||
(!isChecked && policy.logging)) {
policy.logging = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.delete.setOnClickListener(v -> {
DialogInterface.OnClickListener l = (dialog, which) -> {
policyList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, policyList.size());
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
Snackbar.LENGTH_SHORT).show();
dbHelper.deletePolicy(policy);
};
if (FingerprintHelper.useFingerprint()) {
new FingerprintAuthDialog((Activity) v.getContext(),
() -> l.onClick(null, 0)).show();
} else {
new CustomAlertDialog((Activity) v.getContext())
.setTitle(R.string.su_revoke_title)
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
.setPositiveButton(R.string.yes, l)
.setNegativeButton(R.string.no_thanks, null)
.setCancelable(true)
.show();
}
});
}
@Override
public int getItemCount() {
return policyList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.package_name) TextView packageName;
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.master_switch) SwitchCompat masterSwitch;
@BindView(R.id.notification_switch) SwitchCompat notificationSwitch;
@BindView(R.id.logging_switch) SwitchCompat loggingSwitch;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
@BindView(R.id.arrow) ImageView arrow;
@BindView(R.id.trigger) View trigger;
@BindView(R.id.delete) ImageView delete;
@BindView(R.id.more_info) ImageView moreInfo;
Expandable settings;
public ViewHolder(View itemView) {
super(itemView);
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
settings = new ArrowExpandable(new ExpandableViewHolder(expandLayout), arrow);
}
}
}

View File

@@ -1,246 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SearchView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.model.download.DownloadModuleService;
import com.topjohnwu.magisk.model.entity.Module;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.view.MarkDownWindow;
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import butterknife.BindView;
import java9.util.stream.StreamSupport;
public class ReposAdapter
extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder>
implements Event.AutoListener, SearchView.OnQueryTextListener {
private static final int UPDATES = 0;
private static final int INSTALLED = 1;
private static final int OTHERS = 2;
private Map<String, Module> moduleMap;
private RepoDatabaseHelper repoDB;
private List<Pair<Integer, List<Repo>>> repoPairs;
private List<Repo> fullList;
private SearchView mSearch;
public ReposAdapter() {
repoDB = App.self.repoDB;
moduleMap = Collections.emptyMap();
fullList = Collections.emptyList();
repoPairs = new ArrayList<>();
}
@Override
public int getSectionCount() {
return repoPairs.size();
}
@Override
public int getItemCount(int section) {
return repoPairs.get(section).second.size();
}
@Override
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
return new SectionHolder(v);
}
@Override
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
return new RepoHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
switch (repoPairs.get(section).first) {
case UPDATES:
holder.sectionText.setText(R.string.update_available);
break;
case INSTALLED:
holder.sectionText.setText(R.string.installed);
break;
case OTHERS:
holder.sectionText.setText(R.string.not_installed);
break;
}
}
@Override
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
Repo repo = repoPairs.get(section).second.get(position);
Context context = holder.itemView.getContext();
String name = repo.getName();
String version = repo.getVersion();
String author = repo.getAuthor();
String description = repo.getDescription();
String noInfo = context.getString(R.string.no_info_provided);
holder.title.setText(TextUtils.isEmpty(name) ? noInfo : name);
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
holder.infoLayout.setOnClickListener(v ->
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
holder.downloadImage.setOnClickListener(v -> {
new CustomAlertDialog((BaseActivity) context)
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
.setMessage(context.getString(R.string.repo_install_msg, repo.getDownloadFilename()))
.setCancelable(true)
.setPositiveButton(R.string.install, (d, i) ->
startDownload((BaseActivity) context, repo, true))
.setNeutralButton(R.string.download, (d, i) ->
startDownload((BaseActivity) context, repo, false))
.setNegativeButton(R.string.no_thanks, null)
.show();
});
}
private void startDownload(BaseActivity activity, Repo repo, Boolean install) {
activity.runWithExternalRW(() -> {
Intent intent = new Intent(activity, ClassMap.get(DownloadModuleService.class))
.putExtra("repo", repo).putExtra("install", install);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent);
} else {
activity.startService(intent);
}
});
}
private void updateLists() {
if (mSearch != null)
onQueryTextChange(mSearch.getQuery().toString());
else
onQueryTextChange("");
}
private static boolean noCaseContain(String a, String b) {
return a.toLowerCase().contains(b.toLowerCase());
}
public void setSearchView(SearchView view) {
mSearch = view;
mSearch.setOnQueryTextListener(this);
}
public void notifyDBChanged(boolean refresh) {
try (Cursor c = repoDB.getRepoCursor()) {
fullList = new ArrayList<>(c.getCount());
while (c.moveToNext())
fullList.add(new Repo(c));
}
if (refresh)
updateLists();
}
@Override
public void onEvent(int event) {
moduleMap = Event.getResult(event);
updateLists();
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.MODULE_LOAD_DONE};
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String s) {
List<Repo> updates = new ArrayList<>();
List<Repo> installed = new ArrayList<>();
List<Repo> others = new ArrayList<>();
StreamSupport.stream(fullList)
.filter(repo -> noCaseContain(repo.getName(), s)
|| noCaseContain(repo.getAuthor(), s)
|| noCaseContain(repo.getDescription(), s))
.forEach(repo -> {
Module module = moduleMap.get(repo.getId());
if (module != null) {
if (repo.getVersionCode() > module.getVersionCode()) {
// Updates
updates.add(repo);
} else {
installed.add(repo);
}
} else {
others.add(repo);
}
});
repoPairs.clear();
if (!updates.isEmpty())
repoPairs.add(new Pair<>(UPDATES, updates));
if (!installed.isEmpty())
repoPairs.add(new Pair<>(INSTALLED, installed));
if (!others.isEmpty())
repoPairs.add(new Pair<>(OTHERS, others));
notifyDataSetChanged();
return false;
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.section_text) TextView sectionText;
SectionHolder(View itemView) {
super(itemView);
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
}
}
static class RepoHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@BindView(R.id.description) TextView description;
@BindView(R.id.author) TextView author;
@BindView(R.id.info_layout) View infoLayout;
@BindView(R.id.download) ImageView downloadImage;
@BindView(R.id.update_time) TextView updateTime;
RepoHolder(View itemView) {
super(itemView);
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
}
}
}

View File

@@ -1,96 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int SECTION_TYPE = Integer.MIN_VALUE;
@NonNull
@Override
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == SECTION_TYPE)
return onCreateSectionViewHolder(parent);
return onCreateItemViewHolder(parent, viewType);
}
@Override
@SuppressWarnings("unchecked")
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
onBindSectionViewHolder((S) holder, info.section);
else
onBindItemViewHolder((C) holder, info.section, info.position);
}
@Override
final public int getItemCount() {
int size, sec;
size = sec = getSectionCount();
for (int i = 0; i < sec; ++i){
size += getItemCount(i);
}
return size;
}
@Override
final public int getItemViewType(int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
return SECTION_TYPE;
else
return getItemViewType(info.section, info.position);
}
public int getItemViewType(int section, int position) {
return 0;
}
protected int getSectionPosition(int section) {
return getItemPosition(section, -1);
}
protected int getItemPosition(int section, int position) {
int realPosition = 0;
// Previous sections
for (int i = 0; i < section; ++i) {
realPosition += getItemCount(i) + 1;
}
// Current section
realPosition += position + 1;
return realPosition;
}
private PositionInfo getPositionInfo(int position) {
int section = 0;
while (true) {
if (position == 0)
return new PositionInfo(section, -1);
position -= 1;
if (position < getItemCount(section))
return new PositionInfo(section, position);
position -= getItemCount(section++);
}
}
private static class PositionInfo {
int section;
int position;
PositionInfo(int section, int position) {
this.section = section;
this.position = position;
}
}
public abstract int getSectionCount();
public abstract int getItemCount(int section);
public abstract S onCreateSectionViewHolder(ViewGroup parent);
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
public abstract void onBindSectionViewHolder(S holder, int section);
public abstract void onBindItemViewHolder(C holder, int section, int position);
}

View File

@@ -1,103 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.app.Activity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public abstract class StringListAdapter<VH extends StringListAdapter.ViewHolder>
extends RecyclerView.Adapter<VH> {
private RecyclerView rv;
private boolean dynamic;
private int screenWidth;
private int txtWidth = -1;
private int padding;
protected List<String> mList;
public StringListAdapter(List<String> list) {
this(list, false);
}
public StringListAdapter(List<String> list, boolean isDynamic) {
mList = list;
dynamic = isDynamic;
}
@NonNull
@Override
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutRes(), parent, false);
VH vh = createViewHolder(v);
if (txtWidth < 0)
onUpdateTextWidth(vh);
return vh;
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
holder.txt.setText(mList.get(position));
holder.txt.getLayoutParams().width = txtWidth;
if (dynamic)
onUpdateTextWidth(holder);
}
protected void onUpdateTextWidth(VH vh) {
if (txtWidth < 0) {
txtWidth = screenWidth - padding;
} else {
vh.txt.measure(0, 0);
int width = vh.txt.getMeasuredWidth();
if (width > txtWidth) {
txtWidth = width;
vh.txt.getLayoutParams().width = txtWidth;
}
}
if (rv.getWidth() != txtWidth + padding)
rv.requestLayout();
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView rv) {
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) rv.getContext()).getWindowManager()
.getDefaultDisplay().getMetrics(displayMetrics);
screenWidth = displayMetrics.widthPixels;
padding = rv.getPaddingStart() + rv.getPaddingEnd();
this.rv = rv;
}
@Override
public final int getItemCount() {
return mList.size();
}
@LayoutRes
protected abstract int itemLayoutRes();
@NonNull
public abstract VH createViewHolder(@NonNull View v);
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
public TextView txt;
public ViewHolder(@NonNull View itemView) {
super(itemView);
txt = itemView.findViewById(textViewResId());
}
@IdRes
protected abstract int textViewResId();
}
}

View File

@@ -1,144 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.MagiskDB;
import com.topjohnwu.magisk.model.entity.SuLogEntry;
import com.topjohnwu.magisk.view.ExpandableViewHolder;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import butterknife.BindView;
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
private List<List<SuLogEntry>> logEntries;
private Set<Integer> itemExpanded, sectionExpanded;
private MagiskDB suDB;
public SuLogAdapter(MagiskDB db) {
suDB = db;
logEntries = Collections.emptyList();
sectionExpanded = new HashSet<>();
itemExpanded = new HashSet<>();
}
@Override
public int getSectionCount() {
return logEntries.size();
}
@Override
public int getItemCount(int section) {
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
}
@Override
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
return new SectionHolder(v);
}
@Override
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
return new LogViewHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
SuLogEntry entry = logEntries.get(section).get(0);
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
holder.itemView.setOnClickListener(v -> {
RotateAnimation rotate;
if (sectionExpanded.contains(section)) {
holder.arrow.setRotation(0);
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.remove(section);
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
} else {
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.add(section);
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
}
rotate.setDuration(300);
rotate.setFillAfter(true);
holder.arrow.setAnimation(rotate);
});
holder.date.setText(entry.getDateString());
}
@Override
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
SuLogEntry entry = logEntries.get(section).get(position);
int realIdx = getItemPosition(section, position);
holder.expandable.setExpanded(itemExpanded.contains(realIdx));
holder.itemView.setOnClickListener(view -> {
if (holder.expandable.isExpanded()) {
holder.expandable.collapse();
itemExpanded.remove(realIdx);
} else {
holder.expandable.expand();
itemExpanded.add(realIdx);
}
});
Context context = holder.itemView.getContext();
holder.appName.setText(entry.appName);
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
holder.pid.setText(context.getString(R.string.pid, entry.fromPid));
holder.uid.setText(context.getString(R.string.target_uid, entry.toUid));
holder.command.setText(context.getString(R.string.command, entry.command));
holder.time.setText(entry.getTimeString());
}
public void notifyDBChanged() {
logEntries = suDB.getLogs();
itemExpanded.clear();
sectionExpanded.clear();
sectionExpanded.add(0);
notifyDataSetChanged();
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.date) TextView date;
@BindView(R.id.arrow) ImageView arrow;
SectionHolder(View itemView) {
super(itemView);
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
}
}
static class LogViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.action) TextView action;
@BindView(R.id.time) TextView time;
@BindView(R.id.pid) TextView pid;
@BindView(R.id.uid) TextView uid;
@BindView(R.id.cmd) TextView command;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
ExpandableViewHolder expandable;
LogViewHolder(View itemView) {
super(itemView);
new SuLogAdapter$LogViewHolder_ViewBinding(this, itemView);
expandable = new ExpandableViewHolder(expandLayout);
}
}
}

View File

@@ -1,41 +0,0 @@
package com.topjohnwu.magisk.model.adapters;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class TabFragmentAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
private List<String> titleList;
public TabFragmentAdapter(FragmentManager fm) {
super(fm);
fragmentList = new ArrayList<>();
titleList = new ArrayList<>();
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
public void addTab(Fragment fragment, String title) {
fragmentList.add(fragment);
titleList.add(title);
}
}

View File

@@ -6,8 +6,6 @@ import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import androidx.annotation.Nullable;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
@@ -31,6 +29,8 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import androidx.annotation.Nullable;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;

View File

@@ -5,10 +5,10 @@ import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.List;
import androidx.annotation.NonNull;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;

View File

@@ -0,0 +1,16 @@
package com.topjohnwu.magisk.model.entity
import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.utils.packageInfo
import com.topjohnwu.magisk.utils.processes
class HideAppInfo(
val info: ApplicationInfo,
val name: String,
val icon: Drawable
) {
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
}

View File

@@ -0,0 +1,10 @@
package com.topjohnwu.magisk.model.entity
class HideTarget(line: String) {
private val split = line.split(Regex("\\|"), 2)
val packageName = split[0]
val process = split.getOrElse(1) { packageName }
}

View File

@@ -4,10 +4,10 @@ import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;

View File

@@ -0,0 +1,11 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() {
override val layoutRes: Int = R.layout.item_console
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
}

View File

@@ -0,0 +1,92 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toggle
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() {
override val layoutRes: Int = R.layout.item_hide_app
val packageName = item.info.packageName.orEmpty()
val items = DiffObservableList(callback).also {
val items = item.processes.map {
val isHidden = targets.any { target ->
packageName == target.packageName && it == target.process
}
HideProcessRvItem(packageName, it, isHidden)
}
it.update(items)
}
val isHiddenState = KObservableField(currentState)
val isExpanded = KObservableField(false)
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
private val currentState
get() = when (itemsProcess.count { it.isHidden.value }) {
items.size -> IndeterminateState.CHECKED
in 1 until items.size -> IndeterminateState.INDETERMINATE
else -> IndeterminateState.UNCHECKED
}
init {
itemsProcess.forEach {
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
}
}
fun toggle() {
val desiredState = when (isHiddenState.value) {
IndeterminateState.INDETERMINATE,
IndeterminateState.UNCHECKED -> true
IndeterminateState.CHECKED -> false
}
itemsProcess.forEach { it.isHidden.value = desiredState }
isHiddenState.value = currentState
}
fun toggleExpansion() {
if (items.size <= 1) return
isExpanded.toggle()
}
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
}
class HideProcessRvItem(
val packageName: String,
val process: String,
isHidden: Boolean
) : ComparableRvItem<HideProcessRvItem>() {
override val layoutRes: Int = R.layout.item_hide_process
val isHidden = KObservableField(isHidden)
private val rxBus: RxBus by inject()
init {
this.isHidden.addOnPropertyChangedCallback {
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
}
}
fun toggle() = isHidden.toggle()
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: HideProcessRvItem): Boolean =
packageName == other.packageName && process == other.process
}

View File

@@ -0,0 +1,75 @@
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ObservableList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.SuLogEntry
import com.topjohnwu.magisk.utils.toggle
class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log
val items = DiffObservableList(callback)
fun update(list: List<LogItemRvItem>) {
list.firstOrNull()?.isExpanded?.value = true
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: LogRvItem): Boolean = false
override fun itemSameAs(other: LogRvItem): Boolean = false
}
class LogItemRvItem(
val items: ObservableList<ComparableRvItem<*>>
) : ComparableRvItem<LogItemRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log
val date = items.filterIsInstance<LogItemEntryRvItem>().firstOrNull()
?.item?.dateString.orEmpty()
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemRvItem): Boolean = items
.any { !other.items.contains(it) }
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
}
class LogItemEntryRvItem(val item: SuLogEntry) : ComparableRvItem<LogItemEntryRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log_entry
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid &&
item.toUid == other.item.toUid &&
item.fromPid == other.item.fromPid &&
item.packageName == other.item.packageName &&
item.command == other.item.command &&
item.action == other.item.action &&
item.date == other.item.date
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
}
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
override val layoutRes: Int = R.layout.item_page_magisk_log
val items = DiffObservableList(callback)
fun update(list: List<ConsoleRvItem>) {
items.update(list)
}
//two of these will never be present, safe to assume it's unique
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
}

View File

@@ -0,0 +1,66 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources
import androidx.annotation.StringRes
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.toggle
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module
val lastActionNotice = KObservableField("")
val isChecked = KObservableField(item.isEnabled)
val isDeletable = KObservableField(item.willBeRemoved())
init {
isChecked.addOnPropertyChangedCallback {
when (it) {
true -> item.removeDisableFile().notice(R.string.disable_file_removed)
false -> item.createDisableFile().notice(R.string.disable_file_created)
}
}
isDeletable.addOnPropertyChangedCallback {
when (it) {
true -> item.createRemoveFile().notice(R.string.remove_file_created)
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted)
}
}
when {
item.isUpdated -> notice(R.string.update_file_created)
item.willBeRemoved() -> notice(R.string.remove_file_created)
}
}
fun toggle() = isChecked.toggle()
fun toggleDelete() = isDeletable.toggle()
@Suppress("unused")
private fun Any.notice(@StringRes info: Int) {
lastActionNotice.value = get<Resources>().getString(info)
}
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
override fun itemSameAs(other: ModuleRvItem): Boolean = item.name == other.item.name
}
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
override val layoutRes: Int = R.layout.item_repo
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
&& item.lastUpdate == other.item.lastUpdate
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
override fun itemSameAs(other: RepoRvItem): Boolean = item.detailUrl == other.item.detailUrl
}

View File

@@ -0,0 +1,47 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toggle
class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
override val layoutRes: Int = R.layout.item_policy
val isExpanded = KObservableField(false)
val isEnabled = KObservableField(item.policy == Policy.ALLOW)
val shouldNotify = KObservableField(item.notification)
val shouldLog = KObservableField(item.logging)
fun toggle() = isExpanded.toggle()
private val rxBus: RxBus by inject()
init {
isEnabled.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
}
shouldNotify.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
item.notification = it
rxBus.post(PolicyUpdateEvent.Notification(this@PolicyRvItem))
}
shouldLog.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
item.logging = it
rxBus.post(PolicyUpdateEvent.Log(this@PolicyRvItem))
}
}
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
}

View File

@@ -0,0 +1,11 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
override val layoutRes: Int = R.layout.item_section
override fun contentSameAs(other: SectionRvItem) = itemSameAs(other)
override fun itemSameAs(other: SectionRvItem) = text == other.text
}

View File

@@ -0,0 +1,13 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
override val layoutRes: Int = R.layout.item_spinner
override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other)
override fun itemSameAs(other: SpinnerRvItem) = item == other.item
}

View File

@@ -0,0 +1,5 @@
package com.topjohnwu.magisk.model.entity.state
enum class IndeterminateState {
CHECKED, INDETERMINATE, UNCHECKED
}

View File

@@ -0,0 +1,16 @@
package com.topjohnwu.magisk.model.events
import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
sealed class PolicyUpdateEvent(val item: PolicyRvItem) : RxBus.Event {
class Notification(item: PolicyRvItem) : PolicyUpdateEvent(item)
class Log(item: PolicyRvItem) : PolicyUpdateEvent(item)
}
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event

View File

@@ -0,0 +1,40 @@
package com.topjohnwu.magisk.model.events
import android.app.Activity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.entity.Repo
import io.reactivex.subjects.PublishSubject
data class OpenLinkEvent(val url: String) : ViewEvent()
class ManagerInstallEvent : ViewEvent()
class MagiskInstallEvent : ViewEvent()
class ManagerChangelogEvent : ViewEvent()
class MagiskChangelogEvent : ViewEvent()
class UninstallEvent : ViewEvent()
class EnvFixEvent : ViewEvent()
class UpdateSafetyNetEvent : ViewEvent()
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent()
class OpenFilePickerEvent : ViewEvent()
class OpenChangelogEvent(val item: Repo) : ViewEvent()
class InstallModuleEvent(val item: Repo) : ViewEvent()
class PageChangedEvent : ViewEvent()
class PermissionEvent(
val permissions: List<String>,
val callback: PublishSubject<Boolean>
) : ViewEvent()
class BackPressEvent : ViewEvent()
class SuDialogEvent(val policy: Policy) : ViewEvent()
class DieEvent : ViewEvent()

View File

@@ -0,0 +1,7 @@
package com.topjohnwu.magisk.model.flash
interface FlashResultListener {
fun onResult(isSuccess: Boolean)
}

View File

@@ -0,0 +1,62 @@
package com.topjohnwu.magisk.model.flash
import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.tasks.FlashZip
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
sealed class Flashing(
uri: Uri,
private val console: MutableList<String>,
log: MutableList<String>,
private val resultListener: FlashResultListener
) : FlashZip(uri, console, log) {
override fun onResult(success: Boolean) {
if (!success) {
console.add("! Installation failed")
}
resultListener.onResult(success)
}
class Install(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>,
resultListener: FlashResultListener
) : Flashing(uri, console, log, resultListener) {
override fun onResult(success: Boolean) {
if (success) {
Utils.loadModules()
}
super.onResult(success)
}
}
class Uninstall(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>,
resultListener: FlashResultListener
) : Flashing(uri, console, log, resultListener) {
private val context: Context by inject()
override fun onResult(success: Boolean) {
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall " + context.packageName).exec()
}
}
super.onResult(success)
}
}
}

View File

@@ -0,0 +1,51 @@
package com.topjohnwu.magisk.model.flash
import android.net.Uri
import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.superuser.Shell
sealed class Patching(
private val console: MutableList<String>,
logs: MutableList<String>,
private val resultListener: FlashResultListener
) : MagiskInstaller(console, logs) {
override fun onResult(success: Boolean) {
if (success) {
console.add("- All done!")
} else {
Shell.sh("rm -rf $installDir").submit()
console.add("! Installation failed")
}
resultListener.onResult(success)
}
class File(
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
}
class SecondSlot(
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
}
class Direct(
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
override fun operations() =
findImage() && extractZip() && patchBoot() && flashBoot()
}
}

View File

@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.model.navigation
import android.os.Bundle
import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes
import androidx.fragment.app.Fragment
import com.skoumal.teanity.viewevents.NavigationDslMarker
import com.skoumal.teanity.viewevents.ViewEvent
import kotlin.reflect.KClass
class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions,
val animOptions: MagiskAnimBuilder
) : ViewEvent() {
companion object {
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build()
}
@NavigationDslMarker
class Builder {
private var animOptions: MagiskAnimBuilder = MagiskAnimBuilder()
private var navOptions: MagiskNavOptions = MagiskNavOptions()
private val directionsBuilder = MagiskNavDirectionsBuilder()
fun args(builder: Bundle.() -> Unit) = directionsBuilder.args(builder)
fun navAnim(builder: MagiskAnimBuilder.() -> Unit) {
animOptions = MagiskAnimBuilder().apply(builder)
}
fun navOptions(builder: MagiskNavOptions.() -> Unit) {
navOptions = MagiskNavOptions().apply(builder)
}
fun navDirections(builder: MagiskNavDirectionsBuilder.() -> Unit) {
directionsBuilder.apply(builder)
}
internal fun build() = MagiskNavigationEvent(directionsBuilder, navOptions, animOptions)
}
}
@NavigationDslMarker
class MagiskNavDirectionsBuilder {
var destination: KClass<out Fragment>? = null
var isActivity: Boolean = false
val args: Bundle = Bundle()
fun args(builder: Bundle.() -> Unit) = args.apply(builder)
}
@NavigationDslMarker
class MagiskNavOptions {
var popUpTo: KClass<*>? = null
var inclusive: Boolean = false
var clearTask: Boolean = false
var singleTop: Boolean = false
}
@NavigationDslMarker
class MagiskAnimBuilder {
@AnimRes
@AnimatorRes
var enter = 0
@AnimRes
@AnimatorRes
var exit = 0
@AnimRes
@AnimatorRes
var popEnter = 0
@AnimRes
@AnimatorRes
var popExit = 0
val anySet: Boolean get() = enter != 0 || exit != 0 || popEnter != 0 || popExit != 0
}

View File

@@ -0,0 +1,57 @@
package com.topjohnwu.magisk.model.navigation
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
import com.topjohnwu.magisk.ui.module.ReposFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
object Navigation {
fun home() = MagiskNavigationEvent {
navDirections { destination = HomeFragment::class }
navOptions { popUpTo = HomeFragment::class }
}
fun superuser() = MagiskNavigationEvent {
navDirections { destination = SuperuserFragment::class }
}
fun modules() = MagiskNavigationEvent {
navDirections { destination = ModulesFragment::class }
}
fun repos() = MagiskNavigationEvent {
navDirections { destination = ReposFragment::class }
}
fun hide() = MagiskNavigationEvent {
navDirections { destination = MagiskHideFragment::class }
}
fun log() = MagiskNavigationEvent {
navDirections { destination = LogFragment::class }
}
fun settings() = MagiskNavigationEvent {
navDirections { destination = SettingsFragment::class }
}
fun fromSection(section: String) = when (section) {
"superuser" -> superuser()
"modules" -> modules()
"downloads" -> repos()
"magiskhide" -> hide()
"log" -> log()
"settings" -> settings()
else -> home()
}
object Main {
const val OPEN_NAV = 1
}
}

View File

@@ -0,0 +1,13 @@
package com.topjohnwu.magisk.model.navigation
import androidx.fragment.app.Fragment
import kotlin.reflect.KClass
interface Navigator {
//TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it
val baseFragments: List<KClass<out Fragment>>
fun navigateTo(event: MagiskNavigationEvent)
}

View File

@@ -0,0 +1,31 @@
package com.topjohnwu.magisk.model.observer
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import java.io.Serializable
class Observer<T>(vararg dependencies: Observable, private val observer: () -> T) :
ObservableField<T>(*dependencies), Serializable {
val value: T get() = observer()
@Deprecated(
message = "Use KObservableField.value syntax from code",
replaceWith = ReplaceWith("value")
)
override fun get(): T {
return value
}
@Deprecated(
message = "Observer cannot be set",
level = DeprecationLevel.HIDDEN
)
override fun set(newValue: T) {
}
override fun toString(): String {
return "Observer(value=$value)"
}
}

View File

@@ -0,0 +1,40 @@
package com.topjohnwu.magisk.model.permissions
typealias SimpleCallback = () -> Unit
typealias PermissionRationaleCallback = (List<String>) -> Unit
class PermissionRequestBuilder {
private var onSuccessCallback: SimpleCallback = {}
private var onFailureCallback: SimpleCallback = {}
private var onShowRationaleCallback: PermissionRationaleCallback = {}
fun onSuccess(callback: SimpleCallback) {
onSuccessCallback = callback
}
fun onFailure(callback: SimpleCallback) {
onFailureCallback = callback
}
fun onShowRationale(callback: PermissionRationaleCallback) {
onShowRationaleCallback = callback
}
fun build(): PermissionRequest {
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
}
}
class PermissionRequest(
private val onSuccessCallback: SimpleCallback,
private val onFailureCallback: SimpleCallback,
private val onShowRationaleCallback: PermissionRationaleCallback
) {
fun onSuccess() = onSuccessCallback()
fun onFailure() = onFailureCallback()
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
}

View File

@@ -1,84 +0,0 @@
package com.topjohnwu.magisk.model.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
import com.topjohnwu.magisk.utils.DownloadApp;
import com.topjohnwu.magisk.utils.SuLogger;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.Shortcuts;
import com.topjohnwu.superuser.Shell;
public class GeneralReceiver extends BroadcastReceiver {
private String getPkg(Intent i) {
return i.getData() == null ? "" : i.getData().getEncodedSchemeSpecificPart();
}
@Override
public void onReceive(Context context, Intent intent) {
App app = App.self;
if (intent == null)
return;
String action = intent.getAction();
if (action == null)
return;
switch (action) {
case Intent.ACTION_REBOOT:
case Intent.ACTION_BOOT_COMPLETED:
action = intent.getStringExtra("action");
if (action == null) {
// Actual boot completed event
Shell.su("mm_patch_dtbo").submit(result -> {
if (result.isSuccess())
Notifications.dtboPatched();
});
break;
}
switch (action) {
case SuRequestActivity.REQUEST:
Intent i = new Intent(app, ClassMap.get(SuRequestActivity.class))
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
app.startActivity(i);
break;
case SuRequestActivity.LOG:
SuLogger.handleLogs(intent);
break;
case SuRequestActivity.NOTIFY:
SuLogger.handleNotify(intent);
break;
}
break;
case Intent.ACTION_PACKAGE_REPLACED:
// This will only work pre-O
if (Config.get(Config.Key.SU_REAUTH)) {
app.mDB.deletePolicy(getPkg(intent));
}
break;
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
String pkg = getPkg(intent);
app.mDB.deletePolicy(pkg);
Shell.su("magiskhide --rm " + pkg).submit();
break;
case Intent.ACTION_LOCALE_CHANGED:
Shortcuts.setup(context);
break;
case Const.Key.BROADCAST_MANAGER_UPDATE:
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK);
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME));
break;
case Const.Key.BROADCAST_REBOOT:
Shell.su("/system/bin/reboot").submit();
break;
}
}
}

View File

@@ -0,0 +1,80 @@
package com.topjohnwu.magisk.model.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.DownloadApp
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
open class GeneralReceiver : BroadcastReceiver() {
companion object {
const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
const val TEST = "test"
}
private fun getPkg(intent: Intent): String {
return intent.data?.encodedSchemeSpecificPart ?: ""
}
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null)
return
val mDB: MagiskDB = get()
var action: String? = intent.action ?: return
when (action) {
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
action = intent.getStringExtra("action")
if (action == null) {
// Actual boot completed event
Shell.su("mm_patch_dtbo").submit { result ->
if (result.isSuccess)
Notifications.dtboPatched()
}
return
}
when (action) {
REQUEST -> {
val i = Intent(context, ClassMap.get<Any>(SuRequestActivity::class.java))
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
context.startActivity(i)
}
LOG -> SuLogger.handleLogs(intent)
NOTIFY -> SuLogger.handleNotify(intent)
TEST -> Shell.su("magisk --use-broadcast").submit()
}
}
Intent.ACTION_PACKAGE_REPLACED ->
// This will only work pre-O
if (Config.get<Boolean>(Config.Key.SU_REAUTH)!!) {
mDB.deletePolicy(getPkg(intent))
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
mDB.deletePolicy(pkg)
Shell.su("magiskhide --rm $pkg").submit()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
}
Const.Key.BROADCAST_REBOOT -> RootUtils.reboot()
}
}
}

View File

@@ -4,6 +4,12 @@ import android.content.Context;
import android.net.Network;
import android.net.Uri;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,12 +17,6 @@ import androidx.annotation.RequiresApi;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public abstract class DelegateWorker {
private ListenableWorker worker;

View File

@@ -1,85 +0,0 @@
package com.topjohnwu.magisk.tasks;
import android.net.Uri;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.ZipUtils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public abstract class FlashZip {
private Uri mUri;
private File tmpFile;
private List<String> console, logs;
public FlashZip(Uri uri, List<String> out, List<String> err) {
mUri = uri;
console = out;
logs = err;
tmpFile = new File(App.self.getCacheDir(), "install.zip");
}
private boolean unzipAndCheck() throws IOException {
ZipUtils.unzip(tmpFile, tmpFile.getParentFile(), "META-INF/com/google/android", true);
return Shell.su("grep -q '#MAGISK' " + new File(tmpFile.getParentFile(), "updater-script"))
.exec().isSuccess();
}
private boolean flash() throws IOException {
console.add("- Copying zip to temp directory");
try (InputStream in = App.self.getContentResolver().openInputStream(mUri);
OutputStream out = new BufferedOutputStream(new FileOutputStream(tmpFile))) {
if (in == null) throw new FileNotFoundException();
InputStream buf= new BufferedInputStream(in);
ShellUtils.pump(buf, out);
} catch (FileNotFoundException e) {
console.add("! Invalid Uri");
throw e;
} catch (IOException e) {
console.add("! Cannot copy to cache");
throw e;
}
try {
if (!unzipAndCheck()) {
console.add("! This zip is not a Magisk Module!");
return false;
}
} catch (IOException e) {
console.add("! Unzip error");
throw e;
}
console.add("- Installing " + Utils.getNameFromUri(App.self, mUri));
return Shell.su("cd " + tmpFile.getParent(),
"BOOTMODE=true sh update-binary dummy 1 " + tmpFile)
.to(console, logs)
.exec().isSuccess();
}
public void exec() {
App.THREAD_POOL.execute(() -> {
boolean success = false;
try {
success = flash();
} catch (IOException ignored) {}
Shell.su("cd /", "rm -rf " + tmpFile.getParent() + " " + Const.TMP_FOLDER_PATH).submit();
boolean finalSuccess = success;
UiThreadHandler.run(() -> onResult(finalSuccess));
});
}
protected abstract void onResult(boolean success);
}

View File

@@ -0,0 +1,96 @@
package com.topjohnwu.magisk.tasks
import android.net.Uri
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.utils.fileName
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.readUri
import com.topjohnwu.magisk.utils.unzip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
abstract class FlashZip(
private val mUri: Uri,
private val console: MutableList<String>,
private val logs: MutableList<String>
) {
private val app: App by inject()
private val tmpFile: File = File(app.cacheDir, "install.zip")
@Throws(IOException::class)
private fun unzipAndCheck(): Boolean {
val parentFile = tmpFile.parentFile ?: return false
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
val updaterScript = File(parentFile, "updater-script")
return Shell
.su("grep -q '#MAGISK' $updaterScript")
.exec()
.isSuccess
}
@Throws(IOException::class)
private fun flash(): Boolean {
console.add("- Copying zip to temp directory")
runCatching {
app.readUri(mUri).use { input ->
tmpFile.outputStream().use { out -> input.copyTo(out) }
}
}.getOrElse {
when (it) {
is FileNotFoundException -> console.add("! Invalid Uri")
is IOException -> console.add("! Cannot copy to cache")
}
throw it
}
val isMagiskModule = runCatching {
unzipAndCheck()
}.getOrElse {
console.add("! Unzip error")
throw it
}
if (!isMagiskModule) {
console.add("! This zip is not a Magisk Module!")
return false
}
console.add("- Installing ${mUri.fileName}")
val parentFile = tmpFile.parent ?: return false
return Shell
.su(
"cd $parentFile",
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
)
.to(console, logs)
.exec().isSuccess
}
fun exec() = Single
.fromCallable {
runCatching {
flash()
}.getOrElse {
it.printStackTrace()
false
}.apply {
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}")
.submit()
}
}
.subscribeK(onError = { onResult(false) }) { onResult(it) }
.let { Unit } // ignores result disposable
protected abstract fun onResult(success: Boolean)
}

View File

@@ -34,7 +34,7 @@ import java.util.concurrent.Future;
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;
private App app = App.self;
private final App app = App.self;
private Set<String> cached;
private Queue<Pair<String, Date>> moduleQueue;
@@ -116,17 +116,17 @@ public class UpdateRepos {
Pair<String, Date> pair = moduleQueue.poll();
if (pair == null)
return;
Repo repo = app.repoDB.getRepo(pair.first);
Repo repo = app.getRepoDB().getRepo(pair.first);
try {
if (repo == null)
repo = new Repo(pair.first);
else
cached.remove(pair.first);
repo.update(pair.second);
app.repoDB.addRepo(repo);
app.getRepoDB().addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.repoDB.removeRepo(pair.first);
app.getRepoDB().removeRepo(pair.first);
}
}
});
@@ -134,7 +134,7 @@ public class UpdateRepos {
}
private void fullReload() {
Cursor c = app.repoDB.getRawCursor();
Cursor c = app.getRepoDB().getRawCursor();
runTasks(() -> {
while (true) {
Repo repo;
@@ -145,10 +145,10 @@ public class UpdateRepos {
}
try {
repo.update();
app.repoDB.addRepo(repo);
app.getRepoDB().addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.repoDB.removeRepo(repo);
app.getRepoDB().removeRepo(repo);
}
}
});
@@ -157,12 +157,12 @@ public class UpdateRepos {
public void exec(boolean force) {
Event.reset(Event.REPO_LOAD_DONE);
App.THREAD_POOL.execute(() -> {
cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet());
cached = Collections.synchronizedSet(app.getRepoDB().getRepoIDSet());
moduleQueue = new ConcurrentLinkedQueue<>();
if (loadPages()) {
// The leftover cached means they are removed from online repo
app.repoDB.removeRepo(cached);
app.getRepoDB().removeRepo(cached);
} else if (force) {
fullReload();
}

View File

@@ -1,194 +0,0 @@
package com.topjohnwu.magisk.ui;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import com.google.android.material.navigation.NavigationView;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment;
import com.topjohnwu.magisk.ui.home.MagiskFragment;
import com.topjohnwu.magisk.ui.log.LogFragment;
import com.topjohnwu.magisk.ui.module.ModulesFragment;
import com.topjohnwu.magisk.ui.module.ReposFragment;
import com.topjohnwu.magisk.ui.settings.SettingsFragment;
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import butterknife.BindView;
public class MainActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener {
private final Handler mDrawerHandler = new Handler();
private int mDrawerItem;
private static boolean fromShortcut = false;
@BindView(R.id.toolbar) public Toolbar toolbar;
@BindView(R.id.drawer_layout) DrawerLayout drawer;
@BindView(R.id.nav_view) NavigationView navigationView;
private float toolbarElevation;
@Override
public int getDarkTheme() {
return R.style.AppTheme_Dark;
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
if (!SplashActivity.DONE) {
startActivity(new Intent(this, ClassMap.get(SplashActivity.class)));
finish();
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MainActivity_ViewBinding(this);
checkHideSection();
setSupportActionBar(toolbar);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) {
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed tate
}
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
super.onDrawerSlide(drawerView, 0); // this disables the animation
}
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
toolbarElevation = toolbar.getElevation();
}
drawer.addDrawerListener(toggle);
toggle.syncState();
if (savedInstanceState == null) {
String section = getIntent().getStringExtra(Const.Key.OPEN_SECTION);
fromShortcut = section != null;
navigate(section);
}
navigationView.setNavigationItemSelectedListener(this);
}
@Override
public void onBackPressed() {
if (drawer.isDrawerOpen(navigationView)) {
drawer.closeDrawer(navigationView);
} else if (mDrawerItem != R.id.magisk && !fromShortcut) {
navigate(R.id.magisk);
} else {
finish();
}
}
@Override
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
mDrawerHandler.removeCallbacksAndMessages(null);
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
drawer.closeDrawer(navigationView);
return true;
}
public void checkHideSection() {
Menu menu = navigationView.getMenu();
menu.findItem(R.id.magiskhide).setVisible(Shell.rootAccess() &&
(boolean) Config.get(Config.Key.MAGISKHIDE));
menu.findItem(R.id.modules).setVisible(Shell.rootAccess() && Config.magiskVersionCode >= 0);
menu.findItem(R.id.downloads).setVisible(Networking.checkNetworkStatus(this)
&& Shell.rootAccess() && Config.magiskVersionCode >= 0);
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
menu.findItem(R.id.superuser).setVisible(Utils.showSuperUser());
}
public void navigate(String item) {
int itemId = R.id.magisk;
if (item != null) {
switch (item) {
case "superuser":
itemId = R.id.superuser;
break;
case "modules":
itemId = R.id.modules;
break;
case "downloads":
itemId = R.id.downloads;
break;
case "magiskhide":
itemId = R.id.magiskhide;
break;
case "log":
itemId = R.id.log;
break;
case "settings":
itemId = R.id.settings;
break;
}
}
navigate(itemId);
}
public void navigate(int itemId) {
mDrawerItem = itemId;
navigationView.setCheckedItem(itemId);
switch (itemId) {
case R.id.magisk:
fromShortcut = false;
displayFragment(new MagiskFragment(), true);
break;
case R.id.superuser:
displayFragment(new SuperuserFragment(), true);
break;
case R.id.modules:
displayFragment(new ModulesFragment(), true);
break;
case R.id.downloads:
displayFragment(new ReposFragment(), true);
break;
case R.id.magiskhide:
displayFragment(new MagiskHideFragment(), true);
break;
case R.id.log:
displayFragment(new LogFragment(), false);
break;
case R.id.settings:
displayFragment(new SettingsFragment(), true);
break;
}
}
private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) {
supportInvalidateOptionsMenu();
getSupportFragmentManager()
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.replace(R.id.content_frame, navFragment)
.commitNow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
toolbar.setElevation(setElevation ? toolbarElevation : 0);
}
}
}

View File

@@ -0,0 +1,118 @@
package com.topjohnwu.magisk.ui
import android.content.Intent
import android.os.Bundle
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment
import com.topjohnwu.magisk.ui.module.ModulesFragment
import com.topjohnwu.magisk.ui.module.ReposFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.reflect.KClass
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
override val layoutRes: Int = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()
override val navHostId: Int = R.id.main_nav_host
override val defaultPosition: Int = 0
override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class,
SuperuserFragment::class,
MagiskHideFragment::class,
ModulesFragment::class,
ReposFragment::class,
LogFragment::class,
SettingsFragment::class
)
/*override fun getDarkTheme(): Int {
return R.style.AppTheme_Dark
}*/
override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) {
startActivity(Intent(this, ClassMap.get<Any>(SplashActivity::class.java)))
finish()
}
super.onCreate(savedInstanceState)
checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar)
if (savedInstanceState == null) {
intent.getStringExtra(OPEN_SECTION)?.let {
onEventDispatched(Navigation.fromSection(it))
}
}
}
override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title
}
override fun setTitle(titleId: Int) {
supportActionBar?.setTitle(titleId)
}
override fun onTabTransaction(fragment: Fragment?, index: Int) {
val fragmentId = when (fragment) {
is HomeFragment -> R.id.magiskFragment
is SuperuserFragment -> R.id.superuserFragment
is MagiskHideFragment -> R.id.magiskHideFragment
is ModulesFragment -> R.id.modulesFragment
is ReposFragment -> R.id.reposFragment
is LogFragment -> R.id.logFragment
is SettingsFragment -> R.id.settings
else -> return
}
binding.navView.setCheckedItem(fragmentId)
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView)
} else {
super.onBackPressed()
}
}
override fun onSimpleEventDispatched(event: Int) {
super.onSimpleEventDispatched(event)
when (event) {
Navigation.Main.OPEN_NAV -> openNav()
}
}
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible =
Shell.rootAccess() && Config.get<Any>(Config.Key.MAGISKHIDE) as Boolean
menu.findItem(R.id.modulesFragment).isVisible =
Shell.rootAccess() && Config.magiskVersionCode >= 0
menu.findItem(R.id.reposFragment).isVisible =
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Config.magiskVersionCode >= 0)
menu.findItem(R.id.logFragment).isVisible =
Shell.rootAccess()
menu.findItem(R.id.superuserFragment).isVisible =
Utils.showSuperUser()
}
}

View File

@@ -0,0 +1,27 @@
package com.topjohnwu.magisk.ui
import android.view.MenuItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.MagiskViewModel
class MainViewModel : MagiskViewModel() {
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
fun navigationItemPressed(item: MenuItem): Boolean {
when (item.itemId) {
R.id.magiskFragment -> Navigation.home()
R.id.superuserFragment -> Navigation.superuser()
R.id.magiskHideFragment -> Navigation.hide()
R.id.modulesFragment -> Navigation.modules()
R.id.reposFragment -> Navigation.repos()
R.id.logFragment -> Navigation.log()
R.id.settings -> Navigation.settings()
else -> null
}?.publish()?.let { return@navigationItemPressed true }
return false
}
}

View File

@@ -1,97 +0,0 @@
package com.topjohnwu.magisk.ui;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.appcompat.app.AlertDialog;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.tasks.CheckUpdates;
import com.topjohnwu.magisk.tasks.UpdateRepos;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.Shortcuts;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
public class SplashActivity extends BaseActivity {
public static boolean DONE = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Shell.getShell(shell -> {
if (Config.magiskVersionCode > 0 &&
Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
new AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(R.string.unsupport_magisk_message)
.setNegativeButton(R.string.ok, null)
.setOnDismissListener(dialog -> finish())
.show();
} else {
initAndStart();
}
});
}
private void initAndStart() {
String pkg = Config.get(Config.Key.SU_MANAGER);
if (pkg != null && getPackageName().equals(BuildConfig.APPLICATION_ID)) {
Config.remove(Config.Key.SU_MANAGER);
Shell.su("pm uninstall " + pkg).submit();
}
if (TextUtils.equals(pkg, getPackageName())) {
try {
// We are the manager, remove com.topjohnwu.magisk as it could be malware
getPackageManager().getApplicationInfo(BuildConfig.APPLICATION_ID, 0);
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit();
} catch (PackageManager.NameNotFoundException ignored) {}
}
// Dynamic detect all locales
LocaleManager.loadAvailableLocales(R.string.app_changelog);
// Set default configs
Config.initialize();
// Create notification channel on Android O
Notifications.setup(this);
// Schedule periodic update checks
Utils.scheduleUpdateCheck();
CheckUpdates.check();
// Setup shortcuts
Shortcuts.setup(this);
// Create repo database
app.repoDB = new RepoDatabaseHelper(this);
// Magisk working as expected
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
// Load modules
Utils.loadModules(false);
// Load repos
if (Networking.checkNetworkStatus(this))
new UpdateRepos().exec();
}
Intent intent = new Intent(this, ClassMap.get(MainActivity.class));
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION));
DONE = true;
startActivity(intent);
finish();
}
}

View File

@@ -0,0 +1,87 @@
package com.topjohnwu.magisk.ui
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.tasks.CheckUpdates
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
open class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Shell.getShell {
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(R.string.unsupport_magisk_message)
.setNegativeButton(R.string.ok, null)
.setOnDismissListener { finish() }
.show()
} else {
initAndStart()
}
}
}
private fun initAndStart() {
val pkg = Config.get<String>(Config.Key.SU_MANAGER)
if (pkg != null && packageName == BuildConfig.APPLICATION_ID) {
Config.remove(Config.Key.SU_MANAGER)
Shell.su("pm uninstall $pkg").submit()
}
if (TextUtils.equals(pkg, packageName)) {
runCatching {
// We are the manager, remove com.topjohnwu.magisk as it could be malware
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit()
}
}
// Dynamic detect all locales
LocaleManager.loadAvailableLocales(R.string.app_changelog)
// Set default configs
Config.initialize()
// Create notification channel on Android O
Notifications.setup(this)
// Schedule periodic update checks
Utils.scheduleUpdateCheck()
CheckUpdates.check()
// Setup shortcuts
Shortcuts.setup(this)
// Magisk working as expected
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
// Load modules
Utils.loadModules(false)
// Load repos
if (Networking.checkNetworkStatus(this))
UpdateRepos().exec()
}
val intent = Intent(this, ClassMap.get<Any>(MainActivity::class.java))
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
DONE = true
startActivity(intent)
finish()
}
companion object {
var DONE = false
}
}

View File

@@ -0,0 +1,7 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface ActivityResultListener {
fun onActivityResult(resultCode: Int, data: Intent?)
}

View File

@@ -1,159 +0,0 @@
package com.topjohnwu.magisk.ui.base;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.collection.SparseArrayCompat;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.LocaleManager;
public abstract class BaseActivity extends AppCompatActivity implements Event.AutoListener {
static int[] EMPTY_INT_ARRAY = new int[0];
private SparseArrayCompat<ActivityResultListener> resultListeners = new SparseArrayCompat<>();
public App app = App.self;
@Override
public int[] getListeningEvents() {
return EMPTY_INT_ARRAY;
}
@Override
public void onEvent(int event) {}
@StyleRes
public int getDarkTheme() {
return -1;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale));
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Event.register(this);
if (getDarkTheme() != -1 && (boolean) Config.get(Config.Key.DARK_THEME)) {
setTheme(getDarkTheme());
}
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
Event.unregister(this);
super.onDestroy();
}
protected void setFloating() {
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
if (isTablet) {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
params.alpha = 1.0f;
params.dimAmount = 0.6f;
params.flags |= 2;
getWindow().setAttributes(params);
setFinishOnTouchOutside(true);
}
}
protected void lockOrientation() {
if (Build.VERSION.SDK_INT < 18)
setRequestedOrientation(getResources().getConfiguration().orientation);
else
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
}
public void runWithExternalRW(Runnable callback) {
runWithPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, callback);
}
public void runWithPermissions(String[] permissions, Runnable callback) {
runWithPermissions(this, permissions, callback);
}
public static void runWithPermissions(Context context, String[] permissions, Runnable callback) {
boolean granted = true;
for (String perm : permissions) {
if (ContextCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED)
granted = false;
}
if (granted) {
Const.EXTERNAL_PATH.mkdirs();
callback.run();
} else {
// Passed in context should be an activity if not granted, need to show dialog!
if (context instanceof BaseActivity) {
BaseActivity activity = (BaseActivity) context;
int code = callback.hashCode() & 0xFFFF;
activity.resultListeners.put(code, ((i, d) -> callback.run()));
ActivityCompat.requestPermissions(activity, permissions, code);
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
onActivityResultListener(requestCode, resultCode, data);
}
private void onActivityResultListener(int requestCode, int resultCode, Intent data) {
ActivityResultListener listener = resultListeners.get(requestCode);
if (listener != null) {
resultListeners.remove(requestCode);
listener.onActivityResult(resultCode, data);
}
}
public void startActivityForResult(Intent intent, int requestCode, ActivityResultListener listener) {
resultListeners.put(requestCode, listener);
super.startActivityForResult(intent, requestCode);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
boolean grant = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED)
grant = false;
}
if (grant)
onActivityResultListener(requestCode, 0, null);
else
resultListeners.remove(requestCode);
}
public interface ActivityResultListener {
void onActivityResult(int resultCode, Intent data);
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (TextUtils.equals(name, getPackageName() + "_preferences"))
return app.prefs;
return super.getSharedPreferences(name, mode);
}
}

View File

@@ -1,58 +0,0 @@
package com.topjohnwu.magisk.ui.base;
import android.content.Intent;
import androidx.fragment.app.Fragment;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.utils.Event;
import butterknife.Unbinder;
public abstract class BaseFragment extends Fragment implements Event.AutoListener {
public App app = App.self;
protected Unbinder unbinder = null;
@Override
public void onResume() {
super.onResume();
Event.register(this);
}
@Override
public void onPause() {
Event.unregister(this);
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (unbinder != null)
unbinder.unbind();
}
@Override
public void startActivityForResult(Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, (resultCode, data) ->
onActivityResult(requestCode, resultCode, data));
}
public void startActivityForResult(Intent intent, int requestCode,
BaseActivity.ActivityResultListener listener) {
((BaseActivity) requireActivity()).startActivityForResult(intent, requestCode, listener);
}
protected void runWithExternalRW(Runnable callback) {
((BaseActivity) requireActivity()).runWithExternalRW(callback);
}
@Override
public int[] getListeningEvents() {
return BaseActivity.EMPTY_INT_ARRAY;
}
@Override
public void onEvent(int event) {}
}

View File

@@ -8,6 +8,10 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
@@ -16,10 +20,6 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
@@ -28,21 +28,21 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
app.prefs.registerOnSharedPreferenceChangeListener(this);
app.getPrefs().registerOnSharedPreferenceChangeListener(this);
Event.register(this);
return v;
}
@Override
public void onDestroyView() {
app.prefs.unregisterOnSharedPreferenceChangeListener(this);
app.getPrefs().unregisterOnSharedPreferenceChangeListener(this);
Event.unregister(this);
super.onDestroyView();
}
@Override
public int[] getListeningEvents() {
return BaseActivity.EMPTY_INT_ARRAY;
return new int[0];
}
@Override

View File

@@ -0,0 +1,11 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface IBaseLeanback {
fun runWithExternalRW(callback: Runnable)
fun runWithPermissions(vararg permissions: String, callback: Runnable)
fun startActivityForResult(intent: Intent, requestCode: Int, listener: ActivityResultListener)
}

View File

@@ -0,0 +1,210 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.Utils
import timber.log.Timber
import kotlin.reflect.KClass
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
MagiskLeanbackActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
override val numberOfRootFragments: Int get() = baseFragments.size
override val baseFragments: List<KClass<out Fragment>> = listOf()
protected open val defaultPosition: Int = 0
protected val navigationController get() = if (navHostId == 0) null else _navigationController
private val _navigationController by lazy {
if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?")
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
init {
val isDarkTheme = Config.get<Boolean>(Config.Key.DARK_THEME)
val theme = if (isDarkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(LocaleManager.locale)
super.applyOverrideConfiguration(config)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigationController?.apply {
rootFragmentListener = this@MagiskActivity
transactionListener = this@MagiskActivity
initialize(defaultPosition, savedInstanceState)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController?.onSaveInstanceState(outState)
}
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
navigationController?.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
navigationController?.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController?.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController?.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController?.switchTab(index)
}
}
override fun onBackPressed() {
val fragment = navigationController?.currentFrag as? MagiskFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try {
navigationController?.popFragment() ?: throw UnsupportedOperationException()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController?.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
}
}
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) = Unit
override fun onTabTransaction(fragment: Fragment?, index: Int) = Unit
fun openUrl(url: String) = Utils.openLink(this, url.toUri())
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) =
if (report?.areAllPermissionsGranted() == true) {
request.onSuccess()
} else {
request.onFailure()
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = request.onShowRationale(permissions.orEmpty().map { it.name })
})
.check()
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
}

View File

@@ -0,0 +1,52 @@
package com.topjohnwu.magisk.ui.base
import androidx.annotation.CallSuper
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.skoumal.teanity.view.TeanityFragment
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import kotlin.reflect.KClass
abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityFragment<ViewModel, Binding>(), Navigator {
protected val magiskActivity get() = activity as MagiskActivity<*, *>
// We don't need nested fragments
override val baseFragments: List<KClass<Fragment>> = listOf()
override fun navigateTo(event: MagiskNavigationEvent) = magiskActivity.navigateTo(event)
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> magiskActivity.onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(requireActivity())
is PermissionEvent -> magiskActivity.withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
magiskActivity.withPermissions(*permissions, builder = builder)
}
fun openLink(url: String) = magiskActivity.openUrl(url)
open fun onBackPressed(): Boolean = false
}

View File

@@ -0,0 +1,64 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Intent
import androidx.collection.SparseArrayCompat
import androidx.databinding.ViewDataBinding
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.skoumal.teanity.view.TeanityActivity
import com.topjohnwu.magisk.Const
abstract class MagiskLeanbackActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityActivity<ViewModel, Binding>(), IBaseLeanback {
private val resultListeners = SparseArrayCompat<ActivityResultListener>()
@Deprecated("Permissions will be checked in a different streamlined way")
fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() })
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithExternalRW(callback: Runnable) {
runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback)
}
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithPermissions(vararg permissions: String, callback: Runnable) {
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
if (report?.areAllPermissionsGranted() == true) {
Const.EXTERNAL_PATH.mkdirs()
callback.run()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = Unit
})
.check()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultListeners.get(requestCode)?.apply {
resultListeners.remove(requestCode)
onActivityResult(resultCode, data)
}
}
override fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: ActivityResultListener
) {
resultListeners.put(requestCode, listener)
startActivityForResult(intent, requestCode)
}
}

View File

@@ -0,0 +1,31 @@
package com.topjohnwu.magisk.ui.base
import android.app.Activity
import com.skoumal.teanity.extensions.doOnSubscribeUi
import com.skoumal.teanity.viewmodel.LoadingViewModel
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.Event
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener {
override fun onEvent(event: Int) = Timber.i("Event of $event was not handled")
override fun getListeningEvents(): IntArray = intArrayOf()
fun withView(action: Activity.() -> Unit) {
ViewActionEvent(action).publish()
}
fun withPermissions(vararg permissions: String): Observable<Boolean> {
val subject = PublishSubject.create<Boolean>()
return subject.doOnSubscribeUi { PermissionEvent(permissions.toList(), subject).publish() }
}
fun back() = BackPressEvent().publish()
}

View File

@@ -1,274 +0,0 @@
package com.topjohnwu.magisk.ui.flash;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.StringListAdapter;
import com.topjohnwu.magisk.tasks.FlashZip;
import com.topjohnwu.magisk.tasks.MagiskInstaller;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.OnClick;
public class FlashActivity extends BaseActivity {
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.button_panel) LinearLayout buttonPanel;
@BindView(R.id.reboot) Button reboot;
@BindView(R.id.recyclerView) RecyclerView rv;
@BindColor(android.R.color.white) int white;
private List<String> console, logs;
@OnClick(R.id.reboot)
void reboot() {
RootUtils.reboot();
}
@OnClick(R.id.save_logs)
void saveLogs() {
runWithExternalRW(() -> {
Calendar now = Calendar.getInstance();
String filename = String.format(Locale.US,
"magisk_install_log_%04d%02d%02d_%02d%02d%02d.log",
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
File logFile = new File(Const.EXTERNAL_PATH, filename);
try (FileWriter writer = new FileWriter(logFile)) {
for (String s : logs) {
writer.write(s);
writer.write('\n');
}
} catch (IOException e) {
e.printStackTrace();
return;
}
Utils.toast(logFile.getPath(), Toast.LENGTH_LONG);
});
}
@OnClick(R.id.close)
public void close() {
finish();
}
@Override
public void onBackPressed() {
// Prevent user accidentally press back button
}
@Override
public int getDarkTheme() {
return R.style.AppTheme_NoDrawer_Dark;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash);
new FlashActivity_ViewBinding(this);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setTitle(R.string.flashing);
}
setFloating();
setFinishOnTouchOutside(false);
if (!Shell.rootAccess())
reboot.setVisibility(View.GONE);
logs = Collections.synchronizedList(new ArrayList<>());
console = new ConsoleList();
rv.setAdapter(new ConsoleAdapter());
Intent intent = getIntent();
Uri uri = intent.getData();
switch (intent.getStringExtra(Const.Key.FLASH_ACTION)) {
case Const.Value.FLASH_ZIP:
new FlashModule(uri).exec();
break;
case Const.Value.UNINSTALL:
new Uninstall(uri).exec();
break;
case Const.Value.FLASH_MAGISK:
new DirectInstall().exec();
break;
case Const.Value.FLASH_INACTIVE_SLOT:
new SecondSlot().exec();
break;
case Const.Value.PATCH_FILE:
new PatchFile(uri).exec();
break;
}
}
private class ConsoleAdapter extends StringListAdapter<ConsoleAdapter.ViewHolder> {
ConsoleAdapter() {
super(console, true);
}
@Override
protected int itemLayoutRes() {
return R.layout.list_item_console;
}
@NonNull
@Override
public ViewHolder createViewHolder(@NonNull View v) {
return new ViewHolder(v);
}
class ViewHolder extends StringListAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
txt.setTextColor(white);
}
@Override
protected int textViewResId() {
return R.id.txt;
}
}
}
private class ConsoleList extends CallbackList<String> {
ConsoleList() {
super(new ArrayList<>());
}
private void updateUI() {
rv.getAdapter().notifyItemChanged(size() - 1);
rv.postDelayed(() -> rv.smoothScrollToPosition(size() - 1), 10);
}
@Override
public void onAddElement(String s) {
logs.add(s);
updateUI();
}
@Override
public String set(int i, String s) {
String ret = super.set(i, s);
UiThreadHandler.run(this::updateUI);
return ret;
}
}
private class FlashModule extends FlashZip {
FlashModule(Uri uri) {
super(uri, console, logs);
}
@Override
protected void onResult(boolean success) {
if (success) {
Utils.loadModules();
} else {
console.add("! Installation failed");
reboot.setVisibility(View.GONE);
}
buttonPanel.setVisibility(View.VISIBLE);
}
}
private class Uninstall extends FlashModule {
Uninstall(Uri uri) {
super(uri);
}
@Override
protected void onResult(boolean success) {
if (success)
UiThreadHandler.handler.postDelayed(Shell.su("pm uninstall " + getPackageName())::exec, 3000);
else
super.onResult(false);
}
}
private abstract class BaseInstaller extends MagiskInstaller {
BaseInstaller() {
super(console, logs);
}
@Override
protected void onResult(boolean success) {
if (success) {
console.add("- All done!");
} else {
Shell.sh("rm -rf " + installDir).submit();
console.add("! Installation failed");
reboot.setVisibility(View.GONE);
}
buttonPanel.setVisibility(View.VISIBLE);
}
}
private class DirectInstall extends BaseInstaller {
@Override
protected boolean operations() {
return findImage() && extractZip() && patchBoot() && flashBoot();
}
}
private class SecondSlot extends BaseInstaller {
@Override
protected boolean operations() {
return findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA();
}
}
private class PatchFile extends BaseInstaller {
private Uri uri;
PatchFile(Uri u) {
uri = u;
}
@Override
protected boolean operations() {
return extractZip() && handleFile(uri) && patchBoot() && storeBoot();
}
}
}

View File

@@ -0,0 +1,24 @@
package com.topjohnwu.magisk.ui.flash
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.ui.base.MagiskActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash
override val viewModel: FlashViewModel by viewModel {
val uri = intent.data
val action = intent.getStringExtra(Const.Key.FLASH_ACTION) ?: let { finish();"" }
parametersOf(action, uri)
}
override fun onBackPressed() {
if (viewModel.loading) return
super.onBackPressed()
}
}

View File

@@ -0,0 +1,110 @@
package com.topjohnwu.magisk.ui.flash
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.res.Resources
import android.net.Uri
import android.os.Handler
import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.flash.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
import java.util.*
class FlashViewModel(
action: String,
uri: Uri?,
private val resources: Resources
) : MagiskViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false)
val behaviorText = KObservableField(resources.getString(R.string.flashing))
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@FlashViewModel)
}
private val outItems = ObservableArrayList<String>()
private val logItems = Collections.synchronizedList(mutableListOf<String>())
init {
outItems.sendUpdatesTo(items) { it.map { ConsoleRvItem(it) } }
outItems.copyNewInputInto(logItems)
state = State.LOADING
val uri = uri ?: Uri.EMPTY
when (action) {
Const.Value.FLASH_ZIP -> Flashing
.Install(uri, outItems, logItems, this)
.exec()
Const.Value.UNINSTALL -> Flashing
.Uninstall(uri, outItems, logItems, this)
.exec()
Const.Value.FLASH_MAGISK -> Patching
.Direct(outItems, logItems, this)
.exec()
Const.Value.FLASH_INACTIVE_SLOT -> Patching
.SecondSlot(outItems, logItems, this)
.exec()
Const.Value.PATCH_FILE -> Patching
.File(uri, outItems, logItems, this)
.exec()
}
}
override fun onResult(isSuccess: Boolean) {
state = if (isSuccess) State.LOADED else State.LOADING_FAILED
behaviorText.value = when {
isSuccess -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure)
}
if (isSuccess) {
Handler().postDelayed(500) {
showRestartTitle.value = true
}
}
}
fun savePressed() = withPermissions(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
.map { now }
.map { it.toTime(timeFormatStandard) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Const.EXTERNAL_PATH, it) }
.map { file ->
file.bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
file.path
}
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = RootUtils.reboot()
fun backPressed() = back()
}

View File

@@ -0,0 +1,128 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel(
private val packageManager: PackageManager,
rxBus: RxBus
) : MagiskViewModel() {
val query = KObservableField("")
val isShowSystem = KObservableField(false)
private val allItems = mutableListOf<ComparableRvItem<*>>()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@HideViewModel)
}
init {
rxBus.register<HideProcessEvent>()
.subscribeK { toggleItem(it.item) }
.add()
isShowSystem.addOnPropertyChangedCallback { query() }
query.addOnPropertyChangedCallback { query() }
refresh()
}
fun refresh() {
// fetching this for every item is nonsensical, so we add .cache() so the response is all
// the same for every single mapped item, it only actually executes the whole thing the
// first time around.
val hideTargets = Shell.su("magiskhide --ls").toSingle()
.map { it.exec().out }
.flattenAsFlowable { it }
.map { HideTarget(it) }
.toList()
.cache()
Single.fromCallable { packageManager.getInstalledApplications(0) }
.flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map {
val label = Utils.getAppLabel(it, packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}
.filter { it.processes.isNotEmpty() }
.map { HideRvItem(it, hideTargets.blockingGet()) }
.toList()
.map { it.sortWith(compareBy(
{it.isHiddenState.value}, {it.item.name}, {it.packageName})); it }
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK(onError = Timber::e) { items.update(it.first, it.second) }
.add()
}
private fun query() = queryRaw()
.subscribeK { items.update(it.first, it.second) }
.add()
private fun queryRaw(
showSystem: Boolean = isShowSystem.value,
query: String = this.query.value
) = allItems.toSingle()
.map { it.filterIsInstance<HideRvItem>() }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.processes.any { it.contains(query, ignoreCase = true) }
}
.filter {
showSystem || (it.isHiddenState.value != IndeterminateState.UNCHECKED) ||
(it.item.info.flags and ApplicationInfo.FLAG_SYSTEM == 0)
}
.toList()
.map { it to items.calculateDiff(it) }
private fun toggleItem(item: HideProcessRvItem) {
val state = if (item.isHidden.value) "add" else "rm"
"magiskhide --%s %s %s".format(state, item.packageName, item.process)
.let { Shell.su(it) }
.toSingle()
.map { it.submit() }
.subscribeK()
}
companion object {
private val blacklist = listOf(
App.self.packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
)
}
}

View File

@@ -1,101 +0,0 @@
package com.topjohnwu.magisk.ui.hide;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.ApplicationAdapter;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import com.topjohnwu.magisk.utils.Event;
import butterknife.BindView;
public class MagiskHideFragment extends BaseFragment {
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
private SearchView search;
private ApplicationAdapter adapter;
private SearchView.OnQueryTextListener searchListener;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false);
unbinder = new MagiskHideFragment_ViewBinding(this, view);
adapter = new ApplicationAdapter(requireActivity());
recyclerView.setAdapter(adapter);
mSwipeRefreshLayout.setRefreshing(true);
mSwipeRefreshLayout.setOnRefreshListener(adapter::refresh);
searchListener = new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
adapter.filter(query);
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
adapter.filter(newText);
return false;
}
};
requireActivity().setTitle(R.string.magiskhide);
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_magiskhide, menu);
search = (SearchView) menu.findItem(R.id.app_search).getActionView();
search.setOnQueryTextListener(searchListener);
menu.findItem(R.id.show_system).setChecked(Config.get(Config.Key.SHOW_SYSTEM_APP));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.show_system) {
boolean showSystem = !item.isChecked();
item.setChecked(showSystem);
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem);
adapter.setShowSystem(showSystem);
adapter.filter(search.getQuery().toString());
}
return true;
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.MAGISK_HIDE_DONE};
}
@Override
public void onEvent(int event) {
mSwipeRefreshLayout.setRefreshing(false);
adapter.filter(search.getQuery().toString());
}
}

View File

@@ -0,0 +1,64 @@
package com.topjohnwu.magisk.ui.hide
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_magisk_hide
override val viewModel: HideViewModel by viewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magiskhide)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_magiskhide, menu)
menu.apply {
(findItem(R.id.app_search).actionView as? SearchView)
?.setOnQueryTextListener(this@MagiskHideFragment)
val showSystem = Config.get<Boolean>(Config.Key.SHOW_SYSTEM_APP)
findItem(R.id.show_system).isChecked = showSystem
viewModel.isShowSystem.value = showSystem
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.show_system) {
val showSystem = !item.isChecked
item.isChecked = showSystem
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem)
viewModel.isShowSystem.value = showSystem
//adapter!!.setShowSystem(showSystem)
//adapter!!.filter(search!!.query.toString())
}
return true
}
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
override fun onQueryTextChange(query: String?): Boolean {
viewModel.query.value = query.orEmpty()
return false
}
/*override fun onEvent(event: Int) {
//mSwipeRefreshLayout!!.isRefreshing = false
adapter!!.filter(search!!.query.toString())
}*/
}

View File

@@ -0,0 +1,109 @@
package com.topjohnwu.magisk.ui.home
import android.app.Activity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.utils.ISafetyNetHelper
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import dalvik.system.DexClassLoader
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
ISafetyNetHelper.Callback {
override val layoutRes: Int = R.layout.fragment_magisk
override val viewModel: HomeViewModel by viewModel()
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenLinkEvent -> openLink(event.url)
is ManagerInstallEvent -> installManager()
is MagiskInstallEvent -> installMagisk()
is UninstallEvent -> uninstall()
is ManagerChangelogEvent -> changelogManager()
is EnvFixEvent -> fixEnv()
is UpdateSafetyNetEvent -> updateSafetyNet(false)
}
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.magisk)
}
private fun installMagisk() {
// Show Manager update first
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
installManager()
return
}
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show()
}
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
private fun uninstall() = UninstallDialog(requireActivity()).show()
private fun fixEnv() = EnvFixDialog(requireActivity()).show()
private fun changelogManager() = MarkDownWindow
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = Networking
.get(Const.Url.SNET_URL)
.getAsFile(EXT_APK) { updateSafetyNet(true) }
if (!requiresUserInput) {
download()
return
}
CustomAlertDialog(requireActivity())
.setTitle(R.string.proprietary_title)
.setMessage(R.string.proprietary_notice)
.setCancelable(false)
.setPositiveButton(R.string.yes) { _, _ -> download() }
.setNegativeButton(R.string.no_thanks) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
.show()
}
private fun updateSafetyNet(dieOnError: Boolean) {
try {
val loader = DexClassLoader(EXT_APK.path, EXT_APK.parent, null,
ISafetyNetHelper::class.java.classLoader)
val clazz = loader.loadClass("com.topjohnwu.snet.Snet")
val helper = clazz.getMethod("newHelper",
Class::class.java, String::class.java, Activity::class.java, Any::class.java)
.invoke(null, ISafetyNetHelper::class.java, EXT_APK.path,
requireActivity(), this) as ISafetyNetHelper
if (helper.version < Const.SNET_EXT_VER)
throw Exception()
helper.attest()
} catch (e: Exception) {
if (dieOnError) {
viewModel.finishSafetyNetCheck(-1)
return
}
Shell.sh("rm -rf " + EXT_APK.parent).exec()
EXT_APK.parentFile?.mkdir()
downloadSafetyNet(!dieOnError)
}
}
companion object {
val EXT_APK = File("${App.self.filesDir.parent}/snet", "snet.apk")
}
}

View File

@@ -0,0 +1,229 @@
package com.topjohnwu.magisk.ui.home
import android.content.Context
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.tasks.CheckUpdates
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
class HomeViewModel(
private val context: Context
) : MagiskViewModel() {
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Config.keepEnc)
val isKeepVerity = KObservableField(Config.keepVerity)
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
when (magiskState.value) {
MagiskState.NO_ROOT -> TODO()
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.magisk_update_title.res()
}
}
val magiskCurrentVersion = KObservableField("")
val magiskLatestVersion = KObservableField("")
val magiskAdditionalInfo = Observer(magiskState) {
if (Config.get<Boolean>(Config.Key.COREONLY))
R.string.core_only_enabled.res()
else
""
}
val managerState = KObservableField(MagiskState.LOADING)
val managerStateText = Observer(managerState) {
when (managerState.value) {
MagiskState.NO_ROOT -> "wtf"
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
MagiskState.OBSOLETE -> R.string.manager_update_title.res()
}
}
val managerCurrentVersion = KObservableField("")
val managerLatestVersion = KObservableField("")
val managerAdditionalInfo = Observer(managerState) {
if (packageName != BuildConfig.APPLICATION_ID)
"($packageName)"
else
""
}
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text)
val ctsState = KObservableField(SafetyNetState.IDLE)
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
val safetyNetState = Observer(ctsState, basicIntegrityState) {
val cts = ctsState.value
val basic = basicIntegrityState.value
val states = listOf(cts, basic)
when {
states.any { it == SafetyNetState.LOADING } -> State.LOADING
states.any { it == SafetyNetState.IDLE } -> State.LOADING
else -> State.LOADED
}
}
val hasRoot = KObservableField(false)
private var shownDialog = false
init {
Event.register(this)
isForceEncryption.addOnPropertyChangedCallback {
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
}
isKeepVerity.addOnPropertyChangedCallback {
Config.keepVerity = it ?: return@addOnPropertyChangedCallback
}
refresh()
}
override fun onEvent(event: Int) {
updateSelf()
ensureEnv()
}
override fun getListeningEvents(): IntArray = intArrayOf(Event.UPDATE_CHECK_DONE)
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
fun githubPressed() = OpenLinkEvent(Const.Url.SOURCE_CODE_URL).publish()
fun xdaPressed() = OpenLinkEvent(Const.Url.XDA_THREAD).publish()
fun uninstallPressed() = UninstallEvent().publish()
fun advancedPressed() = isAdvancedExpanded.toggle()
fun installPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerInstallEvent().publish()
MagiskItem.MAGISK -> MagiskInstallEvent().publish()
}
fun cardPressed(item: MagiskItem) = when (item) {
MagiskItem.MANAGER -> ManagerChangelogEvent().publish()
MagiskItem.MAGISK -> MagiskChangelogEvent().publish()
}
fun safetyNetPressed() {
ctsState.value = SafetyNetState.LOADING
basicIntegrityState.value = SafetyNetState.LOADING
safetyNetTitle.value = R.string.checking_safetyNet_status
UpdateSafetyNetEvent().publish()
}
fun finishSafetyNetCheck(response: Int) = when {
response and 0x0F == 0 -> {
val hasCtsPassed = response and ISafetyNetHelper.CTS_PASS != 0
val hasBasicIntegrityPassed = response and ISafetyNetHelper.BASIC_PASS != 0
safetyNetTitle.value = R.string.safetyNet_check_success
ctsState.value = if (hasCtsPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
basicIntegrityState.value = if (hasBasicIntegrityPassed) {
SafetyNetState.PASS
} else {
SafetyNetState.FAILED
}
}
response == -2 -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
}
else -> {
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = when (response) {
ISafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
else -> R.string.safetyNet_api_error
}
}
}
fun refresh() {
state = State.LOADING
magiskState.value = MagiskState.LOADING
managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text
Event.reset(this)
Config.remoteMagiskVersionString = null
Config.remoteMagiskVersionCode = -1
hasRoot.value = Shell.rootAccess()
if (Networking.checkNetworkStatus(context)) {
CheckUpdates.check()
} else {
state = State.LOADING_FAILED
}
}
private fun updateSelf() {
state = State.LOADED
magiskState.value = when (Config.magiskVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
!in Config.remoteMagiskVersionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
version.format(Config.magiskVersionString, Config.magiskVersionCode)
} else {
""
}
magiskLatestVersion.value = version
.format(Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)
managerState.value = when (Config.remoteManagerVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
managerCurrentVersion.value = version
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
managerLatestVersion.value = version
.format(Config.remoteManagerVersionString, Config.remoteManagerVersionCode)
}
private fun ensureEnv() {
val invalidStates =
listOf(MagiskState.NOT_INSTALLED, MagiskState.NO_ROOT, MagiskState.LOADING)
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (invalidStates.any { it == magiskState.value } || shownDialog) return
if (!Shell.su("env_check").exec().isSuccess) {
shownDialog = true
EnvFixEvent().publish()
}
}
companion object {
private const val version = "%s (%d)"
}
}

View File

@@ -1,333 +0,0 @@
package com.topjohnwu.magisk.ui.home;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.ChangeBounds;
import androidx.transition.Fade;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.tasks.CheckUpdates;
import com.topjohnwu.magisk.ui.MainActivity;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.ArrowExpandable;
import com.topjohnwu.magisk.view.Expandable;
import com.topjohnwu.magisk.view.ExpandableViewHolder;
import com.topjohnwu.magisk.view.MarkDownWindow;
import com.topjohnwu.magisk.view.SafetyNet;
import com.topjohnwu.magisk.view.UpdateCardHolder;
import com.topjohnwu.magisk.view.dialogs.EnvFixDialog;
import com.topjohnwu.magisk.view.dialogs.MagiskInstallDialog;
import com.topjohnwu.magisk.view.dialogs.ManagerInstallDialog;
import com.topjohnwu.magisk.view.dialogs.UninstallDialog;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import java.util.Locale;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.OnClick;
public class MagiskFragment extends BaseFragment implements SwipeRefreshLayout.OnRefreshListener {
private static boolean shownDialog = false;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.linearLayout) LinearLayout root;
@BindView(R.id.install_option_card) CardView installOptionCard;
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
@BindView(R.id.install_option_expand) ViewGroup optionExpandLayout;
@BindView(R.id.arrow) ImageView arrow;
@BindView(R.id.uninstall_button) CardView uninstallButton;
@BindColor(R.color.red500) int colorBad;
@BindColor(R.color.green500) int colorOK;
@BindColor(R.color.yellow500) int colorWarn;
@BindColor(R.color.green500) int colorNeutral;
@BindColor(R.color.blue500) int colorInfo;
private UpdateCardHolder magisk;
private UpdateCardHolder manager;
private SafetyNet safetyNet;
private Transition transition;
private Expandable optionExpand;
private void magiskInstall(View v) {
// Show Manager update first
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
new ManagerInstallDialog(requireActivity()).show();
return;
}
new MagiskInstallDialog((BaseActivity) requireActivity()).show();
}
private void managerInstall(View v) {
new ManagerInstallDialog(requireActivity()).show();
}
private void openLink(String url) {
Utils.openLink(requireActivity(), Uri.parse(url));
}
@OnClick(R.id.paypal)
void paypal() {
openLink(Const.Url.PAYPAL_URL);
}
@OnClick(R.id.patreon)
void patreon() {
openLink(Const.Url.PATREON_URL);
}
@OnClick(R.id.twitter)
void twitter() {
openLink(Const.Url.TWITTER_URL);
}
@OnClick(R.id.github)
void github() {
openLink(Const.Url.SOURCE_CODE_URL);
}
@OnClick(R.id.xda)
void xda() {
openLink(Const.Url.XDA_THREAD);
}
@OnClick(R.id.uninstall_button)
void uninstall() {
new UninstallDialog(requireActivity()).show();
}
@OnClick(R.id.arrow)
void expandOptions() {
if (optionExpand.isExpanded())
optionExpand.collapse();
else optionExpand.expand();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_magisk, container, false);
unbinder = new MagiskFragment_ViewBinding(this, v);
requireActivity().setTitle(R.string.magisk);
optionExpand = new ArrowExpandable(new ExpandableViewHolder(optionExpandLayout), arrow);
safetyNet = new SafetyNet(v);
magisk = new UpdateCardHolder(inflater, root);
manager = new UpdateCardHolder(inflater, root);
manager.setClickable(vv ->
MarkDownWindow.show(requireActivity(), null,
getResources().openRawResource(R.raw.changelog)));
root.addView(magisk.itemView, 1);
root.addView(manager.itemView, 2);
keepVerityChkbox.setChecked(Config.keepVerity);
keepVerityChkbox.setOnCheckedChangeListener((view, checked) -> Config.keepVerity = checked);
keepEncChkbox.setChecked(Config.keepEnc);
keepEncChkbox.setOnCheckedChangeListener((view, checked) -> Config.keepEnc = checked);
mSwipeRefreshLayout.setOnRefreshListener(this);
magisk.install.setOnClickListener(this::magiskInstall);
manager.install.setOnClickListener(this::managerInstall);
if (Config.get(Config.Key.COREONLY)) {
magisk.additional.setText(R.string.core_only_enabled);
magisk.additional.setVisibility(View.VISIBLE);
}
if (!app.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
manager.additional.setText("(" + app.getPackageName() + ")");
manager.additional.setVisibility(View.VISIBLE);
}
transition = new TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(new Fade(Fade.OUT))
.addTransition(new ChangeBounds())
.addTransition(new Fade(Fade.IN));
updateUI();
return v;
}
@Override
public void onDestroyView() {
super.onDestroyView();
safetyNet.unbinder.unbind();
magisk.unbinder.unbind();
manager.unbinder.unbind();
}
@Override
public void onRefresh() {
mSwipeRefreshLayout.setRefreshing(false);
TransitionManager.beginDelayedTransition(root, transition);
safetyNet.reset();
magisk.reset();
manager.reset();
Config.loadMagiskInfo();
updateUI();
Event.reset(this);
Config.remoteMagiskVersionString = null;
Config.remoteMagiskVersionCode = -1;
shownDialog = false;
// Trigger state check
if (Networking.checkNetworkStatus(app)) {
CheckUpdates.check();
}
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.UPDATE_CHECK_DONE};
}
@Override
public void onEvent(int event) {
updateCheckUI();
}
private void updateUI() {
((MainActivity) requireActivity()).checkHideSection();
int image, color;
String status;
if (Config.magiskVersionCode < 0) {
color = colorBad;
image = R.drawable.ic_cancel;
status = getString(R.string.magisk_version_error);
magisk.status.setText(status);
magisk.currentVersion.setVisibility(View.GONE);
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
status = getString(R.string.magisk);
magisk.currentVersion.setText(getString(R.string.current_installed,
String.format(Locale.US, "v%s (%d)",
Config.magiskVersionString, Config.magiskVersionCode)));
}
magisk.statusIcon.setColorFilter(color);
magisk.statusIcon.setImageResource(image);
manager.statusIcon.setColorFilter(colorOK);
manager.statusIcon.setImageResource(R.drawable.ic_check_circle);
manager.currentVersion.setText(getString(R.string.current_installed,
String.format(Locale.US, "v%s (%d)",
BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
if (!Networking.checkNetworkStatus(app)) {
// No network, updateCheckUI will not be triggered
magisk.status.setText(status);
manager.status.setText(R.string.app_name);
magisk.setValid(false);
manager.setValid(false);
}
}
private void updateCheckUI() {
int image, color;
String status, button = "";
TransitionManager.beginDelayedTransition(root, transition);
if (Config.remoteMagiskVersionCode < 0) {
color = colorNeutral;
image = R.drawable.ic_help;
status = getString(R.string.invalid_update_channel);
} else {
magisk.latestVersion.setText(getString(R.string.latest_version,
String.format(Locale.US, "v%s (%d)",
Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)));
if (Config.remoteMagiskVersionCode > Config.magiskVersionCode) {
color = colorInfo;
image = R.drawable.ic_update;
status = getString(R.string.magisk_update_title);
button = getString(R.string.update);
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
status = getString(R.string.magisk_up_to_date);
button = getString(R.string.install);
}
}
if (Config.magiskVersionCode > 0) {
// Only override status if Magisk is installed
magisk.statusIcon.setImageResource(image);
magisk.statusIcon.setColorFilter(color);
magisk.status.setText(status);
magisk.install.setText(button);
}
if (Config.remoteManagerVersionCode < 0) {
color = colorNeutral;
image = R.drawable.ic_help;
status = getString(R.string.invalid_update_channel);
} else {
manager.latestVersion.setText(getString(R.string.latest_version,
String.format(Locale.US, "v%s (%d)",
Config.remoteManagerVersionString, Config.remoteManagerVersionCode)));
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
color = colorInfo;
image = R.drawable.ic_update;
status = getString(R.string.manager_update_title);
manager.install.setText(R.string.update);
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
status = getString(R.string.manager_up_to_date);
manager.install.setText(R.string.install);
}
}
manager.statusIcon.setImageResource(image);
manager.statusIcon.setColorFilter(color);
manager.status.setText(status);
magisk.setValid(Config.remoteMagiskVersionCode > 0);
manager.setValid(Config.remoteManagerVersionCode > 0);
if (Config.remoteMagiskVersionCode < 0) {
// Hide install related components
installOptionCard.setVisibility(View.GONE);
uninstallButton.setVisibility(View.GONE);
} else {
// Show install related components
installOptionCard.setVisibility(View.VISIBLE);
uninstallButton.setVisibility(Shell.rootAccess() ? View.VISIBLE : View.GONE);
}
if (!shownDialog && Config.magiskVersionCode > 0 &&
!Shell.su("env_check").exec().isSuccess()) {
shownDialog = true;
new EnvFixDialog(requireActivity()).show();
}
}
}

View File

@@ -0,0 +1,6 @@
package com.topjohnwu.magisk.ui.home
enum class MagiskItem {
MANAGER, MAGISK
}

View File

@@ -0,0 +1,6 @@
package com.topjohnwu.magisk.ui.home
enum class MagiskState {
NO_ROOT, NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}

View File

@@ -0,0 +1,5 @@
package com.topjohnwu.magisk.ui.home
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}

View File

@@ -1,47 +0,0 @@
package com.topjohnwu.magisk.ui.log;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.TabFragmentAdapter;
import com.topjohnwu.magisk.ui.MainActivity;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import butterknife.BindView;
public class LogFragment extends BaseFragment {
@BindView(R.id.container) ViewPager viewPager;
@BindView(R.id.tab) TabLayout tab;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_log, container, false);
unbinder = new LogFragment_ViewBinding(this, v);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
((MainActivity) requireActivity()).toolbar.setElevation(0);
}
TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager());
adapter.addTab(new SuLogFragment(), getString(R.string.superuser));
adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk));
tab.setupWithViewPager(viewPager);
tab.setVisibility(View.VISIBLE);
viewPager.setAdapter(adapter);
return v;
}
}

View File

@@ -0,0 +1,53 @@
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentLogBinding
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class LogFragment : MagiskFragment<LogViewModel, FragmentLogBinding>() {
override val layoutRes: Int = R.layout.fragment_log
override val viewModel: LogViewModel by viewModel()
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is PageChangedEvent -> magiskActivity.invalidateOptionsMenu()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logTabs.setupWithViewPager(binding.logContainer, true)
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
magiskActivity.setTitle(R.string.log)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log, menu)
menu.findItem(R.id.menu_save).isVisible = viewModel.currentPage.value == 1
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> viewModel.saveLog()
R.id.menu_clear -> viewModel.clearLog()
R.id.menu_refresh -> viewModel.refresh()
}
return true
}
}

View File

@@ -0,0 +1,122 @@
package com.topjohnwu.magisk.ui.log
import android.content.res.Resources
import androidx.databinding.ObservableArrayList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSuccessUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.model.entity.recycler.*
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.zip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
import me.tatarka.bindingcollectionadapter2.OnItemBind
import java.io.File
import java.io.IOException
import java.util.*
class LogViewModel(
private val resources: Resources,
private val database: MagiskDB
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@LogViewModel)
}
val currentPage = KObservableField(0)
private val currentItem get() = items[currentPage.value]
private val logItem get() = items[0] as LogRvItem
private val magiskLogItem get() = items[1] as MagiskLogRvItem
init {
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
PageChangedEvent().publish()
}
items.addAll(listOf(LogRvItem(), MagiskLogRvItem()))
refresh()
}
override fun getPageTitle(position: Int, item: ComparableRvItem<*>?) = when (item) {
is LogRvItem -> resources.getString(R.string.superuser)
is MagiskLogRvItem -> resources.getString(R.string.magisk)
else -> ""
}
fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true }
.subscribeK()
.add()
fun saveLog() {
val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
)
val logFile = File(Const.EXTERNAL_PATH, filename)
try {
logFile.createNewFile()
} catch (e: IOException) {
return
}
Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit {
SnackbarEvent(logFile.path).publish()
}
}
fun clearLog() = when (currentItem) {
is LogRvItem -> clearLogs { refresh() }
is MagiskLogRvItem -> clearMagiskLogs { refresh() }
else -> Unit
}
private fun clearLogs(callback: () -> Unit) {
Single.fromCallable { database.clearLogs() }
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
callback()
}
.add()
}
private fun clearMagiskLogs(callback: () -> Unit) {
Shell.su("echo -n > " + Const.MAGISK_LOG).submit {
SnackbarEvent(R.string.logs_cleared).publish()
callback()
}
}
private fun updateLogs() = Single.fromCallable { database.logs }
.flattenAsFlowable { it }
.map { it.map { LogItemEntryRvItem(it) } }
.map { LogItemRvItem(ObservableArrayList<ComparableRvItem<*>>().apply { addAll(it) }) }
.toList()
.doOnSuccessUi { logItem.update(it) }
private fun updateMagiskLog() = Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").toSingle()
.map { it.exec() }
.map { it.out }
.flattenAsFlowable { it }
.map { ConsoleRvItem(it) }
.toList()
.doOnSuccessUi { magiskLogItem.update(it) }
}

View File

@@ -1,157 +0,0 @@
package com.topjohnwu.magisk.ui.log;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.StringListAdapter;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.SnackbarMaker;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.NOPList;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.List;
import butterknife.BindView;
public class MagiskLogFragment extends BaseFragment {
@BindView(R.id.recyclerView) RecyclerView rv;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_magisk_log, container, false);
unbinder = new MagiskLogFragment_ViewBinding(this, view);
setHasOptionsMenu(true);
return view;
}
@Override
public void onStart() {
super.onStart();
getActivity().setTitle(R.string.log);
}
@Override
public void onResume() {
super.onResume();
readLogs();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_log, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
readLogs();
return true;
case R.id.menu_save:
runWithExternalRW(this::saveLogs);
return true;
case R.id.menu_clear:
clearLogs();
rv.setAdapter(new MagiskLogAdapter(NOPList.getInstance()));
return true;
default:
return true;
}
}
private void readLogs() {
Shell.su("tail -n 5000 " + Const.MAGISK_LOG).submit(result -> {
rv.setAdapter(new MagiskLogAdapter(result.getOut()));
});
}
private void saveLogs() {
Calendar now = Calendar.getInstance();
String filename = Utils.fmt("magisk_log_%04d%02d%02d_%02d%02d%02d.log",
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
File logFile = new File(Const.EXTERNAL_PATH, filename);
try {
logFile.createNewFile();
} catch (IOException e) {
return;
}
Shell.su("cat " + Const.MAGISK_LOG + " > " + logFile)
.submit(result ->
SnackbarMaker.make(rv, logFile.getPath(), Snackbar.LENGTH_SHORT).show());
}
private void clearLogs() {
Shell.su("echo -n > " + Const.MAGISK_LOG).submit();
SnackbarMaker.make(rv, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
}
private class MagiskLogAdapter extends StringListAdapter<MagiskLogAdapter.ViewHolder> {
MagiskLogAdapter(List<String> list) {
super(list);
if (mList.isEmpty())
mList.add(requireContext().getString(R.string.log_is_empty));
}
@Override
protected int itemLayoutRes() {
return R.layout.list_item_console;
}
@NonNull
@Override
public ViewHolder createViewHolder(@NonNull View v) {
return new ViewHolder(v);
}
@Override
protected void onUpdateTextWidth(ViewHolder holder) {
super.onUpdateTextWidth(holder);
// Find the longest string and update accordingly
int max = 0;
String maxStr = "";
for (String s : mList) {
int len = s.length();
if (len > max) {
max = len;
maxStr = s;
}
}
holder.txt.setText(maxStr);
super.onUpdateTextWidth(holder);
}
public class ViewHolder extends StringListAdapter.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
@Override
protected int textViewResId() {
return R.id.txt;
}
}
}
}

View File

@@ -1,80 +0,0 @@
package com.topjohnwu.magisk.ui.log;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.SuLogAdapter;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import butterknife.BindView;
public class SuLogFragment extends BaseFragment {
@BindView(R.id.empty_rv) TextView emptyRv;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
private SuLogAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_log, menu);
menu.findItem(R.id.menu_save).setVisible(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_su_log, container, false);
unbinder = new SuLogFragment_ViewBinding(this, v);
adapter = new SuLogAdapter(app.mDB);
recyclerView.setAdapter(adapter);
updateList();
return v;
}
private void updateList() {
adapter.notifyDBChanged();
if (adapter.getSectionCount() == 0) {
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
updateList();
return true;
case R.id.menu_clear:
app.mDB.clearLogs();
updateList();
return true;
default:
return true;
}
}
}

View File

@@ -0,0 +1,141 @@
package com.topjohnwu.magisk.ui.module
import android.content.res.Resources
import android.database.Cursor
import androidx.annotation.StringRes
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
import com.topjohnwu.magisk.model.entity.recycler.SectionRvItem
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.Event
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.OnItemBind
class ModuleViewModel(
private val repoDatabase: RepoDatabaseHelper,
private val resources: Resources
) : MagiskViewModel() {
val query = KObservableField("")
private val allItems = mutableListOf<ComparableRvItem<*>>()
val itemsInstalled = DiffObservableList(ComparableRvItem.callback)
val itemsRemote = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@ModuleViewModel)
}
private var queryDisposable: Disposable? = null
init {
query.addOnPropertyChangedCallback {
queryDisposable?.dispose()
queryDisposable = query()
}
Event.register(this)
refresh()
}
override fun getListeningEvents(): IntArray {
return intArrayOf(Event.MODULE_LOAD_DONE, Event.REPO_LOAD_DONE)
}
override fun onEvent(event: Int) = when (event) {
Event.MODULE_LOAD_DONE -> updateModules(Event.getResult(event))
Event.REPO_LOAD_DONE -> updateRepos()
else -> Unit
}
fun fabPressed() = OpenFilePickerEvent().publish()
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh() {
state = State.LOADING
Utils.loadModules(true)
UpdateRepos().exec(true)
}
private fun updateModules(result: Map<String, Module>) = result.values
.map { ModuleRvItem(it) }
.let { itemsInstalled.update(it) }
internal fun updateRepos() {
Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } }
.flattenAsFlowable { it }
.map { RepoRvItem(it) }
.toList()
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK { itemsRemote.update(it.first, it.second) }
.add()
}
private fun query() = queryRaw()
.subscribeK { itemsRemote.update(it.first, it.second) }
private fun queryRaw(query: String = this.query.value) = allItems.toSingle()
.map { it.filterIsInstance<RepoRvItem>() }
.flattenAsFlowable { it }
.filter {
it.item.name.contains(query, ignoreCase = true) ||
it.item.author.contains(query, ignoreCase = true) ||
it.item.description.contains(query, ignoreCase = true)
}
.toList()
.map { if (query.isEmpty()) it.divide() else it }
.map { it to itemsRemote.calculateDiff(it) }
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
val installedModules = filter { installed.any { item -> it.item.id == item.item.id } }
fun installedByID(id: String) = installed.firstOrNull { it.item.id == id }
fun List<RepoRvItem>.filterObsolete() = filter {
val module = installedByID(it.item.id) ?: return@filter false
module.item.versionCode != it.item.versionCode
}
val resultObsolete = installedModules.filterObsolete()
val resultInstalled = installedModules - resultObsolete
val resultRemote = toList() - installedModules
fun buildList(@StringRes text: Int, list: List<RepoRvItem>): List<ComparableRvItem<*>> {
return if (list.isEmpty()) list
else listOf(SectionRvItem(resources.getString(text))) + list
}
return buildList(R.string.update_available, resultObsolete) +
buildList(R.string.installed, resultInstalled) +
buildList(R.string.not_installed, resultRemote)
}
private fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
val out = mutableListOf<Result>()
while (moveToNext()) out.add(transformer(this))
return out
}
}

View File

@@ -1,142 +0,0 @@
package com.topjohnwu.magisk.ui.module;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.ModulesAdapter;
import com.topjohnwu.magisk.model.entity.Module;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import butterknife.BindView;
import butterknife.OnClick;
public class ModulesFragment extends BaseFragment {
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyRv;
@OnClick(R.id.fab)
void selectFile() {
runWithExternalRW(() -> {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/zip");
startActivityForResult(intent, Const.ID.FETCH_ZIP);
});
}
private List<Module> listModules = new ArrayList<>();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_modules, container, false);
unbinder = new ModulesFragment_ViewBinding(this, view);
setHasOptionsMenu(true);
mSwipeRefreshLayout.setOnRefreshListener(() -> {
recyclerView.setVisibility(View.GONE);
Utils.loadModules();
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
mSwipeRefreshLayout.setEnabled(recyclerView.getChildAt(0).getTop() >= 0);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
requireActivity().setTitle(R.string.modules);
return view;
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.MODULE_LOAD_DONE};
}
@Override
public void onEvent(int event) {
updateUI(Event.getResult(event));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
Intent intent = new Intent(getActivity(), ClassMap.get(FlashActivity.class));
intent.setData(data.getData()).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
startActivity(intent);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_reboot, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.reboot:
RootUtils.reboot();
return true;
case R.id.reboot_recovery:
Shell.su("/system/bin/reboot recovery").submit();
return true;
case R.id.reboot_bootloader:
Shell.su("/system/bin/reboot bootloader").submit();
return true;
case R.id.reboot_download:
Shell.su("/system/bin/reboot download").submit();
return true;
default:
return false;
}
}
private void updateUI(Map<String, Module> moduleMap) {
listModules.clear();
listModules.addAll(moduleMap.values());
if (listModules.size() == 0) {
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
recyclerView.setAdapter(new ModulesAdapter(listModules));
}
mSwipeRefreshLayout.setRefreshing(false);
}
}

View File

@@ -0,0 +1,115 @@
package com.topjohnwu.magisk.ui.module
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>() {
override val layoutRes: Int = R.layout.fragment_modules
override val viewModel: ModuleViewModel by sharedViewModel()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
val intent = Intent(activity, ClassMap.get<Any>(FlashActivity::class.java))
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
startActivity(intent)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.modulesContent.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
binding.modulesRefreshLayout.isEnabled = recyclerView.getChildAt(0).top >= 0
}
})
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenFilePickerEvent -> selectFile()
}
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.modules)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_reboot, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.reboot -> {
RootUtils.reboot()
return true
}
R.id.reboot_recovery -> {
Shell.su("/system/bin/reboot recovery").submit()
return true
}
R.id.reboot_bootloader -> {
Shell.su("/system/bin/reboot bootloader").submit()
return true
}
R.id.reboot_download -> {
Shell.su("/system/bin/reboot download").submit()
return true
}
else -> return false
}
}
private fun selectFile() {
magiskActivity.runWithExternalRW {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
/*override fun getListeningEvents(): IntArray {
return intArrayOf(Event.MODULE_LOAD_DONE)
}
override fun onEvent(event: Int) {
updateUI(Event.getResult(event))
}*/
/*private fun updateUI(moduleMap: Map<String, Module>) {
listModules.clear()
listModules.addAll(moduleMap.values)
if (listModules.size == 0) {
emptyRv!!.visibility = View.VISIBLE
recyclerView!!.visibility = View.GONE
} else {
emptyRv!!.visibility = View.GONE
recyclerView!!.visibility = View.VISIBLE
recyclerView!!.adapter = ModulesAdapter(listModules)
}
mSwipeRefreshLayout!!.isRefreshing = false
}*/
}

View File

@@ -1,102 +0,0 @@
package com.topjohnwu.magisk.ui.module;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SearchView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.ReposAdapter;
import com.topjohnwu.magisk.tasks.UpdateRepos;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import com.topjohnwu.magisk.utils.Event;
import butterknife.BindView;
public class ReposFragment extends BaseFragment {
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyRv;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
private ReposAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_repos, container, false);
unbinder = new ReposFragment_ViewBinding(this, view);
mSwipeRefreshLayout.setRefreshing(true);
mSwipeRefreshLayout.setOnRefreshListener(() -> new UpdateRepos().exec(true));
adapter = new ReposAdapter();
recyclerView.setAdapter(adapter);
recyclerView.setVisibility(View.GONE);
requireActivity().setTitle(R.string.downloads);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
Event.unregister(adapter);
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.REPO_LOAD_DONE};
}
@Override
public void onEvent(int event) {
adapter.notifyDBChanged(false);
Event.register(adapter);
mSwipeRefreshLayout.setRefreshing(false);
boolean empty = adapter.getItemCount() == 0;
recyclerView.setVisibility(empty ? View.GONE : View.VISIBLE);
emptyRv.setVisibility(empty ? View.VISIBLE : View.GONE);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_repo, menu);
SearchView search = (SearchView) menu.findItem(R.id.repo_search).getActionView();
adapter.setSearchView(search);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.repo_sort) {
new AlertDialog.Builder(getActivity())
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(R.array.sorting_orders,
Config.get(Config.Key.REPO_ORDER), (d, which) -> {
Config.set(Config.Key.REPO_ORDER, which);
adapter.notifyDBChanged(true);
d.dismiss();
}).show();
}
return true;
}
}

View File

@@ -0,0 +1,104 @@
package com.topjohnwu.magisk.ui.module
import android.app.AlertDialog
import android.content.Intent
import android.os.Build
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.SearchView
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentReposBinding
import com.topjohnwu.magisk.model.download.DownloadModuleService
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.events.InstallModuleEvent
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_repos
override val viewModel: ModuleViewModel by sharedViewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.downloads)
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenChangelogEvent -> openChangelog(event.item)
is InstallModuleEvent -> installModule(event.item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_repo, menu)
(menu.findItem(R.id.repo_search).actionView as? SearchView)
?.setOnQueryTextListener(this)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.repo_sort) {
AlertDialog.Builder(activity)
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(
R.array.sorting_orders,
Config.get<Int>(Config.Key.REPO_ORDER)!!
) { d, which ->
Config.set(Config.Key.REPO_ORDER, which)
viewModel.updateRepos()
d.dismiss()
}.show()
}
return true
}
override fun onQueryTextSubmit(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
override fun onQueryTextChange(p0: String?): Boolean {
viewModel.query.value = p0.orEmpty()
return false
}
private fun openChangelog(item: Repo) {
MarkDownWindow.show(context, null, item.detailUrl)
}
private fun installModule(item: Repo) {
val context = magiskActivity
fun download(install: Boolean) {
context.runWithExternalRW {
val intent = Intent(activity, ClassMap.get<Any>(DownloadModuleService::class.java))
.putExtra("repo", item).putExtra("install", install)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary
} else {
context.startService(intent)
}
}
}
CustomAlertDialog(context)
.setTitle(context.getString(R.string.repo_install_title, item.name))
.setMessage(context.getString(R.string.repo_install_msg, item.downloadFilename))
.setCancelable(true)
.setPositiveButton(R.string.install) { _, _ -> download(true) }
.setNeutralButton(R.string.download) { _, _ -> download(false) }
.setNegativeButton(R.string.no_thanks, null)
.show()
}
}

View File

@@ -8,13 +8,6 @@ import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
@@ -35,18 +28,32 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
public class SettingsFragment extends BasePreferenceFragment {
private ListPreference updateChannel, autoRes, suNotification,
requestTimeout, rootConfig, multiuserConfig, nsConfig;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.app_settings, rootKey);
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
requireActivity().setTitle(R.string.settings);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setStorageDeviceProtected();
setPreferencesFromResource(R.xml.app_settings, rootKey);
boolean showSuperuser = Utils.showSuperUser();
app.prefs.edit()
app.getPrefs().edit()
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
.apply();
@@ -66,8 +73,8 @@ public class SettingsFragment extends BasePreferenceFragment {
return true;
});
findPreference("clear").setOnPreferenceClickListener(pref -> {
app.prefs.edit().remove(Config.Key.ETAG_KEY).apply();
app.repoDB.clearRepo();
app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
app.getRepoDB().clearRepo();
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
return true;
});
@@ -94,12 +101,12 @@ public class SettingsFragment extends BasePreferenceFragment {
if (channel == Config.Value.CUSTOM_CHANNEL) {
View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null);
EditText url = v.findViewById(R.id.custom_url);
url.setText(app.prefs.getString(Config.Key.CUSTOM_CHANNEL, ""));
url.setText(app.getPrefs().getString(Config.Key.CUSTOM_CHANNEL, ""));
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(R.string.ok, (d, i) ->
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
.setNegativeButton(R.string.close, (d, i) ->
Config.set(Config.Key.UPDATE_CHANNEL, prev))
.setOnCancelListener(d ->
@@ -183,7 +190,7 @@ public class SettingsFragment extends BasePreferenceFragment {
case Config.Key.ROOT_ACCESS:
case Config.Key.SU_MULTIUSER_MODE:
case Config.Key.SU_MNT_NS:
app.mDB.setSettings(key, Utils.getPrefsInt(prefs, key));
app.getDB().setSettings(key, Utils.getPrefsInt(prefs, key));
break;
case Config.Key.DARK_THEME:
requireActivity().recreate();

View File

@@ -1,64 +0,0 @@
package com.topjohnwu.magisk.ui.superuser;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.adapters.PolicyAdapter;
import com.topjohnwu.magisk.model.entity.Policy;
import com.topjohnwu.magisk.ui.base.BaseFragment;
import java.util.List;
import butterknife.BindView;
public class SuperuserFragment extends BaseFragment {
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyRv;
private PackageManager pm;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_superuser, container, false);
unbinder = new SuperuserFragment_ViewBinding(this, view);
pm = requireActivity().getPackageManager();
return view;
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(getString(R.string.superuser));
}
@Override
public void onResume() {
super.onResume();
displayPolicyList();
}
private void displayPolicyList() {
List<Policy> policyList = app.mDB.getPolicyList();
if (policyList.size() == 0) {
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
recyclerView.setAdapter(new PolicyAdapter(policyList, app.mDB, pm));
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}
}

View File

@@ -0,0 +1,25 @@
package com.topjohnwu.magisk.ui.superuser
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment :
MagiskFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
override val layoutRes: Int = R.layout.fragment_superuser
override val viewModel: SuperuserViewModel by viewModel()
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.superuser)
}
override fun onResume() {
super.onResume()
viewModel.updatePolicies()
}
}

View File

@@ -0,0 +1,139 @@
package com.topjohnwu.magisk.ui.superuser
import android.content.pm.PackageManager
import android.content.res.Resources
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.applySchedulers
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
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.PolicyRvItem
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.toggle
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.ItemBinding
class SuperuserViewModel(
private val database: MagiskDB,
private val packageManager: PackageManager,
private val resources: Resources,
rxBus: RxBus
) : MagiskViewModel() {
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
itemBinding.bindExtra(BR.viewModel, this@SuperuserViewModel)
}
private var ignoreNext: PolicyRvItem? = null
init {
rxBus.register<PolicyEnableEvent>()
.subscribeK { togglePolicy(it.item, it.enable) }
.add()
rxBus.register<PolicyUpdateEvent>()
.subscribeK { updatePolicy(it) }
.add()
updatePolicies()
}
fun updatePolicies() {
Single.fromCallable { database.policyList }
.flattenAsFlowable { it }
.map { PolicyRvItem(it, it.info.loadIcon(packageManager)) }
.toList()
.applySchedulers()
.applyViewModel(this)
.subscribeK { items.update(it) }
.add()
}
fun deletePressed(item: PolicyRvItem) {
fun updateState() = deletePolicy(item.item)
.map { items.filterIsInstance<PolicyRvItem>().toMutableList() }
.map { it.removeAll { it.item.packageName == item.item.packageName }; it }
.map { it to items.calculateDiff(it) }
.subscribeK { items.update(it.first, it.second) }
.add()
withView {
if (FingerprintHelper.useFingerprint()) {
FingerprintAuthDialog(this) { updateState() }.show()
} else {
CustomAlertDialog(this)
.setTitle(R.string.su_revoke_title)
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
.setPositiveButton(R.string.yes) { _, _ -> updateState() }
.setNegativeButton(R.string.no_thanks, null)
.setCancelable(true)
.show()
}
}
}
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
val textId = if (it.logging) R.string.su_snack_notif_on else R.string.su_snack_notif_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
val textId =
if (it.notification) R.string.su_snack_log_on else R.string.su_snack_log_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
}
private fun updatePolicy(item: PolicyRvItem, onSuccess: (Policy) -> Unit) =
updatePolicy(item.item)
.subscribeK { onSuccess(it) }
.add()
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() {
item.item.policy = if (enable) Policy.ALLOW else Policy.DENY
updatePolicy(item.item)
.map { it.policy == Policy.ALLOW }
.subscribeK {
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
val text = resources.getString(textId).format(item.item.appName)
SnackbarEvent(text).publish()
}
.add()
}
if (FingerprintHelper.useFingerprint()) {
withView {
FingerprintAuthDialog(this, { updateState() }, {
ignoreNext = item
item.isEnabled.toggle()
}).show()
}
} else {
updateState()
}
}
private fun updatePolicy(policy: Policy) =
Single.fromCallable { database.updatePolicy(policy); policy }
.applySchedulers()
private fun deletePolicy(policy: Policy) =
Single.fromCallable { database.deletePolicy(policy); policy }
.applySchedulers()
}

View File

@@ -1,287 +0,0 @@
package com.topjohnwu.magisk.ui.surequest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.text.TextUtils;
import android.view.View;
import android.view.Window;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.entity.Policy;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.SuConnector;
import com.topjohnwu.magisk.utils.SuLogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import java9.lang.Iterables;
public class SuRequestActivity extends BaseActivity {
@BindView(R.id.su_popup) LinearLayout suPopup;
@BindView(R.id.timeout) Spinner timeout;
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.app_name) TextView appNameView;
@BindView(R.id.package_name) TextView packageNameView;
@BindView(R.id.grant_btn) Button grant_btn;
@BindView(R.id.deny_btn) Button deny_btn;
@BindView(R.id.fingerprint) ImageView fingerprintImg;
@BindView(R.id.warning) TextView warning;
private ActionHandler handler;
private Policy policy;
private SharedPreferences timeoutPrefs;
public static final String REQUEST = "request";
public static final String LOG = "log";
public static final String NOTIFY = "notify";
@Override
public int getDarkTheme() {
return R.style.SuRequest_Dark;
}
@Override
public void onBackPressed() {
handler.handleAction(Policy.DENY, -1);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lockOrientation();
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
timeoutPrefs = App.deContext.getSharedPreferences("su_timeout", 0);
Intent intent = getIntent();
String action = intent.getAction();
if (TextUtils.equals(action, REQUEST)) {
if (!handleRequest())
finish();
return;
}
if (TextUtils.equals(action, LOG))
SuLogger.handleLogs(intent);
else if (TextUtils.equals(action, NOTIFY))
SuLogger.handleNotify(intent);
finish();
}
private boolean handleRequest() {
String socketName = getIntent().getStringExtra("socket");
if (socketName == null)
return false;
SuConnector connector;
try {
connector = new SuConnector(socketName) {
@Override
protected void onResponse() throws IOException {
out.writeInt(policy.policy);
}
};
Bundle bundle = connector.readSocketInput();
int uid = Integer.parseInt(bundle.getString("uid"));
app.mDB.clearOutdated();
policy = app.mDB.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.mDB.updatePolicy(policy);
}
handleAction();
}
};
// Never allow com.topjohnwu.magisk (could be malware)
if (TextUtils.equals(policy.packageName, BuildConfig.APPLICATION_ID))
return false;
// If not interactive, response directly
if (policy.policy != Policy.INTERACTIVE) {
handler.handleAction();
return true;
}
switch ((int) Config.get(Config.Key.SU_AUTO_RESPONSE)) {
case Config.Value.SU_AUTO_DENY:
handler.handleAction(Policy.DENY, 0);
return true;
case Config.Value.SU_AUTO_ALLOW:
handler.handleAction(Policy.ALLOW, 0);
return true;
}
showUI();
return true;
}
@SuppressLint("ClickableViewAccessibility")
private void showUI() {
setContentView(R.layout.activity_request);
new SuRequestActivity_ViewBinding(this);
appIcon.setImageDrawable(policy.info.loadIcon(getPackageManager()));
appNameView.setText(policy.appName);
packageNameView.setText(policy.packageName);
warning.setCompoundDrawablesRelativeWithIntrinsicBounds(
AppCompatResources.getDrawable(this, R.drawable.ic_warning), null, null, null);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.allow_timeout, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
timeout.setAdapter(adapter);
timeout.setSelection(timeoutPrefs.getInt(policy.packageName, 0));
CountDownTimer timer = new CountDownTimer(
(int) Config.get(Config.Key.SU_REQUEST_TIMEOUT) * 1000, 1000) {
@Override
public void onTick(long remains) {
deny_btn.setText(getString(R.string.deny) + "(" + remains / 1000 + ")");
}
@Override
public void onFinish() {
deny_btn.setText(getString(R.string.deny));
handler.handleAction(Policy.DENY);
}
};
timer.start();
Runnable cancelTimer = () -> {
timer.cancel();
deny_btn.setText(getString(R.string.deny));
};
handler.addCancel(cancelTimer);
boolean useFP = FingerprintHelper.useFingerprint();
if (useFP) try {
FingerprintHelper helper = new SuFingerprint();
helper.authenticate();
handler.addCancel(helper::cancel);
} catch (Exception e) {
e.printStackTrace();
useFP = false;
}
if (!useFP) {
grant_btn.setOnClickListener(v -> {
handler.handleAction(Policy.ALLOW);
timer.cancel();
});
grant_btn.requestFocus();
}
grant_btn.setVisibility(useFP ? View.GONE : View.VISIBLE);
fingerprintImg.setVisibility(useFP ? View.VISIBLE : View.GONE);
deny_btn.setOnClickListener(v -> {
handler.handleAction(Policy.DENY);
timer.cancel();
});
suPopup.setOnClickListener(v -> cancelTimer.run());
timeout.setOnTouchListener((v, event) -> {
cancelTimer.run();
return false;
});
}
private class SuFingerprint extends FingerprintHelper {
SuFingerprint() throws Exception {}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
warning.setText(errString);
}
@Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
warning.setText(helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
handler.handleAction(Policy.ALLOW);
}
@Override
public void onAuthenticationFailed() {
warning.setText(R.string.auth_fail);
}
}
private class ActionHandler {
private List<Runnable> cancelTasks = new ArrayList<>();
void handleAction() {
done();
}
void handleAction(int action) {
done();
}
void handleAction(int action, int time) {
done();
}
void addCancel(Runnable r) {
cancelTasks.add(r);
}
void done() {
Iterables.forEach(cancelTasks, Runnable::run);
finish();
}
}
}

View File

@@ -0,0 +1,62 @@
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.model.receiver.GeneralReceiver
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, GeneralReceiver.REQUEST)) {
if (!viewModel.handleRequest(intent) {})
finish()
return
}
if (TextUtils.equals(action, GeneralReceiver.LOG))
SuLogger.handleLogs(intent)
else if (TextUtils.equals(action, GeneralReceiver.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
}
}

View File

@@ -0,0 +1,251 @@
package com.topjohnwu.magisk.ui.surequest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.hardware.fingerprint.FingerprintManager
import android.os.CountDownTimer
import android.text.TextUtils
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.SuConnector
import com.topjohnwu.magisk.utils.now
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.IOException
import java.util.concurrent.TimeUnit.*
class SuRequestViewModel(
private val packageManager: PackageManager,
private val database: MagiskDB,
private val timeoutPrefs: SharedPreferences,
private val resources: Resources
) : MagiskViewModel() {
val icon = KObservableField<Drawable?>(null)
val title = KObservableField("")
val packageName = KObservableField("")
val denyText = KObservableField(resources.getString(R.string.deny))
val warningText = KObservableField<CharSequence>(resources.getString(R.string.su_warning))
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
val selectedItemPosition = KObservableField(0)
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
item.bind(binding)
}
var handler: ActionHandler? = null
private var timer: CountDownTimer? = null
private var policy: Policy? = null
set(value) {
field = value
updatePolicy(value)
}
init {
resources.getStringArray(R.array.allow_timeout)
.map { SpinnerRvItem(it) }
.let { items.update(it) }
}
private fun updatePolicy(policy: Policy?) {
policy ?: return
icon.value = policy.info.loadIcon(packageManager)
title.value = policy.appName
packageName.value = policy.packageName
selectedItemPosition.value = timeoutPrefs.getInt(policy.packageName, 0)
}
private fun cancelTimer() {
timer?.cancel()
denyText.value = resources.getString(R.string.deny)
}
fun grantPressed() {
handler?.handleAction(Policy.ALLOW)
timer?.cancel()
}
fun denyPressed() {
handler?.handleAction(Policy.DENY)
timer?.cancel()
}
fun spinnerTouched(): Boolean {
cancelTimer()
return false
}
fun handleRequest(intent: Intent, createUICallback: () -> Unit): Boolean {
val socketName = intent.getStringExtra("socket") ?: return false
val connector: SuConnector
try {
connector = object : SuConnector(socketName) {
@Throws(IOException::class)
override fun onResponse() {
out.writeInt(policy?.policy ?: return)
}
}
val bundle = connector.readSocketInput()
val uid = bundle.getString("uid")?.toIntOrNull() ?: return false
database.clearOutdated()
policy = database.getPolicy(uid) ?: Policy(uid, packageManager)
} catch (e: IOException) {
e.printStackTrace()
return false
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return false
}
handler = object : ActionHandler() {
override fun handleAction() {
connector.response()
done()
}
override fun handleAction(action: Int) {
val pos = selectedItemPosition.value
timeoutPrefs.edit().putInt(policy?.packageName, pos).apply()
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
}
override fun handleAction(action: Int, time: Int) {
policy?.apply {
policy = action
if (time >= 0) {
until = if (time == 0) {
0
} else {
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
}
database.updatePolicy(this)
}
}
policy?.policy = action
handleAction()
}
}
// Never allow com.topjohnwu.magisk (could be malware)
if (TextUtils.equals(policy?.packageName, BuildConfig.APPLICATION_ID))
return false
// If not interactive, response directly
if (policy?.policy != Policy.INTERACTIVE) {
handler?.handleAction()
return true
}
when (Config.get<Any>(Config.Key.SU_AUTO_RESPONSE) as Int) {
Config.Value.SU_AUTO_DENY -> {
handler?.handleAction(Policy.DENY, 0)
return true
}
Config.Value.SU_AUTO_ALLOW -> {
handler?.handleAction(Policy.ALLOW, 0)
return true
}
}
createUICallback()
showUI()
return true
}
@SuppressLint("ClickableViewAccessibility")
private fun showUI() {
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
val millis = SECONDS.toMillis(seconds)
timer = object : CountDownTimer(millis, 1000) {
override fun onTick(remains: Long) {
denyText.value = "%s (%d)"
.format(resources.getString(R.string.deny), remains / 1000)
}
override fun onFinish() {
denyText.value = resources.getString(R.string.deny)
handler?.handleAction(Policy.DENY)
}
}
timer?.start()
handler?.addCancel(Runnable { cancelTimer() })
val useFP = canUseFingerprint.value
if (useFP)
try {
val helper = SuFingerprint()
helper.authenticate()
handler?.addCancel(Runnable { helper.cancel() })
} catch (e: Exception) {
e.printStackTrace()
}
}
private inner class SuFingerprint @Throws(Exception::class)
internal constructor() : FingerprintHelper() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
warningText.value = errString
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
warningText.value = helpString
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
handler?.handleAction(Policy.ALLOW)
}
override fun onAuthenticationFailed() {
warningText.value = resources.getString(R.string.auth_fail)
}
}
open inner class ActionHandler {
private val cancelTasks = mutableListOf<Runnable>()
internal open fun handleAction() {
done()
}
internal open fun handleAction(action: Int) {
done()
}
internal open fun handleAction(action: Int, time: Int) {
done()
}
internal fun addCancel(r: Runnable) {
cancelTasks.add(r)
}
internal fun done() {
cancelTasks.forEach { it.run() }
DieEvent().publish()
}
}
}

View File

@@ -0,0 +1,182 @@
package com.topjohnwu.magisk.utils
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.Toolbar
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.navigation.NavigationView
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit
@BindingAdapter("onNavigationClick")
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
view.setNavigationOnClickListener(listener)
}
@BindingAdapter("onNavigationClick")
fun setOnNavigationClickedListener(
view: NavigationView,
listener: NavigationView.OnNavigationItemSelectedListener
) {
view.setNavigationItemSelectedListener {
(view.parent as? DrawerLayout)?.closeDrawers()
listener.onNavigationItemSelected(it)
}
}
@BindingAdapter("srcCompat")
fun setImageResource(view: AppCompatImageView, @DrawableRes resId: Int) {
view.setImageResource(resId)
}
@BindingAdapter("app:tint")
fun setTint(view: AppCompatImageView, @ColorInt tint: Int) {
view.setColorFilter(tint)
}
@BindingAdapter("isChecked")
fun setChecked(view: AppCompatImageView, isChecked: Boolean) {
val state = when (isChecked) {
true -> IndeterminateState.CHECKED
else -> IndeterminateState.UNCHECKED
}
setChecked(view, state)
}
@BindingAdapter("isChecked")
fun setChecked(view: AppCompatImageView, isChecked: IndeterminateState) {
view.setImageResource(
when (isChecked) {
IndeterminateState.INDETERMINATE -> R.drawable.ic_indeterminate
IndeterminateState.CHECKED -> R.drawable.ic_checked
IndeterminateState.UNCHECKED -> R.drawable.ic_unchecked
}
)
}
@BindingAdapter("position")
fun setPosition(view: ViewPager, position: Int) {
view.currentItem = position
}
@InverseBindingAdapter(attribute = "position", event = "positionChanged")
fun getPosition(view: ViewPager) = view.currentItem
@BindingAdapter("positionChanged")
fun setPositionChangedListener(view: ViewPager, listener: InverseBindingListener) {
view.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageSelected(position: Int) = listener.onChange()
override fun onPageScrollStateChanged(state: Int) = listener.onChange()
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) = listener.onChange()
})
}
@BindingAdapter("invisibleScale")
fun setInvisibleWithScale(view: View, isInvisible: Boolean) {
view.animate()
.scaleX(if (isInvisible) 0f else 1f)
.scaleY(if (isInvisible) 0f else 1f)
.setInterpolator(FastOutSlowInInterpolator())
.start()
}
@BindingAdapter("movieBehavior", "movieBehaviorText")
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
(view.tag as? Disposable)?.dispose()
if (isMovieBehavior) {
val observer = Observable
.interval(150, TimeUnit.MILLISECONDS)
.subscribeK {
view.text = text.replaceRandomWithSpecial()
}
view.tag = observer
} else {
view.text = text
}
}
@BindingAdapter("android:selectedItemPosition")
fun setSelectedItemPosition(view: Spinner, position: Int) {
view.setSelection(position)
}
@InverseBindingAdapter(
attribute = "android:selectedItemPosition",
event = "android:selectedItemPositionAttrChanged"
)
fun getSelectedItemPosition(view: Spinner) = view.selectedItemPosition
@BindingAdapter("android:selectedItemPositionAttrChanged")
fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListener) {
view.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(p0: AdapterView<*>?) {
listener.onChange()
}
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
listener.onChange()
}
}
}
@BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
view.setOnTouchListener(listener)
}
@BindingAdapter("scrollToLast")
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
fun scrollToLast() = view.post {
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
}
fun wait(callback: () -> Unit) {
Observable.timer(1, TimeUnit.SECONDS).subscribeK { callback() }
}
val tag = RecyclerView::class.java.name.sumBy { it.toInt() }
fun RecyclerView.Adapter<*>.setListener() {
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
scrollToLast()
}
}
registerAdapterDataObserver(observer)
view.setTag(tag, observer)
}
fun RecyclerView.Adapter<*>.removeListener() {
val observer = view.getTag(tag) as? RecyclerView.AdapterDataObserver ?: return
unregisterAdapterDataObserver(observer)
}
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
if (shouldScrollToLast) {
trySetListener()
} else {
view.adapter?.removeListener()
}
}

View File

@@ -3,8 +3,6 @@ package com.topjohnwu.magisk.utils;
import android.content.ComponentName;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
@@ -29,6 +27,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import androidx.core.app.NotificationCompat;
public class PatchAPK {
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
@@ -56,7 +56,7 @@ public class PatchAPK {
return builder.toString();
}
private static boolean findAndPatch(byte xml[], String from, String to) {
private static boolean findAndPatch(byte[] xml, String from, String to) {
if (from.length() != to.length())
return false;
CharBuffer buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asCharBuffer();
@@ -83,7 +83,7 @@ public class PatchAPK {
return true;
}
private static boolean findAndPatch(byte xml[], int a, int b) {
private static boolean findAndPatch(byte[] xml, int a, int b) {
IntBuffer buf = ByteBuffer.wrap(xml).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
int len = xml.length / 4;
for (int i = 0; i < len; ++i) {
@@ -122,7 +122,7 @@ public class PatchAPK {
try {
JarMap jar = new JarMap(in);
JarEntry je = jar.getJarEntry(Const.ANDROID_MANIFEST);
byte xml[] = jar.getRawData(je);
byte[] xml = jar.getRawData(je);
if (!findAndPatch(xml, BuildConfig.APPLICATION_ID, pkg) ||
!findAndPatch(xml, R.string.app_name, R.string.re_app_name))

View File

@@ -1,149 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
public class RootUtils extends Shell.Initializer {
public static void rmAndLaunch(String rm, ComponentName component) {
Shell.su(Utils.fmt("(rm_launch %s %s)&", rm, component.flattenToString())).exec();
}
public static void reboot() {
Shell.su("/system/bin/reboot" + (Config.recovery ? " recovery" : "")).submit();
}
public static void startActivity(Intent intent) {
if (intent.getComponent() == null)
return;
List<String> args = new ArrayList<>();
args.add("am");
args.add("start");
intentToCommand(intent, args);
Shell.su(Utils.argsToCommand(args)).exec();
}
public static void intentToCommand(Intent intent, List<String> args) {
if (intent.getAction() != null) {
args.add("-a");
args.add(intent.getAction());
}
if (intent.getComponent() != null) {
args.add("-n");
args.add(intent.getComponent().flattenToString());
}
if (intent.getData() != null) {
args.add("-d");
args.add(intent.getDataString());
}
if (intent.getCategories() != null) {
for (String cat : intent.getCategories()) {
args.add("-c");
args.add(cat);
}
}
if (intent.getType() != null) {
args.add("-t");
args.add(intent.getType());
}
Bundle extras = intent.getExtras();
if (extras != null) {
for (String key : extras.keySet()) {
Object val = extras.get(key);
if (val == null)
continue;
Object value = val;
String arg;
if (val instanceof String) arg = "--es";
else if (val instanceof Boolean) arg = "--ez";
else if (val instanceof Integer) arg = "--ei";
else if (val instanceof Long) arg = "--el";
else if (val instanceof Float) arg = "--ef";
else if (val instanceof Uri) arg = "--eu";
else if (val instanceof ComponentName) {
arg = "--ecn";
value = ((ComponentName) val).flattenToString();
} else if (val instanceof ArrayList) {
ArrayList arr = (ArrayList) val;
if (arr.size() <= 0)
/* Impossible to know the type due to type erasure */
continue;
if (arr.get(0) instanceof Integer) arg = "--eial";
else if (arr.get(0) instanceof Long) arg = "--elal";
else if (arr.get(0) instanceof Float) arg = "--efal";
else if (arr.get(0) instanceof String) arg = "--esal";
else continue; /* Unsupported */
StringBuilder sb = new StringBuilder();
for (Object o : arr) {
sb.append(o.toString().replace(",", "\\,"));
sb.append(',');
}
// Remove trailing comma
sb.deleteCharAt(sb.length() - 1);
value = sb;
} else if (val.getClass().isArray()) {
if (val instanceof int[]) arg = "--eia";
else if (val instanceof long[]) arg = "--ela";
else if (val instanceof float[]) arg = "--efa";
else if (val instanceof String[]) arg = "--esa";
else continue; /* Unsupported */
StringBuilder sb = new StringBuilder();
int len = Array.getLength(val);
for (int i = 0; i < len; ++i) {
sb.append(Array.get(val, i).toString().replace(",", "\\,"));
sb.append(',');
}
// Remove trailing comma
sb.deleteCharAt(sb.length() - 1);
value = sb;
}
else continue; /* Unsupported */
args.add(arg);
args.add(key);
args.add(value.toString());
}
}
args.add("-f");
args.add(String.valueOf(intent.getFlags()));
}
@Override
public boolean onInit(Context context, @NonNull Shell shell) {
Shell.Job job = shell.newJob();
if (shell.isRoot()) {
job.add(context.getResources().openRawResource(R.raw.util_functions))
.add(context.getResources().openRawResource(R.raw.utils));
Const.MAGISK_DISABLE_FILE = new SuFile("/cache/.disable_magisk");
Config.loadMagiskInfo();
} else {
InputStream nonroot = context.getResources().openRawResource(R.raw.nonroot_utils);
job.add(nonroot);
}
job.add("mount_partitions", "get_flags", "run_migrations", "export BOOTMODE=true").exec();
Config.keepVerity = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPVERITY"));
Config.keepEnc = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPFORCEENCRYPT"));
Config.recovery = Boolean.parseBoolean(ShellUtils.fastCmd("echo $RECOVERYMODE"));
return true;
}
}

View File

@@ -0,0 +1,164 @@
package com.topjohnwu.magisk.utils
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.io.SuFile
import java.util.*
import java.lang.reflect.Array as RArray
fun Intent.toCommand(args: MutableList<String>) {
if (action != null) {
args.add("-a")
args.add(action!!)
}
if (component != null) {
args.add("-n")
args.add(component!!.flattenToString())
}
if (data != null) {
args.add("-d")
args.add(dataString!!)
}
if (categories != null) {
for (cat in categories) {
args.add("-c")
args.add(cat)
}
}
if (type != null) {
args.add("-t")
args.add(type!!)
}
val extras = extras
if (extras != null) {
loop@ for (key in extras.keySet()) {
val v = extras.get(key) ?: continue
var value: Any = v
val arg: String
when {
v is String -> arg = "--es"
v is Boolean -> arg = "--ez"
v is Int -> arg = "--ei"
v is Long -> arg = "--el"
v is Float -> arg = "--ef"
v is Uri -> arg = "--eu"
v is ComponentName -> {
arg = "--ecn"
value = v.flattenToString()
}
v is ArrayList<*> -> {
if (v.size <= 0)
/* Impossible to know the type due to type erasure */
continue@loop
arg = if (v[0] is Int)
"--eial"
else if (v[0] is Long)
"--elal"
else if (v[0] is Float)
"--efal"
else if (v[0] is String)
"--esal"
else
continue@loop /* Unsupported */
val sb = StringBuilder()
for (o in v) {
sb.append(o.toString().replace(",", "\\,"))
sb.append(',')
}
// Remove trailing comma
sb.deleteCharAt(sb.length - 1)
value = sb
}
v.javaClass.isArray -> {
arg = if (v is IntArray)
"--eia"
else if (v is LongArray)
"--ela"
else if (v is FloatArray)
"--efa"
else if (v is Array<*> && v.isArrayOf<String>())
"--esa"
else
continue@loop /* Unsupported */
val sb = StringBuilder()
val len = RArray.getLength(v)
for (i in 0 until len) {
sb.append(RArray.get(v, i)!!.toString().replace(",", "\\,"))
sb.append(',')
}
// Remove trailing comma
sb.deleteCharAt(sb.length - 1)
value = sb
}
else -> continue@loop
} /* Unsupported */
args.add(arg)
args.add(key)
args.add(value.toString())
}
}
args.add("-f")
args.add(flags.toString())
}
fun startActivity(intent: Intent) {
if (intent.component == null)
return
val args = ArrayList<String>()
args.add("am")
args.add("start")
intent.toCommand(args)
Shell.su(args.toShellCmd()).exec()
}
class RootUtils : Shell.Initializer() {
override fun onInit(context: Context?, shell: Shell): Boolean {
context ?: return false
val job = shell.newJob()
if (shell.isRoot) {
job.add(context.rawResource(R.raw.util_functions))
.add(context.rawResource(R.raw.utils))
Const.MAGISK_DISABLE_FILE = SuFile("/cache/.disable_magisk")
Config.loadMagiskInfo()
} else {
job.add(context.rawResource(R.raw.nonroot_utils))
}
job.add("mount_partitions",
"get_flags",
"run_migrations",
"export BOOTMODE=true")
.exec()
Config.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean()
Config.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean()
Config.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean()
return true
}
companion object {
@JvmStatic
fun rmAndLaunch(rm: String, component: ComponentName) {
Shell.su("(rm_launch $rm ${component.flattenToString()})").exec()
}
@JvmStatic
fun reboot() {
Shell.su("/system/bin/reboot ${if (Config.recovery) "recovery" else ""}").submit()
}
}
}

View File

@@ -37,7 +37,7 @@ public class SuLogger {
}
} else {
// Doesn't report whether notify or not, check database ourselves
policy = app.mDB.getPolicy(fromUid);
policy = app.getDB().getPolicy(fromUid);
if (policy == null)
return;
notify = policy.notification;
@@ -62,7 +62,7 @@ public class SuLogger {
log.fromPid = pid;
log.command = command;
log.date = new Date();
app.mDB.addLog(log);
app.getDB().addLog(log);
}
private static void handleNotify(Policy policy) {

Some files were not shown because too many files have changed in this diff Show More