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(R.id.update_progress) progressIndicator.isIndeterminate = true // get update_cancel item on bottom navigation val bottomNavigationView = findViewById(R.id.bottom_navigation) val updateCancel = bottomNavigationView.findViewById(R.id.update_cancel_button) // get status text view val statusTextView = findViewById(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(R.id.update_progress_text) val progressIndicator = findViewById(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(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(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(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(R.id.update_progress) runOnUiThread { progressIndicator.isIndeterminate = true } // get status text view val statusTextView = findViewById(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(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(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(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 } }