• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.wallpaperbackup;
18 
19 import static android.app.WallpaperManager.FLAG_LOCK;
20 import static android.app.WallpaperManager.FLAG_SYSTEM;
21 
22 import android.app.AppGlobals;
23 import android.app.WallpaperManager;
24 import android.app.backup.BackupAgent;
25 import android.app.backup.BackupDataInput;
26 import android.app.backup.BackupDataOutput;
27 import android.app.backup.FullBackupDataOutput;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.SharedPreferences;
31 import android.content.pm.IPackageManager;
32 import android.content.pm.PackageInfo;
33 import android.graphics.Rect;
34 import android.os.Environment;
35 import android.os.FileUtils;
36 import android.os.ParcelFileDescriptor;
37 import android.os.RemoteException;
38 import android.os.UserHandle;
39 import android.util.Slog;
40 import android.util.Xml;
41 
42 import libcore.io.IoUtils;
43 
44 import org.xmlpull.v1.XmlPullParser;
45 
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.nio.charset.StandardCharsets;
51 
52 public class WallpaperBackupAgent extends BackupAgent {
53     private static final String TAG = "WallpaperBackup";
54     private static final boolean DEBUG = false;
55 
56     // NB: must be kept in sync with WallpaperManagerService but has no
57     // compile-time visibility.
58 
59     // Target filenames within the system's wallpaper directory
60     static final String WALLPAPER = "wallpaper_orig";
61     static final String WALLPAPER_LOCK = "wallpaper_lock_orig";
62     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";
66     static final String LOCK_IMAGE_STAGE = "wallpaper-lock-stage";
67     static final String INFO_STAGE = "wallpaper-info-stage";
68     static final String EMPTY_SENTINEL = "empty";
69     static final String QUOTA_SENTINEL = "quota";
70 
71     // 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 
76     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;
83 
84     private WallpaperManager mWm;
85 
86     @Override
onCreate()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);
95         mLockWallpaperFile = new File(wallpaperDir, WALLPAPER_LOCK);
96         mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE);
97 
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         }
103     }
104 
105     @Override
onFullBackup(FullBackupDataOutput data)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);
111         final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
112         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 
122             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);
125 
126             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);
132 
133             final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM);
134             final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK);
135 
136                 // 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 
142             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             }
148 
149             // only back up the wallpapers if we've been told they're eligible
150             if (mWallpaperInfo.exists()) {
151                 if (sysChanged || lockChanged || !infoStage.exists()) {
152                     if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
153                     FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage);
154                 }
155                 if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
156                 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);
162                 }
163                 if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
164                 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                 }
174                 if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
175                 fullBackupFile(lockImageStage, data);
176                 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
177             }
178         } catch (Exception e) {
179             Slog.e(TAG, "Unable to back up wallpaper", e);
180         } finally {
181             // 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
onQuotaExceeded(long backupDataBytes, long quotaBytes)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());
199         }
200     }
201 
202     // We use the default onRestoreFile() implementation that will recreate our stage files,
203     // then post-process in onRestoreFinished() to apply the new wallpaper.
204     @Override
onRestoreFinished()205     public void onRestoreFinished() {
206         if (DEBUG) {
207             Slog.v(TAG, "onRestoreFinished()");
208         }
209         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);
213 
214         // 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 
219         try {
220             // It is valid for the imagery to be absent; it means that we were not permitted
221             // to back up the original image on the source device, or there was no user-supplied
222             // wallpaper image present.
223             restoreFromStage(imageStage, infoStage, "wp", sysWhich);
224             restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
225 
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                 if (!lockImageStage.exists()) {
234                     // We have a live wallpaper and no static lock image,
235                     // allow live wallpaper to show "through" on lock screen.
236                     mWm.clear(FLAG_LOCK);
237                 }
238             } else {
239                 if (DEBUG) {
240                     Slog.v(TAG, "Can't use wallpaper service " + wpService);
241                 }
242             }
243         } catch (Exception e) {
244             Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
245         } finally {
246             if (DEBUG) {
247                 Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
248             }
249             infoStage.delete();
250             imageStage.delete();
251             lockImageStage.delete();
252 
253             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
254             prefs.edit()
255                     .putInt(SYSTEM_GENERATION, -1)
256                     .putInt(LOCK_GENERATION, -1)
257                     .commit();
258         }
259     }
260 
restoreFromStage(File stage, File info, String hintTag, int which)261     private void restoreFromStage(File stage, File info, String hintTag, int which)
262             throws IOException {
263         if (stage.exists()) {
264             // Parse the restored info file to find the crop hint.  Note that this currently
265             // relies on a priori knowledge of the wallpaper info file schema.
266             Rect cropHint = parseCropHint(info, hintTag);
267             if (cropHint != null) {
268                 Slog.i(TAG, "Got restored wallpaper; applying which=" + which);
269                 if (DEBUG) {
270                     Slog.v(TAG, "Restored crop hint " + cropHint);
271                 }
272                 try (FileInputStream in = new FileInputStream(stage)) {
273                     mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
274                 } finally {} // auto-closes 'in'
275             }
276         }
277     }
278 
parseCropHint(File wallpaperInfo, String sectionTag)279     private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
280         Rect cropHint = new Rect();
281         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
282             XmlPullParser parser = Xml.newPullParser();
283             parser.setInput(stream, StandardCharsets.UTF_8.name());
284 
285             int type;
286             do {
287                 type = parser.next();
288                 if (type == XmlPullParser.START_TAG) {
289                     String tag = parser.getName();
290                     if (sectionTag.equals(tag)) {
291                         cropHint.left = getAttributeInt(parser, "cropLeft", 0);
292                         cropHint.top = getAttributeInt(parser, "cropTop", 0);
293                         cropHint.right = getAttributeInt(parser, "cropRight", 0);
294                         cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
295                     }
296                 }
297             } while (type != XmlPullParser.END_DOCUMENT);
298         } catch (Exception e) {
299             // Whoops; can't process the info file at all.  Report failure.
300             Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
301             return null;
302         }
303 
304         return cropHint;
305     }
306 
parseWallpaperComponent(File wallpaperInfo, String sectionTag)307     private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
308         ComponentName name = null;
309         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
310             final XmlPullParser parser = Xml.newPullParser();
311             parser.setInput(stream, StandardCharsets.UTF_8.name());
312 
313             int type;
314             do {
315                 type = parser.next();
316                 if (type == XmlPullParser.START_TAG) {
317                     String tag = parser.getName();
318                     if (sectionTag.equals(tag)) {
319                         final String parsedName = parser.getAttributeValue(null, "component");
320                         name = (parsedName != null)
321                                 ? ComponentName.unflattenFromString(parsedName)
322                                 : null;
323                         break;
324                     }
325                 }
326             } while (type != XmlPullParser.END_DOCUMENT);
327         } catch (Exception e) {
328             // Whoops; can't process the info file at all.  Report failure.
329             Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
330             return null;
331         }
332         return name;
333     }
334 
getAttributeInt(XmlPullParser parser, String name, int defValue)335     private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
336         final String value = parser.getAttributeValue(null, name);
337         return (value == null) ? defValue : Integer.parseInt(value);
338     }
339 
servicePackageExists(ComponentName comp)340     private boolean servicePackageExists(ComponentName comp) {
341         try {
342             if (comp != null) {
343                 final IPackageManager pm = AppGlobals.getPackageManager();
344                 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
345                         0, UserHandle.USER_SYSTEM);
346                 return (info != null);
347             }
348         } catch (RemoteException e) {
349             Slog.e(TAG, "Unable to contact package manager");
350         }
351         return false;
352     }
353 
354     //
355     // Key/value API: abstract, therefore required; but not used
356     //
357 
358     @Override
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)359     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
360             ParcelFileDescriptor newState) throws IOException {
361         // Intentionally blank
362     }
363 
364     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)365     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
366             throws IOException {
367         // Intentionally blank
368     }
369 }