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