Home | History | Annotate | Download | only in wear
      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                     + 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      */
    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 */
    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      */
    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      */
    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      */
    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      */
    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 
    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