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 17package com.android.wallpaperbackup; 18 19import static android.app.WallpaperManager.FLAG_LOCK; 20import static android.app.WallpaperManager.FLAG_SYSTEM; 21 22import android.app.AppGlobals; 23import android.app.WallpaperManager; 24import android.app.backup.BackupAgent; 25import android.app.backup.BackupDataInput; 26import android.app.backup.BackupDataOutput; 27import android.app.backup.FullBackupDataOutput; 28import android.content.ComponentName; 29import android.content.Context; 30import android.content.SharedPreferences; 31import android.content.pm.IPackageManager; 32import android.content.pm.PackageInfo; 33import android.graphics.Rect; 34import android.os.Environment; 35import android.os.FileUtils; 36import android.os.ParcelFileDescriptor; 37import android.os.RemoteException; 38import android.os.UserHandle; 39import android.util.Slog; 40import android.util.Xml; 41 42import libcore.io.IoUtils; 43 44import org.xmlpull.v1.XmlPullParser; 45 46import java.io.File; 47import java.io.FileInputStream; 48import java.io.FileOutputStream; 49import java.io.IOException; 50import java.nio.charset.StandardCharsets; 51 52public 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 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 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 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 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 // First off, revert to the factory state 221 mWm.clear(FLAG_SYSTEM | FLAG_LOCK); 222 223 // It is valid for the imagery to be absent; it means that we were not permitted 224 // to back up the original image on the source device, or there was no user-supplied 225 // wallpaper image present. 226 restoreFromStage(imageStage, infoStage, "wp", sysWhich); 227 restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK); 228 229 // And reset to the wallpaper service we should be using 230 ComponentName wpService = parseWallpaperComponent(infoStage, "wp"); 231 if (servicePackageExists(wpService)) { 232 if (DEBUG) { 233 Slog.i(TAG, "Using wallpaper service " + wpService); 234 } 235 mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM); 236 } else { 237 if (DEBUG) { 238 Slog.v(TAG, "Can't use wallpaper service " + wpService); 239 } 240 } 241 } catch (Exception e) { 242 Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage()); 243 } finally { 244 if (DEBUG) { 245 Slog.v(TAG, "Restore finished; clearing backup bookkeeping"); 246 } 247 infoStage.delete(); 248 imageStage.delete(); 249 lockImageStage.delete(); 250 251 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 252 prefs.edit() 253 .putInt(SYSTEM_GENERATION, -1) 254 .putInt(LOCK_GENERATION, -1) 255 .commit(); 256 } 257 } 258 259 private void restoreFromStage(File stage, File info, String hintTag, int which) 260 throws IOException { 261 if (stage.exists()) { 262 // Parse the restored info file to find the crop hint. Note that this currently 263 // relies on a priori knowledge of the wallpaper info file schema. 264 Rect cropHint = parseCropHint(info, hintTag); 265 if (cropHint != null) { 266 Slog.i(TAG, "Got restored wallpaper; applying which=" + which); 267 if (DEBUG) { 268 Slog.v(TAG, "Restored crop hint " + cropHint); 269 } 270 try (FileInputStream in = new FileInputStream(stage)) { 271 mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which); 272 } finally {} // auto-closes 'in' 273 } 274 } 275 } 276 277 private Rect parseCropHint(File wallpaperInfo, String sectionTag) { 278 Rect cropHint = new Rect(); 279 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { 280 XmlPullParser parser = Xml.newPullParser(); 281 parser.setInput(stream, StandardCharsets.UTF_8.name()); 282 283 int type; 284 do { 285 type = parser.next(); 286 if (type == XmlPullParser.START_TAG) { 287 String tag = parser.getName(); 288 if (sectionTag.equals(tag)) { 289 cropHint.left = getAttributeInt(parser, "cropLeft", 0); 290 cropHint.top = getAttributeInt(parser, "cropTop", 0); 291 cropHint.right = getAttributeInt(parser, "cropRight", 0); 292 cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); 293 } 294 } 295 } while (type != XmlPullParser.END_DOCUMENT); 296 } catch (Exception e) { 297 // Whoops; can't process the info file at all. Report failure. 298 Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage()); 299 return null; 300 } 301 302 return cropHint; 303 } 304 305 private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) { 306 ComponentName name = null; 307 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { 308 final XmlPullParser parser = Xml.newPullParser(); 309 parser.setInput(stream, StandardCharsets.UTF_8.name()); 310 311 int type; 312 do { 313 type = parser.next(); 314 if (type == XmlPullParser.START_TAG) { 315 String tag = parser.getName(); 316 if (sectionTag.equals(tag)) { 317 final String parsedName = parser.getAttributeValue(null, "component"); 318 name = (parsedName != null) 319 ? ComponentName.unflattenFromString(parsedName) 320 : null; 321 break; 322 } 323 } 324 } while (type != XmlPullParser.END_DOCUMENT); 325 } catch (Exception e) { 326 // Whoops; can't process the info file at all. Report failure. 327 Slog.w(TAG, "Failed to parse restored component: " + e.getMessage()); 328 return null; 329 } 330 return name; 331 } 332 333 private int getAttributeInt(XmlPullParser parser, String name, int defValue) { 334 final String value = parser.getAttributeValue(null, name); 335 return (value == null) ? defValue : Integer.parseInt(value); 336 } 337 338 private boolean servicePackageExists(ComponentName comp) { 339 try { 340 if (comp != null) { 341 final IPackageManager pm = AppGlobals.getPackageManager(); 342 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(), 343 0, UserHandle.USER_SYSTEM); 344 return (info != null); 345 } 346 } catch (RemoteException e) { 347 Slog.e(TAG, "Unable to contact package manager"); 348 } 349 return false; 350 } 351 352 // 353 // Key/value API: abstract, therefore required; but not used 354 // 355 356 @Override 357 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 358 ParcelFileDescriptor newState) throws IOException { 359 // Intentionally blank 360 } 361 362 @Override 363 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 364 throws IOException { 365 // Intentionally blank 366 } 367}