diff --git a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt index 37ea38a..b6f7206 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/ApkInstaller.kt @@ -4,9 +4,14 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller +import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper +import android.util.Log +import android.widget.Toast +import androidx.core.content.FileProvider +import dev.mi6e4ka.openstore.R import dev.mi6e4ka.openstore.data.model.DownloadLinkResponseItem import kotlinx.coroutines.CoroutineScope import java.io.File @@ -18,8 +23,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import java.io.BufferedOutputStream +import java.io.FileInputStream import java.io.IOException import java.util.concurrent.TimeUnit +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import kotlin.concurrent.atomics.AtomicLong import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.coroutines.cancellation.CancellationException @@ -123,11 +132,6 @@ class ApkInstaller(private val context: Context) { } } - fun isApkAlreadyDownloaded(fileName: String): Boolean { - val apkFile = getApkFile(fileName) - return apkFile.exists() && apkFile.length() > 0 - } - fun getApkFile(fileName: String): File { return File(context.externalCacheDir ?: context.cacheDir, fileName) } @@ -140,33 +144,59 @@ class ApkInstaller(private val context: Context) { } } } - - fun getApkFileSize(fileName: String): Long { - return getApkFile(fileName).length() - } - private val installer = context.packageManager.packageInstaller private val flags = PendingIntent.FLAG_MUTABLE private val intent = Intent(context, PackageInstallerStatusReceiver::class.java) - private val sessionParams = - PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) + + fun installApp( + packageName: String, + apkFiles: List, + onSessionFinished: (Boolean) -> Unit = {}, + onError: (Exception) -> Unit = {} + ) { + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val installerType = prefs.getString("installer_type", "session") + + when (installerType) { + "session" -> { + installViaSession(apkFiles, onSessionFinished, onError) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - setRequestUpdateOwnership(true) + "intent" -> { + installViaIntent(packageName, apkFiles, onError) } } + } - fun installSplitApk(context: Context, apkFiles: List, installerCallback: PackageInstaller.SessionCallback, onError: (e: Exception) -> Unit) { - val sessionId = installer.createSession(sessionParams) - installer.registerSessionCallback( - installerCallback, - Handler(Looper.getMainLooper()) - ) - - val session = installer.openSession(sessionId) + private fun installViaSession( + apkFiles: List, + onSessionFinished: (Boolean) -> Unit, + onError: (Exception) -> Unit + ) { try { + val sessionParams = + PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setRequestUpdateOwnership(true) + } + } + val sessionId = installer.createSession(sessionParams) + installer.registerSessionCallback(object : PackageInstaller.SessionCallback() { + override fun onCreated(sessionId: Int) {} + override fun onBadgingChanged(sessionId: Int) {} + override fun onActiveChanged(sessionId: Int, active: Boolean) {} + override fun onProgressChanged(sessionId: Int, progress: Float) {} + override fun onFinished(id: Int, success: Boolean) { + if (id == sessionId) { + onSessionFinished(success) + installer.unregisterSessionCallback(this) + } + } + }, Handler(Looper.getMainLooper())) + + val session = installer.openSession(sessionId) session.use { activeSession -> apkFiles.forEach { file -> val sizeBytes = file.length() @@ -181,9 +211,59 @@ class ApkInstaller(private val context: Context) { activeSession.commit(pendingIntent.intentSender) } } catch (e: Exception) { - installer.abandonSession(sessionId) + Log.e("ApkInstaller", "Error during session install", e) onError(e) } } -} \ No newline at end of file + private fun installViaIntent( + packageName: String, + apkFiles: List, + onError: (Exception) -> Unit + ) { + try { + val apkUri: Uri + if (apkFiles.size == 1) { + apkUri = FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + apkFiles[0] + ) + } else { + Toast.makeText(context, R.string.intent_installer_split_apk_build, Toast.LENGTH_SHORT) + .show() + val apksFile = File(context.externalCacheDir ?: context.cacheDir, "$packageName.apks") + buildSplitApk(apkFiles, apksFile) + apkUri = FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + apksFile + ) + } + Toast.makeText(context, R.string.intent_installer_warning, Toast.LENGTH_SHORT) + .show() + + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(apkUri, "application/vnd.android.package-archive") + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK + } + context.startActivity(intent) + } catch (e: Exception) { + Log.e("ApkInstaller", "Error during intent install", e) + onError(e) + } + } + + private fun buildSplitApk(apkFiles: List, outputZip: File) { + ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZip))).use { zos -> + apkFiles.forEach { file -> + FileInputStream(file).use { fis -> + val entry = ZipEntry(file.name) + zos.putNextEntry(entry) + fis.copyTo(zos) + zos.closeEntry() + } + } + } + } +} diff --git a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/InstallResultReceiver.kt b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/InstallResultReceiver.kt index 63a4791..8be4958 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/InstallResultReceiver.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/internal/installer/InstallResultReceiver.kt @@ -34,8 +34,14 @@ class PackageInstallerStatusReceiver : BroadcastReceiver() { } } else -> { - Toast.makeText(context, if (isUninstall) R.string.failed_uninstall else R.string.failed_install, Toast.LENGTH_SHORT).show() val errorMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + + if (errorMessage?.contains("INSUFFICIENT_STORAGE", ignoreCase = true) == true) { + Toast.makeText(context, R.string.insufficient_memory, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, if (isUninstall) R.string.failed_uninstall else R.string.failed_install, Toast.LENGTH_SHORT).show() + } + Log.e("app manager", errorMessage ?: "no error message") } } @@ -44,4 +50,4 @@ class PackageInstallerStatusReceiver : BroadcastReceiver() { companion object { const val ACTION_UNINSTALL = "action_uninstall" } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsViewModel.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsViewModel.kt index 005f4d8..85c7c58 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsViewModel.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/details/DetailsViewModel.kt @@ -7,14 +7,11 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast -import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -32,11 +29,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.File import androidx.core.net.toUri -import java.io.BufferedOutputStream -import java.io.FileInputStream -import java.io.FileOutputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream data class DetailsState( val isLoading: Boolean = true, @@ -153,99 +145,40 @@ class DetailsViewModel(private val appPlatform: String, private val apkInstaller cancelDownload = null } - fun buildSplitApk(apkFiles: List, outputZip: File) { - ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZip))).use { zos -> - apkFiles.forEach { file -> - FileInputStream(file).use { fis -> - val entry = ZipEntry(file.name) - zos.putNextEntry(entry) - fis.copyTo(zos) - zos.closeEntry() - } - } - } - } - private fun installApp(context: Context, apkFiles: List) { - viewModelScope.launch { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val installerType = prefs.getString("installer_type", "session") - when(installerType) { - "session" -> - { - _state.update { it.copy(installStatus = InstallStatus.Pending) } - val installerCallback = object : PackageInstaller.SessionCallback() { - override fun onCreated(sessionId: Int) {} - override fun onBadgingChanged(sessionId: Int) {} - override fun onActiveChanged(sessionId: Int, active: Boolean) {} - override fun onProgressChanged(sessionId: Int, progress: Float) {} - override fun onFinished(sessionId: Int, success: Boolean) { - _state.update { it.copy(installStatus = InstallStatus.Success, isDownloading = false)} - if (success) { - _state.update { it.copy(isInstalled = true, isNeedUpgrade = false) } - apkInstaller.cleanApkFiles(apkFiles.map {it.name}) - } - } - } - apkInstaller.installSplitApk( - context, - apkFiles, - installerCallback, - onError = { error -> - if (error.message?.contains("Failed to allocate") == true) { - Toast.makeText( - context, - R.string.insufficient_memory, - Toast.LENGTH_SHORT - ).show() - } else { - println("apk installer $error") - Toast.makeText(context, R.string.installing_error, Toast.LENGTH_SHORT) - .show() - } - _state.update { - it.copy( - installStatus = InstallStatus.Idle, - isDownloading = false - ) - } - }) + val packageName = state.value.item?.packageName ?: "tmp" + _state.update { it.copy(installStatus = InstallStatus.Pending) } + + apkInstaller.installApp( + packageName = packageName, + apkFiles = apkFiles, + onSessionFinished = { success -> + _state.update { it.copy(installStatus = InstallStatus.Success, isDownloading = false)} + if (success) { + _state.update { it.copy(isInstalled = true, isNeedUpgrade = false) } + apkInstaller.cleanApkFiles(apkFiles.map {it.name}) } - "intent" -> { - _state.update { it.copy(installStatus = InstallStatus.Idle, isDownloading = false) } - val apkUri : Uri - if (apkFiles.size == 1) { - apkUri = FileProvider.getUriForFile( - context, - "${context.packageName}.provider", - apkFiles[0] - ) - } else { - Toast.makeText(context, R.string.intent_installer_split_apk_build, Toast.LENGTH_SHORT) - .show() - val apksFile = File(context.externalCacheDir ?: context.cacheDir, "${state.value.item?.packageName ?: "tmp"}.apks") - buildSplitApk(apkFiles, apksFile) - apkUri = FileProvider.getUriForFile( - context, - "${context.packageName}.provider", - apksFile - ) - } - Toast.makeText(context, R.string.intent_installer_warning, Toast.LENGTH_SHORT) + }, + onError = { error -> + if (error.message?.contains("Failed to allocate") == true) { + Toast.makeText( + context, + R.string.insufficient_memory, + Toast.LENGTH_SHORT + ).show() + } else { + println("apk installer $error") + Toast.makeText(context, R.string.installing_error, Toast.LENGTH_SHORT) .show() - - val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType( - apkUri, - "application/vnd.android.package-archive" - ) - flags = - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK - } - context.startActivity(intent) + } + _state.update { + it.copy( + installStatus = InstallStatus.Idle, + isDownloading = false + ) } } - } + ) } private fun getPackageInfo(context: Context, packageName: String): PackageInfo? { diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt index bfe4803..236a02e 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/settings/SettingsScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card import androidx.compose.material3.Checkbox import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -96,7 +97,7 @@ fun SettingsScreen( select = listOf( SettingsSectionSelect( title = "OpenStore", - value = "dev.mi6e4ka.dev" + value = "dev.mi6e4ka.openstore" ), SettingsSectionSelect( title = "RuStore", @@ -162,7 +163,7 @@ fun SettingsCard( if (item.type == ParamType.SWITCH) prefs.getBoolean(item.id, false) else false )} - ElevatedCard ( + Card ( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp), diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesScreen.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesScreen.kt index 9150b6d..ca32ee9 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesScreen.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesScreen.kt @@ -135,7 +135,7 @@ fun UpdatesScreen(navController: NavController) { Icon( painter = painterResource(R.drawable.ic_check_circle_24px), contentDescription = null, - modifier = Modifier.size(64.dp), + modifier = Modifier.size(80.dp), tint = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.size(16.dp)) @@ -278,13 +278,13 @@ fun UpdateAppCard( maxLines = 1, overflow = TextOverflow.Ellipsis ) - Text( - text = "from ${app.installSource}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) +// Text( +// text = "from ${app.installSource}", +// fontSize = 12.sp, +// color = MaterialTheme.colorScheme.secondary, +// maxLines = 1, +// overflow = TextOverflow.Ellipsis +// ) if (isDownloading) { Spacer(modifier = Modifier.size(4.dp)) LinearProgressIndicator( diff --git a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesViewModel.kt b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesViewModel.kt index f089f4c..f780b30 100644 --- a/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesViewModel.kt +++ b/app/src/main/java/dev/mi6e4ka/openstore/ui/screen/updates/UpdatesViewModel.kt @@ -3,11 +3,12 @@ package dev.mi6e4ka.openstore.ui.screen.updates import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.os.Build +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dev.mi6e4ka.openstore.R import dev.mi6e4ka.openstore.data.model.AppUpdateRequestEntry import dev.mi6e4ka.openstore.di.AppModule import dev.mi6e4ka.openstore.internal.installer.ApkInstaller @@ -172,13 +173,8 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() { onSuccess = { apkFiles -> installApp(context, app.packageName, apkFiles) }, - onError = { _ -> - _state.update { - it.copy( - downloadingPackages = it.downloadingPackages - app.packageName, - downloadProgress = it.downloadProgress - app.packageName - ) - } + onError = { error -> + handleError(context, app.packageName, error) }, onCancel = { _state.update { @@ -190,47 +186,46 @@ class UpdatesViewModel(private val apkInstaller: ApkInstaller) : ViewModel() { } ) } catch (e: Exception) { - _state.update { - it.copy( - downloadingPackages = it.downloadingPackages - app.packageName, - downloadProgress = it.downloadProgress - app.packageName - ) - } + handleError(context, app.packageName, e) } } } private fun installApp(context: Context, packageName: String, apkFiles: List) { - viewModelScope.launch { - val installerCallback = object : PackageInstaller.SessionCallback() { - override fun onCreated(sessionId: Int) {} - override fun onBadgingChanged(sessionId: Int) {} - override fun onActiveChanged(sessionId: Int, active: Boolean) {} - override fun onProgressChanged(sessionId: Int, progress: Float) {} - override fun onFinished(sessionId: Int, success: Boolean) { - _state.update { - it.copy( - downloadingPackages = it.downloadingPackages - packageName, - downloadProgress = it.downloadProgress - packageName, - appsWithUpdates = if (success) { - it.appsWithUpdates.filter { app -> app.packageName != packageName } - } else it.appsWithUpdates - ) - } - if (success) { - apkInstaller.cleanApkFiles(apkFiles.map { it.name }) - } - } - } - - apkInstaller.installSplitApk(context, apkFiles, installerCallback, onError = { _ -> + apkInstaller.installApp( + packageName = packageName, + apkFiles = apkFiles, + onSessionFinished = { success -> _state.update { it.copy( downloadingPackages = it.downloadingPackages - packageName, - downloadProgress = it.downloadProgress - packageName + downloadProgress = it.downloadProgress - packageName, + appsWithUpdates = if (success) { + it.appsWithUpdates.filter { app -> app.packageName != packageName } + } else it.appsWithUpdates ) } - }) + if (success) { + apkInstaller.cleanApkFiles(apkFiles.map { it.name }) + } + }, + onError = { error -> + handleError(context, packageName, error) + } + ) + } + + private fun handleError(context: Context, packageName: String, error: Throwable) { + if (error.message?.contains("Failed to allocate") == true) { + Toast.makeText(context, R.string.insufficient_memory, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, R.string.loading_error, Toast.LENGTH_SHORT).show() + } + _state.update { + it.copy( + downloadingPackages = it.downloadingPackages - packageName, + downloadProgress = it.downloadProgress - packageName + ) } } diff --git a/app/src/main/res/drawable/ic_check_circle_24px.xml b/app/src/main/res/drawable/ic_check_circle_24px.xml index 219c474..728be3b 100644 --- a/app/src/main/res/drawable/ic_check_circle_24px.xml +++ b/app/src/main/res/drawable/ic_check_circle_24px.xml @@ -4,7 +4,7 @@ android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> - + diff --git a/app/src/main/res/drawable/ic_refresh_24px.xml b/app/src/main/res/drawable/ic_refresh_24px.xml index f45367f..f4302a5 100644 --- a/app/src/main/res/drawable/ic_refresh_24px.xml +++ b/app/src/main/res/drawable/ic_refresh_24px.xml @@ -4,7 +4,7 @@ android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal"> - +