|
|
|
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
|
|
|
|
}
|
|
|
|
}
|