• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION,
154                     "Unexpected exception while installing " + packageName);
155         }
156     }
157 
158     /**
159      * Retrieve an existing session. Will open if needed, but does not attempt to create.
160      */
getSession(String packageName)161     private PackageInstaller.Session getSession(String packageName) {
162         // Check for already-open session
163         PackageInstaller.Session session = mOpenSessionMap.get(packageName);
164         if (session != null) {
165             try {
166                 // Probe the session to ensure that it's still open. This may or may not
167                 // throw (if non-open), but it may serve as a canary for stale sessions.
168                 session.getNames();
169                 return session;
170             } catch (IOException ioe) {
171                 Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage());
172                 mOpenSessionMap.remove(packageName);
173             } catch (SecurityException se) {
174                 Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage());
175                 mOpenSessionMap.remove(packageName);
176             }
177         }
178         // Check to see if this is a known session
179         PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName);
180         if (sessionInfo == null) {
181             return null;
182         }
183         // Try to open it. If we fail here, assume that the SessionInfo was stale.
184         try {
185             session = mPackageInstaller.openSession(sessionInfo.getSessionId());
186         } catch (SecurityException se) {
187             Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info");
188             mSessionInfoMap.remove(packageName);
189             return null;
190         } catch (IOException ioe) {
191             Log.w(TAG, "IOException opening old session for " + ioe.getMessage()
192                     + " - deleting info");
193             mSessionInfoMap.remove(packageName);
194             return null;
195         }
196         mOpenSessionMap.put(packageName, session);
197         return session;
198     }
199 
200     /** This version throws an IOException when the session cannot be created */
innerCreateSession(String packageName)201     private void innerCreateSession(String packageName) throws IOException {
202         if (mSessionInfoMap.containsKey(packageName)) {
203             Log.w(TAG, "Creating session for " + packageName + " when one already exists");
204             return;
205         }
206         PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
207                 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
208         params.setAppPackageName(packageName);
209 
210         // IOException may be thrown at this point
211         int sessionId = mPackageInstaller.createSession(params);
212         PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
213         mSessionInfoMap.put(packageName, sessionInfo);
214     }
215 
216     /**
217      * Cancel a session based on its sessionId. Package name is for logging only.
218      */
cancelSession(int sessionId, String packageName)219     private void cancelSession(int sessionId, String packageName) {
220         // Close if currently held open
221         closeSession(packageName);
222         // Remove local record
223         mSessionInfoMap.remove(packageName);
224         try {
225             mPackageInstaller.abandonSession(sessionId);
226         } catch (SecurityException se) {
227             // The session no longer exists, so we can exit quietly.
228             return;
229         }
230     }
231 
232     /**
233      * Close a session if it happens to be held open.
234      */
closeSession(String packageName)235     private void closeSession(String packageName) {
236         PackageInstaller.Session session = mOpenSessionMap.remove(packageName);
237         if (session != null) {
238             // Unfortunately close() is not idempotent. Try our best to make this safe.
239             try {
240                 session.close();
241             } catch (Exception e) {
242                 Log.w(TAG, "Unexpected error closing session for " + packageName + ": "
243                         + e.getMessage());
244             }
245         }
246     }
247 
248     /**
249      * Creates a commit callback for the package install that's underway. This will be called
250      * some time after calling session.commit() (above).
251      */
getCommitCallback(final String packageName, final int sessionId, final InstallListener callback)252     private IntentSender getCommitCallback(final String packageName, final int sessionId,
253             final InstallListener callback) {
254         // Create a single-use broadcast receiver
255         BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
256             @Override
257             public void onReceive(Context context, Intent intent) {
258                 mContext.unregisterReceiver(this);
259                 handleCommitCallback(intent, packageName, sessionId, callback);
260             }
261         };
262         // Create a matching intent-filter and register the receiver
263         String action = ACTION_INSTALL_COMMIT + "." + packageName;
264         IntentFilter intentFilter = new IntentFilter();
265         intentFilter.addAction(action);
266         mContext.registerReceiver(broadcastReceiver, intentFilter);
267 
268         // Create a matching PendingIntent and use it to generate the IntentSender
269         Intent broadcastIntent = new Intent(action);
270         PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(),
271                 broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
272         return pendingIntent.getIntentSender();
273     }
274 
275     /**
276      * Examine the extras to determine information about the package update/install, decode
277      * the result, and call the appropriate callback.
278      *
279      * @param intent The intent, which the PackageInstaller will have added Extras to
280      * @param packageName The package name we created the receiver for
281      * @param sessionId The session Id we created the receiver for
282      * @param callback The callback to report success/failure to
283      */
handleCommitCallback(Intent intent, String packageName, int sessionId, InstallListener callback)284     private void handleCommitCallback(Intent intent, String packageName, int sessionId,
285             InstallListener callback) {
286         if (Log.isLoggable(TAG, Log.DEBUG)) {
287             Log.d(TAG, "Installation of " + packageName + " finished with extras "
288                     + intent.getExtras());
289         }
290         String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
291         int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
292         if (status == PackageInstaller.STATUS_SUCCESS) {
293             cancelSession(sessionId, packageName);
294             callback.installSucceeded();
295         } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) {
296             // TODO - use the constant when the correct/final name is in the SDK
297             // TODO This is unexpected, so we are treating as failure for now
298             cancelSession(sessionId, packageName);
299             callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED,
300                     "Unexpected: user action required");
301         } else {
302             cancelSession(sessionId, packageName);
303             int errorCode = getPackageManagerErrorCode(status);
304             Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": "
305                     + statusMessage);
306             callback.installFailed(errorCode, null);
307         }
308     }
309 
getPackageManagerErrorCode(int status)310     private int getPackageManagerErrorCode(int status) {
311         // This is a hack: because PackageInstaller now reports error codes
312         // with small positive values, we need to remap them into a space
313         // that is more compatible with the existing package manager error codes.
314         // See https://sites.google.com/a/google.com/universal-store/documentation
315         //       /android-client/download-error-codes
316         int errorCode;
317         if (status == Integer.MIN_VALUE) {
318             errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST;
319         } else {
320             errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status;
321         }
322         return errorCode;
323     }
324 }