1 /*
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  */
16 package com.android.wallpaper.backup;
17 
18 import android.annotation.SuppressLint;
19 import android.app.WallpaperManager;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.drawable.BitmapDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.os.ParcelFileDescriptor;
31 import android.util.Log;
32 
33 import com.android.wallpaper.asset.BitmapUtils;
34 import com.android.wallpaper.compat.WallpaperManagerCompat;
35 import com.android.wallpaper.module.Injector;
36 import com.android.wallpaper.module.InjectorProvider;
37 import com.android.wallpaper.module.JobSchedulerJobIds;
38 import com.android.wallpaper.module.WallpaperPreferences;
39 import com.android.wallpaper.util.DiskBasedLogger;
40 
41 import java.io.FileInputStream;
42 import java.io.IOException;
43 import java.io.InputStream;
44 
45 import androidx.annotation.Nullable;
46 import androidx.annotation.VisibleForTesting;
47 
48 /**
49  * {@link android.app.job.JobScheduler} job for generating missing hash codes for static wallpapers
50  * on N+ devices.
51  */
52 @SuppressLint("ServiceCast")
53 public class MissingHashCodeGeneratorJobService extends JobService {
54 
55     private static final String TAG = "MissingHashCodeGenerato"; // max 23 characters
56 
57     private Thread mWorkerThread;
58 
schedule(Context context)59     public static void schedule(Context context) {
60         JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
61         JobInfo newJob = new JobInfo.Builder(
62                 JobSchedulerJobIds.JOB_ID_GENERATE_MISSING_HASH_CODES,
63                 new ComponentName(context, MissingHashCodeGeneratorJobService.class))
64                 .setMinimumLatency(0)
65                 .setPersisted(true)
66                 .build();
67         scheduler.schedule(newJob);
68     }
69 
70     @Override
onStartJob(JobParameters jobParameters)71     public boolean onStartJob(JobParameters jobParameters) {
72         Context context = getApplicationContext();
73 
74         // Retrieve WallpaperManager using Context#getSystemService instead of
75         // WallpaperManager#getInstance so it can be mocked out in test.
76         final WallpaperManager wallpaperManager = (WallpaperManager) context.getSystemService(
77                 Context.WALLPAPER_SERVICE);
78 
79         // Generate missing hash codes on a plain worker thread because we need to do some long-running
80         // disk I/O and can call #jobFinished from a background thread.
81         mWorkerThread = new Thread(new Runnable() {
82             @Override
83             public void run() {
84                 Injector injector = InjectorProvider.getInjector();
85                 WallpaperManagerCompat wallpaperManagerCompat = injector.getWallpaperManagerCompat(context);
86                 WallpaperPreferences wallpaperPreferences = injector.getPreferences(context);
87 
88                 boolean isLiveWallpaperSet = wallpaperManager.getWallpaperInfo() != null;
89 
90                 // Generate and set a home wallpaper hash code if there's no live wallpaper set and no hash
91                 // code stored already for the home wallpaper.
92                 if (!isLiveWallpaperSet && wallpaperPreferences.getHomeWallpaperHashCode() == 0) {
93                     wallpaperManager.forgetLoadedWallpaper();
94 
95                     Drawable wallpaperDrawable = wallpaperManagerCompat.getDrawable();
96                     // No work to do if the drawable returned is null due to an underlying platform issue --
97                     // being extra defensive with this check due to instability and variability of underlying
98                     // platform.
99                     if (wallpaperDrawable == null) {
100                         DiskBasedLogger.e(TAG, "WallpaperManager#getDrawable returned null and there's no live "
101                                 + "wallpaper set", context);
102                         jobFinished(jobParameters, false /* needsReschedule */);
103                         return;
104                     }
105 
106                     Bitmap bitmap = ((BitmapDrawable) wallpaperDrawable).getBitmap();
107                     long homeBitmapHash = BitmapUtils.generateHashCode(bitmap);
108 
109                     wallpaperPreferences.setHomeWallpaperHashCode(homeBitmapHash);
110                 }
111 
112                 // Generate and set a lock wallpaper hash code if there's none saved.
113                 if (wallpaperPreferences.getLockWallpaperHashCode() == 0) {
114                     ParcelFileDescriptor parcelFd =
115                             wallpaperManagerCompat.getWallpaperFile(WallpaperManagerCompat.FLAG_LOCK);
116                     boolean isLockWallpaperSet = parcelFd != null;
117 
118                     // Copy the home wallpaper's hash code to lock if there's no distinct lock wallpaper set.
119                     if (!isLockWallpaperSet) {
120                         wallpaperPreferences.setLockWallpaperHashCode(
121                                 wallpaperPreferences.getHomeWallpaperHashCode());
122                         mWorkerThread = null;
123                         jobFinished(jobParameters, false /* needsReschedule */);
124                         return;
125                     }
126 
127                     // Otherwise, generate and set the distinct lock wallpaper image's hash code.
128                     Bitmap lockBitmap = null;
129                     InputStream fileStream = null;
130                     try {
131                         fileStream = new FileInputStream(parcelFd.getFileDescriptor());
132                         lockBitmap = BitmapFactory.decodeStream(fileStream);
133                         parcelFd.close();
134                     } catch (IOException e) {
135                         Log.e(TAG, "IO exception when closing the file descriptor.", e);
136                     } finally {
137                         if (fileStream != null) {
138                             try {
139                                 fileStream.close();
140                             } catch (IOException e) {
141                                 Log.e(TAG, "IO exception when closing input stream for lock screen wallpaper.", e);
142                             }
143                         }
144                     }
145 
146                     if (lockBitmap != null) {
147                         wallpaperPreferences.setLockWallpaperHashCode(BitmapUtils.generateHashCode(lockBitmap));
148                     }
149                     mWorkerThread = null;
150 
151                     jobFinished(jobParameters, false /* needsReschedule */);
152                 }
153             }
154         });
155 
156         mWorkerThread.start();
157 
158         // Return true to indicate that this JobService needs to process work on a separate thread.
159         return true;
160     }
161 
162     @Override
onStopJob(JobParameters jobParameters)163     public boolean onStopJob(JobParameters jobParameters) {
164         // This job has no special execution parameters (i.e., network capability, device idle or
165         // charging), so Android should never call this method to stop the execution of this job early.
166         // Return "false" to indicate that this job should not be rescheduled when it's stopped because
167         // we have to provide an implementation of this method.
168         return false;
169     }
170 
171     @Nullable
172     @VisibleForTesting
getWorkerThread()173   /* package */ Thread getWorkerThread() {
174         return mWorkerThread;
175     }
176 }
177