blob: 19da32c83cd5f9f867cad6b88a71e30acf50164d [file] [log] [blame]
Christopher Tated7faf532016-02-25 12:43:38 -08001/*
2 * Copyright (C) 2016 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.wallpaperbackup;
18
Christopher Tateac482eb2016-07-18 17:31:02 -070019import static android.app.WallpaperManager.FLAG_LOCK;
20import static android.app.WallpaperManager.FLAG_SYSTEM;
21
Christopher Tate2471a372016-08-24 16:23:20 -070022import android.app.AppGlobals;
Christopher Tated7faf532016-02-25 12:43:38 -080023import android.app.WallpaperManager;
24import android.app.backup.BackupAgent;
25import android.app.backup.BackupDataInput;
26import android.app.backup.BackupDataOutput;
27import android.app.backup.FullBackupDataOutput;
Christopher Tate2471a372016-08-24 16:23:20 -070028import android.content.ComponentName;
Christopher Tated7faf532016-02-25 12:43:38 -080029import android.content.Context;
Christopher Tateac482eb2016-07-18 17:31:02 -070030import android.content.SharedPreferences;
Christopher Tate2471a372016-08-24 16:23:20 -070031import android.content.pm.IPackageManager;
32import android.content.pm.PackageInfo;
Christopher Tate61f2d632016-04-27 15:47:13 -070033import android.graphics.Rect;
Christopher Tated7faf532016-02-25 12:43:38 -080034import android.os.Environment;
Christopher Tateac482eb2016-07-18 17:31:02 -070035import android.os.FileUtils;
Christopher Tated7faf532016-02-25 12:43:38 -080036import android.os.ParcelFileDescriptor;
Christopher Tate2471a372016-08-24 16:23:20 -070037import android.os.RemoteException;
Christopher Tated7faf532016-02-25 12:43:38 -080038import android.os.UserHandle;
Christopher Tated7faf532016-02-25 12:43:38 -080039import android.util.Slog;
Christopher Tate61f2d632016-04-27 15:47:13 -070040import android.util.Xml;
41
Christopher Tatef47eff72016-08-09 16:23:14 -070042import libcore.io.IoUtils;
43
Christopher Tate61f2d632016-04-27 15:47:13 -070044import org.xmlpull.v1.XmlPullParser;
Christopher Tated7faf532016-02-25 12:43:38 -080045
46import java.io.File;
Christopher Tate61f2d632016-04-27 15:47:13 -070047import java.io.FileInputStream;
Christopher Tated7faf532016-02-25 12:43:38 -080048import java.io.FileOutputStream;
49import java.io.IOException;
Christopher Tate61f2d632016-04-27 15:47:13 -070050import java.nio.charset.StandardCharsets;
Christopher Tated7faf532016-02-25 12:43:38 -080051
52public class WallpaperBackupAgent extends BackupAgent {
53 private static final String TAG = "WallpaperBackup";
Christopher Tateac482eb2016-07-18 17:31:02 -070054 private static final boolean DEBUG = false;
Christopher Tated7faf532016-02-25 12:43:38 -080055
56 // NB: must be kept in sync with WallpaperManagerService but has no
Christopher Tate61f2d632016-04-27 15:47:13 -070057 // compile-time visibility.
Christopher Tated7faf532016-02-25 12:43:38 -080058
59 // Target filenames within the system's wallpaper directory
60 static final String WALLPAPER = "wallpaper_orig";
Christopher Tatebf13ccf2016-06-28 16:38:40 -070061 static final String WALLPAPER_LOCK = "wallpaper_lock_orig";
Christopher Tated7faf532016-02-25 12:43:38 -080062 static final String WALLPAPER_INFO = "wallpaper_info.xml";
63
64 // Names of our local-data stage files/links
65 static final String IMAGE_STAGE = "wallpaper-stage";
Christopher Tatebf13ccf2016-06-28 16:38:40 -070066 static final String LOCK_IMAGE_STAGE = "wallpaper-lock-stage";
Christopher Tated7faf532016-02-25 12:43:38 -080067 static final String INFO_STAGE = "wallpaper-info-stage";
68 static final String EMPTY_SENTINEL = "empty";
Christopher Tatebf13ccf2016-06-28 16:38:40 -070069 static final String QUOTA_SENTINEL = "quota";
Christopher Tated7faf532016-02-25 12:43:38 -080070
Christopher Tateac482eb2016-07-18 17:31:02 -070071 // Not-for-backup bookkeeping
72 static final String PREFS_NAME = "wbprefs.xml";
73 static final String SYSTEM_GENERATION = "system_gen";
74 static final String LOCK_GENERATION = "lock_gen";
75
Christopher Tatebf13ccf2016-06-28 16:38:40 -070076 private File mWallpaperInfo; // wallpaper metadata file
77 private File mWallpaperFile; // primary wallpaper image file
78 private File mLockWallpaperFile; // lock wallpaper image file
79
80 // If this file exists, it means we exceeded our quota last time
81 private File mQuotaFile;
82 private boolean mQuotaExceeded;
Christopher Tated7faf532016-02-25 12:43:38 -080083
84 private WallpaperManager mWm;
85
86 @Override
87 public void onCreate() {
88 if (DEBUG) {
89 Slog.v(TAG, "onCreate()");
90 }
91
92 File wallpaperDir = Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM);
93 mWallpaperInfo = new File(wallpaperDir, WALLPAPER_INFO);
94 mWallpaperFile = new File(wallpaperDir, WALLPAPER);
Christopher Tatebf13ccf2016-06-28 16:38:40 -070095 mLockWallpaperFile = new File(wallpaperDir, WALLPAPER_LOCK);
Christopher Tated7faf532016-02-25 12:43:38 -080096 mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE);
Christopher Tatebf13ccf2016-06-28 16:38:40 -070097
98 mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL);
99 mQuotaExceeded = mQuotaFile.exists();
100 if (DEBUG) {
101 Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded);
102 }
Christopher Tated7faf532016-02-25 12:43:38 -0800103 }
104
105 @Override
106 public void onFullBackup(FullBackupDataOutput data) throws IOException {
107 // To avoid data duplication and disk churn, use links as the stage.
108 final File filesDir = getFilesDir();
109 final File infoStage = new File(filesDir, INFO_STAGE);
110 final File imageStage = new File (filesDir, IMAGE_STAGE);
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700111 final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
Christopher Tated7faf532016-02-25 12:43:38 -0800112 final File empty = new File (filesDir, EMPTY_SENTINEL);
113
114 try {
115 // We always back up this 'empty' file to ensure that the absence of
116 // storable wallpaper imagery still produces a non-empty backup data
117 // stream, otherwise it'd simply be ignored in preflight.
118 FileOutputStream touch = new FileOutputStream(empty);
119 touch.close();
120 fullBackupFile(empty, data);
121
Christopher Tate61722662016-08-10 16:13:14 -0700122 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
123 final int lastSysGeneration = prefs.getInt(SYSTEM_GENERATION, -1);
124 final int lastLockGeneration = prefs.getInt(LOCK_GENERATION, -1);
Christopher Tate5cb5e892016-06-17 13:24:02 -0700125
Christopher Tate61722662016-08-10 16:13:14 -0700126 final int sysGeneration =
127 mWm.getWallpaperIdForUser(FLAG_SYSTEM, UserHandle.USER_SYSTEM);
128 final int lockGeneration =
129 mWm.getWallpaperIdForUser(FLAG_LOCK, UserHandle.USER_SYSTEM);
130 final boolean sysChanged = (sysGeneration != lastSysGeneration);
131 final boolean lockChanged = (lockGeneration != lastLockGeneration);
Christopher Tate5cb5e892016-06-17 13:24:02 -0700132
Christopher Tate61722662016-08-10 16:13:14 -0700133 final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM);
134 final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK);
Christopher Tateac482eb2016-07-18 17:31:02 -0700135
Christopher Tatef47eff72016-08-09 16:23:14 -0700136 // There might be a latent lock wallpaper file present but unused: don't
137 // include it in the backup if that's the case.
138 ParcelFileDescriptor lockFd = mWm.getWallpaperFile(FLAG_LOCK, UserHandle.USER_SYSTEM);
139 final boolean hasLockWallpaper = (lockFd != null);
140 IoUtils.closeQuietly(lockFd);
141
Christopher Tate61722662016-08-10 16:13:14 -0700142 if (DEBUG) {
143 Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged);
144 Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged);
145 Slog.v(TAG, "sysEligble=" + sysEligible);
146 Slog.v(TAG, "lockEligible=" + lockEligible);
147 }
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700148
Christopher Tate61722662016-08-10 16:13:14 -0700149 // only back up the wallpapers if we've been told they're eligible
Christopher Tatea611fdc2016-09-19 14:05:44 -0700150 if (mWallpaperInfo.exists()) {
Christopher Tate61722662016-08-10 16:13:14 -0700151 if (sysChanged || lockChanged || !infoStage.exists()) {
152 if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
153 FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage);
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700154 }
Christopher Tatea611fdc2016-09-19 14:05:44 -0700155 if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
Christopher Tate61722662016-08-10 16:13:14 -0700156 fullBackupFile(infoStage, data);
157 }
158 if (sysEligible && mWallpaperFile.exists()) {
159 if (sysChanged || !imageStage.exists()) {
160 if (DEBUG) Slog.v(TAG, "New system wallpaper; copying");
161 FileUtils.copyFileOrThrow(mWallpaperFile, imageStage);
Christopher Tated7faf532016-02-25 12:43:38 -0800162 }
Christopher Tatea611fdc2016-09-19 14:05:44 -0700163 if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
Christopher Tate61722662016-08-10 16:13:14 -0700164 fullBackupFile(imageStage, data);
165 prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply();
166 }
167
168 // Don't try to store the lock image if we overran our quota last time
169 if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) {
170 if (lockChanged || !lockImageStage.exists()) {
171 if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying");
172 FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage);
173 }
Christopher Tatea611fdc2016-09-19 14:05:44 -0700174 if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
Christopher Tate61722662016-08-10 16:13:14 -0700175 fullBackupFile(lockImageStage, data);
176 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
Christopher Tated7faf532016-02-25 12:43:38 -0800177 }
178 } catch (Exception e) {
Christopher Tate5f829d22016-07-12 17:22:17 -0700179 Slog.e(TAG, "Unable to back up wallpaper", e);
Christopher Tated7faf532016-02-25 12:43:38 -0800180 } finally {
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700181 // Even if this time we had to back off on attempting to store the lock image
182 // due to exceeding the data quota, try again next time. This will alternate
183 // between "try both" and "only store the primary image" until either there
184 // is no lock image to store, or the quota is raised, or both fit under the
185 // quota.
186 mQuotaFile.delete();
187 }
188 }
189
190 @Override
191 public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
192 if (DEBUG) {
193 Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')');
194 }
195 try (FileOutputStream f = new FileOutputStream(mQuotaFile)) {
196 f.write(0);
197 } catch (Exception e) {
198 Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage());
Christopher Tated7faf532016-02-25 12:43:38 -0800199 }
200 }
201
202 // We use the default onRestoreFile() implementation that will recreate our stage files,
Christopher Tate61f2d632016-04-27 15:47:13 -0700203 // then post-process in onRestoreFinished() to apply the new wallpaper.
Christopher Tated7faf532016-02-25 12:43:38 -0800204 @Override
205 public void onRestoreFinished() {
206 if (DEBUG) {
207 Slog.v(TAG, "onRestoreFinished()");
208 }
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700209 final File filesDir = getFilesDir();
210 final File infoStage = new File(filesDir, INFO_STAGE);
211 final File imageStage = new File (filesDir, IMAGE_STAGE);
212 final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
Christopher Tated7faf532016-02-25 12:43:38 -0800213
Christopher Tatef47eff72016-08-09 16:23:14 -0700214 // If we restored separate lock imagery, the system wallpaper should be
215 // applied as system-only; but if there's no separate lock image, make
216 // sure to apply the restored system wallpaper as both.
217 final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK);
218
Christopher Tated7faf532016-02-25 12:43:38 -0800219 try {
220 // It is valid for the imagery to be absent; it means that we were not permitted
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700221 // to back up the original image on the source device, or there was no user-supplied
222 // wallpaper image present.
Christopher Tatef47eff72016-08-09 16:23:14 -0700223 restoreFromStage(imageStage, infoStage, "wp", sysWhich);
224 restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
Christopher Tate2471a372016-08-24 16:23:20 -0700225
226 // And reset to the wallpaper service we should be using
227 ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
228 if (servicePackageExists(wpService)) {
229 if (DEBUG) {
230 Slog.i(TAG, "Using wallpaper service " + wpService);
231 }
232 mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM);
233 } else {
234 if (DEBUG) {
235 Slog.v(TAG, "Can't use wallpaper service " + wpService);
236 }
237 }
Christopher Tated7faf532016-02-25 12:43:38 -0800238 } catch (Exception e) {
239 Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
Christopher Tated7faf532016-02-25 12:43:38 -0800240 } finally {
Christopher Tated7faf532016-02-25 12:43:38 -0800241 if (DEBUG) {
Christopher Tateac482eb2016-07-18 17:31:02 -0700242 Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
Christopher Tated7faf532016-02-25 12:43:38 -0800243 }
244 infoStage.delete();
245 imageStage.delete();
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700246 lockImageStage.delete();
Christopher Tateac482eb2016-07-18 17:31:02 -0700247
248 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
249 prefs.edit()
250 .putInt(SYSTEM_GENERATION, -1)
251 .putInt(LOCK_GENERATION, -1)
252 .commit();
Christopher Tated7faf532016-02-25 12:43:38 -0800253 }
254 }
255
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700256 private void restoreFromStage(File stage, File info, String hintTag, int which)
257 throws IOException {
258 if (stage.exists()) {
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700259 // Parse the restored info file to find the crop hint. Note that this currently
260 // relies on a priori knowledge of the wallpaper info file schema.
261 Rect cropHint = parseCropHint(info, hintTag);
262 if (cropHint != null) {
Christopher Tateac482eb2016-07-18 17:31:02 -0700263 Slog.i(TAG, "Got restored wallpaper; applying which=" + which);
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700264 if (DEBUG) {
Christopher Tateac482eb2016-07-18 17:31:02 -0700265 Slog.v(TAG, "Restored crop hint " + cropHint);
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700266 }
267 try (FileInputStream in = new FileInputStream(stage)) {
Christopher Tatedd7110d2016-08-08 13:09:02 -0700268 mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700269 } finally {} // auto-closes 'in'
270 }
271 }
272 }
273
274 private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
Christopher Tate61f2d632016-04-27 15:47:13 -0700275 Rect cropHint = new Rect();
276 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
277 XmlPullParser parser = Xml.newPullParser();
278 parser.setInput(stream, StandardCharsets.UTF_8.name());
279
280 int type;
281 do {
282 type = parser.next();
283 if (type == XmlPullParser.START_TAG) {
284 String tag = parser.getName();
Christopher Tatebf13ccf2016-06-28 16:38:40 -0700285 if (sectionTag.equals(tag)) {
Christopher Tate61f2d632016-04-27 15:47:13 -0700286 cropHint.left = getAttributeInt(parser, "cropLeft", 0);
287 cropHint.top = getAttributeInt(parser, "cropTop", 0);
288 cropHint.right = getAttributeInt(parser, "cropRight", 0);
289 cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
290 }
291 }
292 } while (type != XmlPullParser.END_DOCUMENT);
293 } catch (Exception e) {
294 // Whoops; can't process the info file at all. Report failure.
Christopher Tate2471a372016-08-24 16:23:20 -0700295 Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
Christopher Tate61f2d632016-04-27 15:47:13 -0700296 return null;
297 }
298
299 return cropHint;
300 }
301
Christopher Tate2471a372016-08-24 16:23:20 -0700302 private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
303 ComponentName name = null;
304 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
305 final XmlPullParser parser = Xml.newPullParser();
306 parser.setInput(stream, StandardCharsets.UTF_8.name());
307
308 int type;
309 do {
310 type = parser.next();
311 if (type == XmlPullParser.START_TAG) {
312 String tag = parser.getName();
313 if (sectionTag.equals(tag)) {
314 final String parsedName = parser.getAttributeValue(null, "component");
315 name = (parsedName != null)
316 ? ComponentName.unflattenFromString(parsedName)
317 : null;
318 break;
319 }
320 }
321 } while (type != XmlPullParser.END_DOCUMENT);
322 } catch (Exception e) {
323 // Whoops; can't process the info file at all. Report failure.
324 Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
325 return null;
326 }
327 return name;
328 }
329
Christopher Tate61f2d632016-04-27 15:47:13 -0700330 private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
331 final String value = parser.getAttributeValue(null, name);
332 return (value == null) ? defValue : Integer.parseInt(value);
333 }
334
Christopher Tate2471a372016-08-24 16:23:20 -0700335 private boolean servicePackageExists(ComponentName comp) {
336 try {
337 if (comp != null) {
338 final IPackageManager pm = AppGlobals.getPackageManager();
339 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
340 0, UserHandle.USER_SYSTEM);
341 return (info != null);
342 }
343 } catch (RemoteException e) {
344 Slog.e(TAG, "Unable to contact package manager");
345 }
346 return false;
347 }
348
Christopher Tated7faf532016-02-25 12:43:38 -0800349 //
Christopher Tate61f2d632016-04-27 15:47:13 -0700350 // Key/value API: abstract, therefore required; but not used
Christopher Tated7faf532016-02-25 12:43:38 -0800351 //
352
353 @Override
354 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
355 ParcelFileDescriptor newState) throws IOException {
356 // Intentionally blank
357 }
358
359 @Override
360 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
361 throws IOException {
362 // Intentionally blank
363 }
364}