@@ -1217,14 +1217,15 @@ class HomeViewModel(
12171217 }
12181218
12191219 viewModelScope.launch {
1220- val selectedApp = withContext(Dispatchers .IO ) {
1220+ val result = withContext(Dispatchers .IO ) {
12211221 loadLocalApk(app, uri)
12221222 }
12231223
1224- if (selectedApp != null ) {
1225- processSelectedApp(selectedApp)
1226- } else {
1227- app.toast(app.getString(R .string.home_invalid_apk))
1224+ when (result) {
1225+ is ApkLoadResult .Success -> processSelectedApp(result.app)
1226+ is ApkLoadResult .Unreadable -> app.toast(app.getString(R .string.home_invalid_apk_unreadable))
1227+ is ApkLoadResult .NotAnApk -> app.toast(app.getString(R .string.home_invalid_apk_not_an_apk))
1228+ is ApkLoadResult .IoError -> app.toast(app.getString(R .string.home_invalid_apk_io_error))
12281229 }
12291230 }
12301231 }
@@ -2211,22 +2212,25 @@ class HomeViewModel(
22112212 private suspend fun loadLocalApk (
22122213 context : Context ,
22132214 uri : Uri
2214- ): SelectedApp . Local ? = withContext(Dispatchers .IO ) {
2215+ ): ApkLoadResult = withContext(Dispatchers .IO ) {
22152216 try {
22162217 // Copy file to uiTempDir with original extension detection
22172218 val fileName = context.contentResolver.query(uri, null , null , null , null )?.use { cursor ->
22182219 val nameIndex = cursor.getColumnIndex(OpenableColumns .DISPLAY_NAME )
2219- cursor.moveToFirst()
2220- cursor.getString(nameIndex)
2220+ if (cursor.moveToFirst() && nameIndex != - 1 ) cursor.getString(nameIndex) else null
22212221 } ? : " temp_${System .currentTimeMillis()} "
22222222
22232223 val extension = fileName.substringAfterLast(' .' , " apk" ).lowercase()
22242224 val tempFile = filesystem.uiTempDir.resolve(" temp_apk_${System .currentTimeMillis()} .$extension " )
22252225
2226- context.contentResolver.openInputStream(uri)?.use { input ->
2227- tempFile.outputStream().use { output ->
2228- input.copyTo(output)
2229- }
2226+ // openInputStream can return null when the provider is unavailable
2227+ // e.g. Samsung External Storage restricted by Battery Optimization
2228+ val bytesCopied = context.contentResolver.openInputStream(uri)?.use { input ->
2229+ tempFile.outputStream().use { output -> input.copyTo(output) }
2230+ }
2231+ if (bytesCopied == null || bytesCopied == 0L ) {
2232+ tempFile.delete()
2233+ return @withContext ApkLoadResult .Unreadable
22302234 }
22312235
22322236 // Check if it's a split APK archive
@@ -2252,18 +2256,32 @@ class HomeViewModel(
22522256
22532257 if (packageInfo == null ) {
22542258 tempFile.delete()
2255- return @withContext null
2259+ return @withContext ApkLoadResult . NotAnApk
22562260 }
22572261
2258- SelectedApp .Local (
2259- packageName = packageInfo.packageName,
2260- version = packageInfo.versionName ? : " unknown" ,
2261- file = tempFile,
2262- temporary = true
2262+ ApkLoadResult .Success (
2263+ SelectedApp .Local (
2264+ packageName = packageInfo.packageName,
2265+ version = packageInfo.versionName ? : " unknown" ,
2266+ file = tempFile,
2267+ temporary = true
2268+ )
22632269 )
22642270 } catch (e: Exception ) {
22652271 Log .e(tag, " Failed to load APK" , e)
2266- null
2272+ ApkLoadResult . IoError
22672273 }
22682274 }
22692275}
2276+
2277+ /* * Result of attempting to load a local APK file. */
2278+ private sealed interface ApkLoadResult {
2279+ /* * File was read and parsed successfully. */
2280+ data class Success (val app : SelectedApp .Local ) : ApkLoadResult
2281+ /* * File could not be read - provider returned null stream or zero bytes. */
2282+ data object Unreadable : ApkLoadResult
2283+ /* * File was read but is not a valid APK/split archive. */
2284+ data object NotAnApk : ApkLoadResult
2285+ /* * An unexpected IO or system exception occurred while copying or parsing. */
2286+ data object IoError : ApkLoadResult
2287+ }
0 commit comments