mirror of
				https://github.com/topjohnwu/Magisk
				synced 2025-10-31 10:40:52 +01:00 
			
		
		
		
	Compare commits
	
		
			59 Commits
		
	
	
		
			manager-v7
			...
			v20.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 59fd38bbf8 | ||
|   | 06dc6df270 | ||
|   | ff8460b361 | ||
|   | 674d272eaa | ||
|   | c3e00c279d | ||
|   | 175d920c94 | ||
|   | 04920883ea | ||
|   | 5e44b0b9d5 | ||
|   | 23c1a1dab8 | ||
|   | f5d054b93c | ||
|   | d25ae5e0a9 | ||
|   | c42a51dcbb | ||
|   | da3fd92b31 | ||
|   | 4a45ba3c14 | ||
|   | dbc8bed234 | ||
|   | f8b4190a11 | ||
|   | 479972e3ae | ||
|   | 3ea28b0afb | ||
|   | 2b3cc28966 | ||
|   | 751642b39a | ||
|   | d6c2c821a4 | ||
|   | dfc65b95f7 | ||
|   | b45d922463 | ||
|   | f87ee3fcf9 | ||
|   | e0927cd763 | ||
|   | 21099eabfa | ||
|   | abbd2e6b72 | ||
|   | 5b7ddbbb01 | ||
|   | 6352fbb3b2 | ||
|   | d3f49334e2 | ||
|   | c4356171b3 | ||
|   | 5c5625911d | ||
|   | 6a10cc9c55 | ||
|   | 6b317f918e | ||
|   | 08b528dc4f | ||
|   | fc886a5a47 | ||
|   | 0cb90e2e55 | ||
|   | 64113a69b4 | ||
|   | 544bb7459c | ||
|   | 578a50b464 | ||
|   | 3d4081d0af | ||
|   | b763b81f56 | ||
|   | 947dae4900 | ||
|   | debd1d7d54 | ||
|   | cba0d04000 | ||
|   | 695e7e6da0 | ||
|   | 4cd4bfa1d7 | ||
|   | 16b400964b | ||
|   | cf2d02c0dd | ||
|   | 0fcd0de0d1 | ||
|   | 748a35774f | ||
|   | a52a3e38ed | ||
|   | ee0cef06a6 | ||
|   | 0e5a113a0c | ||
|   | a1ccd44013 | ||
|   | 4d91e50d6d | ||
|   | 120668c7bc | ||
|   | d81ccde569 | ||
|   | e8581b4adb | 
| @@ -60,14 +60,26 @@ dependencies { | ||||
|  | ||||
|     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' | ||||
|     implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6' | ||||
|  | ||||
|     def vMarkwon = '3.1.0' | ||||
|     implementation "ru.noties.markwon:core:${vMarkwon}" | ||||
|     implementation "ru.noties.markwon:html:${vMarkwon}" | ||||
|     implementation "ru.noties.markwon:image-svg:${vMarkwon}" | ||||
|     implementation 'io.reactivex.rxjava2:rxjava:2.2.13' | ||||
|     implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' | ||||
|     implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' | ||||
|  | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}" | ||||
|  | ||||
|     def vBAdapt = '3.1.1' | ||||
|     def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter' | ||||
|     implementation "${bindingAdapter}:${vBAdapt}" | ||||
|     implementation "${bindingAdapter}-recyclerview:${vBAdapt}" | ||||
|  | ||||
|     def vMarkwon = '4.1.1' | ||||
|     implementation "io.noties.markwon:core:${vMarkwon}" | ||||
|     implementation "io.noties.markwon:html:${vMarkwon}" | ||||
|     implementation "io.noties.markwon:image:${vMarkwon}" | ||||
|     implementation 'com.caverock:androidsvg:1.4' | ||||
|  | ||||
|     def vLibsu = '2.5.1' | ||||
|     implementation "com.github.topjohnwu.libsu:core:${vLibsu}" | ||||
| @@ -78,13 +90,13 @@ dependencies { | ||||
|     implementation "org.koin:koin-android:${vKoin}" | ||||
|     implementation "org.koin:koin-androidx-viewmodel:${vKoin}" | ||||
|  | ||||
|     def vRetrofit = '2.6.1' | ||||
|     def vRetrofit = '2.6.2' | ||||
|     implementation "com.squareup.retrofit2:retrofit:${vRetrofit}" | ||||
|     implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" | ||||
|     implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}" | ||||
|     implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" | ||||
|  | ||||
|     def vOkHttp = '3.12.2' | ||||
|     def vOkHttp = '3.12.6' | ||||
|     implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" | ||||
|     implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" | ||||
|  | ||||
| @@ -100,20 +112,22 @@ dependencies { | ||||
|             replacedBy('com.github.topjohnwu:room-runtime') | ||||
|         } | ||||
|     } | ||||
|     def vRoom = "2.1.0" | ||||
|     def vRoom = "2.2.0" | ||||
|     implementation "com.github.topjohnwu:room-runtime:${vRoom}" | ||||
|     kapt "androidx.room:room-compiler:${vRoom}" | ||||
|  | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}" | ||||
|     def vNav = "2.1.0" | ||||
|     implementation "androidx.navigation:navigation-fragment-ktx:$vNav" | ||||
|     implementation "androidx.navigation:navigation-ui-ktx:$vNav" | ||||
|  | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||
|     implementation 'androidx.browser:browser:1.0.0' | ||||
|     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03' | ||||
|     implementation 'androidx.preference:preference:1.1.0' | ||||
|     implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04' | ||||
|     implementation 'androidx.recyclerview:recyclerview:1.1.0-beta05' | ||||
|     implementation 'androidx.cardview:cardview:1.0.0' | ||||
|     implementation 'androidx.work:work-runtime:2.2.0' | ||||
|     implementation 'androidx.transition:transition:1.2.0-rc01' | ||||
|     implementation 'androidx.transition:transition:1.2.0' | ||||
|     implementation 'androidx.multidex:multidex:2.0.1' | ||||
|     implementation 'com.google.android.material:material:1.1.0-alpha10' | ||||
|     implementation 'androidx.core:core-ktx:1.1.0' | ||||
|     implementation 'com.google.android.material:material:1.1.0-beta01' | ||||
| } | ||||
|   | ||||
							
								
								
									
										2
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -29,7 +29,7 @@ | ||||
| } | ||||
|  | ||||
| # DelegateWorker | ||||
| -keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker | ||||
| -keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker | ||||
|  | ||||
| # BootSigner | ||||
| -keepclassmembers class com.topjohnwu.signing.BootSigner { *; } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import androidx.annotation.NonNull; | ||||
| import androidx.work.Worker; | ||||
| import androidx.work.WorkerParameters; | ||||
|  | ||||
| import com.topjohnwu.magisk.model.worker.DelegateWorker; | ||||
| import com.topjohnwu.magisk.base.DelegateWorker; | ||||
|  | ||||
| import java.lang.reflect.ParameterizedType; | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import com.topjohnwu.magisk.data.database.RepoDatabase_Impl | ||||
| import com.topjohnwu.magisk.di.ActivityTracker | ||||
| import com.topjohnwu.magisk.di.koinModules | ||||
| import com.topjohnwu.magisk.extensions.get | ||||
| import com.topjohnwu.magisk.net.Networking | ||||
| import com.topjohnwu.magisk.utils.LocaleManager | ||||
| import com.topjohnwu.magisk.utils.RootUtils | ||||
| import com.topjohnwu.superuser.Shell | ||||
| @@ -50,8 +49,6 @@ open class App : Application() { | ||||
|         } | ||||
|  | ||||
|         registerActivityLifecycleCallbacks(get<ActivityTracker>()) | ||||
|  | ||||
|         Networking.init(base) | ||||
|         LocaleManager.setLocale(this) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -13,8 +13,8 @@ object Const { | ||||
|  | ||||
|     // Versions | ||||
|     const val SNET_EXT_VER = 13 | ||||
|     const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc" | ||||
|     const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77" | ||||
|     const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df" | ||||
|     const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df" | ||||
|  | ||||
|     // Misc | ||||
|     const val ANDROID_MANIFEST = "AndroidManifest.xml" | ||||
|   | ||||
							
								
								
									
										123
									
								
								app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| package com.topjohnwu.magisk.base | ||||
|  | ||||
| import android.Manifest | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.content.res.Configuration | ||||
| import android.os.Bundle | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| import androidx.collection.SparseArrayCompat | ||||
| import androidx.core.app.ActivityCompat | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.databinding.DataBindingUtil | ||||
| import androidx.databinding.ViewDataBinding | ||||
| import com.topjohnwu.magisk.BR | ||||
| import com.topjohnwu.magisk.Config | ||||
| import com.topjohnwu.magisk.base.viewmodel.BaseViewModel | ||||
| import com.topjohnwu.magisk.extensions.set | ||||
| import com.topjohnwu.magisk.model.events.EventHandler | ||||
| import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder | ||||
| import com.topjohnwu.magisk.utils.LocaleManager | ||||
| import com.topjohnwu.magisk.utils.currentLocale | ||||
| import kotlin.random.Random | ||||
|  | ||||
| typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit | ||||
|  | ||||
| abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> : | ||||
|         AppCompatActivity(), EventHandler { | ||||
|  | ||||
|     protected lateinit var binding: Binding | ||||
|     protected abstract val layoutRes: Int | ||||
|     protected abstract val viewModel: ViewModel | ||||
|     protected open val snackbarView get() = binding.root | ||||
|     protected open val navHostId: Int = 0 | ||||
|     protected open val defaultPosition: Int = 0 | ||||
|  | ||||
|     private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() } | ||||
|  | ||||
|     init { | ||||
|         val theme = if (Config.darkTheme) { | ||||
|             AppCompatDelegate.MODE_NIGHT_YES | ||||
|         } else { | ||||
|             AppCompatDelegate.MODE_NIGHT_NO | ||||
|         } | ||||
|         AppCompatDelegate.setDefaultNightMode(theme) | ||||
|     } | ||||
|  | ||||
|     override fun applyOverrideConfiguration(config: Configuration?) { | ||||
|         // Force applying our preferred local | ||||
|         config?.setLocale(currentLocale) | ||||
|         super.applyOverrideConfiguration(config) | ||||
|     } | ||||
|  | ||||
|     override fun attachBaseContext(base: Context) { | ||||
|         super.attachBaseContext(LocaleManager.getLocaleContext(base)) | ||||
|     } | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|  | ||||
|         viewModel.viewEvents.observe(this, viewEventObserver) | ||||
|  | ||||
|         binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply { | ||||
|             setVariable(BR.viewModel, viewModel) | ||||
|             lifecycleOwner = this@BaseActivity | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) { | ||||
|         val request = PermissionRequestBuilder().apply(builder).build() | ||||
|         val ungranted = permissions.filter { | ||||
|             ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED | ||||
|         } | ||||
|  | ||||
|         if (ungranted.isEmpty()) { | ||||
|             request.onSuccess() | ||||
|         } else { | ||||
|             val requestCode = Random.nextInt(256, 512) | ||||
|             resultCallbacks[requestCode] =  { result, _ -> | ||||
|                 if (result > 0) | ||||
|                     request.onSuccess() | ||||
|                 else | ||||
|                     request.onFailure() | ||||
|             } | ||||
|             ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) { | ||||
|         withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder) | ||||
|     } | ||||
|  | ||||
|     override fun onRequestPermissionsResult( | ||||
|             requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { | ||||
|         var success = true | ||||
|         for (res in grantResults) { | ||||
|             if (res != PackageManager.PERMISSION_GRANTED) { | ||||
|                 success = false | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|         resultCallbacks[requestCode]?.apply { | ||||
|             resultCallbacks.remove(requestCode) | ||||
|             invoke(this@BaseActivity, if (success) 1 else -1, null) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         super.onActivityResult(requestCode, resultCode, data) | ||||
|         resultCallbacks[requestCode]?.apply { | ||||
|             resultCallbacks.remove(requestCode) | ||||
|             invoke(this@BaseActivity, resultCode, data) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) { | ||||
|         resultCallbacks[requestCode] = listener | ||||
|         startActivityForResult(intent, requestCode) | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										50
									
								
								app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| package com.topjohnwu.magisk.base | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.annotation.CallSuper | ||||
| import androidx.databinding.DataBindingUtil | ||||
| import androidx.databinding.ViewDataBinding | ||||
| import androidx.fragment.app.Fragment | ||||
| import com.topjohnwu.magisk.BR | ||||
| import com.topjohnwu.magisk.base.viewmodel.BaseViewModel | ||||
| import com.topjohnwu.magisk.model.events.EventHandler | ||||
| import com.topjohnwu.magisk.model.events.ViewEvent | ||||
|  | ||||
| abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> : | ||||
|     Fragment(), EventHandler { | ||||
|  | ||||
|     protected val activity get() = requireActivity() as BaseActivity<*, *> | ||||
|     protected lateinit var binding: Binding | ||||
|     protected abstract val layoutRes: Int | ||||
|     protected abstract val viewModel: ViewModel | ||||
|  | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         viewModel.viewEvents.observe(this, viewEventObserver) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateView( | ||||
|             inflater: LayoutInflater, | ||||
|             container: ViewGroup?, | ||||
|             savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply { | ||||
|             setVariable(BR.viewModel, viewModel) | ||||
|             lifecycleOwner = this@BaseFragment | ||||
|         } | ||||
|  | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     @CallSuper | ||||
|     override fun onEventDispatched(event: ViewEvent) { | ||||
|         super.onEventDispatched(event) | ||||
|         activity.onEventDispatched(event) | ||||
|     } | ||||
|  | ||||
|     open fun onBackPressed(): Boolean = false | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,56 @@ | ||||
| package com.topjohnwu.magisk.base | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.SharedPreferences | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.preference.* | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import org.koin.android.ext.android.inject | ||||
|  | ||||
| abstract class BasePreferenceFragment : PreferenceFragmentCompat(), | ||||
|     SharedPreferences.OnSharedPreferenceChangeListener { | ||||
|  | ||||
|     protected val prefs: SharedPreferences by inject() | ||||
|     protected val activity get() = requireActivity() as BaseActivity<*, *> | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         val v = super.onCreateView(inflater, container, savedInstanceState) | ||||
|         prefs.registerOnSharedPreferenceChangeListener(this) | ||||
|         return v | ||||
|     } | ||||
|  | ||||
|     override fun onDestroyView() { | ||||
|         prefs.unregisterOnSharedPreferenceChangeListener(this) | ||||
|         super.onDestroyView() | ||||
|     } | ||||
|  | ||||
|     private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) { | ||||
|         preference.isIconSpaceReserved = false | ||||
|         if (preference is PreferenceGroup) | ||||
|             for (i in 0 until preference.preferenceCount) | ||||
|                 setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i)) | ||||
|     } | ||||
|  | ||||
|     override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) { | ||||
|         if (preferenceScreen != null) | ||||
|             setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen) | ||||
|         super.setPreferenceScreen(preferenceScreen) | ||||
|     } | ||||
|  | ||||
|     override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> = | ||||
|             object : PreferenceGroupAdapter(preferenceScreen) { | ||||
|                 @SuppressLint("RestrictedApi") | ||||
|                 override fun onPreferenceHierarchyChange(preference: Preference?) { | ||||
|                     if (preference != null) | ||||
|                         setAllPreferencesToAvoidHavingExtraSpace(preference) | ||||
|                     super.onPreferenceHierarchyChange(preference) | ||||
|                 } | ||||
|             } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.topjohnwu.magisk.model.worker | ||||
| package com.topjohnwu.magisk.base | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Network | ||||
| @@ -1,24 +1,23 @@ | ||||
| package com.topjohnwu.magisk.ui.base | ||||
| package com.topjohnwu.magisk.base.viewmodel | ||||
|  | ||||
| import android.app.Activity | ||||
| import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork | ||||
| import com.skoumal.teanity.extensions.doOnSubscribeUi | ||||
| import com.skoumal.teanity.extensions.subscribeK | ||||
| import com.skoumal.teanity.util.KObservableField | ||||
| import com.skoumal.teanity.viewmodel.LoadingViewModel | ||||
| import com.topjohnwu.magisk.extensions.doOnSubscribeUi | ||||
| import com.topjohnwu.magisk.extensions.get | ||||
| import com.topjohnwu.magisk.extensions.subscribeK | ||||
| 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.KObservableField | ||||
| import io.reactivex.Observable | ||||
| import io.reactivex.subjects.PublishSubject | ||||
|  | ||||
|  | ||||
| abstract class MagiskViewModel( | ||||
| abstract class BaseViewModel( | ||||
|     initialState: State = State.LOADING | ||||
| ) : LoadingViewModel(initialState) { | ||||
|  | ||||
|     val isConnected = KObservableField(true) | ||||
|     val isConnected = KObservableField(false) | ||||
|  | ||||
|     init { | ||||
|         ReactiveNetwork.observeNetworkConnectivity(get()) | ||||
| @@ -0,0 +1,78 @@ | ||||
| package com.topjohnwu.magisk.base.viewmodel | ||||
|  | ||||
| import androidx.databinding.Bindable | ||||
| import com.topjohnwu.magisk.BR | ||||
| import io.reactivex.* | ||||
|  | ||||
| abstract class LoadingViewModel(defaultState: State = State.LOADING) : | ||||
|     StatefulViewModel<LoadingViewModel.State>(defaultState) { | ||||
|  | ||||
|     val loading @Bindable get() = state == State.LOADING | ||||
|     val loaded @Bindable get() = state == State.LOADED | ||||
|     val loadingFailed @Bindable get() = state == State.LOADING_FAILED | ||||
|  | ||||
|     @Deprecated( | ||||
|         "Direct access is recommended since 0.2. This access method will be removed in 1.0", | ||||
|         ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"), | ||||
|         DeprecationLevel.WARNING | ||||
|     ) | ||||
|     fun setLoading() { | ||||
|         state = State.LOADING | ||||
|     } | ||||
|  | ||||
|     @Deprecated( | ||||
|         "Direct access is recommended since 0.2. This access method will be removed in 1.0", | ||||
|         ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"), | ||||
|         DeprecationLevel.WARNING | ||||
|     ) | ||||
|     fun setLoaded() { | ||||
|         state = State.LOADED | ||||
|     } | ||||
|  | ||||
|     @Deprecated( | ||||
|         "Direct access is recommended since 0.2. This access method will be removed in 1.0", | ||||
|         ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"), | ||||
|         DeprecationLevel.WARNING | ||||
|     ) | ||||
|     fun setLoadingFailed() { | ||||
|         state = State.LOADING_FAILED | ||||
|     } | ||||
|  | ||||
|     override fun notifyStateChanged() { | ||||
|         notifyPropertyChanged(BR.loading) | ||||
|         notifyPropertyChanged(BR.loaded) | ||||
|         notifyPropertyChanged(BR.loadingFailed) | ||||
|     } | ||||
|  | ||||
|     enum class State { | ||||
|         LOADED, LOADING, LOADING_FAILED | ||||
|     } | ||||
|  | ||||
|     //region Rx | ||||
|     protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) = | ||||
|         doOnSubscribe { viewModel.state = State.LOADING } | ||||
|             .doOnError { viewModel.state = State.LOADING_FAILED } | ||||
|             .doOnNext { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|  | ||||
|     protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) = | ||||
|         doOnSubscribe { viewModel.state = State.LOADING } | ||||
|             .doOnError { viewModel.state = State.LOADING_FAILED } | ||||
|             .doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|  | ||||
|     protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) = | ||||
|         doOnSubscribe { viewModel.state = State.LOADING } | ||||
|             .doOnError { viewModel.state = State.LOADING_FAILED } | ||||
|             .doOnComplete { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|             .doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|  | ||||
|     protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) = | ||||
|         doOnSubscribe { viewModel.state = State.LOADING } | ||||
|             .doOnError { viewModel.state = State.LOADING_FAILED } | ||||
|             .doOnNext { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|  | ||||
|     protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) = | ||||
|         doOnSubscribe { viewModel.state = State.LOADING } | ||||
|             .doOnError { viewModel.state = State.LOADING_FAILED } | ||||
|             .doOnComplete { if (allowFinishing) viewModel.state = State.LOADED } | ||||
|     //endregion | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| package com.topjohnwu.magisk.base.viewmodel | ||||
|  | ||||
| import androidx.databinding.Observable | ||||
| import androidx.databinding.PropertyChangeRegistry | ||||
| import androidx.lifecycle.ViewModel | ||||
|  | ||||
| /** | ||||
|  * Copy of [android.databinding.BaseObservable] which extends [ViewModel] | ||||
|  */ | ||||
| abstract class ObservableViewModel : TeanityViewModel(), Observable { | ||||
|  | ||||
|     @Transient | ||||
|     private var callbacks: PropertyChangeRegistry? = null | ||||
|  | ||||
|     @Synchronized | ||||
|     override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) { | ||||
|         if (callbacks == null) { | ||||
|             callbacks = PropertyChangeRegistry() | ||||
|         } | ||||
|         callbacks?.add(callback) | ||||
|     } | ||||
|  | ||||
|     @Synchronized | ||||
|     override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) { | ||||
|         callbacks?.remove(callback) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notifies listeners that all properties of this instance have changed. | ||||
|      */ | ||||
|     @Synchronized | ||||
|     fun notifyChange() { | ||||
|         callbacks?.notifyCallbacks(this, 0, null) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notifies listeners that a specific property has changed. The getter for the property | ||||
|      * that changes should be marked with [android.databinding.Bindable] to generate a field in | ||||
|      * `BR` to be used as `fieldId`. | ||||
|      * | ||||
|      * @param fieldId The generated BR id for the Bindable field. | ||||
|      */ | ||||
|     fun notifyPropertyChanged(fieldId: Int) { | ||||
|         callbacks?.notifyCallbacks(this, fieldId, null) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package com.topjohnwu.magisk.base.viewmodel | ||||
|  | ||||
| abstract class StatefulViewModel<State : Enum<*>>( | ||||
|     val defaultState: State | ||||
| ) : ObservableViewModel() { | ||||
|  | ||||
|     var state: State = defaultState | ||||
|         set(value) { | ||||
|             field = value | ||||
|             notifyStateChanged() | ||||
|         } | ||||
|  | ||||
|     open fun notifyStateChanged() = Unit | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,33 @@ | ||||
| package com.topjohnwu.magisk.base.viewmodel | ||||
|  | ||||
| import androidx.lifecycle.LiveData | ||||
| import androidx.lifecycle.MutableLiveData | ||||
| import androidx.lifecycle.ViewModel | ||||
| import com.topjohnwu.magisk.model.events.SimpleViewEvent | ||||
| import com.topjohnwu.magisk.model.events.ViewEvent | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import io.reactivex.disposables.Disposable | ||||
|  | ||||
| abstract class TeanityViewModel : ViewModel() { | ||||
|  | ||||
|     private val disposables = CompositeDisposable() | ||||
|     private val _viewEvents = MutableLiveData<ViewEvent>() | ||||
|     val viewEvents: LiveData<ViewEvent> get() = _viewEvents | ||||
|  | ||||
|     override fun onCleared() { | ||||
|         super.onCleared() | ||||
|         disposables.clear() | ||||
|     } | ||||
|  | ||||
|     fun <Event : ViewEvent> Event.publish() { | ||||
|         _viewEvents.value = this | ||||
|     } | ||||
|  | ||||
|     fun Int.publish() { | ||||
|         _viewEvents.value = SimpleViewEvent(this) | ||||
|     } | ||||
|  | ||||
|     fun Disposable.add() { | ||||
|         disposables.add(this) | ||||
|     } | ||||
| } | ||||
| @@ -19,10 +19,10 @@ interface GithubRawServices { | ||||
|     @GET("$MAGISK_FILES/master/beta.json") | ||||
|     fun fetchBetaUpdate(): Single<UpdateInfo> | ||||
|  | ||||
|     @GET("$MAGISK_FILES/master/canary_builds/release.json") | ||||
|     @GET("$MAGISK_FILES/canary/release.json") | ||||
|     fun fetchCanaryUpdate(): Single<UpdateInfo> | ||||
|  | ||||
|     @GET("$MAGISK_FILES/master/canary_builds/canary.json") | ||||
|     @GET("$MAGISK_FILES/canary/debug.json") | ||||
|     fun fetchCanaryDebugUpdate(): Single<UpdateInfo> | ||||
|  | ||||
|     @GET | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.topjohnwu.magisk.databinding | ||||
|  | ||||
| import android.view.View | ||||
| import androidx.core.view.isGone | ||||
| import androidx.core.view.isInvisible | ||||
| import androidx.databinding.BindingAdapter | ||||
|  | ||||
| @BindingAdapter("gone") | ||||
| fun setGone(view: View, gone: Boolean) { | ||||
|     view.isGone = gone | ||||
| } | ||||
|  | ||||
| @BindingAdapter("invisible") | ||||
| fun setInvisible(view: View, invisible: Boolean) { | ||||
|     view.isInvisible = invisible | ||||
| } | ||||
|  | ||||
| @BindingAdapter("goneUnless") | ||||
| fun setGoneUnless(view: View, goneUnless: Boolean) { | ||||
|     setGone(view, goneUnless.not()) | ||||
| } | ||||
|  | ||||
| @BindingAdapter("invisibleUnless") | ||||
| fun setInvisibleUnless(view: View, invisibleUnless: Boolean) { | ||||
|     setInvisible(view, invisibleUnless.not()) | ||||
| } | ||||
| @@ -0,0 +1,57 @@ | ||||
| package com.topjohnwu.magisk.databinding | ||||
|  | ||||
| import android.graphics.drawable.GradientDrawable | ||||
| import android.graphics.drawable.InsetDrawable | ||||
| import androidx.databinding.BindingAdapter | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.topjohnwu.magisk.extensions.startEndToLeftRight | ||||
| import com.topjohnwu.magisk.extensions.toPx | ||||
| import com.topjohnwu.magisk.utils.KItemDecoration | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| @BindingAdapter( | ||||
|     "dividerColor", | ||||
|     "dividerHorizontal", | ||||
|     "dividerSize", | ||||
|     "dividerAfterLast", | ||||
|     "dividerMarginStart", | ||||
|     "dividerMarginEnd", | ||||
|     "dividerMarginTop", | ||||
|     "dividerMarginBottom", | ||||
|     requireAll = false | ||||
| ) | ||||
| fun setDivider( | ||||
|     view: RecyclerView, | ||||
|     color: Int, | ||||
|     horizontal: Boolean, | ||||
|     _size: Float, | ||||
|     _afterLast: Boolean?, | ||||
|     marginStartF: Float, | ||||
|     marginEndF: Float, | ||||
|     marginTopF: Float, | ||||
|     marginBottomF: Float | ||||
| ) { | ||||
|     val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL | ||||
|     val size = if (_size > 0) _size.roundToInt() else 1.toPx() | ||||
|     val (width, height) = if (horizontal) size to 1 else 1 to size | ||||
|     val afterLast = _afterLast ?: true | ||||
|  | ||||
|     val marginStart = marginStartF.roundToInt() | ||||
|     val marginEnd = marginEndF.roundToInt() | ||||
|     val marginTop = marginTopF.roundToInt() | ||||
|     val marginBottom = marginBottomF.roundToInt() | ||||
|     val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd) | ||||
|  | ||||
|     val drawable = GradientDrawable().apply { | ||||
|         setSize(width, height) | ||||
|         shape = GradientDrawable.RECTANGLE | ||||
|         setColor(color) | ||||
|     }.let { | ||||
|         InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom) | ||||
|     } | ||||
|  | ||||
|     val decoration = KItemDecoration(view.context, orientation) | ||||
|         .setDeco(drawable) | ||||
|         .apply { showAfterLast = afterLast } | ||||
|     view.addItemDecoration(decoration) | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| package com.topjohnwu.magisk.databinding | ||||
|  | ||||
| import androidx.databinding.ViewDataBinding | ||||
| import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter | ||||
|  | ||||
| open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() { | ||||
|  | ||||
|     override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) { | ||||
|         super.onBindBinding(binding, variableId, layoutRes, position, item) | ||||
|  | ||||
|         item.onBindingBound(binding) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package com.topjohnwu.magisk.databinding | ||||
|  | ||||
| import androidx.annotation.CallSuper | ||||
| import androidx.databinding.ViewDataBinding | ||||
| import com.topjohnwu.magisk.BR | ||||
| import com.topjohnwu.magisk.utils.DiffObservableList | ||||
| import me.tatarka.bindingcollectionadapter2.ItemBinding | ||||
|  | ||||
| abstract class RvItem { | ||||
|  | ||||
|     abstract val layoutRes: Int | ||||
|  | ||||
|     @CallSuper | ||||
|     open fun bind(binding: ItemBinding<*>) { | ||||
|         binding.set(BR.item, layoutRes) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This callback is useful if you want to manipulate your views directly. | ||||
|      * If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter] | ||||
|      * on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience. | ||||
|      */ | ||||
|     open fun onBindingBound(binding: ViewDataBinding) {} | ||||
| } | ||||
|  | ||||
| abstract class ComparableRvItem<in T> : RvItem() { | ||||
|  | ||||
|     abstract fun itemSameAs(other: T): Boolean | ||||
|     abstract fun contentSameAs(other: T): Boolean | ||||
|     @Suppress("UNCHECKED_CAST") | ||||
|     open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T) | ||||
|     @Suppress("UNCHECKED_CAST") | ||||
|     open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T) | ||||
|  | ||||
|     companion object { | ||||
|         val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> { | ||||
|             override fun areItemsTheSame( | ||||
|                 oldItem: ComparableRvItem<*>, | ||||
|                 newItem: ComparableRvItem<*> | ||||
|             ) = oldItem.genericItemSameAs(newItem) | ||||
|  | ||||
|             override fun areContentsTheSame( | ||||
|                 oldItem: ComparableRvItem<*>, | ||||
|                 newItem: ComparableRvItem<*> | ||||
|             ) = oldItem.genericContentSameAs(newItem) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ import android.content.Context | ||||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import androidx.preference.PreferenceManager | ||||
| import com.skoumal.teanity.rxbus.RxBus | ||||
| import com.topjohnwu.magisk.utils.RxBus | ||||
| import org.koin.core.qualifier.named | ||||
| import org.koin.dsl.module | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,18 @@ | ||||
| package com.topjohnwu.magisk.di | ||||
|  | ||||
| import android.content.Context | ||||
| import com.squareup.moshi.JsonAdapter | ||||
| import com.squareup.moshi.Moshi | ||||
| import com.topjohnwu.magisk.BuildConfig | ||||
| import com.topjohnwu.magisk.Const | ||||
| import com.topjohnwu.magisk.data.network.GithubApiServices | ||||
| import com.topjohnwu.magisk.data.network.GithubRawServices | ||||
| import com.topjohnwu.magisk.net.Networking | ||||
| import com.topjohnwu.magisk.net.NoSSLv3SocketFactory | ||||
| import io.noties.markwon.Markwon | ||||
| import io.noties.markwon.html.HtmlPlugin | ||||
| import io.noties.markwon.image.ImagesPlugin | ||||
| import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.logging.HttpLoggingInterceptor | ||||
| import org.koin.dsl.module | ||||
| @@ -16,14 +23,15 @@ import retrofit2.converter.scalars.ScalarsConverterFactory | ||||
| import se.ansman.kotshi.KotshiJsonAdapterFactory | ||||
|  | ||||
| val networkingModule = module { | ||||
|     single { createOkHttpClient() } | ||||
|     single { createMoshiConverterFactory() } | ||||
|     single { createRetrofit(get(), get()) } | ||||
|     single { createOkHttpClient(get()) } | ||||
|     single { createRetrofit(get()) } | ||||
|     single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) } | ||||
|     single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) } | ||||
|     single { createMarkwon(get(), get()) } | ||||
| } | ||||
|  | ||||
| fun createOkHttpClient(): OkHttpClient { | ||||
| @Suppress("DEPRECATION") | ||||
| fun createOkHttpClient(context: Context): OkHttpClient { | ||||
|     val builder = OkHttpClient.Builder() | ||||
|  | ||||
|     if (BuildConfig.DEBUG) { | ||||
| @@ -33,6 +41,10 @@ fun createOkHttpClient(): OkHttpClient { | ||||
|         builder.addInterceptor(httpLoggingInterceptor) | ||||
|     } | ||||
|  | ||||
|     if (!Networking.init(context)) { | ||||
|         builder.sslSocketFactory(NoSSLv3SocketFactory()) | ||||
|     } | ||||
|  | ||||
|     return builder.build() | ||||
| } | ||||
|  | ||||
| @@ -43,13 +55,10 @@ fun createMoshiConverterFactory(): MoshiConverterFactory { | ||||
|     return MoshiConverterFactory.create(moshi) | ||||
| } | ||||
|  | ||||
| fun createRetrofit( | ||||
|     okHttpClient: OkHttpClient, | ||||
|     converterFactory: MoshiConverterFactory | ||||
| ): Retrofit.Builder { | ||||
| fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder { | ||||
|     return Retrofit.Builder() | ||||
|         .addConverterFactory(ScalarsConverterFactory.create()) | ||||
|         .addConverterFactory(converterFactory) | ||||
|         .addConverterFactory(createMoshiConverterFactory()) | ||||
|         .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) | ||||
|         .client(okHttpClient) | ||||
| } | ||||
| @@ -62,4 +71,13 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU | ||||
|         .baseUrl(baseUrl) | ||||
|         .build() | ||||
|         .create(T::class.java) | ||||
| } | ||||
| } | ||||
|  | ||||
| fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon { | ||||
|     return Markwon.builder(context) | ||||
|             .usePlugin(HtmlPlugin.create()) | ||||
|             .usePlugin(ImagesPlugin.create { | ||||
|                 it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient)) | ||||
|             }) | ||||
|             .build() | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,57 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import androidx.databinding.Observable | ||||
| import androidx.databinding.ObservableBoolean | ||||
| import androidx.databinding.ObservableField | ||||
| import androidx.databinding.ObservableInt | ||||
|  | ||||
| fun <T> ObservableField<T>.addOnPropertyChangedCallback( | ||||
|     removeAfterChanged: Boolean = false, | ||||
|     callback: (T?) -> Unit | ||||
| ) { | ||||
|     addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { | ||||
|         override fun onPropertyChanged(sender: Observable?, propertyId: Int) { | ||||
|             callback(get()) | ||||
|             if (removeAfterChanged) removeOnPropertyChangedCallback(this) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fun ObservableInt.addOnPropertyChangedCallback( | ||||
|     removeAfterChanged: Boolean = false, | ||||
|     callback: (Int) -> Unit | ||||
| ) { | ||||
|     addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { | ||||
|         override fun onPropertyChanged(sender: Observable?, propertyId: Int) { | ||||
|             callback(get()) | ||||
|             if (removeAfterChanged) removeOnPropertyChangedCallback(this) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fun ObservableBoolean.addOnPropertyChangedCallback( | ||||
|     removeAfterChanged: Boolean = false, | ||||
|     callback: (Boolean) -> Unit | ||||
| ) { | ||||
|     addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { | ||||
|         override fun onPropertyChanged(sender: Observable?, propertyId: Int) { | ||||
|             callback(get()) | ||||
|             if (removeAfterChanged) removeOnPropertyChangedCallback(this) | ||||
|         } | ||||
|     }) | ||||
| } | ||||
|  | ||||
| inline fun <T> ObservableField<T>.update(block: (T?) -> Unit) { | ||||
|     set(get().apply(block)) | ||||
| } | ||||
|  | ||||
| inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) { | ||||
|     update { | ||||
|         it ?: return@update | ||||
|         block(it) | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun ObservableInt.update(block: (Int) -> Unit) { | ||||
|     set(get().apply(block)) | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import android.content.res.Resources | ||||
| import kotlin.math.ceil | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt() | ||||
|  | ||||
| fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt() | ||||
| @@ -0,0 +1,6 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
|  | ||||
| fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body) | ||||
							
								
								
									
										201
									
								
								app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import androidx.databinding.ObservableField | ||||
| import com.topjohnwu.magisk.utils.KObservableField | ||||
| import io.reactivex.* | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.disposables.Disposables | ||||
| import io.reactivex.functions.BiFunction | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import androidx.databinding.Observable as BindingObservable | ||||
|  | ||||
| fun <T> Observable<T>.applySchedulers( | ||||
|     subscribeOn: Scheduler = Schedulers.io(), | ||||
|     observeOn: Scheduler = AndroidSchedulers.mainThread() | ||||
| ): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn) | ||||
|  | ||||
| fun <T> Flowable<T>.applySchedulers( | ||||
|     subscribeOn: Scheduler = Schedulers.io(), | ||||
|     observeOn: Scheduler = AndroidSchedulers.mainThread() | ||||
| ): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn) | ||||
|  | ||||
| fun <T> Single<T>.applySchedulers( | ||||
|     subscribeOn: Scheduler = Schedulers.io(), | ||||
|     observeOn: Scheduler = AndroidSchedulers.mainThread() | ||||
| ): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn) | ||||
|  | ||||
| fun <T> Maybe<T>.applySchedulers( | ||||
|     subscribeOn: Scheduler = Schedulers.io(), | ||||
|     observeOn: Scheduler = AndroidSchedulers.mainThread() | ||||
| ): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn) | ||||
|  | ||||
| fun Completable.applySchedulers( | ||||
|     subscribeOn: Scheduler = Schedulers.io(), | ||||
|     observeOn: Scheduler = AndroidSchedulers.mainThread() | ||||
| ): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn) | ||||
|  | ||||
| /*=== ALIASES FOR OBSERVABLES ===*/ | ||||
|  | ||||
| typealias OnCompleteListener = () -> Unit | ||||
| typealias OnSuccessListener<T> = (T) -> Unit | ||||
| typealias OnErrorListener = (Throwable) -> Unit | ||||
|  | ||||
| /*=== ALIASES FOR OBSERVABLES ===*/ | ||||
|  | ||||
| fun <T> Observable<T>.subscribeK( | ||||
|         onError: OnErrorListener = { it.printStackTrace() }, | ||||
|         onComplete: OnCompleteListener = {}, | ||||
|         onNext: OnSuccessListener<T> = {} | ||||
| ) = applySchedulers() | ||||
|     .subscribe(onNext, onError, onComplete) | ||||
|  | ||||
| fun <T> Single<T>.subscribeK( | ||||
|         onError: OnErrorListener = { it.printStackTrace() }, | ||||
|         onNext: OnSuccessListener<T> = {} | ||||
| ) = applySchedulers() | ||||
|     .subscribe(onNext, onError) | ||||
|  | ||||
| fun <T> Maybe<T>.subscribeK( | ||||
|         onError: OnErrorListener = { it.printStackTrace() }, | ||||
|         onComplete: OnCompleteListener = {}, | ||||
|         onNext: OnSuccessListener<T> = {} | ||||
| ) = applySchedulers() | ||||
|     .subscribe(onNext, onError, onComplete) | ||||
|  | ||||
| fun <T> Flowable<T>.subscribeK( | ||||
|         onError: OnErrorListener = { it.printStackTrace() }, | ||||
|         onComplete: OnCompleteListener = {}, | ||||
|         onNext: OnSuccessListener<T> = {} | ||||
| ) = applySchedulers() | ||||
|     .subscribe(onNext, onError, onComplete) | ||||
|  | ||||
| fun Completable.subscribeK( | ||||
|         onError: OnErrorListener = { it.printStackTrace() }, | ||||
|         onComplete: OnCompleteListener = {} | ||||
| ) = applySchedulers() | ||||
|     .subscribe(onComplete, onError) | ||||
|  | ||||
|  | ||||
| fun <T> Observable<out T>.updateBy( | ||||
|     field: KObservableField<T?> | ||||
| ) = doOnNextUi { field.value = it } | ||||
|     .doOnErrorUi { field.value = null } | ||||
|  | ||||
| fun <T> Single<out T>.updateBy( | ||||
|     field: KObservableField<T?> | ||||
| ) = doOnSuccessUi { field.value = it } | ||||
|     .doOnErrorUi { field.value = null } | ||||
|  | ||||
| fun <T> Maybe<out T>.updateBy( | ||||
|     field: KObservableField<T?> | ||||
| ) = doOnSuccessUi { field.value = it } | ||||
|     .doOnErrorUi { field.value = null } | ||||
|     .doOnComplete { field.value = field.value } | ||||
|  | ||||
| fun <T> Flowable<out T>.updateBy( | ||||
|     field: KObservableField<T?> | ||||
| ) = doOnNextUi { field.value = it } | ||||
|     .doOnErrorUi { field.value = null } | ||||
|  | ||||
| fun Completable.updateBy( | ||||
|     field: KObservableField<Boolean> | ||||
| ) = doOnCompleteUi { field.value = true } | ||||
|     .doOnErrorUi { field.value = false } | ||||
|  | ||||
|  | ||||
| fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) = | ||||
|     doOnSubscribe { ui { body() } } | ||||
|  | ||||
| fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) = | ||||
|     doOnSubscribe { ui { body() } } | ||||
|  | ||||
| fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) = | ||||
|     doOnSubscribe { ui { body() } } | ||||
|  | ||||
| fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) = | ||||
|     doOnSubscribe { ui { body() } } | ||||
|  | ||||
| fun Completable.doOnSubscribeUi(body: () -> Unit) = | ||||
|     doOnSubscribe { ui { body() } } | ||||
|  | ||||
|  | ||||
| fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) = | ||||
|     doOnError { ui { body(it) } } | ||||
|  | ||||
| fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) = | ||||
|     doOnError { ui { body(it) } } | ||||
|  | ||||
| fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) = | ||||
|     doOnError { ui { body(it) } } | ||||
|  | ||||
| fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) = | ||||
|     doOnError { ui { body(it) } } | ||||
|  | ||||
| fun Completable.doOnErrorUi(body: (Throwable) -> Unit) = | ||||
|     doOnError { ui { body(it) } } | ||||
|  | ||||
|  | ||||
| fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) = | ||||
|     doOnNext { ui { body(it) } } | ||||
|  | ||||
| fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) = | ||||
|     doOnNext { ui { body(it) } } | ||||
|  | ||||
| fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) = | ||||
|     doOnSuccess { ui { body(it) } } | ||||
|  | ||||
| fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) = | ||||
|     doOnSuccess { ui { body(it) } } | ||||
|  | ||||
| fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) = | ||||
|     doOnComplete { ui { body() } } | ||||
|  | ||||
| fun Completable.doOnCompleteUi(body: () -> Unit) = | ||||
|     doOnComplete { ui { body() } } | ||||
|  | ||||
|  | ||||
| fun <T, R> Observable<List<T>>.mapList( | ||||
|     transformer: (T) -> R | ||||
| ) = flatMapIterable { it } | ||||
|     .map(transformer) | ||||
|     .toList() | ||||
|  | ||||
| fun <T, R> Single<List<T>>.mapList( | ||||
|     transformer: (T) -> R | ||||
| ) = flattenAsFlowable { it } | ||||
|     .map(transformer) | ||||
|     .toList() | ||||
|  | ||||
| fun <T, R> Maybe<List<T>>.mapList( | ||||
|     transformer: (T) -> R | ||||
| ) = flattenAsFlowable { it } | ||||
|     .map(transformer) | ||||
|     .toList() | ||||
|  | ||||
| fun <T, R> Flowable<List<T>>.mapList( | ||||
|     transformer: (T) -> R | ||||
| ) = flatMapIterable { it } | ||||
|     .map(transformer) | ||||
|     .toList() | ||||
|  | ||||
| fun <T> ObservableField<T>.toObservable(): Observable<T> { | ||||
|     val observableField = this | ||||
|     return Observable.create { emitter -> | ||||
|         observableField.get()?.let { emitter.onNext(it) } | ||||
|  | ||||
|         val callback = object : BindingObservable.OnPropertyChangedCallback() { | ||||
|             override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) { | ||||
|                 observableField.get()?.let { emitter.onNext(it) } | ||||
|             } | ||||
|         } | ||||
|         observableField.addOnPropertyChangedCallback(callback) | ||||
|         emitter.setDisposable(Disposables.fromAction { | ||||
|             observableField.removeOnPropertyChangedCallback(callback) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <T : Any> T.toSingle() = Single.just(this) | ||||
|  | ||||
| fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) = | ||||
|         Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) }) | ||||
							
								
								
									
										126
									
								
								app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import android.content.Context | ||||
