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 }