Merge change 4682 into donut
* changes: Store the app signatures as part of the backup set
This commit is contained in:
@@ -36,6 +36,7 @@ public class BackupDataOutput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A dataSize of -1 indicates that the record under this key should be deleted
|
||||||
public int writeEntityHeader(String key, int dataSize) throws IOException {
|
public int writeEntityHeader(String key, int dataSize) throws IOException {
|
||||||
int result = writeEntityHeader_native(mBackupWriter, key, dataSize);
|
int result = writeEntityHeader_native(mBackupWriter, key, dataSize);
|
||||||
if (result >= 0) {
|
if (result >= 0) {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ import android.backup.RestoreSet;
|
|||||||
import com.android.internal.backup.LocalTransport;
|
import com.android.internal.backup.LocalTransport;
|
||||||
import com.android.internal.backup.IBackupTransport;
|
import com.android.internal.backup.IBackupTransport;
|
||||||
|
|
||||||
|
import com.android.server.PackageManagerBackupAgent;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
@@ -70,6 +72,7 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
private static final String TAG = "BackupManagerService";
|
private static final String TAG = "BackupManagerService";
|
||||||
private static final boolean DEBUG = true;
|
private static final boolean DEBUG = true;
|
||||||
|
|
||||||
|
// Default time to wait after data changes before we back up the data
|
||||||
private static final long COLLECTION_INTERVAL = 1000;
|
private static final long COLLECTION_INTERVAL = 1000;
|
||||||
//private static final long COLLECTION_INTERVAL = 3 * 60 * 1000;
|
//private static final long COLLECTION_INTERVAL = 3 * 60 * 1000;
|
||||||
|
|
||||||
@@ -104,6 +107,9 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
// Backups that we haven't started yet.
|
// Backups that we haven't started yet.
|
||||||
private HashMap<ApplicationInfo,BackupRequest> mPendingBackups
|
private HashMap<ApplicationInfo,BackupRequest> mPendingBackups
|
||||||
= new HashMap<ApplicationInfo,BackupRequest>();
|
= new HashMap<ApplicationInfo,BackupRequest>();
|
||||||
|
// Do we need to back up the package manager metadata on the next pass?
|
||||||
|
private boolean mDoPackageManager;
|
||||||
|
private static final String PACKAGE_MANAGER_SENTINEL = "@pm@";
|
||||||
// Backups that we have started. These are separate to prevent starvation
|
// Backups that we have started. These are separate to prevent starvation
|
||||||
// if an app keeps re-enqueuing itself.
|
// if an app keeps re-enqueuing itself.
|
||||||
private ArrayList<BackupRequest> mBackupQueue;
|
private ArrayList<BackupRequest> mBackupQueue;
|
||||||
@@ -363,12 +369,13 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
mBackupParticipants.put(uid, set);
|
mBackupParticipants.put(uid, set);
|
||||||
}
|
}
|
||||||
set.add(app);
|
set.add(app);
|
||||||
|
backUpPackageManagerData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the given package's backup services from our known active set. If
|
// Remove the given package's entry from our known active set. If
|
||||||
// 'packageName' is null, *all* backup services will be removed.
|
// 'packageName' is null, *all* participating apps will be removed.
|
||||||
void removePackageParticipantsLocked(String packageName) {
|
void removePackageParticipantsLocked(String packageName) {
|
||||||
if (DEBUG) Log.v(TAG, "removePackageParticipantsLocked: " + packageName);
|
if (DEBUG) Log.v(TAG, "removePackageParticipantsLocked: " + packageName);
|
||||||
List<ApplicationInfo> allApps = null;
|
List<ApplicationInfo> allApps = null;
|
||||||
@@ -406,6 +413,7 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
for (ApplicationInfo entry: set) {
|
for (ApplicationInfo entry: set) {
|
||||||
if (entry.packageName.equals(app.packageName)) {
|
if (entry.packageName.equals(app.packageName)) {
|
||||||
set.remove(entry);
|
set.remove(entry);
|
||||||
|
backUpPackageManagerData();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +426,7 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
|
|
||||||
// Returns the set of all applications that define an android:backupAgent attribute
|
// Returns the set of all applications that define an android:backupAgent attribute
|
||||||
private List<ApplicationInfo> allAgentApps() {
|
private List<ApplicationInfo> allAgentApps() {
|
||||||
|
// !!! TODO: cache this and regenerate only when necessary
|
||||||
List<ApplicationInfo> allApps = mPackageManager.getInstalledApplications(0);
|
List<ApplicationInfo> allApps = mPackageManager.getInstalledApplications(0);
|
||||||
int N = allApps.size();
|
int N = allApps.size();
|
||||||
if (N > 0) {
|
if (N > 0) {
|
||||||
@@ -447,13 +456,29 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
addPackageParticipantsLockedInner(packageName, allApps);
|
addPackageParticipantsLockedInner(packageName, allApps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void backUpPackageManagerData() {
|
||||||
|
// No need to schedule a backup just for the metadata; just piggyback on
|
||||||
|
// the next actual data backup.
|
||||||
|
synchronized(this) {
|
||||||
|
mDoPackageManager = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The queue lock should be held when scheduling a backup pass
|
||||||
|
private void scheduleBackupPassLocked(long timeFromNowMillis) {
|
||||||
|
mBackupHandler.removeMessages(MSG_RUN_BACKUP);
|
||||||
|
mBackupHandler.sendEmptyMessageDelayed(MSG_RUN_BACKUP, timeFromNowMillis);
|
||||||
|
}
|
||||||
|
|
||||||
// Return the given transport
|
// Return the given transport
|
||||||
private IBackupTransport getTransport(int transportID) {
|
private IBackupTransport getTransport(int transportID) {
|
||||||
switch (transportID) {
|
switch (transportID) {
|
||||||
case BackupManager.TRANSPORT_LOCAL:
|
case BackupManager.TRANSPORT_LOCAL:
|
||||||
|
Log.v(TAG, "Supplying local transport");
|
||||||
return mLocalTransport;
|
return mLocalTransport;
|
||||||
|
|
||||||
case BackupManager.TRANSPORT_GOOGLE:
|
case BackupManager.TRANSPORT_GOOGLE:
|
||||||
|
Log.v(TAG, "Supplying Google transport");
|
||||||
return mGoogleTransport;
|
return mGoogleTransport;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -558,7 +583,29 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The transport is up and running; now run all the backups in our queue
|
// The transport is up and running. First, back up the package manager
|
||||||
|
// metadata if necessary
|
||||||
|
boolean doPackageManager;
|
||||||
|
synchronized (BackupManagerService.this) {
|
||||||
|
doPackageManager = mDoPackageManager;
|
||||||
|
mDoPackageManager = false;
|
||||||
|
}
|
||||||
|
if (doPackageManager) {
|
||||||
|
// The package manager doesn't have a proper <application> etc, but since
|
||||||
|
// it's running here in the system process we can just set up its agent
|
||||||
|
// directly and use a synthetic BackupRequest.
|
||||||
|
if (DEBUG) Log.i(TAG, "Running PM backup pass as well");
|
||||||
|
|
||||||
|
PackageManagerBackupAgent pmAgent = new PackageManagerBackupAgent(
|
||||||
|
mPackageManager, allAgentApps());
|
||||||
|
BackupRequest pmRequest = new BackupRequest(new ApplicationInfo(), false);
|
||||||
|
pmRequest.appInfo.packageName = PACKAGE_MANAGER_SENTINEL;
|
||||||
|
processOneBackup(pmRequest,
|
||||||
|
IBackupAgent.Stub.asInterface(pmAgent.onBind()),
|
||||||
|
mTransport);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now run all the backups in our queue
|
||||||
doQueuedBackups(mTransport);
|
doQueuedBackups(mTransport);
|
||||||
|
|
||||||
// Finally, tear down the transport
|
// Finally, tear down the transport
|
||||||
@@ -735,6 +782,8 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
|
// !!! TODO: do the package manager signatures restore first
|
||||||
|
|
||||||
// build the set of apps to restore
|
// build the set of apps to restore
|
||||||
try {
|
try {
|
||||||
RestoreSet[] images = mTransport.getAvailableRestoreSets();
|
RestoreSet[] images = mTransport.getAvailableRestoreSets();
|
||||||
@@ -920,8 +969,7 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
// Schedule a backup pass in a few minutes. As backup-eligible data
|
// Schedule a backup pass in a few minutes. As backup-eligible data
|
||||||
// keeps changing, continue to defer the backup pass until things
|
// keeps changing, continue to defer the backup pass until things
|
||||||
// settle down, to avoid extra overhead.
|
// settle down, to avoid extra overhead.
|
||||||
mBackupHandler.removeMessages(MSG_RUN_BACKUP);
|
scheduleBackupPassLocked(COLLECTION_INTERVAL);
|
||||||
mBackupHandler.sendEmptyMessageDelayed(MSG_RUN_BACKUP, COLLECTION_INTERVAL);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "dataChanged but no participant pkg " + packageName);
|
Log.w(TAG, "dataChanged but no participant pkg " + packageName);
|
||||||
@@ -947,8 +995,7 @@ class BackupManagerService extends IBackupManager.Stub {
|
|||||||
|
|
||||||
if (DEBUG) Log.v(TAG, "Scheduling immediate backup pass");
|
if (DEBUG) Log.v(TAG, "Scheduling immediate backup pass");
|
||||||
synchronized (mQueueLock) {
|
synchronized (mQueueLock) {
|
||||||
mBackupHandler.removeMessages(MSG_RUN_BACKUP);
|
scheduleBackupPassLocked(0);
|
||||||
mBackupHandler.sendEmptyMessage(MSG_RUN_BACKUP);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
267
services/java/com/android/server/PackageManagerBackupAgent.java
Normal file
267
services/java/com/android/server/PackageManagerBackupAgent.java
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 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.server;
|
||||||
|
|
||||||
|
import android.app.BackupAgent;
|
||||||
|
import android.backup.BackupDataInput;
|
||||||
|
import android.backup.BackupDataOutput;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException;
|
||||||
|
import android.content.pm.Signature;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
// !!!TODO: take this out
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We back up the signatures of each package so that during a system restore,
|
||||||
|
* we can verify that the app whose data we think we have matches the app
|
||||||
|
* actually resident on the device.
|
||||||
|
*
|
||||||
|
* Since the Package Manager isn't a proper "application" we just provide a
|
||||||
|
* direct IBackupAgent implementation and hand-construct it at need.
|
||||||
|
*/
|
||||||
|
public class PackageManagerBackupAgent extends BackupAgent {
|
||||||
|
private static final String TAG = "PMBA";
|
||||||
|
private static final boolean DEBUG = true;
|
||||||
|
|
||||||
|
private List<ApplicationInfo> mAllApps;
|
||||||
|
private PackageManager mPackageManager;
|
||||||
|
private HashMap<String, Signature[]> mRestoredSignatures;
|
||||||
|
|
||||||
|
// We're constructed with the set of applications that are participating
|
||||||
|
// in backup. This set changes as apps are installed & removed.
|
||||||
|
PackageManagerBackupAgent(PackageManager packageMgr, List<ApplicationInfo> apps) {
|
||||||
|
mPackageManager = packageMgr;
|
||||||
|
mAllApps = apps;
|
||||||
|
mRestoredSignatures = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Signature[] getRestoredSignatures(String packageName) {
|
||||||
|
if (mRestoredSignatures == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRestoredSignatures.get(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The backed up data is the signature block for each app, keyed by
|
||||||
|
// the package name.
|
||||||
|
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||||
|
ParcelFileDescriptor newState) {
|
||||||
|
HashSet<String> existing = parseStateFile(oldState);
|
||||||
|
|
||||||
|
// For each app we have on device, see if we've backed it up yet. If not,
|
||||||
|
// write its signature block to the output, keyed on the package name.
|
||||||
|
for (ApplicationInfo app : mAllApps) {
|
||||||
|
String packName = app.packageName;
|
||||||
|
if (!existing.contains(packName)) {
|
||||||
|
// We haven't stored this app's signatures yet, so we do that now
|
||||||
|
try {
|
||||||
|
PackageInfo info = mPackageManager.getPackageInfo(packName,
|
||||||
|
PackageManager.GET_SIGNATURES);
|
||||||
|
// build a byte array out of the signature list
|
||||||
|
byte[] sigs = flattenSignatureArray(info.signatures);
|
||||||
|
// !!! TODO: take out this debugging
|
||||||
|
if (DEBUG) {
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
crc.update(sigs);
|
||||||
|
Log.i(TAG, "+ flat sig array for " + packName + " : "
|
||||||
|
+ crc.getValue());
|
||||||
|
}
|
||||||
|
data.writeEntityHeader(packName, sigs.length);
|
||||||
|
data.writeEntityData(sigs, sigs.length);
|
||||||
|
} catch (NameNotFoundException e) {
|
||||||
|
// Weird; we just found it, and now are told it doesn't exist.
|
||||||
|
// Treat it as having been removed from the device.
|
||||||
|
existing.add(packName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Real error writing data
|
||||||
|
Log.e(TAG, "Unable to write package backup data file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We've already backed up this app. Remove it from the set so
|
||||||
|
// we can tell at the end what has disappeared from the device.
|
||||||
|
if (!existing.remove(packName)) {
|
||||||
|
Log.d(TAG, "*** failed to remove " + packName + " from package set!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the only entries in 'existing' are apps that were
|
||||||
|
// mentioned in the saved state file, but appear to no longer be present
|
||||||
|
// on the device. Write a deletion entity for them.
|
||||||
|
for (String app : existing) {
|
||||||
|
try {
|
||||||
|
data.writeEntityHeader(app, -1);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Unable to write package deletions!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, write the new state blob -- just the list of all apps we handled
|
||||||
|
writeStateFile(mAllApps, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Restore" here is a misnomer. What we're really doing is reading back the
|
||||||
|
// set of app signatures associated with each backed-up app in this restore
|
||||||
|
// image. We'll use those later to determine what we can legitimately restore.
|
||||||
|
public void onRestore(BackupDataInput data, ParcelFileDescriptor newState)
|
||||||
|
throws IOException {
|
||||||
|
List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
|
||||||
|
HashMap<String, Signature[]> sigMap = new HashMap<String, Signature[]>();
|
||||||
|
|
||||||
|
while (data.readNextHeader()) {
|
||||||
|
int dataSize = data.getDataSize();
|
||||||
|
byte[] buf = new byte[dataSize];
|
||||||
|
data.readEntityData(buf, 0, dataSize);
|
||||||
|
|
||||||
|
Signature[] sigs = unflattenSignatureArray(buf);
|
||||||
|
String pkg = data.getKey();
|
||||||
|
// !!! TODO: take out this debugging
|
||||||
|
if (DEBUG) {
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
crc.update(buf);
|
||||||
|
Log.i(TAG, "- unflat sig array for " + pkg + " : "
|
||||||
|
+ crc.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationInfo app = new ApplicationInfo();
|
||||||
|
app.packageName = pkg;
|
||||||
|
restoredApps.add(app);
|
||||||
|
sigMap.put(pkg, sigs);
|
||||||
|
}
|
||||||
|
|
||||||
|
mRestoredSignatures = sigMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Util: convert an array of Signatures into a flattened byte buffer. The
|
||||||
|
// flattened format contains enough info to reconstruct the signature array.
|
||||||
|
private byte[] flattenSignatureArray(Signature[] allSigs) {
|
||||||
|
ByteArrayOutputStream outBuf = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(outBuf);
|
||||||
|
|
||||||
|
// build the set of subsidiary buffers
|
||||||
|
try {
|
||||||
|
// first the # of signatures in the array
|
||||||
|
out.writeInt(allSigs.length);
|
||||||
|
|
||||||
|
// then the signatures themselves, length + flattened buffer
|
||||||
|
for (Signature sig : allSigs) {
|
||||||
|
byte[] flat = sig.toByteArray();
|
||||||
|
out.writeInt(flat.length);
|
||||||
|
out.write(flat);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// very strange; we're writing to memory here. abort.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outBuf.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Signature[] unflattenSignatureArray(byte[] buffer) {
|
||||||
|
ByteArrayInputStream inBufStream = new ByteArrayInputStream(buffer);
|
||||||
|
DataInputStream in = new DataInputStream(inBufStream);
|
||||||
|
Signature[] sigs = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int num = in.readInt();
|
||||||
|
sigs = new Signature[num];
|
||||||
|
for (int i = 0; i < num; i++) {
|
||||||
|
int len = in.readInt();
|
||||||
|
byte[] flatSig = new byte[len];
|
||||||
|
in.read(flatSig);
|
||||||
|
sigs[i] = new Signature(flatSig);
|
||||||
|
}
|
||||||
|
} catch (EOFException e) {
|
||||||
|
// clean termination
|
||||||
|
if (sigs == null) {
|
||||||
|
Log.w(TAG, "Empty signature block found");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to unflatten sigs");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util: parse out an existing state file into a usable structure
|
||||||
|
private HashSet<String> parseStateFile(ParcelFileDescriptor stateFile) {
|
||||||
|
HashSet<String> set = new HashSet<String>();
|
||||||
|
// The state file is just the list of app names we have stored signatures for
|
||||||
|
FileInputStream instream = new FileInputStream(stateFile.getFileDescriptor());
|
||||||
|
DataInputStream in = new DataInputStream(instream);
|
||||||
|
|
||||||
|
int bufSize = 256;
|
||||||
|
byte[] buf = new byte[bufSize];
|
||||||
|
try {
|
||||||
|
int nameSize = in.readInt();
|
||||||
|
if (bufSize < nameSize) {
|
||||||
|
bufSize = nameSize + 32;
|
||||||
|
buf = new byte[bufSize];
|
||||||
|
}
|
||||||
|
in.read(buf, 0, nameSize);
|
||||||
|
String pkg = new String(buf, 0, nameSize);
|
||||||
|
set.add(pkg);
|
||||||
|
} catch (EOFException eof) {
|
||||||
|
// safe; we're done
|
||||||
|
} catch (IOException e) {
|
||||||
|
// whoops, bad state file. abort.
|
||||||
|
Log.e(TAG, "Unable to read Package Manager state file");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Util: write a set of names into a new state file
|
||||||
|
private void writeStateFile(List<ApplicationInfo> apps, ParcelFileDescriptor stateFile) {
|
||||||
|
FileOutputStream outstream = new FileOutputStream(stateFile.getFileDescriptor());
|
||||||
|
DataOutputStream out = new DataOutputStream(outstream);
|
||||||
|
|
||||||
|
for (ApplicationInfo app : apps) {
|
||||||
|
try {
|
||||||
|
byte[] pkgNameBuf = app.packageName.getBytes();
|
||||||
|
out.writeInt(pkgNameBuf.length);
|
||||||
|
out.write(pkgNameBuf);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Unable to write package manager state file!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user