1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.packageinstaller.v2.model 18 19 import android.content.Context 20 import android.content.pm.PackageInstaller 21 import android.content.res.AssetFileDescriptor 22 import android.net.Uri 23 import android.util.Log 24 import androidx.lifecycle.LiveData 25 import androidx.lifecycle.MutableLiveData 26 import java.io.IOException 27 import kotlinx.coroutines.Dispatchers 28 import kotlinx.coroutines.withContext 29 30 class SessionStager internal constructor( 31 private val context: Context, 32 private val uri: Uri, 33 private val stagedSessionId: Int 34 ) { 35 36 companion object { 37 private val LOG_TAG = SessionStager::class.java.simpleName 38 } 39 40 private val _progress = MutableLiveData(0) 41 val progress: LiveData<Int> 42 get() = _progress 43 44 suspend fun execute(): Boolean = withContext(Dispatchers.IO) { 45 val pi: PackageInstaller = context.packageManager.packageInstaller 46 var sessionInfo: PackageInstaller.SessionInfo? 47 try { 48 val session = pi.openSession(stagedSessionId) 49 context.contentResolver.openInputStream(uri).use { instream -> 50 session.setStagingProgress(0f) 51 52 if (instream == null) { 53 return@withContext false 54 } 55 56 val sizeBytes = getContentSizeBytes() 57 publishProgress(if (sizeBytes > 0) 0 else -1) 58 59 var totalRead: Long = 0 60 session.openWrite("PackageInstaller", 0, sizeBytes).use { out -> 61 val buffer = ByteArray(1024 * 1024) 62 while (true) { 63 val numRead = instream.read(buffer) 64 if (numRead == -1) { 65 session.fsync(out) 66 break 67 } 68 out.write(buffer, 0, numRead) 69 70 if (sizeBytes > 0) { 71 totalRead += numRead.toLong() 72 val fraction = totalRead.toFloat() / sizeBytes.toFloat() 73 session.setStagingProgress(fraction) 74 publishProgress((fraction * 100.0).toInt()) 75 } 76 } 77 } 78 sessionInfo = pi.getSessionInfo(stagedSessionId) 79 } 80 } catch (e: Exception) { 81 Log.w(LOG_TAG, "Error staging apk from content URI", e) 82 sessionInfo = null 83 } 84 85 return@withContext if (sessionInfo == null 86 || !sessionInfo?.isActive!! 87 || sessionInfo?.resolvedBaseApkPath == null 88 ) { 89 Log.w(LOG_TAG, "Session info is invalid: $sessionInfo") 90 false 91 } else { 92 true 93 } 94 } 95 96 private fun getContentSizeBytes(): Long { 97 return try { 98 context.contentResolver 99 .openAssetFileDescriptor(uri, "r") 100 .use { afd -> afd?.length ?: AssetFileDescriptor.UNKNOWN_LENGTH } 101 } catch (e: IOException) { 102 Log.w(LOG_TAG, "Failed to open asset file descriptor", e) 103 AssetFileDescriptor.UNKNOWN_LENGTH 104 } 105 } 106 107 private suspend fun publishProgress(progressValue: Int) = withContext(Dispatchers.Main) { 108 _progress.value = progressValue 109 } 110 } 111