1 /* 2 * Copyright (C) 2009 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.server.backup; 18 19 import android.app.backup.BackupAgent; 20 import android.app.backup.BackupDataInput; 21 import android.app.backup.BackupDataOutput; 22 import android.content.ComponentName; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.pm.Signature; 29 import android.os.Build; 30 import android.os.ParcelFileDescriptor; 31 import android.util.Slog; 32 33 import java.io.BufferedInputStream; 34 import java.io.BufferedOutputStream; 35 import java.io.ByteArrayInputStream; 36 import java.io.ByteArrayOutputStream; 37 import java.io.DataInputStream; 38 import java.io.DataOutputStream; 39 import java.io.EOFException; 40 import java.io.FileInputStream; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Set; 48 49 import java.util.Objects; 50 51 /** 52 * We back up the signatures of each package so that during a system restore, 53 * we can verify that the app whose data we think we have matches the app 54 * actually resident on the device. 55 * 56 * Since the Package Manager isn't a proper "application" we just provide a 57 * direct IBackupAgent implementation and hand-construct it at need. 58 */ 59 public class PackageManagerBackupAgent extends BackupAgent { 60 private static final String TAG = "PMBA"; 61 private static final boolean DEBUG = false; 62 63 // key under which we store global metadata (individual app metadata 64 // is stored using the package name as a key) 65 private static final String GLOBAL_METADATA_KEY = "@meta@"; 66 67 // key under which we store the identity of the user's chosen default home app 68 private static final String DEFAULT_HOME_KEY = "@home@"; 69 70 // Sentinel: start of state file, followed by a version number 71 private static final String STATE_FILE_HEADER = "=state="; 72 private static final int STATE_FILE_VERSION = 2; 73 74 // Current version of the saved ancestral-dataset file format 75 private static final int ANCESTRAL_RECORD_VERSION = 1; 76 77 private List<PackageInfo> mAllPackages; 78 private PackageManager mPackageManager; 79 // version & signature info of each app in a restore set 80 private HashMap<String, Metadata> mRestoredSignatures; 81 // The version info of each backed-up app as read from the state file 82 private HashMap<String, Metadata> mStateVersions = new HashMap<String, Metadata>(); 83 84 private final HashSet<String> mExisting = new HashSet<String>(); 85 private int mStoredSdkVersion; 86 private String mStoredIncrementalVersion; 87 private ComponentName mStoredHomeComponent; 88 private long mStoredHomeVersion; 89 private ArrayList<byte[]> mStoredHomeSigHashes; 90 91 private boolean mHasMetadata; 92 private ComponentName mRestoredHome; 93 private long mRestoredHomeVersion; 94 private String mRestoredHomeInstaller; 95 private ArrayList<byte[]> mRestoredHomeSigHashes; 96 97 // For compactness we store the SHA-256 hash of each app's Signatures 98 // rather than the Signature blocks themselves. 99 public class Metadata { 100 public int versionCode; 101 public ArrayList<byte[]> sigHashes; 102 Metadata(int version, ArrayList<byte[]> hashes)103 Metadata(int version, ArrayList<byte[]> hashes) { 104 versionCode = version; 105 sigHashes = hashes; 106 } 107 } 108 109 // We're constructed with the set of applications that are participating 110 // in backup. This set changes as apps are installed & removed. PackageManagerBackupAgent(PackageManager packageMgr, List<PackageInfo> packages)111 PackageManagerBackupAgent(PackageManager packageMgr, List<PackageInfo> packages) { 112 init(packageMgr, packages); 113 } 114 PackageManagerBackupAgent(PackageManager packageMgr)115 PackageManagerBackupAgent(PackageManager packageMgr) { 116 init(packageMgr, null); 117 118 evaluateStorablePackages(); 119 } 120 init(PackageManager packageMgr, List<PackageInfo> packages)121 private void init(PackageManager packageMgr, List<PackageInfo> packages) { 122 mPackageManager = packageMgr; 123 mAllPackages = packages; 124 mRestoredSignatures = null; 125 mHasMetadata = false; 126 127 mStoredSdkVersion = Build.VERSION.SDK_INT; 128 mStoredIncrementalVersion = Build.VERSION.INCREMENTAL; 129 } 130 131 // We will need to refresh our understanding of what is eligible for 132 // backup periodically; this entry point serves that purpose. evaluateStorablePackages()133 public void evaluateStorablePackages() { 134 mAllPackages = getStorableApplications(mPackageManager); 135 } 136 getStorableApplications(PackageManager pm)137 public static List<PackageInfo> getStorableApplications(PackageManager pm) { 138 List<PackageInfo> pkgs; 139 pkgs = pm.getInstalledPackages(PackageManager.GET_SIGNATURES); 140 int N = pkgs.size(); 141 for (int a = N-1; a >= 0; a--) { 142 PackageInfo pkg = pkgs.get(a); 143 if (!BackupManagerService.appIsEligibleForBackup(pkg.applicationInfo)) { 144 pkgs.remove(a); 145 } 146 } 147 return pkgs; 148 } 149 hasMetadata()150 public boolean hasMetadata() { 151 return mHasMetadata; 152 } 153 getRestoredMetadata(String packageName)154 public Metadata getRestoredMetadata(String packageName) { 155 if (mRestoredSignatures == null) { 156 Slog.w(TAG, "getRestoredMetadata() before metadata read!"); 157 return null; 158 } 159 160 return mRestoredSignatures.get(packageName); 161 } 162 getRestoredPackages()163 public Set<String> getRestoredPackages() { 164 if (mRestoredSignatures == null) { 165 Slog.w(TAG, "getRestoredPackages() before metadata read!"); 166 return null; 167 } 168 169 // This is technically the set of packages on the originating handset 170 // that had backup agents at all, not limited to the set of packages 171 // that had actually contributed a restore dataset, but it's a 172 // close enough approximation for our purposes and does not require any 173 // additional involvement by the transport to obtain. 174 return mRestoredSignatures.keySet(); 175 } 176 177 // The backed up data is the signature block for each app, keyed by 178 // the package name. onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)179 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 180 ParcelFileDescriptor newState) { 181 if (DEBUG) Slog.v(TAG, "onBackup()"); 182 183 ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(); // we'll reuse these 184 DataOutputStream outputBufferStream = new DataOutputStream(outputBuffer); 185 parseStateFile(oldState); 186 187 // If the stored version string differs, we need to re-backup all 188 // of the metadata. We force this by removing everything from the 189 // "already backed up" map built by parseStateFile(). 190 if (mStoredIncrementalVersion == null 191 || !mStoredIncrementalVersion.equals(Build.VERSION.INCREMENTAL)) { 192 Slog.i(TAG, "Previous metadata " + mStoredIncrementalVersion + " mismatch vs " 193 + Build.VERSION.INCREMENTAL + " - rewriting"); 194 mExisting.clear(); 195 } 196 197 long homeVersion = 0; 198 ArrayList<byte[]> homeSigHashes = null; 199 PackageInfo homeInfo = null; 200 String homeInstaller = null; 201 ComponentName home = getPreferredHomeComponent(); 202 if (home != null) { 203 try { 204 homeInfo = mPackageManager.getPackageInfo(home.getPackageName(), 205 PackageManager.GET_SIGNATURES); 206 homeInstaller = mPackageManager.getInstallerPackageName(home.getPackageName()); 207 homeVersion = homeInfo.versionCode; 208 homeSigHashes = BackupUtils.hashSignatureArray(homeInfo.signatures); 209 } catch (NameNotFoundException e) { 210 Slog.w(TAG, "Can't access preferred home info"); 211 // proceed as though there were no preferred home set 212 home = null; 213 } 214 } 215 216 try { 217 // We need to push a new preferred-home-app record if: 218 // 1. the version of the home app has changed since our last backup; 219 // 2. the home app [or absence] we now use differs from the prior state, 220 // OR 3. it looks like we use the same home app + version as before, but 221 // the signatures don't match so we treat them as different apps. 222 final boolean needHomeBackup = (homeVersion != mStoredHomeVersion) 223 || !Objects.equals(home, mStoredHomeComponent) 224 || (home != null 225 && !BackupUtils.signaturesMatch(mStoredHomeSigHashes, homeInfo)); 226 if (needHomeBackup) { 227 if (DEBUG) { 228 Slog.i(TAG, "Home preference changed; backing up new state " + home); 229 } 230 if (home != null) { 231 outputBufferStream.writeUTF(home.flattenToString()); 232 outputBufferStream.writeLong(homeVersion); 233 outputBufferStream.writeUTF(homeInstaller != null ? homeInstaller : "" ); 234 writeSignatureHashArray(outputBufferStream, homeSigHashes); 235 writeEntity(data, DEFAULT_HOME_KEY, outputBuffer.toByteArray()); 236 } else { 237 data.writeEntityHeader(DEFAULT_HOME_KEY, -1); 238 } 239 } 240 241 /* 242 * Global metadata: 243 * 244 * int SDKversion -- the SDK version of the OS itself on the device 245 * that produced this backup set. Used to reject 246 * backups from later OSes onto earlier ones. 247 * String incremental -- the incremental release name of the OS stored in 248 * the backup set. 249 */ 250 outputBuffer.reset(); 251 if (!mExisting.contains(GLOBAL_METADATA_KEY)) { 252 if (DEBUG) Slog.v(TAG, "Storing global metadata key"); 253 outputBufferStream.writeInt(Build.VERSION.SDK_INT); 254 outputBufferStream.writeUTF(Build.VERSION.INCREMENTAL); 255 writeEntity(data, GLOBAL_METADATA_KEY, outputBuffer.toByteArray()); 256 } else { 257 if (DEBUG) Slog.v(TAG, "Global metadata key already stored"); 258 // don't consider it to have been skipped/deleted 259 mExisting.remove(GLOBAL_METADATA_KEY); 260 } 261 262 // For each app we have on device, see if we've backed it up yet. If not, 263 // write its signature block to the output, keyed on the package name. 264 for (PackageInfo pkg : mAllPackages) { 265 String packName = pkg.packageName; 266 if (packName.equals(GLOBAL_METADATA_KEY)) { 267 // We've already handled the metadata key; skip it here 268 continue; 269 } else { 270 PackageInfo info = null; 271 try { 272 info = mPackageManager.getPackageInfo(packName, 273 PackageManager.GET_SIGNATURES); 274 } catch (NameNotFoundException e) { 275 // Weird; we just found it, and now are told it doesn't exist. 276 // Treat it as having been removed from the device. 277 mExisting.add(packName); 278 continue; 279 } 280 281 if (mExisting.contains(packName)) { 282 // We have backed up this app before. Check whether the version 283 // of the backup matches the version of the current app; if they 284 // don't match, the app has been updated and we need to store its 285 // metadata again. In either case, take it out of mExisting so that 286 // we don't consider it deleted later. 287 mExisting.remove(packName); 288 if (info.versionCode == mStateVersions.get(packName).versionCode) { 289 continue; 290 } 291 } 292 293 if (info.signatures == null || info.signatures.length == 0) 294 { 295 Slog.w(TAG, "Not backing up package " + packName 296 + " since it appears to have no signatures."); 297 continue; 298 } 299 300 // We need to store this app's metadata 301 /* 302 * Metadata for each package: 303 * 304 * int version -- [4] the package's versionCode 305 * byte[] signatures -- [len] flattened signature hash array of the package 306 */ 307 308 // marshal the version code in a canonical form 309 outputBuffer.reset(); 310 outputBufferStream.writeInt(info.versionCode); 311 writeSignatureHashArray(outputBufferStream, 312 BackupUtils.hashSignatureArray(info.signatures)); 313 314 if (DEBUG) { 315 Slog.v(TAG, "+ writing metadata for " + packName 316 + " version=" + info.versionCode 317 + " entityLen=" + outputBuffer.size()); 318 } 319 320 // Now we can write the backup entity for this package 321 writeEntity(data, packName, outputBuffer.toByteArray()); 322 } 323 } 324 325 // At this point, the only entries in 'existing' are apps that were 326 // mentioned in the saved state file, but appear to no longer be present 327 // on the device. We want to preserve the entry for them, however, 328 // because we want the right thing to happen if the user goes through 329 // a backup / uninstall / backup / reinstall sequence. 330 if (DEBUG) { 331 if (mExisting.size() > 0) { 332 StringBuilder sb = new StringBuilder(64); 333 sb.append("Preserving metadata for deleted packages:"); 334 for (String app : mExisting) { 335 sb.append(' '); 336 sb.append(app); 337 } 338 Slog.v(TAG, sb.toString()); 339 } 340 } 341 } catch (IOException e) { 342 // Real error writing data 343 Slog.e(TAG, "Unable to write package backup data file!"); 344 return; 345 } 346 347 // Finally, write the new state blob -- just the list of all apps we handled 348 writeStateFile(mAllPackages, home, homeVersion, homeSigHashes, newState); 349 } 350 writeEntity(BackupDataOutput data, String key, byte[] bytes)351 private static void writeEntity(BackupDataOutput data, String key, byte[] bytes) 352 throws IOException { 353 data.writeEntityHeader(key, bytes.length); 354 data.writeEntityData(bytes, bytes.length); 355 } 356 357 // "Restore" here is a misnomer. What we're really doing is reading back the 358 // set of app signatures associated with each backed-up app in this restore 359 // image. We'll use those later to determine what we can legitimately restore. onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)360 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 361 throws IOException { 362 List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>(); 363 HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>(); 364 if (DEBUG) Slog.v(TAG, "onRestore()"); 365 int storedSystemVersion = -1; 366 367 while (data.readNextHeader()) { 368 String key = data.getKey(); 369 int dataSize = data.getDataSize(); 370 371 if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize); 372 373 // generic setup to parse any entity data 374 byte[] inputBytes = new byte[dataSize]; 375 data.readEntityData(inputBytes, 0, dataSize); 376 ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes); 377 DataInputStream inputBufferStream = new DataInputStream(inputBuffer); 378 379 if (key.equals(GLOBAL_METADATA_KEY)) { 380 int storedSdkVersion = inputBufferStream.readInt(); 381 if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion); 382 if (storedSystemVersion > Build.VERSION.SDK_INT) { 383 // returning before setting the sig map means we rejected the restore set 384 Slog.w(TAG, "Restore set was from a later version of Android; not restoring"); 385 return; 386 } 387 mStoredSdkVersion = storedSdkVersion; 388 mStoredIncrementalVersion = inputBufferStream.readUTF(); 389 mHasMetadata = true; 390 if (DEBUG) { 391 Slog.i(TAG, "Restore set version " + storedSystemVersion 392 + " is compatible with OS version " + Build.VERSION.SDK_INT 393 + " (" + mStoredIncrementalVersion + " vs " 394 + Build.VERSION.INCREMENTAL + ")"); 395 } 396 } else if (key.equals(DEFAULT_HOME_KEY)) { 397 String cn = inputBufferStream.readUTF(); 398 mRestoredHome = ComponentName.unflattenFromString(cn); 399 mRestoredHomeVersion = inputBufferStream.readLong(); 400 mRestoredHomeInstaller = inputBufferStream.readUTF(); 401 mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream); 402 if (DEBUG) { 403 Slog.i(TAG, " read preferred home app " + mRestoredHome 404 + " version=" + mRestoredHomeVersion 405 + " installer=" + mRestoredHomeInstaller 406 + " sig=" + mRestoredHomeSigHashes); 407 } 408 } else { 409 // it's a file metadata record 410 int versionCode = inputBufferStream.readInt(); 411 ArrayList<byte[]> sigs = readSignatureHashArray(inputBufferStream); 412 if (DEBUG) { 413 Slog.i(TAG, " read metadata for " + key 414 + " dataSize=" + dataSize 415 + " versionCode=" + versionCode + " sigs=" + sigs); 416 } 417 418 if (sigs == null || sigs.size() == 0) { 419 Slog.w(TAG, "Not restoring package " + key 420 + " since it appears to have no signatures."); 421 continue; 422 } 423 424 ApplicationInfo app = new ApplicationInfo(); 425 app.packageName = key; 426 restoredApps.add(app); 427 sigMap.put(key, new Metadata(versionCode, sigs)); 428 } 429 } 430 431 // On successful completion, cache the signature map for the Backup Manager to use 432 mRestoredSignatures = sigMap; 433 } 434 writeSignatureHashArray(DataOutputStream out, ArrayList<byte[]> hashes)435 private static void writeSignatureHashArray(DataOutputStream out, ArrayList<byte[]> hashes) 436 throws IOException { 437 // the number of entries in the array 438 out.writeInt(hashes.size()); 439 440 // the hash arrays themselves as length + contents 441 for (byte[] buffer : hashes) { 442 out.writeInt(buffer.length); 443 out.write(buffer); 444 } 445 } 446 readSignatureHashArray(DataInputStream in)447 private static ArrayList<byte[]> readSignatureHashArray(DataInputStream in) { 448 try { 449 int num; 450 try { 451 num = in.readInt(); 452 } catch (EOFException e) { 453 // clean termination 454 Slog.w(TAG, "Read empty signature block"); 455 return null; 456 } 457 458 if (DEBUG) Slog.v(TAG, " ... unflatten read " + num); 459 460 // Sensical? 461 if (num > 20) { 462 Slog.e(TAG, "Suspiciously large sig count in restore data; aborting"); 463 throw new IllegalStateException("Bad restore state"); 464 } 465 466 // This could be a "legacy" block of actual signatures rather than their hashes. 467 // If this is the case, convert them now. We judge based on the payload size: 468 // if the blocks are all 256 bits (32 bytes) then we take them to be SHA-256 hashes; 469 // otherwise we take them to be Signatures. 470 boolean nonHashFound = false; 471 ArrayList<byte[]> sigs = new ArrayList<byte[]>(num); 472 for (int i = 0; i < num; i++) { 473 int len = in.readInt(); 474 byte[] readHash = new byte[len]; 475 in.read(readHash); 476 sigs.add(readHash); 477 if (len != 32) { 478 nonHashFound = true; 479 } 480 } 481 482 if (nonHashFound) { 483 // Replace with the hashes. 484 sigs = BackupUtils.hashSignatureArray(sigs); 485 } 486 487 return sigs; 488 } catch (IOException e) { 489 Slog.e(TAG, "Unable to read signatures"); 490 return null; 491 } 492 } 493 494 // Util: parse out an existing state file into a usable structure parseStateFile(ParcelFileDescriptor stateFile)495 private void parseStateFile(ParcelFileDescriptor stateFile) { 496 mExisting.clear(); 497 mStateVersions.clear(); 498 mStoredSdkVersion = 0; 499 mStoredIncrementalVersion = null; 500 mStoredHomeComponent = null; 501 mStoredHomeVersion = 0; 502 mStoredHomeSigHashes = null; 503 504 // The state file is just the list of app names we have stored signatures for 505 // with the exception of the metadata block, to which is also appended the 506 // version numbers corresponding with the last time we wrote this PM block. 507 // If they mismatch the current system, we'll re-store the metadata key. 508 FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor()); 509 BufferedInputStream inbuffer = new BufferedInputStream(instream); 510 DataInputStream in = new DataInputStream(inbuffer); 511 512 try { 513 boolean ignoreExisting = false; 514 String pkg = in.readUTF(); 515 516 // Validate the state file version is sensical to us 517 if (pkg.equals(STATE_FILE_HEADER)) { 518 int stateVersion = in.readInt(); 519 if (stateVersion > STATE_FILE_VERSION) { 520 Slog.w(TAG, "Unsupported state file version " + stateVersion 521 + ", redoing from start"); 522 return; 523 } 524 pkg = in.readUTF(); 525 } else { 526 // This is an older version of the state file in which the lead element 527 // is not a STATE_FILE_VERSION string. If that's the case, we want to 528 // make sure to write our full backup dataset when given an opportunity. 529 // We trigger that by simply not marking the restored package metadata 530 // as known-to-exist-in-archive. 531 Slog.i(TAG, "Older version of saved state - rewriting"); 532 ignoreExisting = true; 533 } 534 535 // First comes the preferred home app data, if any, headed by the DEFAULT_HOME_KEY tag 536 if (pkg.equals(DEFAULT_HOME_KEY)) { 537 // flattened component name, version, signature of the home app 538 mStoredHomeComponent = ComponentName.unflattenFromString(in.readUTF()); 539 mStoredHomeVersion = in.readLong(); 540 mStoredHomeSigHashes = readSignatureHashArray(in); 541 542 pkg = in.readUTF(); // set up for the next block of state 543 } else { 544 // else no preferred home app on the ancestral device - fall through to the rest 545 } 546 547 // After (possible) home app data comes the global metadata block 548 if (pkg.equals(GLOBAL_METADATA_KEY)) { 549 mStoredSdkVersion = in.readInt(); 550 mStoredIncrementalVersion = in.readUTF(); 551 if (!ignoreExisting) { 552 mExisting.add(GLOBAL_METADATA_KEY); 553 } 554 } else { 555 Slog.e(TAG, "No global metadata in state file!"); 556 return; 557 } 558 559 // The global metadata was last; now read all the apps 560 while (true) { 561 pkg = in.readUTF(); 562 int versionCode = in.readInt(); 563 564 if (!ignoreExisting) { 565 mExisting.add(pkg); 566 } 567 mStateVersions.put(pkg, new Metadata(versionCode, null)); 568 } 569 } catch (EOFException eof) { 570 // safe; we're done 571 } catch (IOException e) { 572 // whoops, bad state file. abort. 573 Slog.e(TAG, "Unable to read Package Manager state file: " + e); 574 } 575 } 576 getPreferredHomeComponent()577 private ComponentName getPreferredHomeComponent() { 578 return mPackageManager.getHomeActivities(new ArrayList<ResolveInfo>()); 579 } 580 581 // Util: write out our new backup state file writeStateFile(List<PackageInfo> pkgs, ComponentName preferredHome, long homeVersion, ArrayList<byte[]> homeSigHashes, ParcelFileDescriptor stateFile)582 private void writeStateFile(List<PackageInfo> pkgs, ComponentName preferredHome, 583 long homeVersion, ArrayList<byte[]> homeSigHashes, ParcelFileDescriptor stateFile) { 584 FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor()); 585 BufferedOutputStream outbuf = new BufferedOutputStream(outstream); 586 DataOutputStream out = new DataOutputStream(outbuf); 587 588 // by the time we get here we know we've done all our backing up 589 try { 590 // state file version header 591 out.writeUTF(STATE_FILE_HEADER); 592 out.writeInt(STATE_FILE_VERSION); 593 594 // If we remembered a preferred home app, record that 595 if (preferredHome != null) { 596 out.writeUTF(DEFAULT_HOME_KEY); 597 out.writeUTF(preferredHome.flattenToString()); 598 out.writeLong(homeVersion); 599 writeSignatureHashArray(out, homeSigHashes); 600 } 601 602 // Conclude with the metadata block 603 out.writeUTF(GLOBAL_METADATA_KEY); 604 out.writeInt(Build.VERSION.SDK_INT); 605 out.writeUTF(Build.VERSION.INCREMENTAL); 606 607 // now write all the app names + versions 608 for (PackageInfo pkg : pkgs) { 609 out.writeUTF(pkg.packageName); 610 out.writeInt(pkg.versionCode); 611 } 612 613 out.flush(); 614 } catch (IOException e) { 615 Slog.e(TAG, "Unable to write package manager state file!"); 616 } 617 } 618 } 619