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