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