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.launcher3.uioverrides.dynamicui;
17 
18 import static android.app.WallpaperManager.FLAG_SYSTEM;
19 
20 import static com.android.launcher3.Utilities.getDevicePrefs;
21 
22 import android.app.WallpaperInfo;
23 import android.app.WallpaperManager;
24 import android.app.job.JobInfo;
25 import android.app.job.JobParameters;
26 import android.app.job.JobScheduler;
27 import android.app.job.JobService;
28 import android.content.BroadcastReceiver;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.content.pm.PackageManager;
34 import android.content.pm.PermissionInfo;
35 import android.graphics.Bitmap;
36 import android.graphics.BitmapFactory;
37 import android.graphics.BitmapRegionDecoder;
38 import android.graphics.Canvas;
39 import android.graphics.Rect;
40 import android.graphics.drawable.Drawable;
41 import android.os.Handler;
42 import android.os.HandlerThread;
43 import android.os.ParcelFileDescriptor;
44 import android.util.Log;
45 import android.util.Pair;
46 
47 import com.android.launcher3.icons.ColorExtractor;
48 
49 import java.io.IOException;
50 import java.util.ArrayList;
51 
52 import androidx.annotation.Nullable;
53 
54 public class WallpaperManagerCompatVL extends WallpaperManagerCompat {
55 
56     private static final String TAG = "WMCompatVL";
57 
58     private static final String VERSION_PREFIX = "1,";
59     private static final String KEY_COLORS = "wallpaper_parsed_colors";
60     private static final String ACTION_EXTRACTION_COMPLETE =
61             "com.android.launcher3.uioverrides.dynamicui.WallpaperManagerCompatVL.EXTRACTION_COMPLETE";
62 
63     public static final int WALLPAPER_COMPAT_JOB_ID = 1;
64 
65     private final ArrayList<OnColorsChangedListenerCompat> mListeners = new ArrayList<>();
66 
67     private final Context mContext;
68     private WallpaperColorsCompat mColorsCompat;
69 
WallpaperManagerCompatVL(Context context)70     WallpaperManagerCompatVL(Context context) {
71         mContext = context;
72 
73         String colors = getDevicePrefs(mContext).getString(KEY_COLORS, "");
74         int wallpaperId = -1;
75         if (colors.startsWith(VERSION_PREFIX)) {
76             Pair<Integer, WallpaperColorsCompat> storedValue = parseValue(colors);
77             wallpaperId = storedValue.first;
78             mColorsCompat = storedValue.second;
79         }
80 
81         if (wallpaperId == -1 || wallpaperId != getWallpaperId(context)) {
82             reloadColors();
83         }
84         context.registerReceiver(new BroadcastReceiver() {
85             @Override
86             public void onReceive(Context context, Intent intent) {
87                 reloadColors();
88             }
89         }, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED));
90 
91         // Register a receiver for results
92         String permission = null;
93         // Find a permission which only we can use.
94         try {
95             for (PermissionInfo info : context.getPackageManager().getPackageInfo(
96                     context.getPackageName(),
97                     PackageManager.GET_PERMISSIONS).permissions) {
98                 if ((info.protectionLevel & PermissionInfo.PROTECTION_SIGNATURE) != 0) {
99                     permission = info.name;
100                 }
101             }
102         } catch (PackageManager.NameNotFoundException e) {
103             // Something went wrong. ignore
104             Log.d(TAG, "Unable to get permission info", e);
105         }
106         mContext.registerReceiver(new BroadcastReceiver() {
107             @Override
108             public void onReceive(Context context, Intent intent) {
109                 handleResult(intent.getStringExtra(KEY_COLORS));
110             }
111         }, new IntentFilter(ACTION_EXTRACTION_COMPLETE), permission, new Handler());
112     }
113 
114     @Nullable
115     @Override
getWallpaperColors(int which)116     public WallpaperColorsCompat getWallpaperColors(int which) {
117         return which == FLAG_SYSTEM ? mColorsCompat : null;
118     }
119 
120     @Override
addOnColorsChangedListener(OnColorsChangedListenerCompat listener)121     public void addOnColorsChangedListener(OnColorsChangedListenerCompat listener) {
122         mListeners.add(listener);
123     }
124 
reloadColors()125     private void reloadColors() {
126         JobInfo job = new JobInfo.Builder(WALLPAPER_COMPAT_JOB_ID,
127                 new ComponentName(mContext, ColorExtractionService.class))
128                 .setMinimumLatency(0).build();
129         ((JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(job);
130     }
131 
handleResult(String result)132     private void handleResult(String result) {
133         getDevicePrefs(mContext).edit().putString(KEY_COLORS, result).apply();
134         mColorsCompat = parseValue(result).second;
135         for (OnColorsChangedListenerCompat listener : mListeners) {
136             listener.onColorsChanged(mColorsCompat, FLAG_SYSTEM);
137         }
138     }
139 
getWallpaperId(Context context)140     private static final int getWallpaperId(Context context) {
141         return context.getSystemService(WallpaperManager.class).getWallpaperId(FLAG_SYSTEM);
142     }
143 
144     /**
145      * Parses the stored value and returns the wallpaper id and wallpaper colors.
146      */
parseValue(String value)147     private static Pair<Integer, WallpaperColorsCompat> parseValue(String value) {
148         String[] parts = value.split(",");
149         Integer wallpaperId = Integer.parseInt(parts[1]);
150         if (parts.length == 2) {
151             // There is no wallpaper color info present, eg when live wallpaper has no preview.
152             return Pair.create(wallpaperId, null);
153         }
154 
155         int primary = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
156         int secondary = parts.length > 3 ? Integer.parseInt(parts[3]) : 0;
157         int tertiary = parts.length > 4 ? Integer.parseInt(parts[4]) : 0;
158 
159         return Pair.create(wallpaperId, new WallpaperColorsCompat(primary, secondary, tertiary,
160                 0 /* hints */));
161     }
162 
163     /**
164      * Intent service to handle color extraction
165      */
166     public static class ColorExtractionService extends JobService implements Runnable {
167         private static final int MAX_WALLPAPER_EXTRACTION_AREA = 112 * 112;
168 
169         private HandlerThread mWorkerThread;
170         private Handler mWorkerHandler;
171         private ColorExtractor mColorExtractor;
172 
173         @Override
onCreate()174         public void onCreate() {
175             super.onCreate();
176             mWorkerThread = new HandlerThread("ColorExtractionService");
177             mWorkerThread.start();
178             mWorkerHandler = new Handler(mWorkerThread.getLooper());
179             mColorExtractor = new ColorExtractor();
180         }
181 
182         @Override
onDestroy()183         public void onDestroy() {
184             super.onDestroy();
185             mWorkerThread.quit();
186         }
187 
188         @Override
onStartJob(final JobParameters jobParameters)189         public boolean onStartJob(final JobParameters jobParameters) {
190             mWorkerHandler.post(this);
191             return true;
192         }
193 
194         @Override
onStopJob(JobParameters jobParameters)195         public boolean onStopJob(JobParameters jobParameters) {
196             mWorkerHandler.removeCallbacksAndMessages(null);
197             return true;
198         }
199 
200         /**
201          * Extracts the wallpaper colors and sends the result back through the receiver.
202          */
203         @Override
run()204         public void run() {
205             int wallpaperId = getWallpaperId(this);
206 
207             Bitmap bitmap = null;
208             Drawable drawable = null;
209 
210             WallpaperManager wm = WallpaperManager.getInstance(this);
211             WallpaperInfo info = wm.getWallpaperInfo();
212             if (info != null) {
213                 // For live wallpaper, extract colors from thumbnail
214                 drawable = info.loadThumbnail(getPackageManager());
215             } else {
216                 try (ParcelFileDescriptor fd = wm.getWallpaperFile(FLAG_SYSTEM)) {
217                     BitmapRegionDecoder decoder = BitmapRegionDecoder
218                             .newInstance(fd.getFileDescriptor(), false);
219 
220                     int requestedArea = decoder.getWidth() * decoder.getHeight();
221                     BitmapFactory.Options options = new BitmapFactory.Options();
222 
223                     if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
224                         double areaRatio =
225                                 (double) requestedArea / MAX_WALLPAPER_EXTRACTION_AREA;
226                         double nearestPowOf2 =
227                                 Math.floor(Math.log(areaRatio) / (2 * Math.log(2)));
228                         options.inSampleSize = (int) Math.pow(2, nearestPowOf2);
229                     }
230                     Rect region = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
231                     bitmap = decoder.decodeRegion(region, options);
232                     decoder.recycle();
233                 } catch (IOException | NullPointerException e) {
234                     Log.e(TAG, "Fetching partial bitmap failed, trying old method", e);
235                 }
236                 if (bitmap == null) {
237                     drawable = wm.getDrawable();
238                 }
239             }
240 
241             if (drawable != null) {
242                 // Calculate how big the bitmap needs to be.
243                 // This avoids unnecessary processing and allocation inside Palette.
244                 final int requestedArea = drawable.getIntrinsicWidth() *
245                         drawable.getIntrinsicHeight();
246                 double scale = 1;
247                 if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
248                     scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
249                 }
250                 bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * scale),
251                         (int) (drawable.getIntrinsicHeight() * scale), Bitmap.Config.ARGB_8888);
252                 final Canvas bmpCanvas = new Canvas(bitmap);
253                 drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
254                 drawable.draw(bmpCanvas);
255             }
256 
257             String value = VERSION_PREFIX + wallpaperId;
258 
259             if (bitmap != null) {
260                 int color = mColorExtractor.findDominantColorByHue(bitmap,
261                         MAX_WALLPAPER_EXTRACTION_AREA);
262                 value += "," + color;
263             }
264 
265             // Send the result
266             sendBroadcast(new Intent(ACTION_EXTRACTION_COMPLETE)
267                     .setPackage(getPackageName())
268                     .putExtra(KEY_COLORS, value));
269         }
270     }
271 }
272