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