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