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 ((sysEligible || lockEligible) && 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 fullBackupFile(infoStage, data); 156 } 157 if (sysEligible && mWallpaperFile.exists()) { 158 if (sysChanged || !imageStage.exists()) { 159 if (DEBUG) Slog.v(TAG, "New system wallpaper; copying"); 160 FileUtils.copyFileOrThrow(mWallpaperFile, imageStage); 161 } 162 fullBackupFile(imageStage, data); 163 prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply(); 164 } 165 166 // Don't try to store the lock image if we overran our quota last time 167 if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) { 168 if (lockChanged || !lockImageStage.exists()) { 169 if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying"); 170 FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage); 171 } 172 fullBackupFile(lockImageStage, data); 173 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply(); 174 } 175 } catch (Exception e) { 176 Slog.e(TAG, "Unable to back up wallpaper", e); 177 } finally { 178 // Even if this time we had to back off on attempting to store the lock image 179 // due to exceeding the data quota, try again next time. This will alternate 180 // between "try both" and "only store the primary image" until either there 181 // is no lock image to store, or the quota is raised, or both fit under the 182 // quota. 183 mQuotaFile.delete(); 184 } 185 } 186 187 @Override onQuotaExceeded(long backupDataBytes, long quotaBytes)188 public void onQuotaExceeded(long backupDataBytes, long quotaBytes) { 189 if (DEBUG) { 190 Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')'); 191 } 192 try (FileOutputStream f = new FileOutputStream(mQuotaFile)) { 193 f.write(0); 194 } catch (Exception e) { 195 Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage()); 196 } 197 } 198 199 // We use the default onRestoreFile() implementation that will recreate our stage files, 200 // then post-process in onRestoreFinished() to apply the new wallpaper. 201 @Override onRestoreFinished()202 public void onRestoreFinished() { 203 if (DEBUG) { 204 Slog.v(TAG, "onRestoreFinished()"); 205 } 206 final File filesDir = getFilesDir(); 207 final File infoStage = new File(filesDir, INFO_STAGE); 208 final File imageStage = new File (filesDir, IMAGE_STAGE); 209 final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE); 210 211 // If we restored separate lock imagery, the system wallpaper should be 212 // applied as system-only; but if there's no separate lock image, make 213 // sure to apply the restored system wallpaper as both. 214 final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK); 215 216 try { 217 // First off, revert to the factory state 218 mWm.clear(FLAG_SYSTEM | FLAG_LOCK); 219 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 } else { 234 if (DEBUG) { 235 Slog.v(TAG, "Can't use wallpaper service " + wpService); 236 } 237 } 238 } catch (Exception e) { 239 Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage()); 240 } finally { 241 if (DEBUG) { 242 Slog.v(TAG, "Restore finished; clearing backup bookkeeping"); 243 } 244 infoStage.delete(); 245 imageStage.delete(); 246 lockImageStage.delete(); 247 248 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 249 prefs.edit() 250 .putInt(SYSTEM_GENERATION, -1) 251 .putInt(LOCK_GENERATION, -1) 252 .commit(); 253 } 254 } 255 restoreFromStage(File stage, File info, String hintTag, int which)256 private void restoreFromStage(File stage, File info, String hintTag, int which) 257 throws IOException { 258 if (stage.exists()) { 259 // 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) { 263 Slog.i(TAG, "Got restored wallpaper; applying which=" + which); 264 if (DEBUG) { 265 Slog.v(TAG, "Restored crop hint " + cropHint); 266 } 267 try (FileInputStream in = new FileInputStream(stage)) { 268 mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which); 269 } finally {} // auto-closes 'in' 270 } 271 } 272 } 273 parseCropHint(File wallpaperInfo, String sectionTag)274 private Rect parseCropHint(File wallpaperInfo, String sectionTag) { 275 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(); 285 if (sectionTag.equals(tag)) { 286 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. 295 Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage()); 296 return null; 297 } 298 299 return cropHint; 300 } 301 parseWallpaperComponent(File wallpaperInfo, String sectionTag)302 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 getAttributeInt(XmlPullParser parser, String name, int defValue)330 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 servicePackageExists(ComponentName comp)335 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 349 // 350 // Key/value API: abstract, therefore required; but not used 351 // 352 353 @Override onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)354 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 355 ParcelFileDescriptor newState) throws IOException { 356 // Intentionally blank 357 } 358 359 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)360 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 361 throws IOException { 362 // Intentionally blank 363 } 364 }