blob: 4df0e710ce5f335c04778571e0926daf54b2b57f [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
2 * Copyright (C) 2017 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 */
16package com.android.wallpaper.module;
17
18import android.annotation.SuppressLint;
19import android.app.Activity;
20import android.app.WallpaperManager;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Bitmap;
24import android.graphics.Bitmap.CompressFormat;
25import android.graphics.BitmapFactory;
26import android.graphics.Point;
27import android.graphics.Rect;
28import android.graphics.drawable.BitmapDrawable;
29import android.os.AsyncTask;
30import android.os.ParcelFileDescriptor;
31import android.support.annotation.Nullable;
32import android.util.Log;
33import android.view.Display;
34import android.view.WindowManager;
35
36import com.android.wallpaper.asset.Asset;
37import com.android.wallpaper.asset.Asset.BitmapReceiver;
38import com.android.wallpaper.asset.Asset.DimensionsReceiver;
39import com.android.wallpaper.asset.BitmapUtils;
40import com.android.wallpaper.asset.StreamableAsset;
41import com.android.wallpaper.asset.StreamableAsset.StreamReceiver;
42import com.android.wallpaper.compat.BuildCompat;
43import com.android.wallpaper.compat.WallpaperManagerCompat;
44import com.android.wallpaper.model.WallpaperInfo;
45import com.android.wallpaper.module.BitmapCropper.Callback;
46import com.android.wallpaper.module.RotatingWallpaperComponentChecker.RotatingWallpaperComponent;
47import com.android.wallpaper.util.BitmapTransformer;
48import com.android.wallpaper.util.DiskBasedLogger;
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -070049import com.android.wallpaper.util.FileMover;
Jon Miranda16ea1b12017-12-12 14:52:48 -080050import com.android.wallpaper.util.ScreenSizeCalculator;
51
52import java.io.ByteArrayInputStream;
53import java.io.ByteArrayOutputStream;
54import java.io.File;
55import java.io.FileInputStream;
56import java.io.FileNotFoundException;
57import java.io.FileOutputStream;
58import java.io.IOException;
59import java.io.InputStream;
60import java.util.List;
61
62/**
63 * Concrete implementation of WallpaperPersister which actually sets wallpapers to the system via
64 * the WallpaperManager.
65 */
66public class DefaultWallpaperPersister implements WallpaperPersister {
67
68 private static final int DEFAULT_COMPRESS_QUALITY = 100;
69 private static final String TAG = "WallpaperPersister";
70
71 private final Context mAppContext; // The application's context.
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -070072 // Context that accesses files in device protected storage
73 private final Context mDeviceProtectedContext;
Jon Miranda16ea1b12017-12-12 14:52:48 -080074 private final WallpaperManager mWallpaperManager;
75 private final WallpaperManagerCompat mWallpaperManagerCompat;
76 private final WallpaperPreferences mWallpaperPreferences;
77 private final RotatingWallpaperComponentChecker mRotatingWallpaperComponentChecker;
78 private final WallpaperChangedNotifier mWallpaperChangedNotifier;
79
80 private WallpaperInfo mWallpaperInfoInPreview;
81
82 @SuppressLint("ServiceCast")
83 public DefaultWallpaperPersister(Context context) {
84 mAppContext = context.getApplicationContext();
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -070085 mDeviceProtectedContext = mAppContext.createDeviceProtectedStorageContext();
Jon Miranda16ea1b12017-12-12 14:52:48 -080086 // Retrieve WallpaperManager using Context#getSystemService instead of
87 // WallpaperManager#getInstance so it can be mocked out in test.
88 Injector injector = InjectorProvider.getInjector();
89 mWallpaperManager = (WallpaperManager) context.getSystemService(Context.WALLPAPER_SERVICE);
90 mWallpaperManagerCompat = injector.getWallpaperManagerCompat(context);
91 mWallpaperPreferences = injector.getPreferences(context);
92 mRotatingWallpaperComponentChecker = injector.getRotatingWallpaperComponentChecker();
93 mWallpaperChangedNotifier = WallpaperChangedNotifier.getInstance();
94 }
95
96 @Override
97 public void setIndividualWallpaper(final WallpaperInfo wallpaper, Asset asset,
98 @Nullable Rect cropRect, float scale, @Destination final int destination,
99 final SetWallpaperCallback callback) {
100 // Set wallpaper without downscaling directly from an input stream if there's no crop rect
101 // specified by the caller and the asset is streamable.
102 if (cropRect == null && asset instanceof StreamableAsset) {
103 ((StreamableAsset) asset).fetchInputStream(new StreamReceiver() {
104 @Override
105 public void onInputStreamOpened(@Nullable InputStream inputStream) {
106 if (inputStream == null) {
107 callback.onError(null /* throwable */);
108 return;
109 }
110 setIndividualWallpaper(wallpaper, inputStream, destination, callback);
111 }
112 });
113 return;
114 }
115
116 // If no crop rect is specified but the wallpaper asset is not streamable, then fall back to
117 // using the device's display size.
118 if (cropRect == null) {
119 Display display = ((WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE))
120 .getDefaultDisplay();
121 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display);
122 asset.decodeBitmap(screenSize.x, screenSize.y, new BitmapReceiver() {
123 @Override
124 public void onBitmapDecoded(@Nullable Bitmap bitmap) {
125 if (bitmap == null) {
126 callback.onError(null /* throwable */);
127 return;
128 }
129 setIndividualWallpaper(wallpaper, bitmap, destination, callback);
130 }
131 });
132 return;
133 }
134
135 BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper();
136 bitmapCropper.cropAndScaleBitmap(asset, scale, cropRect, new Callback() {
137 @Override
138 public void onBitmapCropped(Bitmap croppedBitmap) {
139 setIndividualWallpaper(wallpaper, croppedBitmap, destination, callback);
140 }
141
142 @Override
143 public void onError(@Nullable Throwable e) {
144 callback.onError(e);
145 }
146 });
147 }
148
149 @Override
150 public void setIndividualWallpaperWithPosition(Activity activity, WallpaperInfo wallpaper,
151 @WallpaperPosition int wallpaperPosition, SetWallpaperCallback callback) {
152 Display display = ((WindowManager) mAppContext.getSystemService(Context.WINDOW_SERVICE))
153 .getDefaultDisplay();
154 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(display);
155
156 Asset asset = wallpaper.getAsset(activity);
157 asset.decodeRawDimensions(activity, new DimensionsReceiver() {
158 @Override
159 public void onDimensionsDecoded(@Nullable Point dimensions) {
160 if (dimensions == null) {
161 callback.onError(null);
162 return;
163 }
164
165 switch (wallpaperPosition) {
166 // Crop out screen-sized center portion of the source image if it's larger than the screen
167 // in both dimensions. Otherwise, decode the entire bitmap and fill the space around it to
168 // fill a new screen-sized bitmap with plain black pixels.
169 case WALLPAPER_POSITION_CENTER:
170 setIndividualWallpaperWithCenterPosition(
171 wallpaper, asset, dimensions, screenSize, callback);
172 break;
173
174 // Crop out a screen-size portion of the source image and set the bitmap region.
175 case WALLPAPER_POSITION_CENTER_CROP:
176 setIndividualWallpaperWithCenterCropPosition(
177 wallpaper, asset, dimensions, screenSize, callback);
178 break;
179
180 // Decode full bitmap sized for screen and stretch it to fill the screen dimensions.
181 case WALLPAPER_POSITION_STRETCH:
182 asset.decodeBitmap(screenSize.x, screenSize.y, new BitmapReceiver() {
183 @Override
184 public void onBitmapDecoded(@Nullable Bitmap bitmap) {
185 setIndividualWallpaperStretch(wallpaper, bitmap, screenSize /* stretchSize */,
186 WallpaperPersister.DEST_BOTH, callback);
187 }
188 });
189 break;
190
191 default:
192 Log.e(TAG, "Unsupported wallpaper position option specified: " + wallpaperPosition);
193 callback.onError(null);
194 }
195 }
196 });
197 }
198
199 /**
200 * Sets an individual wallpaper to both home + lock static wallpaper destinations with a center
201 * wallpaper position.
202 *
203 * @param wallpaper The wallpaper model object representing the wallpaper to be set.
204 * @param asset The wallpaper asset that should be used to set a wallpaper.
205 * @param dimensions Raw dimensions of the wallpaper asset.
206 * @param screenSize Dimensions of the device screen.
207 * @param callback Callback used to notify original caller of wallpaper set operation result.
208 */
209 private void setIndividualWallpaperWithCenterPosition(WallpaperInfo wallpaper, Asset asset,
210 Point dimensions, Point screenSize, SetWallpaperCallback callback) {
211 if (dimensions.x >= screenSize.x && dimensions.y >= screenSize.y) {
212 Rect cropRect = new Rect(
213 (dimensions.x - screenSize.x) / 2,
214 (dimensions.y - screenSize.y) / 2,
215 dimensions.x - ((dimensions.x - screenSize.x) / 2),
216 dimensions.y - ((dimensions.y - screenSize.y) / 2));
217 asset.decodeBitmapRegion(cropRect, screenSize.x, screenSize.y, new BitmapReceiver() {
218 @Override
219 public void onBitmapDecoded(@Nullable Bitmap bitmap) {
220 setIndividualWallpaper(wallpaper, bitmap, WallpaperPersister.DEST_BOTH, callback);
221 }
222 });
223 } else {
224 // Decode the full bitmap and pass with the screen size as a fill rect.
225 asset.decodeBitmap(dimensions.x, dimensions.y, new BitmapReceiver() {
226 @Override
227 public void onBitmapDecoded(@Nullable Bitmap bitmap) {
228 if (bitmap == null) {
229 callback.onError(null);
230 return;
231 }
232
233 setIndividualWallpaperFill(wallpaper, bitmap, screenSize /* fillSize */,
234 WallpaperPersister.DEST_BOTH, callback);
235 }
236 });
237 }
238 }
239
240 /**
241 * Sets an individual wallpaper to both home + lock static wallpaper destinations with a center
242 * cropped wallpaper position.
243 *
244 * @param wallpaper The wallpaper model object representing the wallpaper to be set.
245 * @param asset The wallpaper asset that should be used to set a wallpaper.
246 * @param dimensions Raw dimensions of the wallpaper asset.
247 * @param screenSize Dimensions of the device screen.
248 * @param callback Callback used to notify original caller of wallpaper set operation result.
249 */
250 private void setIndividualWallpaperWithCenterCropPosition(WallpaperInfo wallpaper, Asset asset,
251 Point dimensions, Point screenSize, SetWallpaperCallback callback) {
252 float scale = Math.max((float) screenSize.x / dimensions.x,
253 (float) screenSize.y / dimensions.y);
254
255 int scaledImageWidth = (int) (dimensions.x * scale);
256 int scaledImageHeight = (int) (dimensions.y * scale);
257
258 // Crop rect is in post-scale units.
259 Rect cropRect = new Rect(
260 (scaledImageWidth - screenSize.x) / 2,
261 (scaledImageHeight - screenSize.y) / 2,
262 scaledImageWidth - ((scaledImageWidth - screenSize.x) / 2),
263 scaledImageHeight - (((scaledImageHeight - screenSize.y) / 2)));
264
265 setIndividualWallpaper(
266 wallpaper, asset, cropRect, scale, WallpaperPersister.DEST_BOTH, callback);
267 }
268
269 /**
270 * Sets a static individual wallpaper to the system via the WallpaperManager.
271 *
272 * @param wallpaper Wallpaper model object.
273 * @param croppedBitmap Bitmap representing the individual wallpaper image.
274 * @param destination The destination - where to set the wallpaper to.
275 * @param callback Called once the wallpaper was set or if an error occurred.
276 */
277 private void setIndividualWallpaper(WallpaperInfo wallpaper, Bitmap croppedBitmap,
278 @Destination int destination, SetWallpaperCallback callback) {
279 SetWallpaperTask setWallpaperTask =
280 new SetWallpaperTask(wallpaper, croppedBitmap, destination, callback);
281 setWallpaperTask.execute();
282 }
283
284 /**
285 * Sets a static individual wallpaper to the system via the WallpaperManager with a fill option.
286 *
287 * @param wallpaper Wallpaper model object.
288 * @param croppedBitmap Bitmap representing the individual wallpaper image.
289 * @param fillSize Specifies the final bitmap size that should be set to WallpaperManager. This
290 * final bitmap will show the visible area of the provided bitmap after applying a mask with
291 * black background the source bitmap and centering. There may be black borders around the
292 * original bitmap if it's smaller than the fillSize in one or both dimensions.
293 * @param destination The destination - where to set the wallpaper to.
294 * @param callback Called once the wallpaper was set or if an error occurred.
295 */
296 private void setIndividualWallpaperFill(WallpaperInfo wallpaper, Bitmap croppedBitmap,
297 Point fillSize, @Destination int destination, SetWallpaperCallback callback) {
298 SetWallpaperTask setWallpaperTask =
299 new SetWallpaperTask(wallpaper, croppedBitmap, destination, callback);
300 setWallpaperTask.setFillSize(fillSize);
301 setWallpaperTask.execute();
302 }
303
304 /**
305 * Sets a static individual wallpaper to the system via the WallpaperManager with a stretch
306 * option.
307 *
308 * @param wallpaper Wallpaper model object.
309 * @param croppedBitmap Bitmap representing the individual wallpaper image.
310 * @param stretchSize Specifies the final size to which the the bitmap should be stretched prior
311 * to being set to the device.
312 * @param destination The destination - where to set the wallpaper to.
313 * @param callback Called once the wallpaper was set or if an error occurred.
314 */
315 private void setIndividualWallpaperStretch(WallpaperInfo wallpaper, Bitmap croppedBitmap,
316 Point stretchSize, @Destination int destination, SetWallpaperCallback callback) {
317 SetWallpaperTask setWallpaperTask =
318 new SetWallpaperTask(wallpaper, croppedBitmap, destination, callback);
319 setWallpaperTask.setStretchSize(stretchSize);
320 setWallpaperTask.execute();
321 }
322
323 /**
324 * Sets a static individual wallpaper stream to the system via the WallpaperManager.
325 *
326 * @param wallpaper Wallpaper model object.
327 * @param inputStream JPEG or PNG stream of wallpaper image's bytes.
328 * @param destination The destination - where to set the wallpaper to.
329 * @param callback Called once the wallpaper was set or if an error occurred.
330 */
331 private void setIndividualWallpaper(WallpaperInfo wallpaper, InputStream inputStream,
332 @Destination int destination, SetWallpaperCallback callback) {
333 SetWallpaperTask setWallpaperTask =
334 new SetWallpaperTask(wallpaper, inputStream, destination, callback);
335 setWallpaperTask.execute();
336 }
337
338 @Override
339 public boolean setWallpaperInRotation(Bitmap wallpaperBitmap, List<String> attributions,
340 String actionUrl, String collectionId) {
341 @RotatingWallpaperComponent int rotatingWallpaperComponent = mRotatingWallpaperComponentChecker
342 .getCurrentRotatingWallpaperComponent(mAppContext);
343
344 switch (rotatingWallpaperComponent) {
345 case RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC:
346 return setWallpaperInRotationStatic(wallpaperBitmap, attributions, actionUrl, collectionId);
347 case RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_LIVE:
348 return setWallpaperInRotationLive(wallpaperBitmap, attributions, actionUrl, collectionId);
349 default:
350 Log.e(TAG, "Unknown rotating wallpaper component: " + rotatingWallpaperComponent);
351 return false;
352 }
353 }
354
355 @Override
356 public int setWallpaperBitmapInNextRotation(Bitmap wallpaperBitmap) {
357 @RotatingWallpaperComponent int rotatingWallpaperComponent = mRotatingWallpaperComponentChecker
358 .getNextRotatingWallpaperComponent(mAppContext);
359
360 switch (rotatingWallpaperComponent) {
361 case RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC:
362 return setWallpaperBitmapInRotationStatic(wallpaperBitmap);
363 case RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_LIVE:
364 boolean isSuccess = setWallpaperBitmapInRotationLive(wallpaperBitmap, true /* isPreview */);
365 return isSuccess ? 1 : 0;
366 default:
367 Log.e(TAG, "Unknown rotating wallpaper component: " + rotatingWallpaperComponent);
368 return 0;
369 }
370 }
371
372 @Override
373 public boolean finalizeWallpaperForNextRotation(List<String> attributions, String actionUrl,
374 String collectionId, int wallpaperId) {
375 @RotatingWallpaperComponent int rotatingWallpaperComponent = mRotatingWallpaperComponentChecker
376 .getNextRotatingWallpaperComponent(mAppContext);
377 return finalizeWallpaperForRotatingComponent(attributions, actionUrl, collectionId,
378 wallpaperId, rotatingWallpaperComponent);
379 }
380
381 /**
382 * Sets wallpaper image and attributions when a static wallpaper is responsible for presenting the
383 * current "daily wallpaper".
384 */
385 private boolean setWallpaperInRotationStatic(Bitmap wallpaperBitmap, List<String> attributions,
386 String actionUrl, String collectionId) {
387 final int wallpaperId = setWallpaperBitmapInRotationStatic(wallpaperBitmap);
388
389 if (wallpaperId == 0) {
390 return false;
391 }
392
393 return finalizeWallpaperForRotatingComponent(attributions, actionUrl, collectionId,
394 wallpaperId, RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC);
395 }
396
397 /**
398 * Finalizes wallpaper metadata by persisting them to SharedPreferences and finalizes the
399 * wallpaper image for live rotating components by copying the "preview" image to the "final"
400 * image file location.
401 *
402 * @return Whether the operation was successful.
403 */
404 private boolean finalizeWallpaperForRotatingComponent(List<String> attributions,
405 String actionUrl, String collectionId, int wallpaperId,
406 @RotatingWallpaperComponent int rotatingWallpaperComponent) {
407 mWallpaperPreferences.clearHomeWallpaperMetadata();
408
409 boolean isLockWallpaperSet = isSeparateLockScreenWallpaperSet();
410
411 // Persist wallpaper IDs if the rotating wallpaper component is static and this device is
412 // running Android N or later.
413 if (rotatingWallpaperComponent
414 == RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC) {
415 if (BuildCompat.isAtLeastN()) {
416 mWallpaperPreferences.setHomeWallpaperManagerId(wallpaperId);
417
418 // Only copy over wallpaper ID to lock wallpaper if no explicit lock wallpaper is set (so
419 // metadata isn't lost if a user explicitly sets a home-only wallpaper).
420 if (!isLockWallpaperSet) {
421 mWallpaperPreferences.setLockWallpaperId(wallpaperId);
422 }
423 } else { // Pre-N but using static component
424 // Compute bitmap hash code after setting the wallpaper because JPEG compression has likely
425 // changed many pixels' color values. Forget the previously loaded wallpaper bitmap so that
426 // WallpaperManager doesn't return the old wallpaper drawable.
427 mWallpaperManager.forgetLoadedWallpaper();
428 Bitmap bitmap = ((BitmapDrawable) mWallpaperManagerCompat.getDrawable()).getBitmap();
429 long bitmapHash = BitmapUtils.generateHashCode(bitmap);
430
431 mWallpaperPreferences.setHomeWallpaperHashCode(bitmapHash);
432 }
433 } else { // Live wallpaper rotating component.
434
435 // Copy "preview" JPEG to "rotating" JPEG if the preview file exists.
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -0700436 File rotatingWallpaper;
437 try {
438 rotatingWallpaper = moveToDeviceProtectedStorage(
439 NoBackupImageWallpaper.PREVIEW_WALLPAPER_FILE_PATH,
440 NoBackupImageWallpaper.ROTATING_WALLPAPER_FILE_PATH);
441 } catch (Exception e) {
442 DiskBasedLogger.e(
443 TAG,
444 "Unable to move preview to final file for rotating wallpaper " +
445 "file (exception)" + e.toString(),
446 mAppContext);
447 return false;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800448 }
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -0700449 if (rotatingWallpaper == null) {
450 rotatingWallpaper = mDeviceProtectedContext.getFileStreamPath(
451 NoBackupImageWallpaper.ROTATING_WALLPAPER_FILE_PATH);
452 }
Jon Miranda16ea1b12017-12-12 14:52:48 -0800453 try {
454 FileInputStream fis = new FileInputStream(rotatingWallpaper.getAbsolutePath());
455 Bitmap bitmap = BitmapFactory.decodeStream(fis);
456 fis.close();
457
458 if (bitmap != null) {
459 long bitmapHash = BitmapUtils.generateHashCode(bitmap);
460 mWallpaperPreferences.setHomeWallpaperHashCode(bitmapHash);
461 } else {
462 Log.e(TAG, "Unable to decode rotating wallpaper file");
463 return false;
464 }
465 } catch (FileNotFoundException e) {
466 Log.e(TAG, "Rotating wallpaper file not found at path: "
467 + rotatingWallpaper.getAbsolutePath());
468 e.printStackTrace();
469 return false;
470 } catch (IOException e) {
471 Log.e(TAG, "IOException when closing FileInputStream " + e);
472 return false;
473 }
474
475 mWallpaperChangedNotifier.notifyWallpaperChanged();
476
477 // Send a broadcast to {@link RotatingWallpaperChangedReceiver} in the :live_wallpaper
478 // process so the currently displayed wallpaper updates.
479 notifyLiveWallpaperBitmapChanged();
480 }
481
482 mWallpaperPreferences.setHomeWallpaperAttributions(attributions);
483 mWallpaperPreferences.setHomeWallpaperActionUrl(actionUrl);
484 // Only set base image URL for static Backdrop images, not for rotation.
485 mWallpaperPreferences.setHomeWallpaperBaseImageUrl(null);
486 mWallpaperPreferences.setHomeWallpaperCollectionId(collectionId);
487
488 // Set metadata to lock screen also when the rotating wallpaper is a static one so if user sets
489 // a home screen-only wallpaper later, these attributions will still be available.
490 if (rotatingWallpaperComponent
491 == RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC
492 && !isLockWallpaperSet) {
493 mWallpaperPreferences.setLockWallpaperAttributions(attributions);
494 mWallpaperPreferences.setLockWallpaperActionUrl(actionUrl);
495 mWallpaperPreferences.setLockWallpaperCollectionId(collectionId);
496 }
497
498 return true;
499 }
500
501 /**
502 * Sets wallpaper image and attributions when a live wallpaper is responsible for presenting the
503 * current "daily wallpaper".
504 */
505 private boolean setWallpaperInRotationLive(Bitmap wallpaperBitmap, List<String> attributions,
506 String actionUrl, String collectionId) {
507
508 synchronized (RotatingWallpaperLockProvider.getInstance()) {
509 if (!setWallpaperBitmapInRotationLive(wallpaperBitmap, false /* isPreview */)) {
510 return false;
511 }
512
513 return finalizeWallpaperForRotatingComponent(attributions, actionUrl, collectionId,
514 0 /* wallpaperId */, RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_LIVE);
515 }
516 }
517
518 /**
519 * Sets a wallpaper in rotation as a static wallpaper to the {@link WallpaperManager} with the
520 * option allowBackup=false to save user data.
521 *
522 * @return wallpaper ID for the wallpaper bitmap.
523 */
524 private int setWallpaperBitmapInRotationStatic(Bitmap wallpaperBitmap) {
525 // Set wallpaper to home-only instead of both home and lock if there's a distinct lock-only
526 // static wallpaper set so we don't override the lock wallpaper.
527 boolean isLockWallpaperSet = isSeparateLockScreenWallpaperSet();
528
529 int whichWallpaper = (isLockWallpaperSet)
530 ? WallpaperManagerCompat.FLAG_SYSTEM
531 : WallpaperManagerCompat.FLAG_SYSTEM | WallpaperManagerCompat.FLAG_LOCK;
532
533 return setBitmapToWallpaperManagerCompat(wallpaperBitmap, false /* allowBackup */,
534 whichWallpaper);
535 }
536
537 /**
538 * Sets a wallpaper in rotation as a live wallpaper. Writes wallpaper bitmap to a file in internal
539 * storage and sends a broadcast to the live wallpaper notifying it that rotating wallpaper image
540 * data changed.
541 *
542 * @return whether the set wallpaper operation was successful.
543 */
544 private boolean setWallpaperBitmapInRotationLive(Bitmap wallpaperBitmap, boolean isPreview) {
545 File pendingFile;
546 try {
547 pendingFile = File.createTempFile("rotating_pending", ".jpg", mAppContext.getFilesDir());
548 } catch (IOException e) {
549 Log.e(TAG, "Unable to create temp file for rotating wallpaper");
550 return false;
551 }
552
Jon Miranda16ea1b12017-12-12 14:52:48 -0800553 FileOutputStream fos;
554 try {
555 fos = mAppContext.openFileOutput(pendingFile.getName(), Context.MODE_PRIVATE);
556 } catch (FileNotFoundException e) {
557 Log.e(TAG, "Unable to open file output stream for pending rotating wallpaper file");
558 return false;
559 }
560
561 boolean compressedSuccessfully =
562 wallpaperBitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, fos);
563
564 // Close the file stream.
565 try {
566 fos.flush();
567 fos.close();
568 } catch (IOException e) {
569 Log.e(TAG, "Unable to close FileOutputStream for pending rotating wallpaper file"
570 + " (compress succeeded");
571 return false;
572 }
573
574 if (compressedSuccessfully) {
575 // Compressing/writing to disk succeeded, so move the pending file to the final location.
576 try {
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -0700577 if (isPreview) {
578 if (!pendingFile.renameTo(mAppContext.getFileStreamPath(
579 NoBackupImageWallpaper.PREVIEW_WALLPAPER_FILE_PATH))) {
580 return false;
581 }
582 } else {
583 moveToDeviceProtectedStorage(pendingFile.getName(),
584 NoBackupImageWallpaper.ROTATING_WALLPAPER_FILE_PATH);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800585 }
586 } catch (Exception e) {
587 Log.e(TAG, "Unable to rename pending to final file for rotating wallpaper file"
588 + " (exception)" + e.toString());
589 return false;
590 }
591 } else {
592 Log.e(TAG, "Unable to compress the wallpaper bitmap");
593
594 // Delete the pending file since compressing/writing the image to disk failed.
595 try {
596 pendingFile.delete();
597 } catch (SecurityException e) {
598 Log.e(TAG, "Unable to delete pending rotating wallpaper file");
599 return false;
600 }
601
602 return false;
603 }
604
605 mWallpaperChangedNotifier.notifyWallpaperChanged();
606
607 // Send a broadcast to {@link RotatingWallpaperChangedReceiver} in the :live_wallpaper
608 // process so the currently displayed wallpaper updates if the live wallpaper is set to the
609 // device.
610 notifyLiveWallpaperBitmapChanged();
611
612 return true;
613 }
614
615 /**
616 * Sets a wallpaper bitmap to the {@link WallpaperManagerCompat}.
617 *
618 * @return an integer wallpaper ID. This is an actual wallpaper ID on N and later versions of
619 * Android, otherwise on pre-N versions of Android will return a positive integer when the
620 * operation was successful and zero if the operation encountered an error.
621 */
622 private int setBitmapToWallpaperManagerCompat(Bitmap wallpaperBitmap, boolean allowBackup,
623 int whichWallpaper) {
624 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream();
625 if (wallpaperBitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
626 try {
627 byte[] outByteArray = tmpOut.toByteArray();
628 return mWallpaperManagerCompat.setStream(
629 new ByteArrayInputStream(outByteArray),
630 null /* visibleCropHint */,
631 allowBackup,
632 whichWallpaper);
633 } catch (IOException e) {
634 Log.e(TAG, "unable to write stream to wallpaper manager");
635 return 0;
636 }
637 } else {
638 Log.e(TAG, "unable to compress wallpaper");
639 try {
640 return mWallpaperManagerCompat.setBitmap(
641 wallpaperBitmap,
642 null /* visibleCropHint */,
643 allowBackup,
644 whichWallpaper);
645 } catch (IOException e) {
646 Log.e(TAG, "unable to set wallpaper");
647 return 0;
648 }
649 }
650 }
651
652 private int setStreamToWallpaperManagerCompat(InputStream inputStream, boolean allowBackup,
653 int whichWallpaper) {
654 try {
655 return mWallpaperManagerCompat.setStream(inputStream, null, allowBackup, whichWallpaper);
656 } catch (IOException e) {
657 return 0;
658 }
659 }
660
661 @Override
662 public void setWallpaperInfoInPreview(WallpaperInfo wallpaper) {
663 mWallpaperInfoInPreview = wallpaper;
664 }
665
666 @Override
667 public void onLiveWallpaperSet() {
668 android.app.WallpaperInfo currentWallpaperComponent = mWallpaperManager.getWallpaperInfo();
669 android.app.WallpaperInfo previewedWallpaperComponent =
670 mWallpaperInfoInPreview.getWallpaperComponent();
671
672 // If there is no live wallpaper set on the WallpaperManager or it doesn't match the
673 // WallpaperInfo which was last previewed, then do nothing and nullify last previewed wallpaper.
674 if (currentWallpaperComponent == null || previewedWallpaperComponent == null
675 || !currentWallpaperComponent.getPackageName()
676 .equals(previewedWallpaperComponent.getPackageName())) {
677 mWallpaperInfoInPreview = null;
678 return;
679 }
680
681 setLiveWallpaperMetadata();
682 }
683
684 /**
685 * Returns whether a separate lock-screen (static) wallpaper is set to the WallpaperManager.
686 */
687 private boolean isSeparateLockScreenWallpaperSet() {
688 ParcelFileDescriptor lockWallpaperFile =
689 mWallpaperManagerCompat.getWallpaperFile(WallpaperManagerCompat.FLAG_LOCK);
690
691 boolean isLockWallpaperSet = false;
692
693 if (lockWallpaperFile != null) {
694 isLockWallpaperSet = true;
695
696 try {
697 lockWallpaperFile.close();
698 } catch (IOException e) {
699 Log.e(TAG, "Unable to close PFD for lock wallpaper", e);
700 }
701 }
702
703 return isLockWallpaperSet;
704 }
705
706 /**
707 * Sets the live wallpaper's metadata on SharedPreferences.
708 */
709 private void setLiveWallpaperMetadata() {
710 android.app.WallpaperInfo previewedWallpaperComponent =
711 mWallpaperInfoInPreview.getWallpaperComponent();
712
713 mWallpaperPreferences.clearHomeWallpaperMetadata();
714 // NOTE: We explicitly do not also clear the lock wallpaper metadata. Since the user may have
715 // set the live wallpaper on the home screen only, we leave the lock wallpaper metadata intact.
716 // If the user has set the live wallpaper for both home and lock screens, then the
717 // WallpaperRefresher will pick up on that and update the preferences later.
718 mWallpaperPreferences
719 .setHomeWallpaperAttributions(mWallpaperInfoInPreview.getAttributions(mAppContext));
720 mWallpaperPreferences.setHomeWallpaperPackageName(
721 previewedWallpaperComponent.getPackageName());
722 mWallpaperPreferences.setHomeWallpaperCollectionId(
723 mWallpaperInfoInPreview.getCollectionId(mAppContext));
724 mWallpaperPreferences.setWallpaperPresentationMode(
725 WallpaperPreferences.PRESENTATION_MODE_STATIC);
726 mWallpaperPreferences.clearDailyRotations();
727 }
728
729 /**
730 * Notifies the :live_wallpaper process that the contents of the rotating live wallpaper bitmap
731 * changed.
732 */
733 private void notifyLiveWallpaperBitmapChanged() {
Jon Mirandade0b69e2018-03-20 16:03:18 -0700734 Intent intent = new Intent(mAppContext.getPackageName()
735 + NoBackupImageWallpaper.ACTION_ROTATING_WALLPAPER_CHANGED);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800736 // Handled by a runtime-registered receiver in NoBackupImageWallpaper.
737 intent.setPackage(mAppContext.getPackageName());
738 mAppContext.sendBroadcast(intent);
739 }
740
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -0700741 /**
742 * Moves a file from the app's files directory to the device content protected storage
743 * directory.
744 * @param srcFileName Name of the source file (just the name, no path). It's expected to be
745 * located in {@link Context#getFilesDir()} for {@link #mAppContext}
746 * @param dstFileName Name of the destination file (just the name, no path), which will be
747 * located in {@link Context#getFilesDir()}
748 * for {@link #mDeviceProtectedContext}
749 * @return a {@link File} corresponding to the moved file in its new location, or null if
750 * nothing was moved (because srcFileName didn't exist).
751 */
752 @Nullable
753 private File moveToDeviceProtectedStorage(String srcFileName, String dstFileName)
754 throws IOException {
755 return FileMover.moveFileBetweenContexts(mAppContext, srcFileName, mDeviceProtectedContext,
756 dstFileName);
757 }
758
Jon Miranda16ea1b12017-12-12 14:52:48 -0800759 private class SetWallpaperTask extends AsyncTask<Void, Void, Boolean> {
760
761 private final WallpaperInfo mWallpaper;
762 @Destination
763 private final int mDestination;
764 private final WallpaperPersister.SetWallpaperCallback mCallback;
765
766 private Bitmap mBitmap;
767 private InputStream mInputStream;
768
769 /**
770 * Optional parameters for applying a post-decoding fill or stretch transformation.
771 */
772 @Nullable
773 private Point mFillSize;
774 @Nullable
775 private Point mStretchSize;
776
777 SetWallpaperTask(WallpaperInfo wallpaper, Bitmap bitmap, @Destination int destination,
778 WallpaperPersister.SetWallpaperCallback callback) {
779 super();
780 mWallpaper = wallpaper;
781 mBitmap = bitmap;
782 mDestination = destination;
783 mCallback = callback;
784 }
785
786 /**
787 * Constructor for SetWallpaperTask which takes an InputStream instead of a bitmap. The task
788 * will close the InputStream once it is done with it.
789 */
790 SetWallpaperTask(WallpaperInfo wallpaper, InputStream stream,
791 @Destination int destination, WallpaperPersister.SetWallpaperCallback callback) {
792 mWallpaper = wallpaper;
793 mInputStream = stream;
794 mDestination = destination;
795 mCallback = callback;
796 }
797
798 void setFillSize(Point fillSize) {
799 if (mStretchSize != null) {
800 throw new IllegalArgumentException("Can't pass a fill size option if a stretch size is "
801 + "already set.");
802 }
803 mFillSize = fillSize;
804 }
805
806 void setStretchSize(Point stretchSize) {
807 if (mFillSize != null) {
808 throw new IllegalArgumentException("Can't pass a stretch size option if a fill size is "
809 + "already set.");
810 }
811 mStretchSize = stretchSize;
812 }
813
814 @Override
815 protected Boolean doInBackground(Void... unused) {
816 int whichWallpaper;
817 if (mDestination == DEST_HOME_SCREEN) {
818 whichWallpaper = WallpaperManagerCompat.FLAG_SYSTEM;
819 } else if (mDestination == DEST_LOCK_SCREEN) {
820 whichWallpaper = WallpaperManagerCompat.FLAG_LOCK;
821 } else { // DEST_BOTH
822 whichWallpaper = WallpaperManagerCompat.FLAG_SYSTEM
823 | WallpaperManagerCompat.FLAG_LOCK;
824 }
825
826 // NOTE: The rotating wallpaper component must be determined here, _before_ actually setting
827 // the bitmap/stream on WallpaperManagerCompat, to ensure that the
828 // RotatingWallpaperComponentChecker is doing its check while rotation is still enabled.
829 // E.g., if "live wallpaper" is the component, then it needs to check while live wallpaper is
830 // still set as the active wallpaper on the device. Otherwise, the checker would see a static
831 // wallpaper is currently set and it would return the wrong value.
832 @RotatingWallpaperComponent int currentRotatingWallpaperComponent =
833 mRotatingWallpaperComponentChecker.getCurrentRotatingWallpaperComponent(mAppContext);
834
835 boolean wasLockWallpaperSet = LockWallpaperStatusChecker.isLockWallpaperSet(mAppContext);
836
837 boolean allowBackup = mWallpaper.getBackupPermission() == WallpaperInfo.BACKUP_ALLOWED;
838 final int wallpaperId;
839 if (mBitmap != null) {
840 // Apply fill or stretch transformations on mBitmap if necessary.
841 if (mFillSize != null) {
842 mBitmap = BitmapTransformer.applyFillTransformation(mBitmap, mFillSize);
843 }
844 if (mStretchSize != null) {
845 mBitmap = Bitmap.createScaledBitmap(mBitmap, mStretchSize.x, mStretchSize.y, true);
846 }
847
848 wallpaperId = setBitmapToWallpaperManagerCompat(mBitmap, allowBackup, whichWallpaper);
849 } else if (mInputStream != null) {
850 wallpaperId = setStreamToWallpaperManagerCompat(mInputStream, allowBackup, whichWallpaper);
851 } else {
852 Log.e(TAG, "Both the wallpaper bitmap and input stream are null so we're unable to set any "
853 + "kind of wallpaper here.");
854 wallpaperId = 0;
855 }
856
857 if (wallpaperId > 0) {
858 if (mDestination == DEST_HOME_SCREEN
859 && mWallpaperPreferences.getWallpaperPresentationMode()
860 == WallpaperPreferences.PRESENTATION_MODE_ROTATING
861 && !wasLockWallpaperSet
862 && BuildCompat.isAtLeastN()) {
863 copyRotatingWallpaperToLock(currentRotatingWallpaperComponent);
864 }
865 setImageWallpaperMetadata(mDestination, wallpaperId);
866 return true;
867 } else {
868 return false;
869 }
870 }
871
872 @Override
873 protected void onPostExecute(Boolean isSuccess) {
874 if (mInputStream != null) {
875 try {
876 mInputStream.close();
877 } catch (IOException e) {
878 Log.e(TAG, "Failed to close input stream " + e);
879 mCallback.onError(e /* throwable */);
880 return;
881 }
882 }
883
884 if (isSuccess) {
885 mCallback.onSuccess();
886 mWallpaperChangedNotifier.notifyWallpaperChanged();
887 } else {
888 mCallback.onError(null /* throwable */);
889 }
890 }
891
892 /**
893 * Copies home wallpaper metadata to lock, and if rotation was enabled with a live wallpaper
894 * previously, then copies over the rotating wallpaper image to the WallpaperManager also.
895 * <p>
896 * Used to accommodate the case where a user had gone from a home+lock daily rotation to
897 * selecting a static wallpaper on home-only. The image and metadata that was previously
898 * rotating is now copied to the lock screen.
899 *
900 * @param currentRotatingWallpaperComponent The component in which rotating wallpapers were
901 * presented.
902 */
903 private void copyRotatingWallpaperToLock(
904 @RotatingWallpaperComponent int currentRotatingWallpaperComponent) {
905
906 mWallpaperPreferences.setLockWallpaperAttributions(
907 mWallpaperPreferences.getHomeWallpaperAttributions());
908 mWallpaperPreferences.setLockWallpaperActionUrl(
909 mWallpaperPreferences.getHomeWallpaperActionUrl());
910 mWallpaperPreferences.setLockWallpaperCollectionId(
911 mWallpaperPreferences.getHomeWallpaperCollectionId());
912
913 // Set the lock wallpaper ID to what Android set it to, following its having
914 // copied the system wallpaper over to the lock screen when we changed from
915 // "both" to distinct system and lock screen wallpapers.
916 if (currentRotatingWallpaperComponent
917 == RotatingWallpaperComponentChecker.ROTATING_WALLPAPER_COMPONENT_STATIC) {
918 mWallpaperPreferences.setLockWallpaperId(
919 mWallpaperManagerCompat.getWallpaperId(WallpaperManagerCompat.FLAG_LOCK));
920 } else {
Jon Miranda16ea1b12017-12-12 14:52:48 -0800921 try {
Santiago Etchebehere5a7f1dd2018-04-03 15:01:29 -0700922 FileInputStream fileInputStream = mDeviceProtectedContext.openFileInput(
923 NoBackupImageWallpaper.ROTATING_WALLPAPER_FILE_PATH);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800924 int lockWallpaperId = setStreamToWallpaperManagerCompat(
925 fileInputStream, false /* allowBackup */, WallpaperManagerCompat.FLAG_LOCK);
926 fileInputStream.close();
927 mWallpaperPreferences.setLockWallpaperId(lockWallpaperId);
928 } catch (FileNotFoundException e) {
929 Log.e(TAG, "Couldn't copy over previously rotating wallpaper to lock screen.");
930 } catch (IOException e) {
931 Log.e(TAG, "IOException when closing the file input stream " + e);
932 }
933 }
934 }
935
936 /**
937 * Sets the image wallpaper's metadata on SharedPreferences. This method is called after the set
938 * wallpaper operation is successful.
939 *
940 * @param destination Which destination of wallpaper the metadata corresponds to (home screen,
941 * lock screen, or both).
942 * @param wallpaperId The ID of the static wallpaper returned by WallpaperManager, which on N
943 * and later versions of Android uniquely identifies a wallpaper image.
944 */
945 private void setImageWallpaperMetadata(@Destination int destination, int wallpaperId) {
946 if (destination == DEST_HOME_SCREEN || destination == DEST_BOTH) {
947 mWallpaperPreferences.clearHomeWallpaperMetadata();
948 setImageWallpaperHomeMetadata(wallpaperId);
949
950 // Reset presentation mode to STATIC if an individual wallpaper is set to the home screen
951 // because rotation always affects at least the home screen.
952 mWallpaperPreferences.setWallpaperPresentationMode(
953 WallpaperPreferences.PRESENTATION_MODE_STATIC);
954 }
955
956 if (destination == DEST_LOCK_SCREEN || destination == DEST_BOTH) {
957 mWallpaperPreferences.clearLockWallpaperMetadata();
958 setImageWallpaperLockMetadata(wallpaperId);
959 }
960
961 mWallpaperPreferences.clearDailyRotations();
962 }
963
964 private void setImageWallpaperHomeMetadata(int homeWallpaperId) {
965 if (BuildCompat.isAtLeastN()) {
966 mWallpaperPreferences.setHomeWallpaperManagerId(homeWallpaperId);
967 }
968
969 // Compute bitmap hash code after setting the wallpaper because JPEG compression has likely
970 // changed many pixels' color values. Forget the previously loaded wallpaper bitmap so that
971 // WallpaperManager doesn't return the old wallpaper drawable. Do this on N+ devices in
972 // addition to saving the wallpaper ID for the purpose of backup & restore.
973 mWallpaperManager.forgetLoadedWallpaper();
974 mBitmap = ((BitmapDrawable) mWallpaperManagerCompat.getDrawable()).getBitmap();
975 long bitmapHash = BitmapUtils.generateHashCode(mBitmap);
976
977 mWallpaperPreferences.setHomeWallpaperHashCode(bitmapHash);
978
979 mWallpaperPreferences.setHomeWallpaperAttributions(mWallpaper.getAttributions(mAppContext));
980 mWallpaperPreferences.setHomeWallpaperBaseImageUrl(mWallpaper.getBaseImageUrl());
981 mWallpaperPreferences.setHomeWallpaperActionUrl(mWallpaper.getActionUrl(mAppContext));
982 mWallpaperPreferences.setHomeWallpaperCollectionId(mWallpaper.getCollectionId(mAppContext));
983 mWallpaperPreferences.setHomeWallpaperRemoteId(mWallpaper.getWallpaperId());
984 }
985
986 private void setImageWallpaperLockMetadata(int lockWallpaperId) {
987 mWallpaperPreferences.setLockWallpaperId(lockWallpaperId);
988 mWallpaperPreferences.setLockWallpaperAttributions(mWallpaper.getAttributions(mAppContext));
989 mWallpaperPreferences.setLockWallpaperActionUrl(mWallpaper.getActionUrl(mAppContext));
990 mWallpaperPreferences.setLockWallpaperCollectionId(mWallpaper.getCollectionId(mAppContext));
991
992 // Save the lock wallpaper image's hash code as well for the sake of backup & restore because
993 // WallpaperManager-generated IDs are specific to a physical device and cannot be used to
994 // identify a wallpaper image on another device after restore is complete.
995 saveLockWallpaperHashCode();
996 }
997
998 private void saveLockWallpaperHashCode() {
999 Bitmap lockBitmap = null;
1000
1001 ParcelFileDescriptor parcelFd = mWallpaperManagerCompat.getWallpaperFile(
1002 WallpaperManagerCompat.FLAG_LOCK);
1003
1004 if (parcelFd == null) {
1005 return;
1006 }
1007
1008 InputStream fileStream = null;
1009 try {
1010 fileStream = new FileInputStream(parcelFd.getFileDescriptor());
1011 lockBitmap = BitmapFactory.decodeStream(fileStream);
1012 parcelFd.close();
1013 } catch (IOException e) {
1014 Log.e(TAG, "IO exception when closing the file descriptor.");
1015 } finally {
1016 if (fileStream != null) {
1017 try {
1018 fileStream.close();
1019 } catch (IOException e) {
1020 Log.e(TAG, "IO exception when closing the input stream for the lock screen WP.");
1021 }
1022 }
1023 }
1024
1025 if (lockBitmap != null) {
1026 long bitmapHash = BitmapUtils.generateHashCode(lockBitmap);
1027 mWallpaperPreferences.setLockWallpaperHashCode(bitmapHash);
1028 }
1029 }
1030 }
1031}