blob: 0c2131203808eec6d79486fbb4b1dbf4bb68cf8c [file] [log] [blame]
Richard Uhlere95d0552018-12-27 15:03:41 +00001/*
2 * Copyright (C) 2018 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
17package com.android.server.rollback;
18
19import android.Manifest;
20import android.app.AppOpsManager;
21import android.content.BroadcastReceiver;
22import android.content.Context;
23import android.content.IIntentReceiver;
24import android.content.IIntentSender;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.IntentSender;
28import android.content.pm.PackageInfo;
29import android.content.pm.PackageInstaller;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManagerInternal;
32import android.content.pm.PackageParser;
33import android.content.pm.ParceledListSlice;
34import android.content.pm.StringParceledListSlice;
35import android.content.rollback.IRollbackManager;
36import android.content.rollback.PackageRollbackInfo;
37import android.content.rollback.RollbackInfo;
38import android.net.Uri;
39import android.os.Binder;
40import android.os.Bundle;
41import android.os.Environment;
42import android.os.Handler;
43import android.os.HandlerThread;
44import android.os.IBinder;
45import android.os.ParcelFileDescriptor;
46import android.util.Log;
47
48import com.android.internal.annotations.GuardedBy;
49import com.android.server.LocalServices;
50import com.android.server.pm.PackageManagerServiceUtils;
51
52import libcore.io.IoUtils;
53
54import org.json.JSONArray;
55import org.json.JSONException;
56import org.json.JSONObject;
57
58import java.io.File;
59import java.io.IOException;
60import java.io.PrintWriter;
61import java.nio.file.Files;
62import java.time.Instant;
63import java.time.format.DateTimeParseException;
64import java.time.temporal.ChronoUnit;
65import java.util.ArrayList;
66import java.util.HashSet;
67import java.util.Iterator;
68import java.util.List;
69import java.util.Set;
70import java.util.concurrent.LinkedBlockingQueue;
71import java.util.concurrent.TimeUnit;
72
73/**
74 * Implementation of service that manages APK level rollbacks.
75 */
76class RollbackManagerServiceImpl extends IRollbackManager.Stub {
77
78 private static final String TAG = "RollbackManager";
79
80 // Rollbacks expire after 48 hours.
81 // TODO: How to test rollback expiration works properly?
82 private static final long ROLLBACK_LIFETIME_DURATION_MILLIS = 48 * 60 * 60 * 1000;
83
84 // Lock used to synchronize accesses to in-memory rollback data
85 // structures. By convention, methods with the suffix "Locked" require
86 // mLock is held when they are called.
87 private final Object mLock = new Object();
88
89 // Package rollback data available to be used for rolling back a package.
90 // This list is null until the rollback data has been loaded.
91 @GuardedBy("mLock")
92 private List<PackageRollbackData> mAvailableRollbacks;
93
94 // The list of recently executed rollbacks.
95 // This list is null until the rollback data has been loaded.
96 @GuardedBy("mLock")
97 private List<RollbackInfo> mRecentlyExecutedRollbacks;
98
99 // Data for available rollbacks and recently executed rollbacks is
100 // persisted in storage. Assuming the rollback data directory is
101 // /data/rollback, we use the following directory structure
102 // to store this data:
103 // /data/rollback/
104 // available/
105 // com.package.A-XXX/
106 // base.apk
107 // rollback.json
108 // com.package.B-YYY/
109 // base.apk
110 // rollback.json
111 // recently_executed.json
112 // TODO: Use AtomicFile for rollback.json and recently_executed.json.
113 private final File mRollbackDataDir;
114 private final File mAvailableRollbacksDir;
115 private final File mRecentlyExecutedRollbacksFile;
116
117 private final Context mContext;
118 private final HandlerThread mHandlerThread;
119
120 RollbackManagerServiceImpl(Context context) {
121 mContext = context;
122 mHandlerThread = new HandlerThread("RollbackManagerServiceHandler");
123 mHandlerThread.start();
124
125 mRollbackDataDir = new File(Environment.getDataDirectory(), "rollback");
126 mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
127 mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
128
129 // Kick off loading of the rollback data from strorage in a background
130 // thread.
131 // TODO: Consider loading the rollback data directly here instead, to
132 // avoid the need to call ensureRollbackDataLoaded every time before
133 // accessing the rollback data?
134 // TODO: Test that this kicks off initial scheduling of rollback
135 // expiration.
136 getHandler().post(() -> ensureRollbackDataLoaded());
137
138 IntentFilter filter = new IntentFilter();
139 filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
140 filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
141 filter.addDataScheme("package");
142 mContext.registerReceiver(new BroadcastReceiver() {
143 @Override
144 public void onReceive(Context context, Intent intent) {
145 String action = intent.getAction();
146 if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
147 String packageName = intent.getData().getSchemeSpecificPart();
148 onPackageReplaced(packageName);
149 }
150 if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) {
151 String packageName = intent.getData().getSchemeSpecificPart();
152 onPackageFullyRemoved(packageName);
153 }
154 }
155 }, filter, null, getHandler());
156
157 IntentFilter enableRollbackFilter = new IntentFilter();
158 enableRollbackFilter.addAction(Intent.ACTION_PACKAGE_ENABLE_ROLLBACK);
159 try {
160 enableRollbackFilter.addDataType("application/vnd.android.package-archive");
161 } catch (IntentFilter.MalformedMimeTypeException e) {
162 Log.e(TAG, "addDataType", e);
163 }
164
165 mContext.registerReceiver(new BroadcastReceiver() {
166 @Override
167 public void onReceive(Context context, Intent intent) {
168 if (Intent.ACTION_PACKAGE_ENABLE_ROLLBACK.equals(intent.getAction())) {
169 int token = intent.getIntExtra(
170 PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_TOKEN, -1);
171 int installFlags = intent.getIntExtra(
172 PackageManagerInternal.EXTRA_ENABLE_ROLLBACK_INSTALL_FLAGS, 0);
173 File newPackageCodePath = new File(intent.getData().getPath());
174
175 getHandler().post(() -> {
176 boolean success = enableRollback(installFlags, newPackageCodePath);
177 int ret = PackageManagerInternal.ENABLE_ROLLBACK_SUCCEEDED;
178 if (!success) {
179 ret = PackageManagerInternal.ENABLE_ROLLBACK_FAILED;
180 }
181
182 PackageManagerInternal pm = LocalServices.getService(
183 PackageManagerInternal.class);
184 pm.setEnableRollbackCode(token, ret);
185 });
186
187 // We're handling the ordered broadcast. Abort the
188 // broadcast because there is no need for it to go to
189 // anyone else.
190 abortBroadcast();
191 }
192 }
193 }, enableRollbackFilter, null, getHandler());
194 }
195
196 @Override
197 public RollbackInfo getAvailableRollback(String packageName) {
198 mContext.enforceCallingOrSelfPermission(
199 android.Manifest.permission.MANAGE_ROLLBACKS,
200 "getAvailableRollback");
201
202 PackageRollbackInfo.PackageVersion installedVersion =
203 getInstalledPackageVersion(packageName);
204 if (installedVersion == null) {
205 return null;
206 }
207
208 synchronized (mLock) {
209 // TODO: Have ensureRollbackDataLoadedLocked return the list of
210 // available rollbacks, to hopefully avoid forgetting to call it?
211 ensureRollbackDataLoadedLocked();
212 for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
213 PackageRollbackData data = mAvailableRollbacks.get(i);
Richard Uhler04e84e02019-01-02 11:44:00 +0000214 if (data.info.packageName.equals(packageName)
215 && data.info.higherVersion.equals(installedVersion)) {
Richard Uhlere95d0552018-12-27 15:03:41 +0000216 // TODO: For atomic installs, check all dependent packages
217 // for available rollbacks and include that info here.
218 return new RollbackInfo(data.info);
219 }
220 }
221 }
222
223 return null;
224 }
225
226 @Override
227 public StringParceledListSlice getPackagesWithAvailableRollbacks() {
228 mContext.enforceCallingOrSelfPermission(
229 android.Manifest.permission.MANAGE_ROLLBACKS,
230 "getPackagesWithAvailableRollbacks");
231
232 // TODO: This may return packages whose rollback is out of date or
233 // expired. Presumably that's okay because the package rollback could
234 // be expired anyway between when the caller calls this method and
235 // when the caller calls getAvailableRollback for more details.
236 final Set<String> packageNames = new HashSet<>();
237 synchronized (mLock) {
238 ensureRollbackDataLoadedLocked();
239 for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
240 PackageRollbackData data = mAvailableRollbacks.get(i);
241 packageNames.add(data.info.packageName);
242 }
243 }
244 return new StringParceledListSlice(new ArrayList<>(packageNames));
245 }
246
247 @Override
248 public ParceledListSlice<RollbackInfo> getRecentlyExecutedRollbacks() {
249 mContext.enforceCallingOrSelfPermission(
250 android.Manifest.permission.MANAGE_ROLLBACKS,
251 "getRecentlyExecutedRollbacks");
252
253 synchronized (mLock) {
254 ensureRollbackDataLoadedLocked();
255 List<RollbackInfo> rollbacks = new ArrayList<>(mRecentlyExecutedRollbacks);
256 return new ParceledListSlice<>(rollbacks);
257 }
258 }
259
260 @Override
261 public void executeRollback(RollbackInfo rollback, String callerPackageName,
262 IntentSender statusReceiver) {
263 mContext.enforceCallingOrSelfPermission(
264 android.Manifest.permission.MANAGE_ROLLBACKS,
265 "executeRollback");
266
267 final int callingUid = Binder.getCallingUid();
268 AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
269 appOps.checkPackage(callingUid, callerPackageName);
270
271 getHandler().post(() ->
272 executeRollbackInternal(rollback, callerPackageName, statusReceiver));
273 }
274
275 /**
276 * Performs the actual work to execute a rollback.
277 * The work is done on the current thread. This may be a long running
278 * operation.
279 */
280 private void executeRollbackInternal(RollbackInfo rollback,
281 String callerPackageName, IntentSender statusReceiver) {
282 String packageName = rollback.targetPackage.packageName;
283 Log.i(TAG, "Initiating rollback of " + packageName);
284
285 PackageRollbackInfo.PackageVersion installedVersion =
286 getInstalledPackageVersion(packageName);
287 if (installedVersion == null) {
288 // TODO: Test this case
289 sendFailure(statusReceiver, "Target package to roll back is not installed");
290 return;
291 }
292
293 if (!rollback.targetPackage.higherVersion.equals(installedVersion)) {
294 // TODO: Test this case
295 sendFailure(statusReceiver, "Target package version to roll back not installed.");
296 return;
297 }
298
299 // TODO: We assume that between now and the time we commit the
300 // downgrade install, the currently installed package version does not
301 // change. This is not safe to assume, particularly in the case of a
302 // rollback racing with a roll-forward fix of a buggy package.
303 // Figure out how to ensure we don't commit the rollback if
304 // roll forward happens at the same time.
305 PackageRollbackData data = null;
306 synchronized (mLock) {
307 ensureRollbackDataLoadedLocked();
308 for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
309 PackageRollbackData available = mAvailableRollbacks.get(i);
310 // TODO: Check if available.info.lowerVersion matches
311 // rollback.targetPackage.lowerVersion?
Richard Uhler04e84e02019-01-02 11:44:00 +0000312 if (available.info.packageName.equals(packageName)
313 && available.info.higherVersion.equals(installedVersion)) {
Richard Uhlere95d0552018-12-27 15:03:41 +0000314 data = available;
315 break;
316 }
317 }
318 }
319
320 if (data == null) {
321 sendFailure(statusReceiver, "Rollback not available");
322 return;
323 }
324
325 // Get a context for the caller to use to install the downgraded
326 // version of the package.
327 Context context = null;
328 try {
329 context = mContext.createPackageContext(callerPackageName, 0);
330 } catch (PackageManager.NameNotFoundException e) {
331 sendFailure(statusReceiver, "Invalid callerPackageName");
332 return;
333 }
334
335 PackageManager pm = context.getPackageManager();
336 try {
337 PackageInstaller.Session session = null;
338
339 PackageInstaller packageInstaller = pm.getPackageInstaller();
340 PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
341 PackageInstaller.SessionParams.MODE_FULL_INSTALL);
342 params.setAllowDowngrade(true);
343 int sessionId = packageInstaller.createSession(params);
344 session = packageInstaller.openSession(sessionId);
345
346 // TODO: Will it always be called "base.apk"? What about splits?
347 File baseApk = new File(data.backupDir, "base.apk");
348 try (ParcelFileDescriptor fd = ParcelFileDescriptor.open(baseApk,
349 ParcelFileDescriptor.MODE_READ_ONLY)) {
350 final long token = Binder.clearCallingIdentity();
351 try {
352 session.write("base.apk", 0, baseApk.length(), fd);
353 } finally {
354 Binder.restoreCallingIdentity(token);
355 }
356 }
357
358 final LocalIntentReceiver receiver = new LocalIntentReceiver();
359 session.commit(receiver.getIntentSender());
360
361 Intent result = receiver.getResult();
362 int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
363 PackageInstaller.STATUS_FAILURE);
364 if (status != PackageInstaller.STATUS_SUCCESS) {
365 sendFailure(statusReceiver, "Rollback downgrade install failed: "
366 + result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE));
367 return;
368 }
369
370 addRecentlyExecutedRollback(rollback);
371 sendSuccess(statusReceiver);
372
373 Intent broadcast = new Intent(Intent.ACTION_PACKAGE_ROLLBACK_EXECUTED,
374 Uri.fromParts("package", packageName, Manifest.permission.MANAGE_ROLLBACKS));
375
376 // TODO: This call emits the warning "Calling a method in the
377 // system process without a qualified user". Fix that.
378 mContext.sendBroadcast(broadcast);
379 } catch (IOException e) {
380 Log.e(TAG, "Unable to roll back " + packageName, e);
381 sendFailure(statusReceiver, "IOException: " + e.toString());
382 return;
383 }
384 }
385
386 @Override
387 public void reloadPersistedData() {
388 mContext.enforceCallingOrSelfPermission(
389 android.Manifest.permission.MANAGE_ROLLBACKS,
390 "reloadPersistedData");
391
392 synchronized (mLock) {
393 mAvailableRollbacks = null;
394 mRecentlyExecutedRollbacks = null;
395 }
396 getHandler().post(() -> ensureRollbackDataLoaded());
397 }
398
399 @Override
400 public void expireRollbackForPackage(String packageName) {
401 mContext.enforceCallingOrSelfPermission(
402 android.Manifest.permission.MANAGE_ROLLBACKS,
403 "expireRollbackForPackage");
404
405 // TODO: Should this take a package version number in addition to
406 // package name? For now, just remove all rollbacks matching the
407 // package name. This method is only currently used to facilitate
408 // testing anyway.
409 synchronized (mLock) {
410 ensureRollbackDataLoadedLocked();
411 Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
412 while (iter.hasNext()) {
413 PackageRollbackData data = iter.next();
414 if (data.info.packageName.equals(packageName)) {
415 iter.remove();
416 removeFile(data.backupDir);
417 }
418 }
419 }
420 }
421
422 /**
423 * Load rollback data from storage if it has not already been loaded.
424 * After calling this funciton, mAvailableRollbacks and
425 * mRecentlyExecutedRollbacks will be non-null.
426 */
427 private void ensureRollbackDataLoaded() {
428 synchronized (mLock) {
429 ensureRollbackDataLoadedLocked();
430 }
431 }
432
433 /**
434 * Load rollback data from storage if it has not already been loaded.
435 * After calling this function, mAvailableRollbacks and
436 * mRecentlyExecutedRollbacks will be non-null.
437 */
438 @GuardedBy("mLock")
439 private void ensureRollbackDataLoadedLocked() {
440 if (mAvailableRollbacks == null) {
441 loadRollbackDataLocked();
442 }
443 }
444
445 /**
446 * Load rollback data from storage.
447 * Note: We do potentially heavy IO here while holding mLock, because we
448 * have to have the rollback data loaded before we can do anything else
449 * meaningful.
450 */
451 @GuardedBy("mLock")
452 private void loadRollbackDataLocked() {
453 mAvailableRollbacksDir.mkdirs();
454 mAvailableRollbacks = new ArrayList<>();
455 for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
456 if (rollbackDir.isDirectory()) {
457 // TODO: How to detect and clean up an invalid rollback
458 // directory? We don't know if it's invalid because something
459 // went wrong, or if it's only temporarily invalid because
460 // it's in the process of being created.
461 try {
462 File jsonFile = new File(rollbackDir, "rollback.json");
463 String jsonString = IoUtils.readFileAsString(jsonFile.getAbsolutePath());
464 JSONObject jsonObject = new JSONObject(jsonString);
465 String packageName = jsonObject.getString("packageName");
466 long higherVersionCode = jsonObject.getLong("higherVersionCode");
467 long lowerVersionCode = jsonObject.getLong("lowerVersionCode");
468 Instant timestamp = Instant.parse(jsonObject.getString("timestamp"));
469 PackageRollbackData data = new PackageRollbackData(
470 new PackageRollbackInfo(packageName,
471 new PackageRollbackInfo.PackageVersion(higherVersionCode),
472 new PackageRollbackInfo.PackageVersion(lowerVersionCode)),
473 rollbackDir, timestamp);
474 mAvailableRollbacks.add(data);
475 } catch (IOException | JSONException | DateTimeParseException e) {
476 Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
477 }
478 }
479 }
480
481 mRecentlyExecutedRollbacks = new ArrayList<>();
482 if (mRecentlyExecutedRollbacksFile.exists()) {
483 try {
484 // TODO: How to cope with changes to the format of this file from
485 // when RollbackStore is updated in the future?
486 String jsonString = IoUtils.readFileAsString(
487 mRecentlyExecutedRollbacksFile.getAbsolutePath());
488 JSONObject object = new JSONObject(jsonString);
489 JSONArray array = object.getJSONArray("recentlyExecuted");
490 for (int i = 0; i < array.length(); ++i) {
491 JSONObject element = array.getJSONObject(i);
492 String packageName = element.getString("packageName");
493 long higherVersionCode = element.getLong("higherVersionCode");
494 long lowerVersionCode = element.getLong("lowerVersionCode");
495 PackageRollbackInfo target = new PackageRollbackInfo(packageName,
496 new PackageRollbackInfo.PackageVersion(higherVersionCode),
497 new PackageRollbackInfo.PackageVersion(lowerVersionCode));
498 RollbackInfo rollback = new RollbackInfo(target);
499 mRecentlyExecutedRollbacks.add(rollback);
500 }
501 } catch (IOException | JSONException e) {
502 // TODO: What to do here? Surely we shouldn't just forget about
503 // everything after the point of exception?
504 Log.e(TAG, "Failed to read recently executed rollbacks", e);
505 }
506 }
507
508 scheduleExpiration(0);
509 }
510
511 /**
512 * Called when a package has been replaced with a different version.
513 * Removes all backups for the package not matching the currently
514 * installed package version.
515 */
516 private void onPackageReplaced(String packageName) {
517 // TODO: Could this end up incorrectly deleting a rollback for a
518 // package that is about to be installed?
519 PackageRollbackInfo.PackageVersion installedVersion =
520 getInstalledPackageVersion(packageName);
521
522 synchronized (mLock) {
523 ensureRollbackDataLoadedLocked();
524 Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
525 while (iter.hasNext()) {
526 PackageRollbackData data = iter.next();
527 if (data.info.packageName.equals(packageName)
528 && !data.info.higherVersion.equals(installedVersion)) {
529 iter.remove();
530 removeFile(data.backupDir);
531 }
532 }
533 }
534 }
535
536 /**
537 * Called when a package has been completely removed from the device.
538 * Removes all backups and rollback history for the given package.
539 */
540 private void onPackageFullyRemoved(String packageName) {
541 expireRollbackForPackage(packageName);
542
543 synchronized (mLock) {
544 ensureRollbackDataLoadedLocked();
545 Iterator<RollbackInfo> iter = mRecentlyExecutedRollbacks.iterator();
546 boolean changed = false;
547 while (iter.hasNext()) {
548 RollbackInfo rollback = iter.next();
549 if (packageName.equals(rollback.targetPackage.packageName)) {
550 iter.remove();
551 changed = true;
552 }
553 }
554
555 if (changed) {
556 saveRecentlyExecutedRollbacksLocked();
557 }
558 }
559 }
560
561 /**
562 * Write the list of recently executed rollbacks to storage.
563 * Note: This happens while mLock is held, which should be okay because we
564 * expect executed rollbacks to be modified only in exceptional cases.
565 */
566 @GuardedBy("mLock")
567 private void saveRecentlyExecutedRollbacksLocked() {
568 try {
569 JSONObject json = new JSONObject();
570 JSONArray array = new JSONArray();
571 json.put("recentlyExecuted", array);
572
573 for (int i = 0; i < mRecentlyExecutedRollbacks.size(); ++i) {
574 RollbackInfo rollback = mRecentlyExecutedRollbacks.get(i);
575 JSONObject element = new JSONObject();
576 element.put("packageName", rollback.targetPackage.packageName);
577 element.put("higherVersionCode", rollback.targetPackage.higherVersion.versionCode);
578 element.put("lowerVersionCode", rollback.targetPackage.lowerVersion.versionCode);
579 array.put(element);
580 }
581
582 PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
583 pw.println(json.toString());
584 pw.close();
585 } catch (IOException | JSONException e) {
586 // TODO: What to do here?
587 Log.e(TAG, "Failed to save recently executed rollbacks", e);
588 }
589 }
590
591 /**
592 * Records that the given package has been recently rolled back.
593 */
594 private void addRecentlyExecutedRollback(RollbackInfo rollback) {
595 // TODO: if the list of rollbacks gets too big, trim it to only those
596 // that are necessary to keep track of.
597 synchronized (mLock) {
598 ensureRollbackDataLoadedLocked();
599 mRecentlyExecutedRollbacks.add(rollback);
600 saveRecentlyExecutedRollbacksLocked();
601 }
602 }
603
604 /**
605 * Notifies an IntentSender of failure.
606 *
607 * @param statusReceiver where to send the failure
608 * @param message the failure message.
609 */
610 private void sendFailure(IntentSender statusReceiver, String message) {
611 Log.e(TAG, message);
612 try {
613 // TODO: More context on which rollback failed?
614 // TODO: More refined failure code?
615 final Intent fillIn = new Intent();
616 fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
617 fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
618 statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
619 } catch (IntentSender.SendIntentException e) {
620 // Nowhere to send the result back to, so don't bother.
621 }
622 }
623
624 /**
625 * Notifies an IntentSender of success.
626 */
627 private void sendSuccess(IntentSender statusReceiver) {
628 try {
629 final Intent fillIn = new Intent();
630 fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS);
631 statusReceiver.sendIntent(mContext, 0, fillIn, null, null);
632 } catch (IntentSender.SendIntentException e) {
633 // Nowhere to send the result back to, so don't bother.
634 }
635 }
636
637 // Check to see if anything needs expiration, and if so, expire it.
638 // Schedules future expiration as appropriate.
639 // TODO: Handle cases where the user changes time on the device.
640 private void runExpiration() {
641 Instant now = Instant.now();
642 Instant oldest = null;
643 synchronized (mLock) {
644 ensureRollbackDataLoadedLocked();
645
646 Iterator<PackageRollbackData> iter = mAvailableRollbacks.iterator();
647 while (iter.hasNext()) {
648 PackageRollbackData data = iter.next();
649 if (!now.isBefore(data.timestamp.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS))) {
650 iter.remove();
651 removeFile(data.backupDir);
652 } else if (oldest == null || oldest.isAfter(data.timestamp)) {
653 oldest = data.timestamp;
654 }
655 }
656 }
657
658 if (oldest != null) {
659 scheduleExpiration(now.until(oldest.plusMillis(ROLLBACK_LIFETIME_DURATION_MILLIS),
660 ChronoUnit.MILLIS));
661 }
662 }
663
664 /**
665 * Schedules an expiration check to be run after the given duration in
666 * milliseconds has gone by.
667 */
668 private void scheduleExpiration(long duration) {
669 getHandler().postDelayed(() -> runExpiration(), duration);
670 }
671
672 private Handler getHandler() {
673 return mHandlerThread.getThreadHandler();
674 }
675
676 /**
677 * Called via broadcast by the package manager when a package is being
678 * staged for install with rollback enabled. Called before the package has
679 * been installed.
680 *
681 * @param id the id of the enable rollback request
682 * @param installFlags information about what is being installed.
683 * @param newPackageCodePath path to the package about to be installed.
684 * @return true if enabling the rollback succeeds, false otherwise.
685 */
686 private boolean enableRollback(int installFlags, File newPackageCodePath) {
687 if ((installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
688 Log.e(TAG, "Rollbacks not supported for instant app install");
689 return false;
690 }
691 if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
692 Log.e(TAG, "Rollbacks not supported for apex install");
693 return false;
694 }
695
696 // Get information about the package to be installed.
697 PackageParser.PackageLite newPackage = null;
698 try {
699 newPackage = PackageParser.parsePackageLite(newPackageCodePath, 0);
700 } catch (PackageParser.PackageParserException e) {
701 Log.e(TAG, "Unable to parse new package", e);
702 return false;
703 }
704
705 String packageName = newPackage.packageName;
706 Log.i(TAG, "Enabling rollback for install of " + packageName);
707
708 PackageRollbackInfo.PackageVersion newVersion =
709 new PackageRollbackInfo.PackageVersion(newPackage.versionCode);
710
711 // Get information about the currently installed package.
712 PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
713 PackageParser.Package installedPackage = pm.getPackage(packageName);
714 if (installedPackage == null) {
715 // TODO: Support rolling back fresh package installs rather than
716 // fail here. Test this case.
717 Log.e(TAG, packageName + " is not installed");
718 return false;
719 }
720 PackageRollbackInfo.PackageVersion installedVersion =
721 new PackageRollbackInfo.PackageVersion(installedPackage.getLongVersionCode());
722
723 File backupDir;
724 try {
725 backupDir = Files.createTempDirectory(
726 mAvailableRollbacksDir.toPath(), packageName + "-").toFile();
727 } catch (IOException e) {
728 Log.e(TAG, "Unable to create rollback for " + packageName, e);
729 return false;
730 }
731
732 // TODO: Should the timestamp be for when we commit the install, not
733 // when we create the pending one?
734 Instant timestamp = Instant.now();
735 try {
736 JSONObject json = new JSONObject();
737 json.put("packageName", packageName);
738 json.put("higherVersionCode", newVersion.versionCode);
739 json.put("lowerVersionCode", installedVersion.versionCode);
740 json.put("timestamp", timestamp.toString());
741
742 File jsonFile = new File(backupDir, "rollback.json");
743 PrintWriter pw = new PrintWriter(jsonFile);
744 pw.println(json.toString());
745 pw.close();
746 } catch (IOException | JSONException e) {
747 Log.e(TAG, "Unable to create rollback for " + packageName, e);
748 removeFile(backupDir);
749 return false;
750 }
751
752 // TODO: Copy by hard link instead to save on cpu and storage space?
753 int status = PackageManagerServiceUtils.copyPackage(installedPackage.codePath, backupDir);
754 if (status != PackageManager.INSTALL_SUCCEEDED) {
755 Log.e(TAG, "Unable to copy package for rollback for " + packageName);
756 removeFile(backupDir);
757 return false;
758 }
759
760 PackageRollbackData data = new PackageRollbackData(
761 new PackageRollbackInfo(packageName, newVersion, installedVersion),
762 backupDir, timestamp);
763
764 synchronized (mLock) {
765 ensureRollbackDataLoadedLocked();
766 mAvailableRollbacks.add(data);
767 }
768
769 return true;
770 }
771
772 // TODO: Don't copy this from PackageManagerShellCommand like this?
773 private static class LocalIntentReceiver {
774 private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
775
776 private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
777 @Override
778 public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
779 IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
780 try {
781 mResult.offer(intent, 5, TimeUnit.SECONDS);
782 } catch (InterruptedException e) {
783 throw new RuntimeException(e);
784 }
785 }
786 };
787
788 public IntentSender getIntentSender() {
789 return new IntentSender((IIntentSender) mLocalSender);
790 }
791
792 public Intent getResult() {
793 try {
794 return mResult.take();
795 } catch (InterruptedException e) {
796 throw new RuntimeException(e);
797 }
798 }
799 }
800
801 /**
802 * Deletes a file completely.
803 * If the file is a directory, its contents are deleted as well.
804 * Has no effect if the directory does not exist.
805 */
806 private void removeFile(File file) {
807 if (file.isDirectory()) {
808 for (File child : file.listFiles()) {
809 removeFile(child);
810 }
811 }
812 if (file.exists()) {
813 file.delete();
814 }
815 }
816
817 /**
818 * Gets the version of the package currently installed.
819 * Returns null if the package is not currently installed.
820 */
821 private PackageRollbackInfo.PackageVersion getInstalledPackageVersion(String packageName) {
822 PackageManager pm = mContext.getPackageManager();
823 PackageInfo pkgInfo = null;
824 try {
825 pkgInfo = pm.getPackageInfo(packageName, 0);
826 } catch (PackageManager.NameNotFoundException e) {
827 return null;
828 }
829
830 return new PackageRollbackInfo.PackageVersion(pkgInfo.getLongVersionCode());
831 }
832}