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             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 }