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 android.app.WallpaperManager; 20 import android.app.backup.BackupAgent; 21 import android.app.backup.BackupDataInput; 22 import android.app.backup.BackupDataOutput; 23 import android.app.backup.FullBackupDataOutput; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.os.Environment; 27 import android.os.ParcelFileDescriptor; 28 import android.os.UserHandle; 29 import android.system.Os; 30 import android.util.Slog; 31 import android.util.Xml; 32 33 import org.xmlpull.v1.XmlPullParser; 34 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.nio.charset.StandardCharsets; 40 41 public class WallpaperBackupAgent extends BackupAgent { 42 private static final String TAG = "WallpaperBackup"; 43 private static final boolean DEBUG = false; 44 45 // NB: must be kept in sync with WallpaperManagerService but has no 46 // compile-time visibility. 47 48 // Target filenames within the system's wallpaper directory 49 static final String WALLPAPER = "wallpaper_orig"; 50 static final String WALLPAPER_INFO = "wallpaper_info.xml"; 51 52 // Names of our local-data stage files/links 53 static final String IMAGE_STAGE = "wallpaper-stage"; 54 static final String INFO_STAGE = "wallpaper-info-stage"; 55 static final String EMPTY_SENTINEL = "empty"; 56 57 private File mWallpaperInfo; // wallpaper metadata file 58 private File mWallpaperFile; // primary wallpaper image file 59 60 private WallpaperManager mWm; 61 62 @Override onCreate()63 public void onCreate() { 64 if (DEBUG) { 65 Slog.v(TAG, "onCreate()"); 66 } 67 68 File wallpaperDir = Environment.getUserSystemDirectory(UserHandle.USER_SYSTEM); 69 mWallpaperInfo = new File(wallpaperDir, WALLPAPER_INFO); 70 mWallpaperFile = new File(wallpaperDir, WALLPAPER); 71 mWm = (WallpaperManager) getSystemService(Context.WALLPAPER_SERVICE); 72 } 73 74 @Override onFullBackup(FullBackupDataOutput data)75 public void onFullBackup(FullBackupDataOutput data) throws IOException { 76 // To avoid data duplication and disk churn, use links as the stage. 77 final File filesDir = getFilesDir(); 78 final File infoStage = new File(filesDir, INFO_STAGE); 79 final File imageStage = new File (filesDir, IMAGE_STAGE); 80 final File empty = new File (filesDir, EMPTY_SENTINEL); 81 82 try { 83 // We always back up this 'empty' file to ensure that the absence of 84 // storable wallpaper imagery still produces a non-empty backup data 85 // stream, otherwise it'd simply be ignored in preflight. 86 FileOutputStream touch = new FileOutputStream(empty); 87 touch.close(); 88 fullBackupFile(empty, data); 89 90 // only back up the wallpaper if we've been told it's allowed 91 if (mWm.isWallpaperBackupEligible()) { 92 if (DEBUG) { 93 Slog.v(TAG, "Wallpaper is backup-eligible; linking & writing"); 94 } 95 96 // In case of prior muddled state 97 infoStage.delete(); 98 imageStage.delete(); 99 100 Os.link(mWallpaperInfo.getCanonicalPath(), infoStage.getCanonicalPath()); 101 fullBackupFile(infoStage, data); 102 Os.link(mWallpaperFile.getCanonicalPath(), imageStage.getCanonicalPath()); 103 fullBackupFile(imageStage, data); 104 } else { 105 if (DEBUG) { 106 Slog.v(TAG, "Wallpaper not backup-eligible; writing no data"); 107 } 108 } 109 } catch (Exception e) { 110 Slog.e(TAG, "Unable to back up wallpaper: " + e.getMessage()); 111 } finally { 112 if (DEBUG) { 113 Slog.v(TAG, "Removing backup stage links"); 114 } 115 infoStage.delete(); 116 imageStage.delete(); 117 } 118 } 119 120 // We use the default onRestoreFile() implementation that will recreate our stage files, 121 // then post-process in onRestoreFinished() to apply the new wallpaper. 122 @Override onRestoreFinished()123 public void onRestoreFinished() { 124 if (DEBUG) { 125 Slog.v(TAG, "onRestoreFinished()"); 126 } 127 final File infoStage = new File(getFilesDir(), INFO_STAGE); 128 final File imageStage = new File (getFilesDir(), IMAGE_STAGE); 129 130 try { 131 // It is valid for the imagery to be absent; it means that we were not permitted 132 // to back up the original image on the source device. 133 if (imageStage.exists()) { 134 if (DEBUG) { 135 Slog.v(TAG, "Got restored wallpaper; applying"); 136 } 137 138 // Parse the restored info file to find the crop hint. Note that this currently 139 // relies on a priori knowledge of the wallpaper info file schema. 140 Rect cropHint = parseCropHint(infoStage); 141 if (cropHint != null) { 142 if (DEBUG) { 143 Slog.v(TAG, "Restored crop hint " + cropHint + "; now writing data"); 144 } 145 WallpaperManager wm = getSystemService(WallpaperManager.class); 146 try (FileInputStream in = new FileInputStream(imageStage)) { 147 wm.setStream(in, cropHint, true, WallpaperManager.FLAG_SYSTEM); 148 } finally {} // auto-closes 'in' 149 } 150 } 151 } catch (Exception e) { 152 Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage()); 153 } finally { 154 if (DEBUG) { 155 Slog.v(TAG, "Removing restore stage files"); 156 } 157 infoStage.delete(); 158 imageStage.delete(); 159 } 160 } 161 parseCropHint(File wallpaperInfo)162 private Rect parseCropHint(File wallpaperInfo) { 163 Rect cropHint = new Rect(); 164 try (FileInputStream stream = new FileInputStream(wallpaperInfo)) { 165 XmlPullParser parser = Xml.newPullParser(); 166 parser.setInput(stream, StandardCharsets.UTF_8.name()); 167 168 int type; 169 do { 170 type = parser.next(); 171 if (type == XmlPullParser.START_TAG) { 172 String tag = parser.getName(); 173 if ("wp".equals(tag)) { 174 cropHint.left = getAttributeInt(parser, "cropLeft", 0); 175 cropHint.top = getAttributeInt(parser, "cropTop", 0); 176 cropHint.right = getAttributeInt(parser, "cropRight", 0); 177 cropHint.bottom = getAttributeInt(parser, "cropBottom", 0); 178 } 179 } 180 } while (type != XmlPullParser.END_DOCUMENT); 181 } catch (Exception e) { 182 // Whoops; can't process the info file at all. Report failure. 183 Slog.w(TAG, "Failed to parse restored metadata: " + e.getMessage()); 184 return null; 185 } 186 187 return cropHint; 188 } 189 getAttributeInt(XmlPullParser parser, String name, int defValue)190 private int getAttributeInt(XmlPullParser parser, String name, int defValue) { 191 final String value = parser.getAttributeValue(null, name); 192 return (value == null) ? defValue : Integer.parseInt(value); 193 } 194 195 // 196 // Key/value API: abstract, therefore required; but not used 197 // 198 199 @Override onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)200 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 201 ParcelFileDescriptor newState) throws IOException { 202 // Intentionally blank 203 } 204 205 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)206 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 207 throws IOException { 208 // Intentionally blank 209 } 210 }