1 /* 2 * Copyright (C) 2016 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; 18 19 import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH; 20 21 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID; 22 23 import android.app.Activity; 24 import android.app.AlertDialog; 25 import android.app.Dialog; 26 import android.app.DialogFragment; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.pm.PackageInstaller; 31 import android.content.pm.PackageManager; 32 import android.content.res.AssetFileDescriptor; 33 import android.Manifest; 34 import android.net.Uri; 35 import android.os.AsyncTask; 36 import android.os.Bundle; 37 import android.os.ParcelFileDescriptor; 38 import android.os.Process; 39 import android.util.Log; 40 import android.view.View; 41 import android.widget.ProgressBar; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 46 import java.io.File; 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.io.OutputStream; 50 51 /** 52 * If a package gets installed from a content URI this step stages the installation session 53 * reading bytes from the URI. 54 */ 55 public class InstallStaging extends Activity { 56 private static final String LOG_TAG = InstallStaging.class.getSimpleName(); 57 58 private static final String STAGED_SESSION_ID = "STAGED_SESSION_ID"; 59 60 private @Nullable PackageInstaller mInstaller; 61 62 /** Currently running task that loads the file from the content URI into a file */ 63 private @Nullable StagingAsyncTask mStagingTask; 64 65 /** The session the package is in */ 66 private int mStagedSessionId; 67 68 private AlertDialog mDialog; 69 70 @Override onCreate(@ullable Bundle savedInstanceState)71 protected void onCreate(@Nullable Bundle savedInstanceState) { 72 super.onCreate(savedInstanceState); 73 74 mInstaller = getPackageManager().getPackageInstaller(); 75 76 setFinishOnTouchOutside(true); 77 78 AlertDialog.Builder builder = new AlertDialog.Builder(this); 79 80 builder.setIcon(R.drawable.ic_file_download); 81 builder.setTitle(getString(R.string.app_name_unknown)); 82 builder.setView(R.layout.install_content_view); 83 builder.setNegativeButton(getString(R.string.cancel), 84 (ignored, ignored2) -> { 85 if (mStagingTask != null) { 86 mStagingTask.cancel(true); 87 } 88 89 cleanupStagingSession(); 90 91 setResult(RESULT_CANCELED); 92 finish(); 93 }); 94 builder.setOnCancelListener(dialog -> { 95 if (mStagingTask != null) { 96 mStagingTask.cancel(true); 97 } 98 99 cleanupStagingSession(); 100 101 setResult(RESULT_CANCELED); 102 finish(); 103 }); 104 mDialog = builder.create(); 105 mDialog.show(); 106 mDialog.requireViewById(com.android.packageinstaller.R.id.staging) 107 .setVisibility(View.VISIBLE); 108 109 if (savedInstanceState != null) { 110 mStagedSessionId = savedInstanceState.getInt(STAGED_SESSION_ID, 0); 111 } 112 } 113 114 @Override onResume()115 protected void onResume() { 116 super.onResume(); 117 118 // This is the first onResume in a single life of the activity. 119 if (mStagingTask == null) { 120 if (mStagedSessionId > 0) { 121 final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo( 122 mStagedSessionId); 123 if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) { 124 Log.w(LOG_TAG, "Session " + mStagedSessionId + " in funky state; ignoring"); 125 if (info != null) { 126 cleanupStagingSession(); 127 } 128 mStagedSessionId = 0; 129 } 130 } 131 132 // Session does not exist, or became invalid. 133 if (mStagedSessionId <= 0) { 134 // Create session here to be able to show error. 135 final Uri packageUri = getIntent().getData(); 136 final AssetFileDescriptor afd = openAssetFileDescriptor(packageUri); 137 try { 138 ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null; 139 PackageInstaller.SessionParams params = createSessionParams( 140 mInstaller, getIntent(), pfd, packageUri.toString()); 141 mStagedSessionId = mInstaller.createSession(params); 142 } catch (IOException e) { 143 Log.w(LOG_TAG, "Failed to create a staging session", e); 144 showError(); 145 return; 146 } finally { 147 PackageUtil.safeClose(afd); 148 } 149 } 150 151 mStagingTask = new StagingAsyncTask(); 152 mStagingTask.execute(); 153 } 154 } 155 156 @Override onSaveInstanceState(Bundle outState)157 protected void onSaveInstanceState(Bundle outState) { 158 super.onSaveInstanceState(outState); 159 160 outState.putInt(STAGED_SESSION_ID, mStagedSessionId); 161 } 162 163 @Override onDestroy()164 protected void onDestroy() { 165 if (mStagingTask != null) { 166 mStagingTask.cancel(true); 167 } 168 if (mDialog != null) { 169 mDialog.dismiss(); 170 } 171 super.onDestroy(); 172 } 173 openAssetFileDescriptor(Uri uri)174 private AssetFileDescriptor openAssetFileDescriptor(Uri uri) { 175 try { 176 return getContentResolver().openAssetFileDescriptor(uri, "r"); 177 } catch (Exception e) { 178 Log.w(LOG_TAG, "Failed to open asset file descriptor", e); 179 return null; 180 } 181 } 182 createSessionParams( @onNull PackageInstaller installer, @NonNull Intent intent, @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName)183 private static PackageInstaller.SessionParams createSessionParams( 184 @NonNull PackageInstaller installer, @NonNull Intent intent, 185 @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) { 186 PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( 187 PackageInstaller.SessionParams.MODE_FULL_INSTALL); 188 final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER); 189 params.setPackageSource( 190 referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE 191 : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE); 192 params.setInstallAsInstantApp(false); 193 params.setReferrerUri(referrerUri); 194 params.setOriginatingUri(intent 195 .getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)); 196 params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, 197 Process.INVALID_UID)); 198 params.setInstallerPackageName(intent.getStringExtra( 199 Intent.EXTRA_INSTALLER_PACKAGE_NAME)); 200 params.setInstallReason(PackageManager.INSTALL_REASON_USER); 201 // Disable full screen intent usage by for sideloads. 202 params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT, 203 PackageInstaller.SessionParams.PERMISSION_STATE_DENIED); 204 205 if (pfd != null) { 206 try { 207 final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd, 208 debugPathName, 0); 209 params.setAppPackageName(result.getPackageName()); 210 params.setInstallLocation(result.getInstallLocation()); 211 params.setSize(result.calculateInstalledSize(params, pfd)); 212 } catch (PackageInstaller.PackageParsingException | IOException e) { 213 Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e); 214 Log.e(LOG_TAG, 215 "Cannot calculate installed size " + debugPathName 216 + ". Try only apk size."); 217 params.setSize(pfd.getStatSize()); 218 } 219 } else { 220 Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults."); 221 } 222 return params; 223 } 224 cleanupStagingSession()225 private void cleanupStagingSession() { 226 if (mStagedSessionId > 0) { 227 try { 228 mInstaller.abandonSession(mStagedSessionId); 229 } catch (SecurityException ignored) { 230 231 } 232 mStagedSessionId = 0; 233 } 234 } 235 236 /** 237 * Show an error message and set result as error. 238 */ showError()239 private void showError() { 240 getFragmentManager().beginTransaction() 241 .add(new ErrorDialog(), "error").commitAllowingStateLoss(); 242 243 Intent result = new Intent(); 244 result.putExtra(Intent.EXTRA_INSTALL_RESULT, 245 PackageManager.INSTALL_FAILED_INVALID_APK); 246 setResult(RESULT_FIRST_USER, result); 247 } 248 249 /** 250 * Dialog for errors while staging. 251 */ 252 public static class ErrorDialog extends DialogFragment { 253 private Activity mActivity; 254 255 @Override onAttach(Context context)256 public void onAttach(Context context) { 257 super.onAttach(context); 258 259 mActivity = (Activity) context; 260 } 261 262 @Override onCreateDialog(Bundle savedInstanceState)263 public Dialog onCreateDialog(Bundle savedInstanceState) { 264 AlertDialog alertDialog = new AlertDialog.Builder(mActivity) 265 .setMessage(R.string.Parse_error_dlg_text) 266 .setPositiveButton(R.string.ok, 267 (dialog, which) -> mActivity.finish()) 268 .create(); 269 alertDialog.setCanceledOnTouchOutside(false); 270 271 return alertDialog; 272 } 273 274 @Override onCancel(DialogInterface dialog)275 public void onCancel(DialogInterface dialog) { 276 super.onCancel(dialog); 277 278 mActivity.finish(); 279 } 280 } 281 282 private final class StagingAsyncTask extends 283 AsyncTask<Void, Integer, PackageInstaller.SessionInfo> { 284 private ProgressBar mProgressBar = null; 285 getContentSizeBytes()286 private long getContentSizeBytes() { 287 try (AssetFileDescriptor afd = openAssetFileDescriptor(getIntent().getData())) { 288 return afd != null ? afd.getLength() : UNKNOWN_LENGTH; 289 } catch (IOException ignored) { 290 return UNKNOWN_LENGTH; 291 } 292 } 293 294 @Override onPreExecute()295 protected void onPreExecute() { 296 final long sizeBytes = getContentSizeBytes(); 297 if (sizeBytes > 0 && mDialog != null) { 298 mProgressBar = mDialog.requireViewById(R.id.progress_indeterminate); 299 } 300 if (mProgressBar != null) { 301 mProgressBar.setProgress(0); 302 mProgressBar.setMax(100); 303 mProgressBar.setIndeterminate(false); 304 } 305 } 306 307 @Override doInBackground(Void... params)308 protected PackageInstaller.SessionInfo doInBackground(Void... params) { 309 Uri packageUri = getIntent().getData(); 310 try (PackageInstaller.Session session = mInstaller.openSession(mStagedSessionId); 311 InputStream in = getContentResolver().openInputStream(packageUri)) { 312 session.setStagingProgress(0); 313 314 if (in == null) { 315 return null; 316 } 317 318 long sizeBytes = getContentSizeBytes(); 319 320 long totalRead = 0; 321 try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) { 322 byte[] buffer = new byte[1024 * 1024]; 323 while (true) { 324 int numRead = in.read(buffer); 325 326 if (numRead == -1) { 327 session.fsync(out); 328 break; 329 } 330 331 if (isCancelled()) { 332 break; 333 } 334 335 out.write(buffer, 0, numRead); 336 if (sizeBytes > 0) { 337 totalRead += numRead; 338 float fraction = ((float) totalRead / (float) sizeBytes); 339 session.setStagingProgress(fraction); 340 publishProgress((int) (fraction * 100.0)); 341 } 342 } 343 } 344 345 return mInstaller.getSessionInfo(mStagedSessionId); 346 } catch (IOException | SecurityException | IllegalStateException 347 | IllegalArgumentException e) { 348 Log.w(LOG_TAG, "Error staging apk from content URI", e); 349 return null; 350 } 351 } 352 353 @Override onProgressUpdate(Integer... progress)354 protected void onProgressUpdate(Integer... progress) { 355 if (mProgressBar != null && progress != null && progress.length > 0) { 356 mProgressBar.setProgress(progress[0], true); 357 } 358 } 359 360 @Override onPostExecute(PackageInstaller.SessionInfo sessionInfo)361 protected void onPostExecute(PackageInstaller.SessionInfo sessionInfo) { 362 if (sessionInfo == null || !sessionInfo.isActive() 363 || sessionInfo.getResolvedBaseApkPath() == null) { 364 Log.w(LOG_TAG, "Session info is invalid: " + sessionInfo); 365 cleanupStagingSession(); 366 showError(); 367 return; 368 } 369 370 // Pass the staged session to the installer. 371 Intent installIntent = new Intent(getIntent()); 372 installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class); 373 installIntent.setData(Uri.fromFile(new File(sessionInfo.getResolvedBaseApkPath()))); 374 375 installIntent.putExtra(EXTRA_STAGED_SESSION_ID, mStagedSessionId); 376 377 if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { 378 installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 379 } 380 381 installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); 382 383 startActivity(installIntent); 384 385 InstallStaging.this.finish(); 386 } 387 } 388 } 389