1 /*
2 **
3 ** Copyright 2007, The Android Open Source Project
4 **
5 ** Licensed under the Apache License, Version 2.0 (the "License");
6 ** you may not use this file except in compliance with the License.
7 ** You may obtain a copy of the License at
8 **
9 **     http://www.apache.org/licenses/LICENSE-2.0
10 **
11 ** Unless required by applicable law or agreed to in writing, software
12 ** distributed under the License is distributed on an "AS IS" BASIS,
13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 ** See the License for the specific language governing permissions and
15 ** limitations under the License.
16 */
17 package com.android.packageinstaller;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
20 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
21 
22 import android.Manifest;
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.app.AppOpsManager;
26 import android.app.Dialog;
27 import android.app.DialogFragment;
28 import android.content.ActivityNotFoundException;
29 import android.content.ContentResolver;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.Intent;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.InstallSourceInfo;
35 import android.content.pm.PackageInfo;
36 import android.content.pm.PackageInstaller;
37 import android.content.pm.PackageInstaller.SessionInfo;
38 import android.content.pm.PackageManager;
39 import android.content.pm.PackageManager.ApplicationInfoFlags;
40 import android.content.pm.PackageManager.NameNotFoundException;
41 import android.graphics.drawable.BitmapDrawable;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.Handler;
45 import android.os.Looper;
46 import android.os.Process;
47 import android.os.UserHandle;
48 import android.os.UserManager;
49 import android.provider.Settings;
50 import android.text.TextUtils;
51 import android.util.Log;
52 import android.view.View;
53 import android.widget.Button;
54 import android.widget.TextView;
55 
56 import androidx.annotation.NonNull;
57 
58 import java.io.File;
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 /**
63  * This activity is launched when a new application is installed via side loading
64  * The package is first parsed and the user is notified of parse errors via a dialog.
65  * If the package is successfully parsed, the user is notified to turn on the install unknown
66  * applications setting. A memory check is made at this point and the user is notified of out
67  * of memory conditions if any. If the package is already existing on the device,
68  * a confirmation dialog (to replace the existing package) is presented to the user.
69  * Based on the user response the package is then installed by launching InstallAppConfirm
70  * sub activity. All state transitions are handled in this activity
71  */
72 public class PackageInstallerActivity extends AlertActivity {
73     private static final String TAG = "PackageInstaller";
74 
75     private static final int REQUEST_TRUST_EXTERNAL_SOURCE = 1;
76 
77     static final String SCHEME_PACKAGE = "package";
78 
79     static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE";
80     static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG";
81     static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";
82     static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID";
83     static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET";
84     private static final String ALLOW_UNKNOWN_SOURCES_KEY =
85             PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY";
86 
87     private int mSessionId = -1;
88     private Uri mPackageURI;
89     private Uri mOriginatingURI;
90     private Uri mReferrerURI;
91     private int mOriginatingUid = Process.INVALID_UID;
92     private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid
93     private int mActivityResultCode = Activity.RESULT_CANCELED;
94     private int mPendingUserActionReason = -1;
95 
96     private final boolean mLocalLOGV = false;
97     PackageManager mPm;
98     AppOpsManager mAppOpsManager;
99     UserManager mUserManager;
100     PackageInstaller mInstaller;
101     PackageInfo mPkgInfo;
102     String mCallingPackage;
103     private String mCallingAttributionTag;
104     ApplicationInfo mSourceInfo;
105 
106     /**
107      * A collection of unknown sources listeners that are actively listening for app ops mode
108      * changes
109      */
110     private List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1);
111 
112     // ApplicationInfo object primarily used for already existing applications
113     private ApplicationInfo mAppInfo;
114 
115     // Buttons to indicate user acceptance
116     private Button mOk;
117 
118     private PackageUtil.AppSnippet mAppSnippet;
119 
120     static final String PREFS_ALLOWED_SOURCES = "allowed_sources";
121 
122     // Dialog identifiers used in showDialog
123     private static final int DLG_BASE = 0;
124     private static final int DLG_PACKAGE_ERROR = DLG_BASE + 1;
125     private static final int DLG_OUT_OF_SPACE = DLG_BASE + 2;
126     private static final int DLG_INSTALL_ERROR = DLG_BASE + 3;
127     private static final int DLG_ANONYMOUS_SOURCE = DLG_BASE + 4;
128     private static final int DLG_EXTERNAL_SOURCE_BLOCKED = DLG_BASE + 5;
129 
130     // If unknown sources are temporary allowed
131     private boolean mAllowUnknownSources;
132 
133     // Would the mOk button be enabled if this activity would be resumed
134     private boolean mEnableOk = false;
135 
startInstallConfirm()136     private void startInstallConfirm() {
137         TextView viewToEnable;
138 
139         if (mAppInfo != null) {
140             viewToEnable = requireViewById(R.id.install_confirm_question_update);
141 
142             final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel();
143             final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage);
144             if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
145                     && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) {
146                 viewToEnable.setText(
147                         getString(R.string.install_confirm_question_update_owner_reminder,
148                                 requestedUpdateOwnerLabel, existingUpdateOwnerLabel));
149                 mOk.setText(R.string.update_anyway);
150             } else {
151                 mOk.setText(R.string.update);
152             }
153         } else {
154             // This is a new application with no permissions.
155             viewToEnable = requireViewById(R.id.install_confirm_question);
156         }
157 
158         viewToEnable.setVisibility(View.VISIBLE);
159 
160         mEnableOk = true;
161         mOk.setEnabled(true);
162         mOk.setFilterTouchesWhenObscured(true);
163     }
164 
getExistingUpdateOwnerLabel()165     private CharSequence getExistingUpdateOwnerLabel() {
166         try {
167             final String packageName = mPkgInfo.packageName;
168             final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName);
169             final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName();
170             return getApplicationLabel(existingUpdateOwner);
171         } catch (NameNotFoundException e) {
172             return null;
173         }
174     }
175 
getApplicationLabel(String packageName)176     private CharSequence getApplicationLabel(String packageName) {
177         try {
178             final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName,
179                     ApplicationInfoFlags.of(0));
180             return mPm.getApplicationLabel(appInfo);
181         } catch (NameNotFoundException e) {
182             return null;
183         }
184     }
185 
186     /**
187      * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}.
188      *
189      * @param id The dialog type to add
190      */
showDialogInner(int id)191     private void showDialogInner(int id) {
192         if (mLocalLOGV) Log.i(TAG, "showDialogInner(" + id + ")");
193         DialogFragment currentDialog =
194                 (DialogFragment) getFragmentManager().findFragmentByTag("dialog");
195         if (currentDialog != null) {
196             currentDialog.dismissAllowingStateLoss();
197         }
198 
199         DialogFragment newDialog = createDialog(id);
200         if (newDialog != null) {
201             getFragmentManager().beginTransaction()
202                     .add(newDialog, "dialog").commitAllowingStateLoss();
203         }
204     }
205 
206     /**
207      * Create a new dialog.
208      *
209      * @param id The id of the dialog (determines dialog type)
210      *
211      * @return The dialog
212      */
createDialog(int id)213     private DialogFragment createDialog(int id) {
214         if (mLocalLOGV) Log.i(TAG, "createDialog(" + id + ")");
215         switch (id) {
216             case DLG_PACKAGE_ERROR:
217                 return PackageUtil.SimpleErrorDialog.newInstance(R.string.Parse_error_dlg_text);
218             case DLG_OUT_OF_SPACE:
219                 return OutOfSpaceDialog.newInstance(
220                         mPm.getApplicationLabel(mPkgInfo.applicationInfo));
221             case DLG_INSTALL_ERROR:
222                 return InstallErrorDialog.newInstance(
223                         mPm.getApplicationLabel(mPkgInfo.applicationInfo));
224             case DLG_EXTERNAL_SOURCE_BLOCKED:
225                 return ExternalSourcesBlockedDialog.newInstance(mOriginatingPackage);
226             case DLG_ANONYMOUS_SOURCE:
227                 return AnonymousSourceDialog.newInstance();
228         }
229         return null;
230     }
231 
232     @Override
onActivityResult(int request, int result, Intent data)233     public void onActivityResult(int request, int result, Intent data) {
234         if (request == REQUEST_TRUST_EXTERNAL_SOURCE) {
235             // Log the fact that the app is requesting an install, and is now allowed to do it
236             // (before this point we could only log that it's requesting an install, but isn't
237             // allowed to do it yet).
238             String appOpStr =
239                     AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
240             int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid,
241                     mOriginatingPackage, mCallingAttributionTag,
242                     "Successfully started package installation activity");
243             if (appOpMode == AppOpsManager.MODE_ALLOWED) {
244                 // The user has just allowed this package to install other packages
245                 // (via Settings).
246                 mAllowUnknownSources = true;
247                 DialogFragment currentDialog =
248                         (DialogFragment) getFragmentManager().findFragmentByTag("dialog");
249                 if (currentDialog != null) {
250                     currentDialog.dismissAllowingStateLoss();
251                 }
252                 initiateInstall();
253             } else {
254                 finish();
255             }
256         } else {
257             finish();
258         }
259     }
260 
getPackageNameForUid(int sourceUid)261     private String getPackageNameForUid(int sourceUid) {
262         String[] packagesForUid = mPm.getPackagesForUid(sourceUid);
263         if (packagesForUid == null) {
264             return null;
265         }
266         if (packagesForUid.length > 1) {
267             if (mCallingPackage != null) {
268                 for (String packageName : packagesForUid) {
269                     if (packageName.equals(mCallingPackage)) {
270                         return packageName;
271                     }
272                 }
273             }
274             Log.i(TAG, "Multiple packages found for source uid " + sourceUid);
275         }
276         return packagesForUid[0];
277     }
278 
isInstallRequestFromUnknownSource(Intent intent)279     private boolean isInstallRequestFromUnknownSource(Intent intent) {
280         if (mCallingPackage != null && intent.getBooleanExtra(
281                 Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
282             if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) {
283                 // Privileged apps can bypass unknown sources check if they want.
284                 return false;
285             }
286         }
287         if (mSourceInfo != null && checkPermission(Manifest.permission.INSTALL_PACKAGES,
288                 -1 /* pid */, mSourceInfo.uid) == PackageManager.PERMISSION_GRANTED) {
289             return false;
290         }
291         return true;
292     }
293 
initiateInstall()294     private void initiateInstall() {
295         String pkgName = mPkgInfo.packageName;
296         // Check if there is already a package on the device with this name
297         // but it has been renamed to something else.
298         String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
299         if (oldName != null && oldName.length > 0 && oldName[0] != null) {
300             pkgName = oldName[0];
301             mPkgInfo.packageName = pkgName;
302             mPkgInfo.applicationInfo.packageName = pkgName;
303         }
304         // Check if package is already installed. display confirmation dialog if replacing pkg
305         try {
306             // This is a little convoluted because we want to get all uninstalled
307             // apps, but this may include apps with just data, and if it is just
308             // data we still want to count it as "installed".
309             mAppInfo = mPm.getApplicationInfo(pkgName,
310                     PackageManager.MATCH_UNINSTALLED_PACKAGES);
311             if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
312                 mAppInfo = null;
313             }
314         } catch (NameNotFoundException e) {
315             mAppInfo = null;
316         }
317 
318         startInstallConfirm();
319     }
320 
setPmResult(int pmResult)321     void setPmResult(int pmResult) {
322         Intent result = new Intent();
323         result.putExtra(Intent.EXTRA_INSTALL_RESULT, pmResult);
324         setResult(pmResult == PackageManager.INSTALL_SUCCEEDED
325                 ? RESULT_OK : RESULT_FIRST_USER, result);
326     }
327 
generateStubPackageInfo(String packageName)328     private static PackageInfo generateStubPackageInfo(String packageName) {
329         final PackageInfo info = new PackageInfo();
330         final ApplicationInfo aInfo = new ApplicationInfo();
331         info.applicationInfo = aInfo;
332         info.packageName = info.applicationInfo.packageName = packageName;
333         return info;
334     }
335 
336     @Override
onCreate(Bundle icicle)337     protected void onCreate(Bundle icicle) {
338         if (mLocalLOGV) Log.i(TAG, "creating for user " + UserHandle.myUserId());
339         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
340 
341         super.onCreate(null);
342 
343         if (icicle != null) {
344             mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY);
345         }
346         setFinishOnTouchOutside(true);
347 
348         mPm = getPackageManager();
349         mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
350         mInstaller = mPm.getPackageInstaller();
351         mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
352 
353         final Intent intent = getIntent();
354         final String action = intent.getAction();
355 
356         mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
357         mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
358         mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
359         mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
360                 Process.INVALID_UID);
361         mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID)
362                 ? getPackageNameForUid(mOriginatingUid) : null;
363 
364         final Object packageSource;
365         if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) {
366             final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
367                     -1 /* defaultValue */);
368             final SessionInfo info = mInstaller.getSessionInfo(sessionId);
369             String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null;
370             if (info == null || !info.isSealed() || resolvedPath == null) {
371                 Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
372                 finish();
373                 return;
374             }
375 
376             mSessionId = sessionId;
377             packageSource = Uri.fromFile(new File(resolvedPath));
378             mOriginatingURI = null;
379             mReferrerURI = null;
380             mPendingUserActionReason = info.getPendingUserActionReason();
381         } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) {
382             final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID,
383                     -1 /* defaultValue */);
384             final SessionInfo info = mInstaller.getSessionInfo(sessionId);
385             if (info == null || !info.isPreApprovalRequested()) {
386                 Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
387                 finish();
388                 return;
389             }
390 
391             mSessionId = sessionId;
392             packageSource = info;
393             mOriginatingURI = null;
394             mReferrerURI = null;
395             mPendingUserActionReason = info.getPendingUserActionReason();
396         } else {
397             // Two possible callers:
398             // 1. InstallStart with "SCHEME_PACKAGE".
399             // 2. InstallStaging with "SCHEME_FILE" and EXTRA_STAGED_SESSION_ID with staged
400             // session id.
401             mSessionId = -1;
402             packageSource = intent.getData();
403             mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
404             mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
405             mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE;
406         }
407 
408         // if there's nothing to do, quietly slip into the ether
409         if (packageSource == null) {
410             Log.w(TAG, "Unspecified source");
411             setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
412             finish();
413             return;
414         }
415 
416         final boolean wasSetUp = processAppSnippet(packageSource);
417 
418         if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp);
419 
420         if (!wasSetUp) {
421             return;
422         }
423     }
424 
425     @Override
onResume()426     protected void onResume() {
427         super.onResume();
428 
429         if (mLocalLOGV) Log.i(TAG, "onResume(): mAppSnippet=" + mAppSnippet);
430 
431         if (mAppSnippet != null) {
432             // load dummy layout with OK button disabled until we override this layout in
433             // startInstallConfirm
434             bindUi();
435             checkIfAllowedAndInitiateInstall();
436         }
437 
438         if (mOk != null) {
439             mOk.setEnabled(mEnableOk);
440         }
441     }
442 
443     @Override
onPause()444     protected void onPause() {
445         super.onPause();
446 
447         if (mOk != null) {
448             // Don't allow the install button to be clicked as there might be overlays
449             mOk.setEnabled(false);
450         }
451     }
452 
453     @Override
onSaveInstanceState(Bundle outState)454     protected void onSaveInstanceState(Bundle outState) {
455         super.onSaveInstanceState(outState);
456 
457         outState.putBoolean(ALLOW_UNKNOWN_SOURCES_KEY, mAllowUnknownSources);
458     }
459 
460     @Override
onDestroy()461     protected void onDestroy() {
462         super.onDestroy();
463         while (!mActiveUnknownSourcesListeners.isEmpty()) {
464             unregister(mActiveUnknownSourcesListeners.get(0));
465         }
466     }
467 
bindUi()468     private void bindUi() {
469         mAlert.setIcon(mAppSnippet.icon);
470         mAlert.setTitle(mAppSnippet.label);
471         mAlert.setView(R.layout.install_content_view);
472         mAlert.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.install),
473                 (ignored, ignored2) -> {
474                     if (mOk.isEnabled()) {
475                         if (mSessionId != -1) {
476                             setActivityResult(RESULT_OK);
477                             finish();
478                         } else {
479                             startInstall();
480                         }
481                     }
482                 }, null);
483         mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
484                 (ignored, ignored2) -> {
485                     // Cancel and finish
486                     setActivityResult(RESULT_CANCELED);
487                     finish();
488                 }, null);
489         setupAlert();
490 
491         mOk = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
492         mOk.setEnabled(false);
493 
494         if (!mOk.isInTouchMode()) {
495             mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus();
496         }
497     }
498 
setActivityResult(int resultCode)499     private void setActivityResult(int resultCode) {
500         mActivityResultCode = resultCode;
501         super.setResult(resultCode);
502     }
503 
504     @Override
finish()505     public void finish() {
506         if (mSessionId != -1) {
507             if (mActivityResultCode == Activity.RESULT_OK) {
508                 mInstaller.setPermissionsResult(mSessionId, true);
509             } else {
510                 mInstaller.setPermissionsResult(mSessionId, false);
511             }
512         }
513         super.finish();
514     }
515 
516     /**
517      * Check if it is allowed to install the package and initiate install if allowed.
518      */
checkIfAllowedAndInitiateInstall()519     private void checkIfAllowedAndInitiateInstall() {
520         if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
521             if (mLocalLOGV) Log.i(TAG, "install allowed");
522             initiateInstall();
523         } else {
524             handleUnknownSources();
525         }
526     }
527 
handleUnknownSources()528     private void handleUnknownSources() {
529         if (mOriginatingPackage == null) {
530             Log.i(TAG, "No source found for package " + mPkgInfo.packageName);
531             showDialogInner(DLG_ANONYMOUS_SOURCE);
532             return;
533         }
534         // Shouldn't use static constant directly, see b/65534401.
535         final String appOpStr =
536                 AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
537         final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid,
538                 mOriginatingPackage, mCallingAttributionTag,
539                 "Started package installation activity");
540         if (mLocalLOGV) Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode);
541         switch (appOpMode) {
542             case AppOpsManager.MODE_DEFAULT:
543                 mAppOpsManager.setMode(appOpStr, mOriginatingUid,
544                         mOriginatingPackage, AppOpsManager.MODE_ERRORED);
545                 // fall through
546             case AppOpsManager.MODE_ERRORED:
547                 showDialogInner(DLG_EXTERNAL_SOURCE_BLOCKED);
548                 break;
549             case AppOpsManager.MODE_ALLOWED:
550                 initiateInstall();
551                 break;
552             default:
553                 Log.e(TAG, "Invalid app op mode " + appOpMode
554                         + " for OP_REQUEST_INSTALL_PACKAGES found for uid " + mOriginatingUid);
555                 finish();
556                 break;
557         }
558     }
559 
560     /**
561      * Parse the Uri and set up the installer for this package.
562      *
563      * @param packageUri The URI to parse
564      *
565      * @return {@code true} iff the installer could be set up
566      */
processPackageUri(final Uri packageUri)567     private boolean processPackageUri(final Uri packageUri) {
568         mPackageURI = packageUri;
569         final String scheme = packageUri.getScheme();
570         final String packageName = packageUri.getSchemeSpecificPart();
571 
572         if (mLocalLOGV) Log.i(TAG, "processPackageUri(): uri=" + packageUri + ", scheme=" + scheme);
573 
574         switch (scheme) {
575             case SCHEME_PACKAGE: {
576                 for (UserHandle handle : mUserManager.getUserHandles(true)) {
577                     PackageManager pmForUser = createContextAsUser(handle, 0)
578                                                 .getPackageManager();
579                     try {
580                         if (pmForUser.canPackageQuery(mCallingPackage, packageName)) {
581                             mPkgInfo = pmForUser.getPackageInfo(packageName,
582                                     PackageManager.GET_PERMISSIONS
583                                             | PackageManager.MATCH_UNINSTALLED_PACKAGES);
584                         }
585                     } catch (NameNotFoundException e) {
586                     }
587                 }
588                 if (mPkgInfo == null) {
589                     Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart()
590                             + " not available. Discontinuing installation");
591                     showDialogInner(DLG_PACKAGE_ERROR);
592                     setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
593                     return false;
594                 }
595                 CharSequence label = mPm.getApplicationLabel(mPkgInfo.applicationInfo);
596                 if (mLocalLOGV) Log.i(TAG, "creating snippet for " + label);
597                 mAppSnippet = new PackageUtil.AppSnippet(label,
598                         mPm.getApplicationIcon(mPkgInfo.applicationInfo));
599             } break;
600 
601             case ContentResolver.SCHEME_FILE: {
602                 File sourceFile = new File(packageUri.getPath());
603                 mPkgInfo = PackageUtil.getPackageInfo(this, sourceFile,
604                         PackageManager.GET_PERMISSIONS);
605 
606                 // Check for parse errors
607                 if (mPkgInfo == null) {
608                     Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
609                     showDialogInner(DLG_PACKAGE_ERROR);
610                     setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
611                     return false;
612                 }
613                 if (mLocalLOGV) Log.i(TAG, "creating snippet for local file " + sourceFile);
614                 mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
615             } break;
616 
617             default: {
618                 throw new IllegalArgumentException("Unexpected URI scheme " + packageUri);
619             }
620         }
621 
622         return true;
623     }
624 
625     /**
626      * Use the SessionInfo and set up the installer for pre-commit install session.
627      *
628      * @param info The SessionInfo to compose
629      *
630      * @return {@code true} iff the installer could be set up
631      */
processSessionInfo(@onNull SessionInfo info)632     private boolean processSessionInfo(@NonNull SessionInfo info) {
633         mPkgInfo = generateStubPackageInfo(info.getAppPackageName());
634         mAppSnippet = new PackageUtil.AppSnippet(info.getAppLabel(),
635                 info.getAppIcon() != null ? new BitmapDrawable(getResources(), info.getAppIcon())
636                         : getPackageManager().getDefaultActivityIcon());
637         return true;
638     }
639 
640     /**
641      * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install
642      * session) to set up the installer for this install.
643      *
644      * @param source The source of package URI or SessionInfo
645      *
646      * @return {@code true} iff the installer could be set up
647      */
processAppSnippet(@onNull Object source)648     private boolean processAppSnippet(@NonNull Object source) {
649         if (source instanceof Uri) {
650             return processPackageUri((Uri) source);
651         } else if (source instanceof SessionInfo) {
652             return processSessionInfo((SessionInfo) source);
653         }
654 
655         return false;
656     }
657 
658     @Override
onBackPressed()659     public void onBackPressed() {
660         if (mSessionId != -1) {
661             setActivityResult(RESULT_CANCELED);
662         }
663         super.onBackPressed();
664     }
665 
startInstall()666     private void startInstall() {
667         String installerPackageName = getIntent().getStringExtra(
668                 Intent.EXTRA_INSTALLER_PACKAGE_NAME);
669         int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);
670 
671         // Start subactivity to actually install the application
672         Intent newIntent = new Intent();
673         newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
674                 mPkgInfo.applicationInfo);
675         newIntent.setData(mPackageURI);
676         newIntent.setClass(this, InstallInstalling.class);
677         if (mOriginatingURI != null) {
678             newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
679         }
680         if (mReferrerURI != null) {
681             newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
682         }
683         if (mOriginatingUid != Process.INVALID_UID) {
684             newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
685         }
686         if (installerPackageName != null) {
687             newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
688                     installerPackageName);
689         }
690         if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
691             newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
692         }
693         if (stagedSessionId > 0) {
694             newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId);
695         }
696         if (mAppSnippet != null) {
697             newIntent.putExtra(EXTRA_APP_SNIPPET, mAppSnippet);
698         }
699         newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
700         if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
701         startActivity(newIntent);
702         finish();
703     }
704 
705     /**
706      * Dialog to show when the source of apk can not be identified
707      */
708     public static class AnonymousSourceDialog extends DialogFragment {
newInstance()709         static AnonymousSourceDialog newInstance() {
710             return new AnonymousSourceDialog();
711         }
712 
713         @Override
onCreateDialog(Bundle savedInstanceState)714         public Dialog onCreateDialog(Bundle savedInstanceState) {
715             return new AlertDialog.Builder(getActivity())
716                     .setMessage(R.string.anonymous_source_warning)
717                     .setPositiveButton(R.string.anonymous_source_continue,
718                             ((dialog, which) -> {
719                                 PackageInstallerActivity activity = ((PackageInstallerActivity)
720                                         getActivity());
721 
722                                 activity.mAllowUnknownSources = true;
723                                 activity.initiateInstall();
724                             }))
725                     .setNegativeButton(R.string.cancel, ((dialog, which) -> getActivity().finish()))
726                     .create();
727         }
728 
729         @Override
onCancel(DialogInterface dialog)730         public void onCancel(DialogInterface dialog) {
731             getActivity().finish();
732         }
733     }
734 
735     /**
736      * An error dialog shown when the device is out of space
737      */
738     public static class OutOfSpaceDialog extends AppErrorDialog {
newInstance(@onNull CharSequence applicationLabel)739         static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) {
740             OutOfSpaceDialog dialog = new OutOfSpaceDialog();
741             dialog.setArgument(applicationLabel);
742             return dialog;
743         }
744 
745         @Override
createDialog(@onNull CharSequence argument)746         protected Dialog createDialog(@NonNull CharSequence argument) {
747             String dlgText = getString(R.string.out_of_space_dlg_text, argument);
748             return new AlertDialog.Builder(getActivity())
749                     .setMessage(dlgText)
750                     .setPositiveButton(R.string.manage_applications, (dialog, which) -> {
751                         // launch manage applications
752                         Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE");
753                         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
754                         startActivity(intent);
755                         getActivity().finish();
756                     })
757                     .setNegativeButton(R.string.cancel, (dialog, which) -> getActivity().finish())
758                     .create();
759         }
760     }
761 
762     /**
763      * A generic install-error dialog
764      */
765     public static class InstallErrorDialog extends AppErrorDialog {
766         static AppErrorDialog newInstance(@NonNull CharSequence applicationLabel) {
767             InstallErrorDialog dialog = new InstallErrorDialog();
768             dialog.setArgument(applicationLabel);
769             return dialog;
770         }
771 
772         @Override
773         protected Dialog createDialog(@NonNull CharSequence argument) {
774             return new AlertDialog.Builder(getActivity())
775                     .setNeutralButton(R.string.ok, (dialog, which) -> getActivity().finish())
776                     .setMessage(getString(R.string.install_failed_msg, argument))
777                     .create();
778         }
779     }
780 
781     private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener {
782 
783         @Override
784         public void onOpChanged(String op, String packageName) {
785             if (!mOriginatingPackage.equals(packageName)) {
786                 return;
787             }
788             unregister(this);
789             mActiveUnknownSourcesListeners.remove(this);
790             if (isDestroyed()) {
791                 return;
792             }
793             new Handler(Looper.getMainLooper()).postDelayed(() -> {
794                 if (!isDestroyed()) {
795                     startActivity(getIntent());
796                     // The start flag (FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP) doesn't
797                     // work for the multiple user case, i.e. the caller task user and started
798                     // Activity user are not the same. To avoid having multiple PIAs in the task,
799                     // finish the current PackageInstallerActivity
800                     finish();
801                 }
802             }, 500);
803 
804         }
805 
806     }
807 
808     private void register(UnknownSourcesListener listener) {
809         mAppOpsManager.startWatchingMode(
810                 AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, mOriginatingPackage,
811                 listener);
812         mActiveUnknownSourcesListeners.add(listener);
813     }
814 
815     private void unregister(UnknownSourcesListener listener) {
816         mAppOpsManager.stopWatchingMode(listener);
817         mActiveUnknownSourcesListeners.remove(listener);
818     }
819 
820     /**
821      * An error dialog shown when external sources are not allowed
822      */
823     public static class ExternalSourcesBlockedDialog extends AppErrorDialog {
824         static AppErrorDialog newInstance(@NonNull String originationPkg) {
825             ExternalSourcesBlockedDialog dialog =
826                     new ExternalSourcesBlockedDialog();
827             dialog.setArgument(originationPkg);
828             return dialog;
829         }
830 
831         @Override
832         protected Dialog createDialog(@NonNull CharSequence argument) {
833 
834             final PackageInstallerActivity activity = (PackageInstallerActivity)getActivity();
835             try {
836                 PackageManager pm = activity.getPackageManager();
837 
838                 ApplicationInfo sourceInfo = pm.getApplicationInfo(argument.toString(), 0);
839 
840                 return new AlertDialog.Builder(activity)
841                         .setTitle(pm.getApplicationLabel(sourceInfo))
842                         .setIcon(pm.getApplicationIcon(sourceInfo))
843                         .setMessage(R.string.untrusted_external_source_warning)
844                         .setPositiveButton(R.string.external_sources_settings,
845                                 (dialog, which) -> {
846                                     Intent settingsIntent = new Intent();
847                                     settingsIntent.setAction(
848                                             Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
849                                     final Uri packageUri = Uri.parse("package:" + argument);
850                                     settingsIntent.setData(packageUri);
851                                     settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY);
852                                     try {
853                                         activity.register(activity.new UnknownSourcesListener());
854                                         activity.startActivityForResult(settingsIntent,
855                                                 REQUEST_TRUST_EXTERNAL_SOURCE);
856                                     } catch (ActivityNotFoundException exc) {
857                                         Log.e(TAG, "Settings activity not found for action: "
858                                                 + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
859                                     }
860                                 })
861                         .setNegativeButton(R.string.cancel,
862                                 (dialog, which) -> activity.finish())
863                         .create();
864             } catch (NameNotFoundException e) {
865                 Log.e(TAG, "Did not find app info for " + argument);
866                 activity.finish();
867                 return null;
868             }
869         }
870     }
871 
872     /**
873      * Superclass for all error dialogs. Stores a single CharSequence argument
874      */
875     public abstract static class AppErrorDialog extends DialogFragment {
876         private static final String ARGUMENT_KEY = AppErrorDialog.class.getName() + "ARGUMENT_KEY";
877 
878         protected void setArgument(@NonNull CharSequence argument) {
879             Bundle args = new Bundle();
880             args.putCharSequence(ARGUMENT_KEY, argument);
881             setArguments(args);
882         }
883 
884         protected abstract Dialog createDialog(@NonNull CharSequence argument);
885 
886         @Override
887         public Dialog onCreateDialog(Bundle savedInstanceState) {
888             return createDialog(getArguments().getString(ARGUMENT_KEY));
889         }
890 
891         @Override
892         public void onCancel(DialogInterface dialog) {
893             getActivity().finish();
894         }
895     }
896 }
897