We're currently seeing the WallpaperBackupAgent generate data for a backup even when nothing has changed. In order to verify that any fix resolves this we should put appropriate tests in place which first verify the issue and then break and need updating when the issue is resolved. This CL adds some initial tests to give us the test infrastructure we can use to create the null-diff tests. Test: atest WallpaperBackupAgentTests Bug: 140995339 Change-Id: I188e39f9a032b0b9be01faa77597a8698d9e4c57
387 lines
16 KiB
Java
387 lines
16 KiB
Java
/*
|
|
* 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.wallpaperbackup;
|
|
|
|
import static android.app.WallpaperManager.FLAG_LOCK;
|
|
import static android.app.WallpaperManager.FLAG_SYSTEM;
|
|
|
|
import android.app.AppGlobals;
|
|
import android.app.WallpaperManager;
|
|
import android.app.backup.BackupAgent;
|
|
import android.app.backup.BackupDataInput;
|
|
import android.app.backup.BackupDataOutput;
|
|
import android.app.backup.FullBackupDataOutput;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
import android.content.pm.IPackageManager;
|
|
import android.content.pm.PackageInfo;
|
|
import android.graphics.Rect;
|
|
import android.os.Environment;
|
|
import android.os.FileUtils;
|
|
import android.os.ParcelFileDescriptor;
|
|
import android.os.RemoteException;
|
|
import android.os.UserHandle;
|
|
import android.util.Slog;
|
|
import android.util.Xml;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
|
|
import libcore.io.IoUtils;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
public class WallpaperBackupAgent extends BackupAgent {
|
|
private static final String TAG = "WallpaperBackup";
|
|
private static final boolean DEBUG = false;
|
|
|
|
// NB: must be kept in sync with WallpaperManagerService but has no
|
|
// compile-time visibility.
|
|
|
|
// Target filenames within the system's wallpaper directory
|
|
static final String WALLPAPER = "wallpaper_orig";
|
|
static final String WALLPAPER_LOCK = "wallpaper_lock_orig";
|
|
static final String WALLPAPER_INFO = "wallpaper_info.xml";
|
|
|
|
// Names of our local-data stage files/links
|
|
static final String IMAGE_STAGE = "wallpaper-stage";
|
|
static final String LOCK_IMAGE_STAGE = "wallpaper-lock-stage";
|
|
static final String INFO_STAGE = "wallpaper-info-stage";
|
|
static final String EMPTY_SENTINEL = "empty";
|
|
static final String QUOTA_SENTINEL = "quota";
|
|
|
|
// Not-for-backup bookkeeping
|
|
static final String PREFS_NAME = "wbprefs.xml";
|
|
static final String SYSTEM_GENERATION = "system_gen";
|
|
static final String LOCK_GENERATION = "lock_gen";
|
|
|
|
private File mWallpaperInfo; // wallpaper metadata file
|
|
private File mWallpaperFile; // primary wallpaper image file
|
|
private File mLockWallpaperFile; // lock wallpaper image file
|
|
|
|
// If this file exists, it means we exceeded our quota last time
|
|
private File mQuotaFile;
|
|
private boolean mQuotaExceeded;
|
|
|
|
private WallpaperManager mWm;
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "onCreate()");
|
|
}
|
|
|
|
File wallpaperDir = getWallpaperDir();
|
|
mWallpaperInfo = new File(wallpaperDir, WALLPAPER_INFO);
|
|
mWallpaperFile = new File(wallpaperDir, WALLPAPER);
|
|
mLockWallpaperFile = new File(wallpaperDir, WALLPAPER_LOCK);
|
|
mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE);
|
|
|
|
mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL);
|
|
mQuotaExceeded = mQuotaFile.exists();
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded);
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
protected File getWallpaperDir() {
|
|
return Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM);
|
|
}
|
|
|
|
@Override
|
|
public void onFullBackup(FullBackupDataOutput data) throws IOException {
|
|
// To avoid data duplication and disk churn, use links as the stage.
|
|
final File filesDir = getFilesDir();
|
|
final File infoStage = new File(filesDir, INFO_STAGE);
|
|
final File imageStage = new File (filesDir, IMAGE_STAGE);
|
|
final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
|
|
final File empty = new File (filesDir, EMPTY_SENTINEL);
|
|
|
|
try {
|
|
// We always back up this 'empty' file to ensure that the absence of
|
|
// storable wallpaper imagery still produces a non-empty backup data
|
|
// stream, otherwise it'd simply be ignored in preflight.
|
|
if (!empty.exists()) {
|
|
FileOutputStream touch = new FileOutputStream(empty);
|
|
touch.close();
|
|
}
|
|
backupFile(empty, data);
|
|
|
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
final int lastSysGeneration = prefs.getInt(SYSTEM_GENERATION, -1);
|
|
final int lastLockGeneration = prefs.getInt(LOCK_GENERATION, -1);
|
|
|
|
final int sysGeneration =
|
|
mWm.getWallpaperIdForUser(FLAG_SYSTEM, UserHandle.USER_SYSTEM);
|
|
final int lockGeneration =
|
|
mWm.getWallpaperIdForUser(FLAG_LOCK, UserHandle.USER_SYSTEM);
|
|
final boolean sysChanged = (sysGeneration != lastSysGeneration);
|
|
final boolean lockChanged = (lockGeneration != lastLockGeneration);
|
|
|
|
final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM);
|
|
final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK);
|
|
|
|
// There might be a latent lock wallpaper file present but unused: don't
|
|
// include it in the backup if that's the case.
|
|
ParcelFileDescriptor lockFd = mWm.getWallpaperFile(FLAG_LOCK, UserHandle.USER_SYSTEM);
|
|
final boolean hasLockWallpaper = (lockFd != null);
|
|
IoUtils.closeQuietly(lockFd);
|
|
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged);
|
|
Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged);
|
|
Slog.v(TAG, "sysEligble=" + sysEligible);
|
|
Slog.v(TAG, "lockEligible=" + lockEligible);
|
|
}
|
|
|
|
// only back up the wallpapers if we've been told they're eligible
|
|
if (mWallpaperInfo.exists()) {
|
|
if (sysChanged || lockChanged || !infoStage.exists()) {
|
|
if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
|
|
FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage);
|
|
}
|
|
if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
|
|
backupFile(infoStage, data);
|
|
}
|
|
if (sysEligible && mWallpaperFile.exists()) {
|
|
if (sysChanged || !imageStage.exists()) {
|
|
if (DEBUG) Slog.v(TAG, "New system wallpaper; copying");
|
|
FileUtils.copyFileOrThrow(mWallpaperFile, imageStage);
|
|
}
|
|
if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
|
|
backupFile(imageStage, data);
|
|
prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply();
|
|
}
|
|
|
|
// Don't try to store the lock image if we overran our quota last time
|
|
if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) {
|
|
if (lockChanged || !lockImageStage.exists()) {
|
|
if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying");
|
|
FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage);
|
|
}
|
|
if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
|
|
backupFile(lockImageStage, data);
|
|
prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
|
|
}
|
|
} catch (Exception e) {
|
|
Slog.e(TAG, "Unable to back up wallpaper", e);
|
|
} finally {
|
|
// Even if this time we had to back off on attempting to store the lock image
|
|
// due to exceeding the data quota, try again next time. This will alternate
|
|
// between "try both" and "only store the primary image" until either there
|
|
// is no lock image to store, or the quota is raised, or both fit under the
|
|
// quota.
|
|
mQuotaFile.delete();
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
// fullBackupFile is final, so we intercept backups here in tests.
|
|
protected void backupFile(File file, FullBackupDataOutput data) {
|
|
fullBackupFile(file, data);
|
|
}
|
|
|
|
@Override
|
|
public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
|
|
if (DEBUG) {
|
|
Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')');
|
|
}
|
|
try (FileOutputStream f = new FileOutputStream(mQuotaFile)) {
|
|
f.write(0);
|
|
} catch (Exception e) {
|
|
Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// We use the default onRestoreFile() implementation that will recreate our stage files,
|
|
// then post-process in onRestoreFinished() to apply the new wallpaper.
|
|
@Override
|
|
public void onRestoreFinished() {
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "onRestoreFinished()");
|
|
}
|
|
final File filesDir = getFilesDir();
|
|
final File infoStage = new File(filesDir, INFO_STAGE);
|
|
final File imageStage = new File (filesDir, IMAGE_STAGE);
|
|
final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
|
|
|
|
// If we restored separate lock imagery, the system wallpaper should be
|
|
// applied as system-only; but if there's no separate lock image, make
|
|
// sure to apply the restored system wallpaper as both.
|
|
final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK);
|
|
|
|
try {
|
|
// It is valid for the imagery to be absent; it means that we were not permitted
|
|
// to back up the original image on the source device, or there was no user-supplied
|
|
// wallpaper image present.
|
|
restoreFromStage(imageStage, infoStage, "wp", sysWhich);
|
|
restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
|
|
|
|
// And reset to the wallpaper service we should be using
|
|
ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
|
|
if (servicePackageExists(wpService)) {
|
|
if (DEBUG) {
|
|
Slog.i(TAG, "Using wallpaper service " + wpService);
|
|
}
|
|
mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM);
|
|
if (!lockImageStage.exists()) {
|
|
// We have a live wallpaper and no static lock image,
|
|
// allow live wallpaper to show "through" on lock screen.
|
|
mWm.clear(FLAG_LOCK);
|
|
}
|
|
} else {
|
|
// If we've restored a live wallpaper, but the component doesn't exist,
|
|
// we should log it as an error so we can easily identify the problem
|
|
// in reports from users
|
|
if (wpService != null) {
|
|
Slog.e(TAG, "Wallpaper service " + wpService + " isn't available.");
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
|
|
} finally {
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
|
|
}
|
|
infoStage.delete();
|
|
imageStage.delete();
|
|
lockImageStage.delete();
|
|
|
|
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
prefs.edit()
|
|
.putInt(SYSTEM_GENERATION, -1)
|
|
.putInt(LOCK_GENERATION, -1)
|
|
.commit();
|
|
}
|
|
}
|
|
|
|
private void restoreFromStage(File stage, File info, String hintTag, int which)
|
|
throws IOException {
|
|
if (stage.exists()) {
|
|
// Parse the restored info file to find the crop hint. Note that this currently
|
|
// relies on a priori knowledge of the wallpaper info file schema.
|
|
Rect cropHint = parseCropHint(info, hintTag);
|
|
if (cropHint != null) {
|
|
Slog.i(TAG, "Got restored wallpaper; applying which=" + which);
|
|
if (DEBUG) {
|
|
Slog.v(TAG, "Restored crop hint " + cropHint);
|
|
}
|
|
try (FileInputStream in = new FileInputStream(stage)) {
|
|
mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
|
|
} finally {} // auto-closes 'in'
|
|
}
|
|
}
|
|
}
|
|
|
|
private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
|
|
Rect cropHint = new Rect();
|
|
try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
|
|
XmlPullParser parser = Xml.newPullParser();
|
|
parser.setInput(stream, StandardCharsets.UTF_8.name());
|
|
|
|
int type;
|
|
do {
|
|
type = parser.next();
|
|
if (type == XmlPullParser.START_TAG) {
|
|
String tag = parser.getName();
|
|
if (sectionTag.equals(tag)) {
|
|
cropHint.left = getAttributeInt(parser, "cropLeft", 0);
|
|
cropHint.top = getAttributeInt(parser, "cropTop", 0);
|
|
cropHint.right = getAttributeInt(parser, "cropRight", 0);
|
|
cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
|
|
}
|
|
}
|
|
} while (type != XmlPullParser.END_DOCUMENT);
|
|
} catch (Exception e) {
|
|
// Whoops; can't process the info file at all. Report failure.
|
|
Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
|
|
return null;
|
|
}
|
|
|
|
return cropHint;
|
|
}
|
|
|
|
private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
|
|
ComponentName name = null;
|
|
try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
|
|
final XmlPullParser parser = Xml.newPullParser();
|
|
parser.setInput(stream, StandardCharsets.UTF_8.name());
|
|
|
|
int type;
|
|
do {
|
|
type = parser.next();
|
|
if (type == XmlPullParser.START_TAG) {
|
|
String tag = parser.getName();
|
|
if (sectionTag.equals(tag)) {
|
|
final String parsedName = parser.getAttributeValue(null, "component");
|
|
name = (parsedName != null)
|
|
? ComponentName.unflattenFromString(parsedName)
|
|
: null;
|
|
break;
|
|
}
|
|
}
|
|
} while (type != XmlPullParser.END_DOCUMENT);
|
|
} catch (Exception e) {
|
|
// Whoops; can't process the info file at all. Report failure.
|
|
Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
|
|
return null;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
|
|
final String value = parser.getAttributeValue(null, name);
|
|
return (value == null) ? defValue : Integer.parseInt(value);
|
|
}
|
|
|
|
private boolean servicePackageExists(ComponentName comp) {
|
|
try {
|
|
if (comp != null) {
|
|
final IPackageManager pm = AppGlobals.getPackageManager();
|
|
final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
|
|
0, UserHandle.USER_SYSTEM);
|
|
return (info != null);
|
|
}
|
|
} catch (RemoteException e) {
|
|
Slog.e(TAG, "Unable to contact package manager");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// Key/value API: abstract, therefore required; but not used
|
|
//
|
|
|
|
@Override
|
|
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
|
ParcelFileDescriptor newState) throws IOException {
|
|
// Intentionally blank
|
|
}
|
|
|
|
@Override
|
|
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
|
|
throws IOException {
|
|
// Intentionally blank
|
|
}
|
|
} |