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 }