| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.packageinstaller.wear; |
| |
| import android.annotation.TargetApi; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.IntentSender; |
| import android.content.pm.PackageInstaller; |
| import android.os.Build; |
| import android.os.ParcelFileDescriptor; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Implementation of package manager installation using modern PackageInstaller api. |
| * |
| * Heavily copied from Wearsky/Finsky implementation |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| public class PackageInstallerImpl { |
| private static final String TAG = "PackageInstallerImpl"; |
| |
| /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */ |
| private static final String ACTION_INSTALL_COMMIT = |
| "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT"; |
| |
| private final Context mContext; |
| private final PackageInstaller mPackageInstaller; |
| private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap; |
| private final Map<String, PackageInstaller.Session> mOpenSessionMap; |
| |
| public PackageInstallerImpl(Context context) { |
| mContext = context.getApplicationContext(); |
| mPackageInstaller = mContext.getPackageManager().getPackageInstaller(); |
| |
| // Capture a map of known sessions |
| // This list will be pruned a bit later (stale sessions will be canceled) |
| mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>(); |
| List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions(); |
| for (int i = 0; i < mySessions.size(); i++) { |
| PackageInstaller.SessionInfo sessionInfo = mySessions.get(i); |
| String packageName = sessionInfo.getAppPackageName(); |
| PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo); |
| |
| // Checking for old info is strictly for logging purposes |
| if (oldInfo != null) { |
| Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo |
| .getSessionId() + " & keeping " + mySessions.get(i).getSessionId()); |
| } |
| } |
| mOpenSessionMap = new HashMap<String, PackageInstaller.Session>(); |
| } |
| |
| /** |
| * This callback will be made after an installation attempt succeeds or fails. |
| */ |
| public interface InstallListener { |
| /** |
| * This callback signals that preflight checks have succeeded and installation |
| * is beginning. |
| */ |
| void installBeginning(); |
| |
| /** |
| * This callback signals that installation has completed. |
| */ |
| void installSucceeded(); |
| |
| /** |
| * This callback signals that installation has failed. |
| */ |
| void installFailed(int errorCode, String errorDesc); |
| } |
| |
| /** |
| * This is a placeholder implementation that bundles an entire "session" into a single |
| * call. This will be replaced by more granular versions that allow longer session lifetimes, |
| * download progress tracking, etc. |
| * |
| * This must not be called on main thread. |
| */ |
| public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor, |
| final InstallListener callback) { |
| // 0. Generic try/catch block because I am not really sure what exceptions (other than |
| // IOException) might be thrown by PackageInstaller and I want to handle them |
| // at least slightly gracefully. |
| try { |
| // 1. Create or recover a session, and open it |
| // Try recovery first |
| PackageInstaller.Session session = null; |
| PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); |
| if (sessionInfo != null) { |
| // See if it's openable, or already held open |
| session = getSession(packageName); |
| } |
| // If open failed, or there was no session, create a new one and open it. |
| // If we cannot create or open here, the failure is terminal. |
| if (session == null) { |
| try { |
| innerCreateSession(packageName); |
| } catch (IOException ioe) { |
| Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage()); |
| callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION, |
| "Could not create session"); |
| mSessionInfoMap.remove(packageName); |
| return; |
| } |
| sessionInfo = mSessionInfoMap.get(packageName); |
| try { |
| session = mPackageInstaller.openSession(sessionInfo.getSessionId()); |
| mOpenSessionMap.put(packageName, session); |
| } catch (SecurityException se) { |
| Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage()); |
| callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION, |
| "Can't open session"); |
| mSessionInfoMap.remove(packageName); |
| return; |
| } |
| } |
| |
| // 2. Launch task to handle file operations. |
| InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor, |
| callback, session, |
| getCommitCallback(packageName, sessionInfo.getSessionId(), callback)); |
| task.execute(); |
| if (task.isError()) { |
| cancelSession(sessionInfo.getSessionId(), packageName); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Unexpected exception while installing: " + packageName + ": " |
| + e.getMessage()); |
| callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION, |
| "Unexpected exception while installing " + packageName); |
| } |
| } |
| |
| /** |
| * Retrieve an existing session. Will open if needed, but does not attempt to create. |
| */ |
| private PackageInstaller.Session getSession(String packageName) { |
| // Check for already-open session |
| PackageInstaller.Session session = mOpenSessionMap.get(packageName); |
| if (session != null) { |
| try { |
| // Probe the session to ensure that it's still open. This may or may not |
| // throw (if non-open), but it may serve as a canary for stale sessions. |
| session.getNames(); |
| return session; |
| } catch (IOException ioe) { |
| Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage()); |
| mOpenSessionMap.remove(packageName); |
| } catch (SecurityException se) { |
| Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage()); |
| mOpenSessionMap.remove(packageName); |
| } |
| } |
| // Check to see if this is a known session |
| PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); |
| if (sessionInfo == null) { |
| return null; |
| } |
| // Try to open it. If we fail here, assume that the SessionInfo was stale. |
| try { |
| session = mPackageInstaller.openSession(sessionInfo.getSessionId()); |
| } catch (SecurityException se) { |
| Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info"); |
| mSessionInfoMap.remove(packageName); |
| return null; |
| } catch (IOException ioe) { |
| Log.w(TAG, "IOException opening old session for " + ioe.getMessage() |
| + " - deleting info"); |
| mSessionInfoMap.remove(packageName); |
| return null; |
| } |
| mOpenSessionMap.put(packageName, session); |
| return session; |
| } |
| |
| /** This version throws an IOException when the session cannot be created */ |
| private void innerCreateSession(String packageName) throws IOException { |
| if (mSessionInfoMap.containsKey(packageName)) { |
| Log.w(TAG, "Creating session for " + packageName + " when one already exists"); |
| return; |
| } |
| PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( |
| PackageInstaller.SessionParams.MODE_FULL_INSTALL); |
| params.setAppPackageName(packageName); |
| |
| // IOException may be thrown at this point |
| int sessionId = mPackageInstaller.createSession(params); |
| PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); |
| mSessionInfoMap.put(packageName, sessionInfo); |
| } |
| |
| /** |
| * Cancel a session based on its sessionId. Package name is for logging only. |
| */ |
| private void cancelSession(int sessionId, String packageName) { |
| // Close if currently held open |
| closeSession(packageName); |
| // Remove local record |
| mSessionInfoMap.remove(packageName); |
| try { |
| mPackageInstaller.abandonSession(sessionId); |
| } catch (SecurityException se) { |
| // The session no longer exists, so we can exit quietly. |
| return; |
| } |
| } |
| |
| /** |
| * Close a session if it happens to be held open. |
| */ |
| private void closeSession(String packageName) { |
| PackageInstaller.Session session = mOpenSessionMap.remove(packageName); |
| if (session != null) { |
| // Unfortunately close() is not idempotent. Try our best to make this safe. |
| try { |
| session.close(); |
| } catch (Exception e) { |
| Log.w(TAG, "Unexpected error closing session for " + packageName + ": " |
| + e.getMessage()); |
| } |
| } |
| } |
| |
| /** |
| * Creates a commit callback for the package install that's underway. This will be called |
| * some time after calling session.commit() (above). |
| */ |
| private IntentSender getCommitCallback(final String packageName, final int sessionId, |
| final InstallListener callback) { |
| // Create a single-use broadcast receiver |
| BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| mContext.unregisterReceiver(this); |
| handleCommitCallback(intent, packageName, sessionId, callback); |
| } |
| }; |
| // Create a matching intent-filter and register the receiver |
| String action = ACTION_INSTALL_COMMIT + "." + packageName; |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(action); |
| mContext.registerReceiver(broadcastReceiver, intentFilter); |
| |
| // Create a matching PendingIntent and use it to generate the IntentSender |
| Intent broadcastIntent = new Intent(action); |
| PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(), |
| broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT |
| | PendingIntent.FLAG_MUTABLE); |
| return pendingIntent.getIntentSender(); |
| } |
| |
| /** |
| * Examine the extras to determine information about the package update/install, decode |
| * the result, and call the appropriate callback. |
| * |
| * @param intent The intent, which the PackageInstaller will have added Extras to |
| * @param packageName The package name we created the receiver for |
| * @param sessionId The session Id we created the receiver for |
| * @param callback The callback to report success/failure to |
| */ |
| private void handleCommitCallback(Intent intent, String packageName, int sessionId, |
| InstallListener callback) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Installation of " + packageName + " finished with extras " |
| + intent.getExtras()); |
| } |
| String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); |
| int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE); |
| if (status == PackageInstaller.STATUS_SUCCESS) { |
| cancelSession(sessionId, packageName); |
| callback.installSucceeded(); |
| } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) { |
| // TODO - use the constant when the correct/final name is in the SDK |
| // TODO This is unexpected, so we are treating as failure for now |
| cancelSession(sessionId, packageName); |
| callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED, |
| "Unexpected: user action required"); |
| } else { |
| cancelSession(sessionId, packageName); |
| int errorCode = getPackageManagerErrorCode(status); |
| Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": " |
| + statusMessage); |
| callback.installFailed(errorCode, null); |
| } |
| } |
| |
| private int getPackageManagerErrorCode(int status) { |
| // This is a hack: because PackageInstaller now reports error codes |
| // with small positive values, we need to remap them into a space |
| // that is more compatible with the existing package manager error codes. |
| // See https://sites.google.com/a/google.com/universal-store/documentation |
| // /android-client/download-error-codes |
| int errorCode; |
| if (status == Integer.MIN_VALUE) { |
| errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST; |
| } else { |
| errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status; |
| } |
| return errorCode; |
| } |
| } |