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 if (!empty.exists()) { 119 FileOutputStream touch = new FileOutputStream(empty); 120 touch.close(); 121 } 122 fullBackupFile(empty, data); 123 124 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 125 final int lastSysGeneration = prefs.getInt(SYSTEM_GENERATION, -1); 126 final int lastLockGeneration = prefs.getInt(LOCK_GENERATION, -1); 127 128 final int sysGeneration = 129 mWm.getWallpaperIdForUser(FLAG_SYSTEM, UserHandle.USER_SYSTEM); 130 final int lockGeneration = 131 mWm.getWallpaperIdForUser(FLAG_LOCK, UserHandle.USER_SYSTEM); 132 final boolean sysChanged = (sysGeneration != lastSysGeneration); 133 final boolean lockChanged = (lockGeneration != lastLockGeneration); 134 135 final boolean sysEligible = mWm.isWallpaperBackupEligible(FLAG_SYSTEM); 136 final boolean lockEligible = mWm.isWallpaperBackupEligible(FLAG_LOCK); 137 138 // There might be a latent lock wallpaper file present but unused: don't 139 // include it in the backup if that's the case. 140 ParcelFileDescriptor lockFd = mWm.getWallpaperFile(FLAG_LOCK, UserHandle.USER_SYSTEM); 141 final boolean hasLockWallpaper = (lockFd != null); 142 IoUtils.closeQuietly(lockFd); 143 144 if (DEBUG) { 145 Slog.v(TAG, "sysGen=" + sysGeneration + " : sysChanged=" + sysChanged); 146 Slog.v(TAG, "lockGen=" + lockGeneration + " : lockChanged=" + lockChanged); 147 Slog.v(TAG, "sysEligble=" + sysEligible); 148 Slog.v(TAG, "lockEligible=" + lockEligible); 149 } 150 151 // only back up the wallpapers if we've been told they're eligible 152 if (mWallpaperInfo.exists()) { 153 if (sysChanged || lockChanged || !infoStage.exists()) { 154 if (DEBUG) Slog.v(TAG, "New wallpaper configuration; copying"); 155 FileUtils.copyFileOrThrow(mWallpaperInfo, infoStage); 156 } 157 if (DEBUG) Slog.v(TAG, "Storing wallpaper metadata"); 158 fullBackupFile(infoStage, data); 159 } 160 if (sysEligible && mWallpaperFile.exists()) { 161 if (sysChanged || !imageStage.exists()) { 162 if (DEBUG) Slog.v(TAG, "New system wallpaper; copying"); 163 FileUtils.copyFileOrThrow(mWallpaperFile, imageStage); 164 } 165 if (DEBUG) Slog.v(TAG, "Storing system wallpaper image"); 166 fullBackupFile(imageStage, data); 167 prefs.edit().putInt(SYSTEM_GENERATION, sysGeneration).apply(); 168 } 169 170 // Don't try to store the lock image if we overran our quota last time 171 if (lockEligible && hasLockWallpaper && mLockWallpaperFile.exists() && !mQuotaExceeded) { 172 if (lockChanged || !lockImageStage.exists()) { 173 if (DEBUG) Slog.v(TAG, "New lock wallpaper; copying"); 174 FileUtils.copyFileOrThrow(mLockWallpaperFile, lockImageStage); 175 } 176 if (DEBUG) Slog.v(TAG, "Storing lock wallpaper image"); 177 fullBackupFile(lockImageStage, data); 178 prefs.edit().putInt(LOCK_GENERATION, lockGeneration).apply(); 179 } 180 } catch (Exception e) { 181 Slog.e(TAG, "Unable to back up wallpaper", e); 182 } finally { 183 // Even if this time we had to back off on attempting to store the lock image 184 // due to exceeding the data quota, try again next time. This will alternate 185 // between "try both" and "only store the primary image" until either there 186 // is no lock image to store, or the quota is raised, or both fit under the 187 // quota. 188 mQuotaFile.delete(); 189 } 190 } 191 192 @Override onQuotaExceeded(long backupDataBytes, long quotaBytes)193 public void onQuotaExceeded(long backupDataBytes, long quotaBytes) { 194 if (DEBUG) { 195 Slog.i(TAG, "Quota exceeded (" + backupDataBytes + " vs " + quotaBytes + ')'); 196 } 197 try (FileOutputStream f = new FileOutputStream(mQuotaFile)) { 198 f.write(0); 199 } catch (Exception e) { 200 Slog.w(TAG, "Unable to record quota-exceeded: " + e.getMessage()); 201 } 202 } 203 204 // We use the default onRestoreFile() implementation that will recreate our stage files, 205 // then post-process in onRestoreFinished() to apply the new wallpaper. 206 @Override onRestoreFinished()207 public void onRestoreFinished() { 208 if (DEBUG) { 209 Slog.v(TAG, "onRestoreFinished()"); 210 } 211 final File filesDir = getFilesDir(); 212 final File infoStage = new File(filesDir, INFO_STAGE); 213 final File imageStage = new File (filesDir, IMAGE_STAGE); 214 final File lockImageStage = new File (filesDir, LOCK_IMAGE_STAGE); 215 216 // If we restored separate lock imagery, the system wallpaper should be 217 // applied as system-only; but if there's no separate lock image, make 218 // sure to apply the restored system wallpaper as both. 219 final int sysWhich = FLAG_SYSTEM | (lockImageStage.exists() ? 0 : FLAG_LOCK); 220 221 try { 222 // It is valid for the imagery to be absent; it means that we were not permitted 223 // to back up the original image on the source device, or there was no user-supplied 224 // wallpaper image present. 225 restoreFromStage(imageStage, infoStage, "wp", sysWhich); 226 restoreFromStage(lockImageStage, infoStage, "kwp", FLAG_LOCK); 227 228 // And reset to the wallpaper service we should be using 229 ComponentName wpService = parseWallpaperComponent(infoStage, "wp"); 230 if (servicePackageExists(wpService)) { 231 if (DEBUG) { 232 Slog.i(TAG, "Using wallpaper service " + wpService); 233 } 234 mWm.setWallpaperComponent(wpService, UserHandle.USER_SYSTEM); 235 if (!lockImageStage.exists()) { 236 // We have a live wallpaper and no static lock image, 237 // allow live wallpaper to show "through" on lock screen. 238 mWm.clear(FLAG_LOCK); 239 } 240 } else { 241 if (DEBUG) { 242 Slog.v(TAG, "Can't use wallpaper service " + wpService); 243 } 244 } 245 } catch (Exception e) { 246 Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage()); 247 } finally { 248 if (DEBUG) { 249 Slog.v(TAG, "Restore finished; clearing backup bookkeeping"); 250 } 251 infoStage.delete(); 252 imageStage.delete(); 253 lockImageStage.delete(); 254 255 SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 256 prefs.edit() 257 .putInt(SYSTEM_GENERATION, -1) 258 .putInt(LOCK_GENERATION, -1) 259 .commit(); 260 } 261 } 262 restoreFromStage(File stage, File info, String hintTag, int which)263 private void restoreFromStage(File stage, File info, String hintTag, int which) 264 throws IOException { 265 if (stage.exists()) { 266 // Parse the restored info file to find the crop hint. Note that this currently 267 // relies on a priori knowledge of the wallpaper info file schema. 268 Rect cropHint = parseCropHint(info, hintTag); 269 if (cropHint != null) { 270 Slog.i(TAG, "Got restored wallpaper; applying which=" + which); 271 if (DEBUG) { 272 Slog.v(TAG, "Restored crop hint " + cropHint); 273 } 274 try (FileInputStream in = new FileInputStream(stage)) { 275 mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which); 276 } finally {} // auto-closes 'in' 277 } 278 } 279 } 280 parseCropHint(File wallpaperInfo, String sectionTag)281 private Rect parseCropHint(File wallpaperInfo, String sectionTag) { 282 Rect cropHint = new Rect(); 283 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { 284 XmlPullParser parser = Xml.newPullParser(); 285 parser.setInput(stream, StandardCharsets.UTF_8.name()); 286 287 int type; 288 do { 289 type = parser.next(); 290 if (type == XmlPullParser.START_TAG) { 291 String tag = parser.getName(); 292 if (sectionTag.equals(tag)) { 293 cropHint.left = getAttributeInt(parser, "cropLeft", 0); 294 cropHint.top = getAttributeInt(parser, "cropTop", 0); 295 cropHint.right = getAttributeInt(parser, "cropRight", 0); 296 cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); 297 } 298 } 299 } while (type != XmlPullParser.END_DOCUMENT); 300 } catch (Exception e) { 301 // Whoops; can't process the info file at all. Report failure. 302 Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage()); 303 return null; 304 } 305 306 return cropHint; 307 } 308 parseWallpaperComponent(File wallpaperInfo, String sectionTag)309 private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) { 310 ComponentName name = null; 311 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { 312 final XmlPullParser parser = Xml.newPullParser(); 313 parser.setInput(stream, StandardCharsets.UTF_8.name()); 314 315 int type; 316 do { 317 type = parser.next(); 318 if (type == XmlPullParser.START_TAG) { 319 String tag = parser.getName(); 320 if (sectionTag.equals(tag)) { 321 final String parsedName = parser.getAttributeValue(null, "component"); 322 name = (parsedName != null) 323 ? ComponentName.unflattenFromString(parsedName) 324 : null; 325 break; 326 } 327 } 328 } while (type != XmlPullParser.END_DOCUMENT); 329 } catch (Exception e) { 330 // Whoops; can't process the info file at all. Report failure. 331 Slog.w(TAG, "Failed to parse restored component: " + e.getMessage()); 332 return null; 333 } 334 return name; 335 } 336 getAttributeInt(XmlPullParser parser, String name, int defValue)337 private int getAttributeInt(XmlPullParser parser, String name, int defValue) { 338 final String value = parser.getAttributeValue(null, name); 339 return (value == null) ? defValue : Integer.parseInt(value); 340 } 341 servicePackageExists(ComponentName comp)342 private boolean servicePackageExists(ComponentName comp) { 343 try { 344 if (comp != null) { 345 final IPackageManager pm = AppGlobals.getPackageManager(); 346 final PackageInfo info = pm.getPackageInfo(comp.getPackageName(), 347 0, UserHandle.USER_SYSTEM); 348 return (info != null); 349 } 350 } catch (RemoteException e) { 351 Slog.e(TAG, "Unable to contact package manager"); 352 } 353 return false; 354 } 355 356 // 357 // Key/value API: abstract, therefore required; but not used 358 // 359 360 @Override onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)361 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 362 ParcelFileDescriptor newState) throws IOException { 363 // Intentionally blank 364 } 365 366 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)367 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 368 throws IOException { 369 // Intentionally blank 370 } 371 }