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