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

386 lines
17 KiB
Kotlin

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 androidx.core.content.FileProvider
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 io.noties.markwon.Markwon
import org.json.JSONException
import org.json.JSONObject
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (MainApplication.isMatomoAllowed()) {
TrackHelper.track().screen(this).with(MainApplication.INSTANCE!!.tracker)
}
setContentView(R.layout.activity_update)
// 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)
// 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
// set changelog text. changelog could be markdown, so we need to convert it
val changelogTextView = findViewById<MaterialTextView>(R.id.update_changelog)
val markwon = Markwon.create(this@UpdateActivity)
markwon.setMarkdown(changelogTextView,
AppUpdateManager.appUpdateManager.changes.toString()
)
}
// return
} else {
runOnUiThread {
// set status text to no update available
statusTextView.setText(R.string.no_update_available)
// set changelog text. changelog could be markdown, so we need to convert it
val changelogTextView = findViewById<MaterialTextView>(R.id.update_changelog)
val markwon = Markwon.create(this@UpdateActivity)
markwon.setMarkdown(changelogTextView,
AppUpdateManager.appUpdateManager.changes.toString()
)
}
}
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)
val lastestJSON: ByteArray?
// get changelog from
// make a request to https://production-api.androidacy.com/ammm/updates/check with appVersionCode and token/device_id/client_id
// and the changelog is the json string in changelog
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
val url = "https://production-api.androidacy.com/ammm/updates/check?appVersionCode=${BuildConfig.VERSION_CODE}&token=$token&device_id=$deviceId&client_id=$clientId"
try {
lastestJSON = Http.doHttpGet(url, false)
} catch (e: Exception) {
// when logging, REMOVE the json from the log
Timber.e(e, "Error downloading update info")
runOnUiThread {
progressIndicator.isIndeterminate = false
progressIndicator.setProgressCompat(100, false)
statusTextView.setText(R.string.error_download_update)
}
return
}
// convert to JSON
val latestJSON = JSONObject(String(lastestJSON))
val changelog = latestJSON.getString("changelog")
runOnUiThread {
// set changelog text. changelog could be markdown, so we need to convert it
val changelogTextView = findViewById<MaterialTextView>(R.id.update_changelog)
val markwon = Markwon.create(this@UpdateActivity)
markwon.setMarkdown(changelogTextView, 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
}
}