mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 19:42:20 +02:00
246 lines
No EOL
8.8 KiB
Kotlin
246 lines
No EOL
8.8 KiB
Kotlin
/*
|
|
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package io.timelimit.android.update
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.provider.Settings
|
|
import android.util.JsonReader
|
|
import android.util.Log
|
|
import android.widget.Toast
|
|
import androidx.fragment.app.FragmentActivity
|
|
import io.timelimit.android.BuildConfig
|
|
import io.timelimit.android.R
|
|
import io.timelimit.android.async.Threads
|
|
import io.timelimit.android.coroutines.executeAndWait
|
|
import io.timelimit.android.coroutines.waitForResponse
|
|
import io.timelimit.android.crypto.HexString
|
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
|
import io.timelimit.android.logic.DefaultAppLogic
|
|
import io.timelimit.android.sync.network.api.assertSuccess
|
|
import io.timelimit.android.sync.network.api.httpClient
|
|
import io.timelimit.android.ui.update.InstallUpdateDialogFragment
|
|
import okhttp3.Request
|
|
import okio.buffer
|
|
import okio.sink
|
|
import okio.source
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.security.MessageDigest
|
|
|
|
|
|
object UpdateIntegration {
|
|
private const val LOG_TAG = "UpdateIntegration"
|
|
|
|
private const val CERTIFICATE = BuildConfig.updateServerBuildsCertificate
|
|
const val CONFIG_URL = BuildConfig.updateServer
|
|
|
|
fun doesSupportUpdates(context: Context): Boolean {
|
|
if (CONFIG_URL.isEmpty() || BuildConfig.storeCompilant) {
|
|
return false
|
|
}
|
|
|
|
if (CERTIFICATE.isEmpty()) {
|
|
return true
|
|
}
|
|
|
|
val signatures = getApplicationSignature(context)
|
|
|
|
return signatures.find { it.equals(CERTIFICATE, ignoreCase = true) } != null
|
|
}
|
|
|
|
private fun getApplicationSignature(context: Context): List<String> {
|
|
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
// new signature
|
|
|
|
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNING_CERTIFICATES).signingInfo!!.apkContentsSigners
|
|
} else {
|
|
// old signature
|
|
// this is "unsafe", but it is not used for security features
|
|
|
|
context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures!!
|
|
}
|
|
|
|
return signatures.map { HexString.toHex(MessageDigest.getInstance("SHA-256").digest(it.toByteArray())) }
|
|
}
|
|
|
|
fun updateSaveFile(context: Context): File = File(context.filesDir, "update.apk")
|
|
|
|
suspend fun deleteUpdateFile(context: Context) {
|
|
Threads.update.executeAndWait {
|
|
updateSaveFile(context).delete()
|
|
}
|
|
}
|
|
|
|
private suspend fun isDownloadedFileValid(status: UpdateStatus, context: Context): Boolean = Threads.update.executeAndWait {
|
|
isDownloadedFileValidSync(status, context)
|
|
}
|
|
|
|
fun isDownloadedFileValidSync(status: UpdateStatus, context: Context): Boolean {
|
|
if (!doesSupportUpdates(context)) {
|
|
return false
|
|
}
|
|
|
|
return try {
|
|
val file = updateSaveFile(context)
|
|
val info = context.packageManager.getPackageArchiveInfo(file.absolutePath, 0)!!
|
|
|
|
if (
|
|
info.packageName != BuildConfig.APPLICATION_ID ||
|
|
info.versionCode != status.versionCode ||
|
|
info.versionCode < BuildConfig.VERSION_CODE
|
|
) {
|
|
// no need to check the signatures here, the package installer will do it
|
|
|
|
false
|
|
} else {
|
|
val digest = MessageDigest.getInstance("SHA-512")
|
|
|
|
val buffer = ByteArray(1024 * 1024)
|
|
|
|
file.inputStream().use { stream ->
|
|
while (true) {
|
|
val length = stream.read(buffer)
|
|
|
|
if (length < 0) {
|
|
break
|
|
} else {
|
|
digest.update(buffer, 0, length)
|
|
}
|
|
}
|
|
}
|
|
|
|
HexString.toHex(digest.digest()).equals(status.sha512, ignoreCase = true)
|
|
}
|
|
} catch (ex: Exception) {
|
|
if (BuildConfig.DEBUG) {
|
|
Log.w(LOG_TAG, "could not verify APK", ex)
|
|
}
|
|
|
|
false
|
|
}
|
|
}
|
|
|
|
suspend fun getUpdateStatus(context: Context): UpdateStatus {
|
|
if (!doesSupportUpdates(context)) {
|
|
throw IOException()
|
|
}
|
|
|
|
httpClient.newCall(
|
|
Request.Builder().url(CONFIG_URL).build()
|
|
).waitForResponse().use { response ->
|
|
response.assertSuccess()
|
|
|
|
return Threads.update.executeAndWait {
|
|
JsonReader(response.body!!.charStream()).use { reader ->
|
|
UpdateStatus.parse(reader)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun downloadAndVerifyUpdate(status: UpdateStatus, context: Context) {
|
|
if (isDownloadedFileValid(status, context)) {
|
|
return
|
|
}
|
|
|
|
httpClient.newCall(
|
|
Request.Builder().url(status.url).build()
|
|
).waitForResponse().use { response ->
|
|
response.assertSuccess()
|
|
|
|
Threads.update.executeAndWait {
|
|
response.body!!.source().use { source ->
|
|
updateSaveFile(context).sink().buffer().use { sink ->
|
|
sink.writeAll(source)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isDownloadedFileValid(status, context)) {
|
|
deleteUpdateFile(context)
|
|
|
|
throw IOException()
|
|
}
|
|
}
|
|
|
|
fun hasRequiredPermission(context: Context): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
|
context.packageManager.canRequestPackageInstalls() ||
|
|
DefaultAppLogic.with(context).platformIntegration.getCurrentProtectionLevel() == ProtectionLevel.DeviceOwner
|
|
else
|
|
true
|
|
|
|
fun requestPermission(context: Context) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
Toast.makeText(context, R.string.update_toast_needs_permission, Toast.LENGTH_LONG).show()
|
|
|
|
context.startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse("package:${BuildConfig.APPLICATION_ID}")));
|
|
}
|
|
}
|
|
|
|
fun installUpdate(fragmentActivity: FragmentActivity) {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
// compatibility version for old android versions
|
|
// it needs a file uri
|
|
// someone else could replace the APK before it is installed ...
|
|
val context = fragmentActivity.applicationContext
|
|
val externalFile = File(context.externalCacheDir, "update.apk")
|
|
|
|
Threads.update.submit {
|
|
try {
|
|
externalFile.sink().buffer().use { sink ->
|
|
updateSaveFile(context).source().use { source ->
|
|
sink.writeAll(source)
|
|
}
|
|
}
|
|
|
|
Threads.mainThreadHandler.post {
|
|
try {
|
|
context.startActivity(
|
|
Intent(Intent.ACTION_VIEW)
|
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
.setData(Uri.fromFile(externalFile))
|
|
)
|
|
} catch (ex: Exception) {
|
|
if (BuildConfig.DEBUG) {
|
|
Log.w(LOG_TAG, "error during installation", ex)
|
|
}
|
|
|
|
Threads.mainThreadHandler.post {
|
|
Toast.makeText(context!!, R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
}
|
|
} catch (ex: Exception) {
|
|
if (BuildConfig.DEBUG) {
|
|
Log.w(LOG_TAG, "error during installation", ex)
|
|
}
|
|
|
|
Threads.mainThreadHandler.post {
|
|
Toast.makeText(context!!, R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
InstallUpdateDialogFragment().show(fragmentActivity.supportFragmentManager)
|
|
}
|
|
}
|
|
} |