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            // 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                if (!lockImageStage.exists()) {
234                    // We have a live wallpaper and no static lock image,
235                    // allow live wallpaper to show "through" on lock screen.
236                    mWm.clear(FLAG_LOCK);
237                }
238            } else {
239                if (DEBUG) {
240                    Slog.v(TAG, "Can't use wallpaper service " + wpService);
241                }
242            }
243        } catch (Exception e) {
244            Slog.e(TAG, "Unable to restore wallpaper: " + e.getMessage());
245        } finally {
246            if (DEBUG) {
247                Slog.v(TAG, "Restore finished; clearing backup bookkeeping");
248            }
249            infoStage.delete();
250            imageStage.delete();
251            lockImageStage.delete();
252
253            SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
254            prefs.edit()
255                    .putInt(SYSTEM_GENERATION, -1)
256                    .putInt(LOCK_GENERATION, -1)
257                    .commit();
258        }
259    }
260
261    private void restoreFromStage(File stage, File info, String hintTag, int which)
262            throws IOException {
263        if (stage.exists()) {
264            // Parse the restored info file to find the crop hint.  Note that this currently
265            // relies on a priori knowledge of the wallpaper info file schema.
266            Rect cropHint = parseCropHint(info, hintTag);
267            if (cropHint != null) {
268                Slog.i(TAG, "Got restored wallpaper; applying which=" + which);
269                if (DEBUG) {
270                    Slog.v(TAG, "Restored crop hint " + cropHint);
271                }
272                try (FileInputStream in = new FileInputStream(stage)) {
273                    mWm.setStream(in, cropHint.isEmpty() ? null : cropHint, true, which);
274                } finally {} // auto-closes 'in'
275            }
276        }
277    }
278
279    private Rect parseCropHint(File wallpaperInfo, String sectionTag) {
280        Rect cropHint = new Rect();
281        try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
282            XmlPullParser parser = Xml.newPullParser();
283            parser.setInput(stream, StandardCharsets.UTF_8.name());
284
285            int type;
286            do {
287                type = parser.next();
288                if (type == XmlPullParser.START_TAG) {
289                    String tag = parser.getName();
290                    if (sectionTag.equals(tag)) {
291                        cropHint.left = getAttributeInt(parser, "cropLeft", 0);
292                        cropHint.top = getAttributeInt(parser, "cropTop", 0);
293                        cropHint.right = getAttributeInt(parser, "cropRight", 0);
294                        cropHint.bottom = getAttributeInt(parser, "cropBottom", 0);
295                    }
296                }
297            } while (type != XmlPullParser.END_DOCUMENT);
298        } catch (Exception e) {
299            // Whoops; can't process the info file at all.  Report failure.
300            Slog.w(TAG, "Failed to parse restored crop: " + e.getMessage());
301            return null;
302        }
303
304        return cropHint;
305    }
306
307    private ComponentName parseWallpaperComponent(File wallpaperInfo, String sectionTag) {
308        ComponentName name = null;
309        try (FileInputStream stream = new FileInputStream(wallpaperInfo)) {
310            final XmlPullParser parser = Xml.newPullParser();
311            parser.setInput(stream, StandardCharsets.UTF_8.name());
312
313            int type;
314            do {
315                type = parser.next();
316                if (type == XmlPullParser.START_TAG) {
317                    String tag = parser.getName();
318                    if (sectionTag.equals(tag)) {
319                        final String parsedName = parser.getAttributeValue(null, "component");
320                        name = (parsedName != null)
321                                ? ComponentName.unflattenFromString(parsedName)
322                                : null;
323                        break;
324                    }
325                }
326            } while (type != XmlPullParser.END_DOCUMENT);
327        } catch (Exception e) {
328            // Whoops; can't process the info file at all.  Report failure.
329            Slog.w(TAG, "Failed to parse restored component: " + e.getMessage());
330            return null;
331        }
332        return name;
333    }
334
335    private int getAttributeInt(XmlPullParser parser, String name, int defValue) {
336        final String value = parser.getAttributeValue(null, name);
337        return (value == null) ? defValue : Integer.parseInt(value);
338    }
339
340    private boolean servicePackageExists(ComponentName comp) {
341        try {
342            if (comp != null) {
343                final IPackageManager pm = AppGlobals.getPackageManager();
344                final PackageInfo info = pm.getPackageInfo(comp.getPackageName(),
345                        0, UserHandle.USER_SYSTEM);
346                return (info != null);
347            }
348        } catch (RemoteException e) {
349            Slog.e(TAG, "Unable to contact package manager");
350        }
351        return false;
352    }
353
354    //
355    // Key/value API: abstract, therefore required; but not used
356    //
357
358    @Override
359    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
360            ParcelFileDescriptor newState) throws IOException {
361        // Intentionally blank
362    }
363
364    @Override
365    public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
366            throws IOException {
367        // Intentionally blank
368    }
369}