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 com.android.packageinstaller.PackageInstallerActivity.EXTRA_APP_SNIPPET;
20 import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
21 
22 import android.app.Activity;
23 import android.app.AlertDialog;
24 import android.app.PendingIntent;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageInstaller;
29 import android.content.pm.PackageManager;
30 import android.net.Uri;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Looper;
35 import android.util.Log;
36 import android.view.View;
37 import android.widget.Button;
38 import androidx.annotation.Nullable;
39 import com.android.packageinstaller.common.EventResultPersister;
40 import com.android.packageinstaller.common.InstallEventReceiver;
41 import java.io.File;
42 import java.io.IOException;
43 
44 /**
45  * Send package to the package manager and handle results from package manager. Once the
46  * installation succeeds, start {@link InstallSuccess} or {@link InstallFailed}.
47  * <p>This has two phases: First send the data to the package manager, then wait until the package
48  * manager processed the result.</p>
49  */
50 public class InstallInstalling extends Activity {
51     private static final String LOG_TAG = InstallInstalling.class.getSimpleName();
52 
53     private static final String SESSION_ID = "com.android.packageinstaller.SESSION_ID";
54     private static final String INSTALL_ID = "com.android.packageinstaller.INSTALL_ID";
55 
56     private static final String BROADCAST_ACTION =
57             "com.android.packageinstaller.ACTION_INSTALL_COMMIT";
58 
59     /** Task that sends the package to the package installer */
60     private InstallingAsyncTask mInstallingTask;
61 
62     /** Id of the session to install the package */
63     private int mSessionId;
64 
65     /** Id of the install event we wait for */
66     private int mInstallId;
67 
68     /** URI of package to install */
69     private Uri mPackageURI;
70 
71     /** The button that can cancel this dialog */
72     private Button mCancelButton;
73 
74     private AlertDialog mDialog;
75 
76     @Override
onCreate(@ullable Bundle savedInstanceState)77     protected void onCreate(@Nullable Bundle savedInstanceState) {
78         super.onCreate(savedInstanceState);
79 
80         ApplicationInfo appInfo = getIntent()
81                 .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
82         mPackageURI = getIntent().getData();
83 
84         if (PackageInstallerActivity.SCHEME_PACKAGE.equals(mPackageURI.getScheme())) {
85             try {
86                 getPackageManager().installExistingPackage(appInfo.packageName);
87                 launchSuccess();
88             } catch (PackageManager.NameNotFoundException e) {
89                 launchFailure(PackageInstaller.STATUS_FAILURE,
90                         PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
91             }
92         } else {
93             // ContentResolver.SCHEME_FILE
94             // STAGED_SESSION_ID extra contains an ID of a previously staged install session.
95             final File sourceFile = new File(mPackageURI.getPath());
96 
97             // Dialogs displayed while changing update-owner have a blank icon. To fix this,
98             // fetch the appSnippet from the source file again
99             PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
100             getIntent().putExtra(EXTRA_APP_SNIPPET, as);
101 
102             AlertDialog.Builder builder = new AlertDialog.Builder(this);
103 
104             builder.setIcon(as.icon);
105             builder.setTitle(as.label);
106             builder.setView(R.layout.install_content_view);
107             builder.setNegativeButton(getString(R.string.cancel),
108                     (ignored, ignored2) -> {
109                         if (mInstallingTask != null) {
110                             mInstallingTask.cancel(true);
111                         }
112 
113                         if (mSessionId > 0) {
114                             getPackageManager().getPackageInstaller().abandonSession(mSessionId);
115                             mSessionId = 0;
116                         }
117 
118                         setResult(RESULT_CANCELED);
119                         finish();
120                     });
121             builder.setCancelable(false);
122             mDialog = builder.create();
123             mDialog.show();
124             mDialog.requireViewById(R.id.installing).setVisibility(View.VISIBLE);
125 
126             if (savedInstanceState != null) {
127                 mSessionId = savedInstanceState.getInt(SESSION_ID);
128                 mInstallId = savedInstanceState.getInt(INSTALL_ID);
129 
130                 // Reregister for result; might instantly call back if result was delivered while
131                 // activity was destroyed
132                 try {
133                     InstallEventReceiver.addObserver(this, mInstallId,
134                             this::launchFinishBasedOnResult);
135                 } catch (EventResultPersister.OutOfIdsException e) {
136                     // Does not happen
137                 }
138             } else {
139                 try {
140                     mInstallId = InstallEventReceiver
141                             .addObserver(this, EventResultPersister.GENERATE_NEW_ID,
142                                     this::launchFinishBasedOnResult);
143                 } catch (EventResultPersister.OutOfIdsException e) {
144                     launchFailure(PackageInstaller.STATUS_FAILURE,
145                             PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
146                 }
147 
148                 mSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);
149                 // Try to open session previously staged in InstallStaging.
150                 try (PackageInstaller.Session ignored =
151                              getPackageManager().getPackageInstaller().openSession(
152                         mSessionId)) {
153                     Log.d(LOG_TAG, "Staged session is valid, proceeding with the install");
154                 } catch (IOException | SecurityException e) {
155                     Log.e(LOG_TAG, "Invalid session id passed", e);
156                     launchFailure(PackageInstaller.STATUS_FAILURE,
157                             PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
158                 }
159             }
160 
161             mCancelButton = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
162         }
163     }
164 
165     /**
166      * Launch the "success" version of the final package installer dialog
167      */
launchSuccess()168     private void launchSuccess() {
169         Intent successIntent = new Intent(getIntent());
170         successIntent.setClass(this, InstallSuccess.class);
171         successIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
172 
173         startActivity(successIntent);
174         finish();
175     }
176 
177     /**
178      * Launch the "failure" version of the final package installer dialog
179      *
180      * @param statusCode    The generic status code as returned by the package installer.
181      * @param legacyStatus  The status as used internally in the package manager.
182      * @param statusMessage The status description.
183      */
launchFailure(int statusCode, int legacyStatus, String statusMessage)184     private void launchFailure(int statusCode, int legacyStatus, String statusMessage) {
185         Intent failureIntent = new Intent(getIntent());
186         failureIntent.setClass(this, InstallFailed.class);
187         failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
188         failureIntent.putExtra(PackageInstaller.EXTRA_STATUS, statusCode);
189         failureIntent.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, legacyStatus);
190         failureIntent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage);
191 
192         startActivity(failureIntent);
193         finish();
194     }
195 
196     @Override
onResume()197     protected void onResume() {
198         super.onResume();
199 
200         // This is the first onResume in a single life of the activity
201         if (mInstallingTask == null) {
202             PackageInstaller installer = getPackageManager().getPackageInstaller();
203             PackageInstaller.SessionInfo sessionInfo = installer.getSessionInfo(mSessionId);
204 
205             if (sessionInfo != null && !sessionInfo.isActive()) {
206                 mInstallingTask = new InstallingAsyncTask();
207                 mInstallingTask.execute();
208             } else {
209                 // we will receive a broadcast when the install is finished
210                 mCancelButton.setEnabled(false);
211                 setFinishOnTouchOutside(false);
212             }
213         }
214     }
215 
216     @Override
onSaveInstanceState(Bundle outState)217     protected void onSaveInstanceState(Bundle outState) {
218         super.onSaveInstanceState(outState);
219 
220         outState.putInt(SESSION_ID, mSessionId);
221         outState.putInt(INSTALL_ID, mInstallId);
222     }
223 
224     @Override
onBackPressed()225     public void onBackPressed() {
226         if (mCancelButton.isEnabled()) {
227             super.onBackPressed();
228         }
229     }
230 
231     @Override
onDestroy()232     protected void onDestroy() {
233         if (mInstallingTask != null) {
234             mInstallingTask.cancel(true);
235             synchronized (mInstallingTask) {
236                 while (!mInstallingTask.isDone) {
237                     try {
238                         mInstallingTask.wait();
239                     } catch (InterruptedException e) {
240                         Log.i(LOG_TAG, "Interrupted while waiting for installing task to cancel",
241                                 e);
242                     }
243                 }
244             }
245         }
246 
247         InstallEventReceiver.removeObserver(this, mInstallId);
248 
249         super.onDestroy();
250     }
251 
252     @Override
finish()253     public void finish() {
254         if (mDialog != null) {
255             mDialog.dismiss();
256         }
257         super.finish();
258     }
259 
260     /**
261      * Launch the appropriate finish activity (success or failed) for the installation result.
262      *
263      * @param statusCode    The installation result.
264      * @param legacyStatus  The installation as used internally in the package manager.
265      * @param statusMessage The detailed installation result.
266      * @param serviceId     Id for PowerManager.WakeLock service. Used only by Wear devices
267      *                      during an uninstall.
268      */
launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage, int serviceId )269     private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage,
270             int serviceId /* ignore */) {
271         if (statusCode == PackageInstaller.STATUS_SUCCESS) {
272             launchSuccess();
273         } else {
274             launchFailure(statusCode, legacyStatus, statusMessage);
275         }
276     }
277 
278     /**
279      * Send the package to the package installer and then register a event result observer that
280      * will call {@link #launchFinishBasedOnResult(int, int, String, int)}
281      */
282     private final class InstallingAsyncTask extends AsyncTask<Void, Void,
283             PackageInstaller.Session> {
284         volatile boolean isDone;
285 
286         @Override
doInBackground(Void... params)287         protected PackageInstaller.Session doInBackground(Void... params) {
288             try {
289                 return getPackageManager().getPackageInstaller().openSession(mSessionId);
290             } catch (IOException e) {
291                 return null;
292             } finally {
293                 synchronized (this) {
294                     isDone = true;
295                     notifyAll();
296                 }
297             }
298         }
299 
300         @Override
onPostExecute(PackageInstaller.Session session)301         protected void onPostExecute(PackageInstaller.Session session) {
302             if (session != null) {
303                 Intent broadcastIntent = new Intent(BROADCAST_ACTION);
304                 broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
305                 broadcastIntent.setPackage(getPackageName());
306                 broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId);
307 
308                 PendingIntent pendingIntent = PendingIntent.getBroadcast(
309                         InstallInstalling.this,
310                         mInstallId,
311                         broadcastIntent,
312                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
313 
314                 // Delay committing the session by 100ms to fix a UI glitch while displaying the
315                 // Update-Owner change dialog on top of the Installing dialog
316                 new Handler(Looper.getMainLooper()).postDelayed(() -> {
317                     try {
318                         session.commit(pendingIntent.getIntentSender());
319                     } catch (Exception e) {
320                         Log.e(LOG_TAG, "Cannot install package: ", e);
321                         launchFailure(PackageInstaller.STATUS_FAILURE,
322                             PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
323                         return;
324                     }
325                 }, 100);
326                 mCancelButton.setEnabled(false);
327                 setFinishOnTouchOutside(false);
328             } else {
329                 getPackageManager().getPackageInstaller().abandonSession(mSessionId);
330 
331                 if (!isCancelled()) {
332                     launchFailure(PackageInstaller.STATUS_FAILURE,
333                             PackageManager.INSTALL_FAILED_INVALID_APK, null);
334                 }
335             }
336         }
337     }
338 }
339