summaryrefslogtreecommitdiffstats
path: root/src/com/android/packageinstaller/wear/PackageInstallerImpl.java
blob: 3dee7817c9f8fa866ade35a10265178d8bdddc5f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/*
 * 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);
            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);
        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;
    }
}