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 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 */ 84 void installBeginning(); 85 86 /** 87 * This callback signals that installation has completed. 88 */ 89 void installSucceeded(); 90 91 /** 92 * This callback signals that installation has failed. 93 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 }