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.wear;
18 
19 import android.annotation.TargetApi;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.IntentSender;
26 import android.content.pm.PackageInstaller;
27 import android.os.Build;
28 import android.os.ParcelFileDescriptor;
29 import android.util.Log;
30 
31 import java.io.IOException;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Map;
35 
36 /**
37  * Implementation of package manager installation using modern PackageInstaller api.
38  *
39  * Heavily copied from Wearsky/Finsky implementation
40  */
41 @TargetApi(Build.VERSION_CODES.LOLLIPOP)
42 public class PackageInstallerImpl {
43     private static final String TAG = "PackageInstallerImpl";
44 
45     /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */
46     private static final String ACTION_INSTALL_COMMIT =
47             "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT";
48 
49     private final Context mContext;
50     private final PackageInstaller mPackageInstaller;
51     private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap;
52     private final Map<String, PackageInstaller.Session> mOpenSessionMap;
53 
PackageInstallerImpl(Context context)54     public PackageInstallerImpl(Context context) {
55         mContext = context.getApplicationContext();
56         mPackageInstaller = mContext.getPackageManager().getPackageInstaller();
57 
58         // Capture a map of known sessions
59         // This list will be pruned a bit later (stale sessions will be canceled)
60         mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>();
61         List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions();
62         for (int i = 0; i < mySessions.size(); i++) {
63             PackageInstaller.SessionInfo sessionInfo = mySessions.get(i);
64             String packageName = sessionInfo.getAppPackageName();
65             PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo);
66 
67             // Checking for old info is strictly for logging purposes
68             if (oldInfo != null) {
69                 Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo
70                         .getSessionId() + " & keeping " + mySessions.get(i).getSessionId());
71             }
72         }
73         mOpenSessionMap = new HashMap<String, PackageInstaller.Session>();
74     }
75 
76     /**
77      * This callback will be made after an installation attempt succeeds or fails.
78      */
79     public interface InstallListener {
80         /**
81          * This callback signals that preflight checks have succeeded and installation
82          * is beginning.
83          */
installBeginning()84         void installBeginning();
85 
86         /**
87          * This callback signals that installation has completed.
88          */
installSucceeded()89         void installSucceeded();
90 
91         /**
92          * This callback signals that installation has failed.
93          */
installFailed(int errorCode, String errorDesc)94         void installFailed(int errorCode, String errorDesc);
95     }
96 
97     /**
98      * This is a placeholder implementation that bundles an entire "session" into a single
99      * call. This will be replaced by more granular versions that allow longer session lifetimes,
100      * download progress tracking, etc.
101      *
102      * This must not be called on main thread.
103      */
install(final String packageName, ParcelFileDescriptor parcelFileDescriptor, final InstallListener callback)104     public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor,
105             final InstallListener callback) {
106         // 0. Generic try/catch block because I am not really sure what exceptions (other than
107         // IOException) might be thrown by PackageInstaller and I want to handle them
108         // at least slightly gracefully.
109         try {
110             // 1. Create or recover a session, and open it
111             // Try recovery first
112             PackageInstaller.Session session = null;
113             PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName);
114             if (sessionInfo != null) {
115                 // See if it's openable, or already held open
116                 session = getSession(packageName);
117             }
118             // If open failed, or there was no session, create a new one and open it.
119             // If we cannot create or open here, the failure is terminal.
120             if (session == null) {
121                 try {
122                     innerCreateSession(packageName);
123                 } catch (IOException ioe) {
124                     Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage());
125                     callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION,
126                             "Could not create session");
127                     mSessionInfoMap.remove(packageName);
128                     return;
129                 }
130                 sessionInfo = mSessionInfoMap.get(packageName);
131                 try {
132                     session = mPackageInstaller.openSession(sessionInfo.getSessionId());
133                     mOpenSessionMap.put(packageName, session);
134                 } catch (SecurityException se) {
135                     Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage());
136                     callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION,
137                             "Can't open session");
138                     mSessionInfoMap.remove(packageName);
139                     return;
140                 }
141             }
142 
143             // 2. Launch task to handle file operations.
144             InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor,
145                     callback, session,
146                     getCommitCallback(packageName, sessionInfo.getSessionId(), callback));
147             task.execute();
148             if (task.isError()) {
149                 cancelSession(sessionInfo.getSessionId(), packageName);
150             }
151         } catch (Exception e) {
152             Log.e(TAG, "Unexpected exception while installing: " + packageName + ": "
153                     + e.getMessage());
154             callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION,
155                     "Unexpected exception while installing " + packageName);
156         }
157     }
158 
159     /**
160      * Retrieve an existing session. Will open if needed, but does not attempt to create.
161      */
getSession(String packageName)162     private PackageInstaller.Session getSession(String packageName) {
163         // Check for already-open session
164         PackageInstaller.Session session = mOpenSessionMap.get(packageName);
165         if (session != null) {
166             try {
167                 // Probe the session to ensure that it's still open. This may or may not
168                 // throw (if non-open), but it may serve as a canary for stale sessions.
169                 session.getNames();
170                 return session;
171             } catch (IOException ioe) {
172                 Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage());
173                 mOpenSessionMap.remove(packageName);
174             } catch (SecurityException se) {
175                 Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage());
176                 mOpenSessionMap.remove(packageName);
177             }
178         }
179         // Check to see if this is a known session
180         PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName);
181         if (sessionInfo == null) {
182             return null;
183         }
184         // Try to open it. If we fail here, assume that the SessionInfo was stale.
185         try {
186             session = mPackageInstaller.openSession(sessionInfo.getSessionId());
187         } catch (SecurityException se) {
188             Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info");
189             mSessionInfoMap.remove(packageName);
190             return null;
191         } catch (IOException ioe) {
192             Log.w(TAG, "IOException opening old session for " + ioe.getMessage()
193                     + " - deleting info");
194             mSessionInfoMap.remove(packageName);
195             return null;
196         }
197         mOpenSessionMap.put(packageName, session);
198         return session;
199     }
200 
201     /** This version throws an IOException when the session cannot be created */
innerCreateSession(String packageName)202     private void innerCreateSession(String packageName) throws IOException {
203         if (mSessionInfoMap.containsKey(packageName)) {
204             Log.w(TAG, "Creating session for " + packageName + " when one already exists");
205             return;
206         }
207         PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
208                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
209         params.setAppPackageName(packageName);
210 
211         // IOException may be thrown at this point
212         int sessionId = mPackageInstaller.createSession(params);
213         PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
214         mSessionInfoMap.put(packageName, sessionInfo);
215     }
216 
217     /**
218      * Cancel a session based on its sessionId. Package name is for logging only.
219      */
cancelSession(int sessionId, String packageName)220     private void cancelSession(int sessionId, String packageName) {
221         // Close if currently held open
222         closeSession(packageName);
223         // Remove local record
224         mSessionInfoMap.remove(packageName);
225         try {
226             mPackageInstaller.abandonSession(sessionId);
227         } catch (SecurityException se) {
228             // The session no longer exists, so we can exit quietly.
229             return;
230         }
231     }
232 
233     /**
234      * Close a session if it happens to be held open.
235      */
closeSession(String packageName)236     private void closeSession(String packageName) {
237         PackageInstaller.Session session = mOpenSessionMap.remove(packageName);
238         if (session != null) {
239             // Unfortunately close() is not idempotent. Try our best to make this safe.
240             try {
241                 session.close();
242             } catch (Exception e) {
243                 Log.w(TAG, "Unexpected error closing session for " + packageName + ": "
244                         + e.getMessage());
245             }
246         }
247     }
248 
249     /**
250      * Creates a commit callback for the package install that's underway. This will be called
251      * some time after calling session.commit() (above).
252      */
getCommitCallback(final String packageName, final int sessionId, final InstallListener callback)253     private IntentSender getCommitCallback(final String packageName, final int sessionId,
254             final InstallListener callback) {
255         // Create a single-use broadcast receiver
256         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
257             @Override
258             public void onReceive(Context context, Intent intent) {
259                 mContext.unregisterReceiver(this);
260                 handleCommitCallback(intent, packageName, sessionId, callback);
261             }
262         };
263         // Create a matching intent-filter and register the receiver
264         String action = ACTION_INSTALL_COMMIT + "." + packageName;
265         IntentFilter intentFilter = new IntentFilter();
266         intentFilter.addAction(action);
267         mContext.registerReceiver(broadcastReceiver, intentFilter);
268 
269         // Create a matching PendingIntent and use it to generate the IntentSender
270         Intent broadcastIntent = new Intent(action);
271         PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(),
272                 broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
273         return pendingIntent.getIntentSender();
274     }
275 
276     /**
277      * Examine the extras to determine information about the package update/install, decode
278      * the result, and call the appropriate callback.
279      *
280      * @param intent The intent, which the PackageInstaller will have added Extras to
281      * @param packageName The package name we created the receiver for
282      * @param sessionId The session Id we created the receiver for
283      * @param callback The callback to report success/failure to
284      */
handleCommitCallback(Intent intent, String packageName, int sessionId, InstallListener callback)285     private void handleCommitCallback(Intent intent, String packageName, int sessionId,
286             InstallListener callback) {
287         if (Log.isLoggable(TAG, Log.DEBUG)) {
288             Log.d(TAG, "Installation of " + packageName + " finished with extras "
289                     + intent.getExtras());
290         }
291         String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
292         int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
293         if (status == PackageInstaller.STATUS_SUCCESS) {
294             cancelSession(sessionId, packageName);
295             callback.installSucceeded();
296         } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) {
297             // TODO - use the constant when the correct/final name is in the SDK
298             // TODO This is unexpected, so we are treating as failure for now
299             cancelSession(sessionId, packageName);
300             callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED,
301                     "Unexpected: user action required");
302         } else {
303             cancelSession(sessionId, packageName);
304             int errorCode = getPackageManagerErrorCode(status);
305             Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": "
306                     + statusMessage);
307             callback.installFailed(errorCode, null);
308         }
309     }
310 
getPackageManagerErrorCode(int status)311     private int getPackageManagerErrorCode(int status) {
312         // This is a hack: because PackageInstaller now reports error codes
313         // with small positive values, we need to remap them into a space
314         // that is more compatible with the existing package manager error codes.
315         // See https://sites.google.com/a/google.com/universal-store/documentation
316         //       /android-client/download-error-codes
317         int errorCode;
318         if (status == Integer.MIN_VALUE) {
319             errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST;
320         } else {
321             errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status;
322         }
323         return errorCode;
324     }
325 }
326