• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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