| import android.content.res.ColorStateList | ||||
| import android.view.View | ||||
| import android.widget.TextView | ||||
| import androidx.annotation.ColorInt | ||||
| import androidx.annotation.ColorRes | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.view.ViewCompat | ||||
| import androidx.fragment.app.Fragment | ||||
| import com.google.android.material.snackbar.Snackbar | ||||
|  | ||||
| fun AppCompatActivity.snackbar( | ||||
|     view: View, | ||||
|     @StringRes messageRes: Int, | ||||
|     length: Int = Snackbar.LENGTH_SHORT, | ||||
|     f: Snackbar.() -> Unit = {} | ||||
| ) { | ||||
|     snackbar(view, getString(messageRes), length, f) | ||||
| } | ||||
|  | ||||
| fun AppCompatActivity.snackbar( | ||||
|     view: View, | ||||
|     message: String, | ||||
|     length: Int = Snackbar.LENGTH_SHORT, | ||||
|     f: Snackbar.() -> Unit = {} | ||||
| ) = Snackbar.make(view, message, length) | ||||
|     .apply(f) | ||||
|     .show() | ||||
|  | ||||
| fun Fragment.snackbar( | ||||
|     view: View, | ||||
|     @StringRes messageRes: Int, | ||||
|     length: Int = Snackbar.LENGTH_SHORT, | ||||
|     f: Snackbar.() -> Unit = {} | ||||
| ) { | ||||
|     snackbar(view, getString(messageRes), length, f) | ||||
| } | ||||
|  | ||||
| fun Fragment.snackbar( | ||||
|     view: View, | ||||
|     message: String, | ||||
|     length: Int = Snackbar.LENGTH_SHORT, | ||||
|     f: Snackbar.() -> Unit = {} | ||||
| ) = Snackbar.make(view, message, length) | ||||
|     .apply(f) | ||||
|     .show() | ||||
|  | ||||
| fun Snackbar.action(init: KSnackbar.() -> Unit) = apply { | ||||
|     val config = KSnackbar().apply(init) | ||||
|  | ||||
|     setAction(config.title(context), config.onClickListener) | ||||
|  | ||||
|     when { | ||||
|         config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply) | ||||
|         config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply) | ||||
|     } | ||||
| } | ||||
|  | ||||
| class KSnackbar { | ||||
|     var colorRes: Int = -1 | ||||
|     var colorStateListRes: Int = -1 | ||||
|  | ||||
|     var title: CharSequence = "" | ||||
|     var titleRes: Int = -1 | ||||
|  | ||||
|     internal var onClickListener: (View) -> Unit = {} | ||||
|     internal val hasValidColor get() = colorRes != -1 | ||||
|     internal val hasValidColorStateList get() = colorStateListRes != -1 | ||||
|  | ||||
|     fun onClicked(listener: (View) -> Unit) { | ||||
|         onClickListener = listener | ||||
|     } | ||||
|  | ||||
|     internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title | ||||
|     internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes) | ||||
|     internal fun color(context: Context) = context.colorCompat(colorRes) | ||||
| } | ||||
|  | ||||
| @Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}")) | ||||
| fun Snackbar.action( | ||||
|     @StringRes actionRes: Int, | ||||
|     @ColorRes colorRes: Int? = null, | ||||
|     listener: (View) -> Unit | ||||
| ) { | ||||
|     view.resources.getString(actionRes) | ||||
|     colorRes?.let { ContextCompat.getColor(view.context, colorRes) } | ||||
|     action {} | ||||
| } | ||||
|  | ||||
| @Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}")) | ||||
| fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) { | ||||
|     setAction(action, listener) | ||||
|     color?.let { setActionTextColor(color) } | ||||
| } | ||||
|  | ||||
| fun Snackbar.textColorRes(@ColorRes colorRes: Int) { | ||||
|     textColor(context.colorCompat(colorRes) ?: return) | ||||
| } | ||||
|  | ||||
| fun Snackbar.textColor(@ColorInt color: Int) { | ||||
|     val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text) | ||||
|     tv.setTextColor(color) | ||||
| } | ||||
|  | ||||
| fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) { | ||||
|     backgroundColor(context.colorCompat(colorRes) ?: return) | ||||
| } | ||||
|  | ||||
| fun Snackbar.backgroundColor(@ColorInt color: Int) { | ||||
|     ViewCompat.setBackgroundTintList( | ||||
|         view, | ||||
|         ColorStateList.valueOf(color) | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Snackbar.alert() { | ||||
|     textColor(0xF44336) | ||||
| } | ||||
|  | ||||
| fun Snackbar.success() { | ||||
|     textColor(0x4CAF50) | ||||
| } | ||||
| @@ -8,10 +8,18 @@ import android.content.pm.PackageInfo | ||||
| import android.content.pm.PackageManager | ||||
| import android.content.pm.PackageManager.* | ||||
| import android.content.res.Configuration | ||||
| import android.content.res.Resources | ||||
| import android.database.Cursor | ||||
| import android.net.Uri | ||||
| import android.os.Build | ||||
| import android.provider.OpenableColumns | ||||
| import android.view.View | ||||
| import androidx.annotation.ColorRes | ||||
| import androidx.annotation.DrawableRes | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.net.toUri | ||||
| import com.topjohnwu.magisk.utils.FileProvider | ||||
| import com.topjohnwu.magisk.utils.Utils | ||||
| import com.topjohnwu.magisk.utils.currentLocale | ||||
| import java.io.File | ||||
| import java.io.FileNotFoundException | ||||
| @@ -119,3 +127,33 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String { | ||||
|  | ||||
|     return loadLabel(pm).toString() | ||||
| } | ||||
|  | ||||
| fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null | ||||
|  | ||||
| fun Context.colorCompat(@ColorRes id: Int) = try { | ||||
|     ContextCompat.getColor(this, id) | ||||
| } catch (e: Resources.NotFoundException) { | ||||
|     null | ||||
| } | ||||
|  | ||||
| fun Context.colorStateListCompat(@ColorRes id: Int) = try { | ||||
|     ContextCompat.getColorStateList(this, id) | ||||
| } catch (e: Resources.NotFoundException) { | ||||
|     null | ||||
| } | ||||
|  | ||||
| fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id) | ||||
| /** | ||||
|  * Pass [start] and [end] dimensions, function will return left and right | ||||
|  * with respect to RTL layout direction | ||||
|  */ | ||||
| fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> { | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && | ||||
|         resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL | ||||
|     ) { | ||||
|         return end to start | ||||
|     } | ||||
|     return start to end | ||||
| } | ||||
|  | ||||
| fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri()) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import com.skoumal.teanity.util.KObservableField | ||||
| import com.topjohnwu.magisk.utils.KObservableField | ||||
|  | ||||
|  | ||||
| fun KObservableField<Boolean>.toggle() { | ||||
|   | ||||
| @@ -2,8 +2,7 @@ package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import androidx.collection.SparseArrayCompat | ||||
| import androidx.databinding.ObservableList | ||||
| import com.skoumal.teanity.extensions.subscribeK | ||||
| import com.skoumal.teanity.util.DiffObservableList | ||||
| import com.topjohnwu.magisk.utils.DiffObservableList | ||||
| import io.reactivex.disposables.Disposable | ||||
|  | ||||
| fun <T> MutableList<T>.update(newList: List<T>) { | ||||
| @@ -26,8 +25,8 @@ fun List<String>.toShellCmd(): String { | ||||
| } | ||||
|  | ||||
| fun <T1, T2> ObservableList<T1>.sendUpdatesTo( | ||||
|     target: DiffObservableList<T2>, | ||||
|     mapper: (List<T1>) -> List<T2> | ||||
|         target: DiffObservableList<T2>, | ||||
|         mapper: (List<T1>) -> List<T2> | ||||
| ) = addOnListChangedCallback(object : | ||||
|     ObservableList.OnListChangedCallback<ObservableList<T1>>() { | ||||
|     override fun onChanged(sender: ObservableList<T1>?) { | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| package com.topjohnwu.magisk.extensions | ||||
|  | ||||
| import io.reactivex.Single | ||||
| import io.reactivex.functions.BiFunction | ||||
|  | ||||
| fun <T : Any> T.toSingle() = Single.just(this) | ||||
|  | ||||
| fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) = | ||||
|     Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) }) | ||||
| @@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.binding | ||||
|  | ||||
| import androidx.databinding.ViewDataBinding | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.skoumal.teanity.databinding.ComparableRvItem | ||||
| import com.topjohnwu.magisk.databinding.ComparableRvItem | ||||
| import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem | ||||
| import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat | ||||
| import com.topjohnwu.magisk.ClassMap | ||||
| import com.topjohnwu.magisk.R | ||||
| import com.topjohnwu.magisk.extensions.chooser | ||||
| import com.topjohnwu.magisk.extensions.exists | ||||
| import com.topjohnwu.magisk.extensions.provide | ||||
| import com.topjohnwu.magisk.model.entity.internal.Configuration.* | ||||
| import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary | ||||
| @@ -17,6 +18,7 @@ import com.topjohnwu.magisk.model.entity.internal.DownloadSubject | ||||
| import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* | ||||
| import com.topjohnwu.magisk.ui.flash.FlashActivity | ||||
| import com.topjohnwu.magisk.utils.APKInstall | ||||
| import org.koin.core.get | ||||
| import java.io.File | ||||
| import kotlin.random.Random.Default.nextInt | ||||
|  | ||||
| @@ -76,8 +78,14 @@ open class DownloadService : RemoteFileService() { | ||||
|  | ||||
|     private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk) | ||||
|     = when (val conf = subject.configuration) { | ||||
|         Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) | ||||
|             .addAction(0, R.string.download_open_self, fileIntent(subject.file)) | ||||
|         Download -> this.apply { | ||||
|             fileIntent(subject.file.parentFile!!) | ||||
|                 .takeIf { it.exists(get()) } | ||||
|                 ?.let { addAction(0, R.string.download_open_parent, it.chooser()) } | ||||
|             fileIntent(subject.file) | ||||
|                 .takeIf { it.exists(get()) } | ||||
|                 ?.let { addAction(0, R.string.download_open_self, it.chooser()) } | ||||
|         } | ||||
|         Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file)) | ||||
|         is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary)) | ||||
|         is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri)) | ||||
| @@ -86,8 +94,14 @@ open class DownloadService : RemoteFileService() { | ||||
|  | ||||
|     private fun NotificationCompat.Builder.addActionsInternal(subject: Module) | ||||
|     = when (subject.configuration) { | ||||
|         Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!)) | ||||
|             .addAction(0, R.string.download_open_self, fileIntent(subject.file)) | ||||
|         Download -> this.apply { | ||||
|             fileIntent(subject.file.parentFile!!) | ||||
|                 .takeIf { it.exists(get()) } | ||||
|                 ?.let { addAction(0, R.string.download_open_parent, it.chooser()) } | ||||
|             fileIntent(subject.file) | ||||
|                 .takeIf { it.exists(get()) } | ||||
|                 ?.let { addAction(0, R.string.download_open_self, it.chooser()) } | ||||
|         } | ||||
|         is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file)) | ||||
|         else -> this | ||||
|     } | ||||
| @@ -115,7 +129,6 @@ open class DownloadService : RemoteFileService() { | ||||
|             .setDataAndType(file.provide(this), file.type) | ||||
|             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|             .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             .chooser() | ||||
|     } | ||||
|  | ||||
|     class Builder { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user