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