• 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.provider.Settings;
40 import android.util.Slog;
41 import android.util.Xml;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.content.PackageMonitor;
45 
46 import libcore.io.IoUtils;
47 
48 import org.xmlpull.v1.XmlPullParser;
49 
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.FileOutputStream;
53 import java.io.IOException;
54 
55 public class WallpaperBackupAgent extends BackupAgent {
56     private static final String TAG = "WallpaperBackup";
57     private static final boolean DEBUG = false;
58 
59     // NB: must be kept in sync with WallpaperManagerService but has no
60     // compile-time visibility.
61 
62     // Target filenames within the system's wallpaper directory
63     static final String WALLPAPER = "wallpaper_orig";
64     static final String WALLPAPER_LOCK = "wallpaper_lock_orig";
65     static final String WALLPAPER_INFO = "wallpaper_info.xml";
66 
67     // Names of our local-data stage files/links
68     static final String IMAGE_STAGE = "wallpaper-stage";
69     static final String LOCK_IMAGE_STAGE = "wallpaper-lock-stage";
70     static final String INFO_STAGE = "wallpaper-info-stage";
71     static final String EMPTY_SENTINEL = "empty";
72     static final String QUOTA_SENTINEL = "quota";
73 
74     // Not-for-backup bookkeeping
75     static final String PREFS_NAME = "wbprefs.xml";
76     static final String SYSTEM_GENERATION = "system_gen";
77     static final String LOCK_GENERATION = "lock_gen";
78 
79     private File mWallpaperInfo;        // wallpaper metadata file
80     private File mWallpaperFile;        // primary wallpaper image file
81     private File mLockWallpaperFile;    // lock wallpaper image file
82 
83     // If this file exists, it means we exceeded our quota last time
84     private File mQuotaFile;
85     private boolean mQuotaExceeded;
86 
87     private WallpaperManager mWm;
88 
89     @Override
onCreate()90     public void onCreate() {
91         if (DEBUG) {
92             Slog.v(TAG, "onCreate()");
93         }
94 
95         File wallpaperDir = getWallpaperDir();
96         mWallpaperInfo = new File(wallpaperDir, WALLPAPER_INFO);
97         mWallpaperFile = new File(wallpaperDir, WALLPAPER);
98         mLockWallpaperFile = new File(wallpaperDir, WALLPAPER_LOCK);
99         mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE);
100 
101         mQuotaFile = new File(getFilesDir(), QUOTA_SENTINEL);
102         mQuotaExceeded = mQuotaFile.exists();
103         if (DEBUG) {
104             Slog.v(TAG, "quota file " + mQuotaFile.getPath() + " exists=" + mQuotaExceeded);
105         }
106     }
107 
108     @VisibleForTesting
getWallpaperDir()109     protected File getWallpaperDir() {
110         return Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM);
111     }
112 
113     @Override
onFullBackup(FullBackupDataOutput data)114     public void onFullBackup(FullBackupDataOutput data) throws IOException {
115         // To avoid data duplication and disk churn, use links as the stage.
116         final File filesDir = getFilesDir();
117         final File infoStage = new File(filesDir, INFO_STAGE);
118         final File imageStage = new File (filesDir, IMAGE_STAGE);
119         final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
120         final File empty = new File (filesDir, EMPTY_SENTINEL);
121 
122         try {
123             // We always back up this 'empty' file to ensure that the absence of
124             // storable wallpaper imagery still produces a non-empty backup data
125             // stream, otherwise it'd simply be ignored in preflight.
126             if (!empty.exists()) {
127                 FileOutputStream touch = new FileOutputStream(empty);
128                 touch.close();
129             }
130             backupFile(empty, data);
131 
132             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
133             final int lastSysGeneration = prefs.getInt(SYSTEM_GENERATION, -1);
134             final int lastLockGeneration = prefs.getInt(LOCK_GENERATION, -1);
135 
136             final int sysGeneration =
137                     mWm.getWallpaperIdForUser(FLAG_SYSTEM, UserHandle.USER_SYSTEM);
138             final int lockGeneration =
139                     mWm.getWallpaperIdForUser(FLAG_LOCK, UserHandle.USER_SYSTEM);
140             final boolean sysChanged = (sysGeneration != lastSysGeneration);
141             final boolean lockChanged = (lockGeneration != lastLockGeneration);
142 
143             final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM);
144             final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK);
145 
146                 // There might be a latent lock wallpaper file present but unused: don't
147                 // include it in the backup if that's the case.
148                 ParcelFileDescriptor lockFd = mWm.getWallpaperFile(FLAG_LOCK, UserHandle.USER_SYSTEM);
149                 final boolean hasLockWallpaper = (lockFd != null);
150                 IoUtils.closeQuietly(lockFd);
151 
152             if (DEBUG) {
153                 Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged);
154                 Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged);
155                 Slog.v(TAG, "sysEligble=" + sysEligible);
156                 Slog.v(TAG, "lockEligible=" + lockEligible);
157                 Slog.v(TAG, "hasLockWallpaper=" + hasLockWallpaper);
158             }
159 
160             // only back up the wallpapers if we've been told they're eligible
161             if (mWallpaperInfo.exists()) {
162                 if (sysChanged || lockChanged || !infoStage.exists()) {
163                     if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying");
164                     FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage);
165                 }
166                 if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata");
167                 backupFile(infoStage, data);
168             } else {
169                 Slog.w(TAG, "Wallpaper metadata file doesn't exist: " +
170                         mWallpaperFile.getPath());
171             }
172             if (sysEligible && mWallpaperFile.exists()) {
173                 if (sysChanged || !imageStage.exists()) {
174                     if (DEBUG) Slog.v(TAG, "New system wallpaper; copying");
175                     FileUtils.copyFileOrThrow(mWallpaperFile, imageStage);
176                 }
177                 if (DEBUG) Slog.v(TAG, "Storing system wallpaper image");
178                 backupFile(imageStage, data);
179                 prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply();
180             } else {
181                 Slog.w(TAG, "Not backing up wallpaper as one of conditions is not "
182                         + "met: sysEligible = " + sysEligible + " wallpaperFileExists = "
183                         + mWallpaperFile.exists());
184             }
185 
186             // If there's no lock wallpaper, then we have nothing to add to the backup.
187             if (lockGeneration == -1) {
188                 if (lockChanged && lockImageStage.exists()) {
189                     if (DEBUG) Slog.v(TAG, "Removed lock wallpaper; deleting");
190                     lockImageStage.delete();
191                 }
192                 Slog.d(TAG, "No lockscreen wallpaper set, add nothing to backup");
193                 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
194                 return;
195             }
196 
197             // Don't try to store the lock image if we overran our quota last time
198             if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) {
199                 if (lockChanged || !lockImageStage.exists()) {
200                     if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying");
201                     FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage);
202                 }
203                 if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image");
204                 backupFile(lockImageStage, data);
205                 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply();
206             } else {
207                 Slog.w(TAG, "Not backing up lockscreen wallpaper as one of conditions is not "
208                         + "met: lockEligible = " + lockEligible + " hasLockWallpaper = "
209                         + hasLockWallpaper + " lockWallpaperFileExists = "
210                         + mLockWallpaperFile.exists() + " quotaExceeded (last time) = "
211                         + mQuotaExceeded);
212             }
213         } catch (Exception e) {
214             Slog.e(TAG, "Unable to back up wallpaper", e);
215         } finally {
216             // Even if this time we had to back off on attempting to store the lock image
217             // due to exceeding the data quota, try again next time.  This will alternate
218             // between "try both" and "only store the primary image" until either there
219             // is no lock image to store, or the quota is raised, or both fit under the
220             // quota.
221             mQuotaFile.delete();
222         }
223     }
224 
225     @VisibleForTesting
226     // fullBackupFile is final, so we intercept backups here in tests.
backupFile(File file, FullBackupDataOutput data)227     protected void backupFile(File file, FullBackupDataOutput data) {
228         fullBackupFile(file, data);
229     }
230 
231     @Override
onQuotaExceeded(long backupDataBytes, long quotaBytes)232     public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
233         Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')');
234         try (FileOutputStream f = new FileOutputStream(mQuotaFile)) {
235             f.write(0);
236         } catch (Exception e) {
237             Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage());
238         }
239     }
240 
241     // We use the default onRestoreFile() implementation that will recreate our stage files,
242     // then post-process in onRestoreFinished() to apply the new wallpaper.
243     @Override
onRestoreFinished()244     public void onRestoreFinished() {
245         Slog.v(TAG, "onRestoreFinished()");
246         final File filesDir = getFilesDir();
247         final File infoStage = new File(filesDir, INFO_STAGE);
248         final File imageStage = new File (filesDir, IMAGE_STAGE);
249         final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE);
250 
251         // If we restored separate lock imagery, the system wallpaper should be
252         // applied as system-only; but if there's no separate lock image, make
253         // sure to apply the restored system wallpaper as both.
254         final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK);
255 
256         try {
257             // It is valid for the imagery to be absent; it means that we were not permitted
258             // to back up the original image on the source device, or there was no user-supplied
259             // wallpaper image present.
260             restoreFromStage(imageStage, infoStage, "wp", sysWhich);
261             restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK);
262 
263             // And reset to the wallpaper service we should be using
264             ComponentName wpService = parseWallpaperComponent(infoStage, "wp");
265             updateWallpaperComponent(wpService, !lockImageStage.exists());
266         } catch (Exception e) {
267             Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
268         } finally {
269             Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
270             infoStage.delete();
271             imageStage.delete();
272             lockImageStage.delete();
273 
274             SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
275             prefs.edit()
276                     .putInt(SYSTEM_GENERATION, -1)
277                     .putInt(LOCK_GENERATION, -1)
278                     .commit();
279         }
280     }
281 
282     @VisibleForTesting
updateWallpaperComponent(ComponentName wpService, boolean applyToLock)283     void updateWallpaperComponent(ComponentName wpService, boolean applyToLock) throws IOException {
284         if (servicePackageExists(wpService)) {
285             Slog.i(TAG, "Using wallpaper service " + wpService);
286             mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM);
287             if (applyToLock) {
288                 // We have a live wallpaper and no static lock image,
289                 // allow live wallpaper to show "through" on lock screen.
290                 mWm.clear(FLAG_LOCK);
291             }
292         } else {
293             // If we've restored a live wallpaper, but the component doesn't exist,
294             // we should log it as an error so we can easily identify the problem
295             // in reports from users
296             if (wpService != null) {
297                 applyComponentAtInstall(wpService, applyToLock);
298                 Slog.w(TAG, "Wallpaper service " + wpService + " isn't available. "
299                         + " Will try to apply later");
300             }
301         }
302     }
303 
restoreFromStage(File stage, File info, String hintTag, int which)304     private void restoreFromStage(File stage, File info, String hintTag, int which)
305             throws IOException {
306         if (stage.exists()) {
307             // Parse the restored info file to find the crop hint.  Note that this currently
308             // relies on a priori knowledge of the wallpaper info file schema.
309             Rect cropHint = parseCropHint(info, hintTag);
310             if (cropHint != null) {
311                 Slog.i(TAG, "Got restored wallpaper; applying which=" + which
312                         + "; cropHint = " + cropHint);
313                 try (FileInputStream in = new FileInputStream(stage)) {
314                     mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
315                 } finally {} // auto-closes 'in'
316             }
317         } else {
318             Slog.d(TAG, "Restore data doesn't exist for file " + stage.getPath());
319         }
320     }
321 
parseCropHint(File wallpaperInfo, String sectionTag)322     private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
323         Rect cropHint = new Rect();
324         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
325             XmlPullParser parser = Xml.resolvePullParser(stream);
326 
327             int type;
328             do {
329                 type = parser.next();
330                 if (type == XmlPullParser.START_TAG) {
331                     String tag = parser.getName();
332                     if (sectionTag.equals(tag)) {
333                         cropHint.left = getAttributeInt(parser, "cropLeft", 0);
334                         cropHint.top = getAttributeInt(parser, "cropTop", 0);
335                         cropHint.right = getAttributeInt(parser, "cropRight", 0);
336                         cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
337                     }
338                 }
339             } while (type != XmlPullParser.END_DOCUMENT);
340         } catch (Exception e) {
341             // Whoops; can't process the info file at all.  Report failure.
342             Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
343             return null;
344         }
345 
346         return cropHint;
347     }
348 
parseWallpaperComponent(File wallpaperInfo, String sectionTag)349     private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
350         ComponentName name = null;
351         try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
352             final XmlPullParser parser = Xml.resolvePullParser(stream);
353 
354             int type;
355             do {
356                 type = parser.next();
357                 if (type == XmlPullParser.START_TAG) {
358                     String tag = parser.getName();
359                     if (sectionTag.equals(tag)) {
360                         final String parsedName = parser.getAttributeValue(null, "component");
361                         name = (parsedName != null)
362                                 ? ComponentName.unflattenFromString(parsedName)
363                                 : null;
364                         break;
365                     }
366                 }
367             } while (type != XmlPullParser.END_DOCUMENT);
368         } catch (Exception e) {
369             // Whoops; can't process the info file at all.  Report failure.
370             Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
371             return null;
372         }
373         return name;
374     }
375 
getAttributeInt(XmlPullParser parser, String name, int defValue)376     private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
377         final String value = parser.getAttributeValue(null, name);
378         return (value == null) ? defValue : Integer.parseInt(value);
379     }
380 
381     @VisibleForTesting
servicePackageExists(ComponentName comp)382     boolean servicePackageExists(ComponentName comp) {
383         try {
384             if (comp != null) {
385                 final IPackageManager pm = AppGlobals.getPackageManager();
386                 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
387                         0, UserHandle.USER_SYSTEM);
388                 return (info != null);
389             }
390         } catch (RemoteException e) {
391             Slog.e(TAG, "Unable to contact package manager");
392         }
393         return false;
394     }
395 
396     //
397     // Key/value API: abstract, therefore required; but not used
398     //
399 
400     @Override
onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)401     public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
402             ParcelFileDescriptor newState) throws IOException {
403         // Intentionally blank
404     }
405 
406     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)407     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
408             throws IOException {
409         // Intentionally blank
410     }
411 
applyComponentAtInstall(ComponentName componentName, boolean applyToLock)412     private void applyComponentAtInstall(ComponentName componentName, boolean applyToLock) {
413         PackageMonitor packageMonitor = getWallpaperPackageMonitor(componentName, applyToLock);
414         packageMonitor.register(getBaseContext(), null, UserHandle.ALL, true);
415     }
416 
417     @VisibleForTesting
getWallpaperPackageMonitor(ComponentName componentName, boolean applyToLock)418     PackageMonitor getWallpaperPackageMonitor(ComponentName componentName, boolean applyToLock) {
419         return new PackageMonitor() {
420             @Override
421             public void onPackageAdded(String packageName, int uid) {
422                 if (!isDeviceInRestore()) {
423                     // We don't want to reapply the wallpaper outside a restore.
424                     unregister();
425                     return;
426                 }
427 
428                 if (componentName.getPackageName().equals(packageName)) {
429                     Slog.d(TAG, "Applying component " + componentName);
430                     mWm.setWallpaperComponent(componentName);
431                     if (applyToLock) {
432                         try {
433                             mWm.clear(FLAG_LOCK);
434                         } catch (IOException e) {
435                             Slog.w(TAG, "Failed to apply live wallpaper to lock screen: " + e);
436                         }
437                     }
438                     // We're only expecting to restore the wallpaper component once.
439                     unregister();
440                 }
441             }
442         };
443     }
444 
445     @VisibleForTesting
446     boolean isDeviceInRestore() {
447         try {
448             boolean isInSetup = Settings.Secure.getInt(getBaseContext().getContentResolver(),
449                     Settings.Secure.USER_SETUP_COMPLETE) == 0;
450             boolean isInDeferredSetup = Settings.Secure.getInt(getBaseContext()
451                             .getContentResolver(),
452                     Settings.Secure.USER_SETUP_PERSONALIZATION_STATE) ==
453                     Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED;
454             return isInSetup || isInDeferredSetup;
455         } catch (Settings.SettingNotFoundException e) {
456             Slog.w(TAG, "Failed to check if the user is in restore: " + e);
457             return false;
458         }
459     }
460 }