You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MagiskModuleManager/app/src/main/java/com/fox2code/mmm/UpdateActivity.kt

412 lines
19 KiB
Kotlin

/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.core.content.FileProvider
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
import com.fox2code.foxcompat.app.FoxActivity
import com.fox2code.mmm.androidacy.AndroidacyRepoData
import com.fox2code.mmm.utils.io.net.Http
import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.textview.MaterialTextView
import org.json.JSONException
import org.matomo.sdk.extra.TrackHelper
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Objects
class UpdateActivity : FoxActivity() {
private var chgWv: WebView? = null
private var url: String = String()
@SuppressLint("RestrictedApi", "SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_update)
chgWv = findViewById(R.id.changelog_webview)
if (MainApplication.isMatomoAllowed()) {
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
}
val changelogWebView = chgWv!!
val webSettings = changelogWebView.settings
webSettings.userAgentString = Http.androidacyUA
val cookieManager = CookieManager.getInstance()
cookieManager.setAcceptCookie(true)
cookieManager.setAcceptThirdPartyCookies(changelogWebView, true)
webSettings.domStorageEnabled = true
webSettings.javaScriptEnabled = true
webSettings.cacheMode = WebSettings.LOAD_DEFAULT
webSettings.allowFileAccess = false
webSettings.allowContentAccess = false
webSettings.mediaPlaybackRequiresUserGesture = false
// enable webview debugging on debug builds
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
// if app is in dark mode, force dark mode on webview
if (MainApplication.INSTANCE!!.isDarkTheme) {
// for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings, true)
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDark(webSettings, WebSettingsCompat.FORCE_DARK_ON)
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDarkStrategy(
webSettings,
WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY
)
}
}
// Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) {
val allowList: MutableSet<String> = HashSet()
allowList.add("https://*.androidacy.com")
WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings, allowList)
}
// Get the progress bar and make it indeterminate for now
val progressIndicator = findViewById<LinearProgressIndicator>(R.id.update_progress)
progressIndicator.isIndeterminate = true
// get update_cancel item on bottom navigation
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_navigation)
val updateCancel =
bottomNavigationView.findViewById<BottomNavigationItemView>(R.id.update_cancel_button)
// get status text view
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text)
// set status text to please wait
statusTextView.setText(R.string.please_wait)
val updateThread: Thread = object : Thread() {
override fun run() {
// Now, parse the intent
val extras = intent.action
// if extras is null, then we are in a bad state or user launched the activity manually
if (extras == null) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.error_no_extras)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
return
}
// get action
val action = ACTIONS.valueOf(extras)
// if action is null, then we are in a bad state or user launched the activity manually
if (Objects.isNull(action)) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.error_no_action)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
// return
return
}
// For check action, we need to check if there is an update using the AppUpdateManager.peekShouldUpdate()
when (action) {
ACTIONS.CHECK -> {
checkForUpdate()
}
ACTIONS.DOWNLOAD -> {
try {
downloadUpdate()
} catch (e: JSONException) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.error_download_update)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
}
}
}
ACTIONS.INSTALL -> {
// ensure path was passed and points to a file within our cache directory. replace .. and url encoded characters
val path =
intent.getStringExtra("path")?.trim { it <= ' ' }
?.replace("\\.\\.".toRegex(), "")?.replace("%2e%2e".toRegex(), "")
if (path!!.isEmpty()) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.no_file_found)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
return
}
// check and sanitize file path
// path must be in our cache directory
if (!path.startsWith(cacheDir.absolutePath)) {
throw SecurityException("Path is not in cache directory: $path")
}
val file = File(path)
val parentFile = file.parentFile
try {
if (parentFile == null || !parentFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Path is not in cache directory: $path")
}
} catch (e: IOException) {
throw SecurityException("Path is not in cache directory: $path")
}
if (!file.exists()) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.no_file_found)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
// return
return
}
if (file.parentFile != cacheDir) {
// set status text to error
runOnUiThread {
statusTextView.setText(R.string.no_file_found)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
// return
return
}
// set status text to installing
statusTextView.setText(R.string.installing_update)
// set progress bar to indeterminate
progressIndicator.isIndeterminate = true
// install update
installUpdate(file)
}
}
}
}
// on click, finish the activity and anything running in it
updateCancel.setOnClickListener { _: View? ->
// end any download
updateThread.interrupt()
forceBackPressed()
finish()
}
updateThread.start()
}
@SuppressLint("RestrictedApi")
fun checkForUpdate() {
// get status text view
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text)
val progressIndicator = findViewById<LinearProgressIndicator>(R.id.update_progress)
runOnUiThread {
progressIndicator.isIndeterminate = true
// set status text to checking for update
statusTextView.setText(R.string.checking_for_update)
// set progress bar to indeterminate
progressIndicator.isIndeterminate = true
}
// check for update
val shouldUpdate = AppUpdateManager.appUpdateManager.checkUpdate(true)
var token = AndroidacyRepoData.token
if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
Timber.w("Invalid token, not checking for updates")
token = AndroidacyRepoData.getInstance().requestNewToken()
}
val deviceId = AndroidacyRepoData.generateDeviceId()
val clientId = BuildConfig.ANDROIDACY_CLIENT_ID
url = "https://production-api.androidacy.com/amm/updates/check?appVersionCode=${BuildConfig.VERSION_CODE}&token=$token&device_id=$deviceId&client_id=$clientId"
// if shouldUpdate is true, then we have an update
if (shouldUpdate) {
runOnUiThread {
// set status text to update available
statusTextView.setText(R.string.update_available)
// set button text to download
val button = findViewById<BottomNavigationItemView>(R.id.action_update)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
button.tooltipText = getString(R.string.download_update)
}
button.isEnabled = true
}
// return
} else {
runOnUiThread {
// set status text to no update available
statusTextView.setText(R.string.no_update_available)
val changelogWebView = chgWv!!
changelogWebView.loadUrl(url.replace("updates/check", "changelog"))
}
}
runOnUiThread {
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
}
return
}
@Throws(JSONException::class)
fun downloadUpdate() {
val progressIndicator = findViewById<LinearProgressIndicator>(R.id.update_progress)
runOnUiThread { progressIndicator.isIndeterminate = true }
// get status text view
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text)
var token = AndroidacyRepoData.token
if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
Timber.w("Invalid token, not checking for updates")
token = AndroidacyRepoData.getInstance().requestNewToken()
}
val deviceId = AndroidacyRepoData.generateDeviceId()
val clientId = BuildConfig.ANDROIDACY_CLIENT_ID
url = "https://production-api.androidacy.com/amm/updates/check?appVersionCode=${BuildConfig.VERSION_CODE}&token=$token&device_id=$deviceId&client_id=$clientId"
runOnUiThread {
val changelogWebView = chgWv!!
changelogWebView.loadUrl(url.replace("updates/check", "changelog"))
}
// get the download url
var downloadUrl = url.replace("check", "download")
// append arch to download url. coerce anything like arm64-* or aarch64-* to arm64 and anything like arm-* or armeabi-* to arm
downloadUrl += if (Build.SUPPORTED_ABIS[0].contains("arm64") || Build.SUPPORTED_ABIS[0].contains("aarch64")) {
"&arch=arm64"
} else if (Build.SUPPORTED_ABIS[0].contains("arm") || Build.SUPPORTED_ABIS[0].contains("armeabi")) {
"&arch=arm"
} else if (Build.SUPPORTED_ABIS[0].contains("x86_64")) {
"&arch=x86_64"
} else if (Build.SUPPORTED_ABIS[0].contains("x86")) {
"&arch=x86"
} else {
// assume universal and hope for the best, because we don't know what to do
Timber.w("Unknown arch ${Build.SUPPORTED_ABIS[0]} when downloading update, assuming universal")
"&arch=universal"
}
runOnUiThread {
// set status text to downloading update
statusTextView.text = getString(R.string.downloading_update, 0)
// set progress bar to 0
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(0, false)
}
// download the update
var update = ByteArray(0)
try {
update = Http.doHttpGet(downloadUrl) { downloaded: Int, total: Int, _: Boolean ->
runOnUiThread {
// update progress bar
progressIndicator.setProgressCompat(
(downloaded.toFloat() / total.toFloat() * 100).toInt(),
true
)
// update status text
statusTextView.text = getString(
R.string.downloading_update,
(downloaded.toFloat() / total.toFloat() * 100).toInt()
)
}
}
} catch (e: Exception) {
runOnUiThread {
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
statusTextView.setText(R.string.error_download_update)
}
}
// if update is null, then we are in a bad state
if (Objects.isNull(update)) {
runOnUiThread {
// set status text to error
statusTextView.setText(R.string.error_download_update)
// set progress bar to error
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
}
// return
return
}
// set status text to installing update
runOnUiThread {
statusTextView.setText(R.string.installing_update)
// set progress bar to 100
progressIndicator.isIndeterminate = true
progressIndicator.setProgressCompat(100, false)
}
// save the update to the cache
var updateFile: File? = null
var fileOutputStream: FileOutputStream? = null
try {
updateFile = File(cacheDir, "update.apk")
fileOutputStream = FileOutputStream(updateFile)
fileOutputStream.write(update)
} catch (e: IOException) {
runOnUiThread {
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
statusTextView.setText(R.string.error_download_update)
}
} finally {
if (Objects.nonNull(updateFile)) {
updateFile?.deleteOnExit()
}
try {
fileOutputStream?.close()
} catch (ignored: IOException) {
}
}
// install the update
installUpdate(updateFile)
// return
return
}
@SuppressLint("RestrictedApi")
private fun installUpdate(updateFile: File?) {
// get status text view
runOnUiThread {
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text)
// set status text to installing update
statusTextView.setText(R.string.installing_update)
// set progress bar to 100
val progressIndicator = findViewById<LinearProgressIndicator>(R.id.update_progress)
progressIndicator.isIndeterminate = true
progressIndicator.setProgressCompat(100, false)
}
// request install permissions
val intent = Intent(Intent.ACTION_VIEW)
val context = applicationContext
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.file-provider",
updateFile!!
)
intent.setDataAndTypeAndNormalize(uri, "application/vnd.android.package-archive")
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
// return
return
}
enum class ACTIONS {
// action can be CHECK, DOWNLOAD, INSTALL
CHECK, DOWNLOAD, INSTALL
}
}