1 /* 2 * Copyright (C) 2019 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 17 package com.android.launcher3.icons; 18 19 import static android.content.Intent.ACTION_DATE_CHANGED; 20 import static android.content.Intent.ACTION_TIMEZONE_CHANGED; 21 import static android.content.Intent.ACTION_TIME_CHANGED; 22 import static android.content.res.Resources.ID_NULL; 23 import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; 24 25 import android.annotation.TargetApi; 26 import android.content.BroadcastReceiver; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.ActivityInfo; 32 import android.content.pm.LauncherActivityInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.PackageManager.NameNotFoundException; 35 import android.content.res.Resources; 36 import android.content.res.TypedArray; 37 import android.graphics.drawable.AdaptiveIconDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.graphics.drawable.InsetDrawable; 40 import android.os.Build; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.os.PatternMatcher; 44 import android.os.Process; 45 import android.os.UserHandle; 46 import android.os.UserManager; 47 import android.text.TextUtils; 48 import android.util.Log; 49 50 import androidx.annotation.Nullable; 51 import androidx.core.os.BuildCompat; 52 53 import com.android.launcher3.util.SafeCloseable; 54 55 import java.util.Calendar; 56 import java.util.function.Supplier; 57 58 /** 59 * Class to handle icon loading from different packages 60 */ 61 public class IconProvider { 62 63 private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"; 64 static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier( 65 "config_icon_mask", "string", "android"); 66 67 private static final String TAG = "IconProvider"; 68 private static final boolean DEBUG = false; 69 public static final boolean ATLEAST_T = BuildCompat.isAtLeastT(); 70 71 private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons"; 72 73 private static final String SYSTEM_STATE_SEPARATOR = " "; 74 75 protected final Context mContext; 76 private final ComponentName mCalendar; 77 private final ComponentName mClock; 78 IconProvider(Context context)79 public IconProvider(Context context) { 80 mContext = context; 81 mCalendar = parseComponentOrNull(context, R.string.calendar_component_name); 82 mClock = parseComponentOrNull(context, R.string.clock_component_name); 83 } 84 85 /** 86 * Adds any modification to the provided systemState for dynamic icons. This system state 87 * is used by caches to check for icon invalidation. 88 */ getSystemStateForPackage(String systemState, String packageName)89 public String getSystemStateForPackage(String systemState, String packageName) { 90 if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { 91 return systemState + SYSTEM_STATE_SEPARATOR + getDay(); 92 } else { 93 return systemState; 94 } 95 } 96 97 /** 98 * Loads the icon for the provided LauncherActivityInfo 99 */ getIcon(LauncherActivityInfo info, int iconDpi)100 public Drawable getIcon(LauncherActivityInfo info, int iconDpi) { 101 return getIconWithOverrides(info.getApplicationInfo().packageName, iconDpi, 102 () -> info.getIcon(iconDpi)); 103 } 104 105 /** 106 * Loads the icon for the provided activity info 107 */ getIcon(ActivityInfo info)108 public Drawable getIcon(ActivityInfo info) { 109 return getIcon(info, mContext.getResources().getConfiguration().densityDpi); 110 } 111 112 /** 113 * Loads the icon for the provided activity info 114 */ getIcon(ActivityInfo info, int iconDpi)115 public Drawable getIcon(ActivityInfo info, int iconDpi) { 116 return getIconWithOverrides(info.applicationInfo.packageName, iconDpi, 117 () -> loadActivityInfoIcon(info, iconDpi)); 118 } 119 120 @TargetApi(Build.VERSION_CODES.TIRAMISU) getIconWithOverrides(String packageName, int iconDpi, Supplier<Drawable> fallback)121 private Drawable getIconWithOverrides(String packageName, int iconDpi, 122 Supplier<Drawable> fallback) { 123 ThemeData td = getThemeDataForPackage(packageName); 124 125 Drawable icon = null; 126 if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { 127 icon = loadCalendarDrawable(iconDpi, td); 128 } else if (mClock != null && mClock.getPackageName().equals(packageName)) { 129 icon = ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi, td); 130 } 131 if (icon == null) { 132 icon = fallback.get(); 133 if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && td != null) { 134 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon; 135 if (aid.getMonochrome() == null) { 136 icon = new AdaptiveIconDrawable(aid.getBackground(), 137 aid.getForeground(), td.loadPaddedDrawable()); 138 } 139 } 140 } 141 return icon; 142 } 143 getThemeDataForPackage(String packageName)144 protected ThemeData getThemeDataForPackage(String packageName) { 145 return null; 146 } 147 loadActivityInfoIcon(ActivityInfo ai, int density)148 private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) { 149 final int iconRes = ai.getIconResource(); 150 Drawable icon = null; 151 // Get the preferred density icon from the app's resources 152 if (density != 0 && iconRes != 0) { 153 try { 154 final Resources resources = mContext.getPackageManager() 155 .getResourcesForApplication(ai.applicationInfo); 156 icon = resources.getDrawableForDensity(iconRes, density); 157 } catch (NameNotFoundException | Resources.NotFoundException exc) { } 158 } 159 // Get the default density icon 160 if (icon == null) { 161 icon = ai.loadIcon(mContext.getPackageManager()); 162 } 163 return icon; 164 } 165 166 @TargetApi(Build.VERSION_CODES.TIRAMISU) loadCalendarDrawable(int iconDpi, @Nullable ThemeData td)167 private Drawable loadCalendarDrawable(int iconDpi, @Nullable ThemeData td) { 168 PackageManager pm = mContext.getPackageManager(); 169 try { 170 final Bundle metadata = pm.getActivityInfo( 171 mCalendar, 172 PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA) 173 .metaData; 174 final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName()); 175 final int id = getDynamicIconId(metadata, resources); 176 if (id != ID_NULL) { 177 if (DEBUG) Log.d(TAG, "Got icon #" + id); 178 Drawable drawable = resources.getDrawableForDensity(id, iconDpi, null /* theme */); 179 if (ATLEAST_T && drawable instanceof AdaptiveIconDrawable && td != null) { 180 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable; 181 if (aid.getMonochrome() != null) { 182 return drawable; 183 } 184 if ("array".equals(td.mResources.getResourceTypeName(td.mResID))) { 185 TypedArray ta = td.mResources.obtainTypedArray(td.mResID); 186 int monoId = ta.getResourceId(IconProvider.getDay(), ID_NULL); 187 ta.recycle(); 188 return monoId == ID_NULL ? drawable 189 : new AdaptiveIconDrawable(aid.getBackground(), aid.getForeground(), 190 new ThemeData(td.mResources, monoId).loadPaddedDrawable()); 191 } 192 } 193 return drawable; 194 } 195 } catch (PackageManager.NameNotFoundException e) { 196 if (DEBUG) { 197 Log.d(TAG, "Could not get activityinfo or resources for package: " 198 + mCalendar.getPackageName()); 199 } 200 } 201 return null; 202 } 203 204 /** 205 * @param metadata metadata of the default activity of Calendar 206 * @param resources from the Calendar package 207 * @return the resource id for today's Calendar icon; 0 if resources cannot be found. 208 */ getDynamicIconId(Bundle metadata, Resources resources)209 private int getDynamicIconId(Bundle metadata, Resources resources) { 210 if (metadata == null) { 211 return ID_NULL; 212 } 213 String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX; 214 final int arrayId = metadata.getInt(key, ID_NULL); 215 if (arrayId == ID_NULL) { 216 return ID_NULL; 217 } 218 try { 219 return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL); 220 } catch (Resources.NotFoundException e) { 221 if (DEBUG) { 222 Log.d(TAG, "package defines '" + key + "' but corresponding array not found"); 223 } 224 return ID_NULL; 225 } 226 } 227 228 /** 229 * @return Today's day of the month, zero-indexed. 230 */ getDay()231 private static int getDay() { 232 return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1; 233 } 234 parseComponentOrNull(Context context, int resId)235 private static ComponentName parseComponentOrNull(Context context, int resId) { 236 String cn = context.getString(resId); 237 return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn); 238 } 239 240 /** 241 * Returns a string representation of the current system icon state 242 */ getSystemIconState()243 public String getSystemIconState() { 244 return (CONFIG_ICON_MASK_RES_ID == ID_NULL 245 ? "" : mContext.getResources().getString(CONFIG_ICON_MASK_RES_ID)); 246 } 247 248 /** 249 * Registers a callback to listen for various system dependent icon changes. 250 */ registerIconChangeListener(IconChangeListener listener, Handler handler)251 public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) { 252 return new IconChangeReceiver(listener, handler); 253 } 254 255 public static class ThemeData { 256 257 final Resources mResources; 258 final int mResID; 259 ThemeData(Resources resources, int resID)260 public ThemeData(Resources resources, int resID) { 261 mResources = resources; 262 mResID = resID; 263 } 264 loadPaddedDrawable()265 Drawable loadPaddedDrawable() { 266 if (!"drawable".equals(mResources.getResourceTypeName(mResID))) { 267 return null; 268 } 269 Drawable d = mResources.getDrawable(mResID).mutate(); 270 d = new InsetDrawable(d, .2f); 271 float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction()); 272 Drawable fg = new InsetDrawable(d, inset); 273 return fg; 274 } 275 } 276 277 private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable { 278 279 private final IconChangeListener mCallback; 280 private String mIconState; 281 IconChangeReceiver(IconChangeListener callback, Handler handler)282 IconChangeReceiver(IconChangeListener callback, Handler handler) { 283 mCallback = callback; 284 mIconState = getSystemIconState(); 285 286 287 IntentFilter packageFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); 288 packageFilter.addDataScheme("package"); 289 packageFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); 290 mContext.registerReceiver(this, packageFilter, null, handler); 291 292 if (mCalendar != null || mClock != null) { 293 final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED); 294 if (mCalendar != null) { 295 filter.addAction(Intent.ACTION_TIME_CHANGED); 296 filter.addAction(ACTION_DATE_CHANGED); 297 } 298 mContext.registerReceiver(this, filter, null, handler); 299 } 300 } 301 302 @Override onReceive(Context context, Intent intent)303 public void onReceive(Context context, Intent intent) { 304 switch (intent.getAction()) { 305 case ACTION_TIMEZONE_CHANGED: 306 if (mClock != null) { 307 mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle()); 308 } 309 // follow through 310 case ACTION_DATE_CHANGED: 311 case ACTION_TIME_CHANGED: 312 if (mCalendar != null) { 313 for (UserHandle user 314 : context.getSystemService(UserManager.class).getUserProfiles()) { 315 mCallback.onAppIconChanged(mCalendar.getPackageName(), user); 316 } 317 } 318 break; 319 case ACTION_OVERLAY_CHANGED: { 320 String newState = getSystemIconState(); 321 if (!mIconState.equals(newState)) { 322 mIconState = newState; 323 mCallback.onSystemIconStateChanged(mIconState); 324 } 325 break; 326 } 327 } 328 } 329 330 @Override close()331 public void close() { 332 mContext.unregisterReceiver(this); 333 } 334 } 335 336 /** 337 * Listener for receiving icon changes 338 */ 339 public interface IconChangeListener { 340 341 /** 342 * Called when the icon for a particular app changes 343 */ onAppIconChanged(String packageName, UserHandle user)344 void onAppIconChanged(String packageName, UserHandle user); 345 346 /** 347 * Called when the global icon state changed, which can typically affect all icons 348 */ onSystemIconStateChanged(String iconState)349 void onSystemIconStateChanged(String iconState); 350 } 351 } 352