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 }