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.ApplicationInfo; 32 import android.content.pm.ComponentInfo; 33 import android.content.pm.PackageItemInfo; 34 import android.content.pm.PackageManager; 35 import android.content.pm.PackageManager.NameNotFoundException; 36 import android.content.res.Resources; 37 import android.content.res.TypedArray; 38 import android.graphics.drawable.AdaptiveIconDrawable; 39 import android.graphics.drawable.Drawable; 40 import android.graphics.drawable.InsetDrawable; 41 import android.os.Build; 42 import android.os.Bundle; 43 import android.os.Handler; 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.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.core.os.BuildCompat; 53 54 import com.android.launcher3.util.SafeCloseable; 55 56 import java.util.Calendar; 57 import java.util.Objects; 58 59 /** 60 * Class to handle icon loading from different packages 61 */ 62 public class IconProvider { 63 64 private static final String TAG = "IconProvider"; 65 private static final boolean DEBUG = false; 66 public static final boolean ATLEAST_T = BuildCompat.isAtLeastT(); 67 68 private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons"; 69 70 private static final String SYSTEM_STATE_SEPARATOR = " "; 71 72 protected final Context mContext; 73 private final ComponentName mCalendar; 74 private final ComponentName mClock; 75 76 @NonNull 77 protected String mSystemState = ""; 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 * Returns a string representing the current state of the app icon. It can be used as a 87 * identifier to invalidate any resources loaded from the app. 88 * It also incorporated ay system state, that can affect the loaded resource 89 * 90 * @see #updateSystemState() 91 */ getStateForApp(@ullable ApplicationInfo appInfo)92 public String getStateForApp(@Nullable ApplicationInfo appInfo) { 93 if (appInfo == null) { 94 return mSystemState; 95 } 96 97 if (mCalendar != null && mCalendar.getPackageName().equals(appInfo.packageName)) { 98 return mSystemState + SYSTEM_STATE_SEPARATOR + getDay() + SYSTEM_STATE_SEPARATOR 99 + getApplicationInfoHash(appInfo); 100 } else { 101 return mSystemState + SYSTEM_STATE_SEPARATOR + getApplicationInfoHash(appInfo); 102 } 103 } 104 105 /** 106 * Returns a hash to uniquely identify a particular version of appInfo 107 */ getApplicationInfoHash(@onNull ApplicationInfo appInfo)108 protected String getApplicationInfoHash(@NonNull ApplicationInfo appInfo) { 109 // The hashString in source dir changes with every install 110 return appInfo.sourceDir; 111 } 112 113 /** 114 * Loads the icon for the provided activity info 115 */ getIcon(ComponentInfo info)116 public Drawable getIcon(ComponentInfo info) { 117 return getIcon(info, mContext.getResources().getConfiguration().densityDpi); 118 } 119 120 /** 121 * Loads the icon for the provided component info 122 */ getIcon(ComponentInfo info, int iconDpi)123 public Drawable getIcon(ComponentInfo info, int iconDpi) { 124 return getIcon(info, info.applicationInfo, iconDpi); 125 } 126 127 /** 128 * Loads the icon for the provided application info 129 */ getIcon(ApplicationInfo info)130 public Drawable getIcon(ApplicationInfo info) { 131 return getIcon(info, mContext.getResources().getConfiguration().densityDpi); 132 } 133 134 /** 135 * Loads the icon for the provided application info 136 */ getIcon(ApplicationInfo info, int iconDpi)137 public Drawable getIcon(ApplicationInfo info, int iconDpi) { 138 return getIcon(info, info, iconDpi); 139 } 140 getIcon(PackageItemInfo info, ApplicationInfo appInfo, int iconDpi)141 private Drawable getIcon(PackageItemInfo info, ApplicationInfo appInfo, int iconDpi) { 142 String packageName = info.packageName; 143 ThemeData td = getThemeDataForPackage(packageName); 144 145 Drawable icon = null; 146 if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { 147 icon = loadCalendarDrawable(iconDpi, td); 148 } else if (mClock != null && mClock.getPackageName().equals(packageName)) { 149 icon = ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi); 150 } 151 if (icon == null) { 152 icon = loadPackageIcon(info, appInfo, iconDpi); 153 if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && td != null) { 154 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon; 155 if (aid.getMonochrome() == null) { 156 icon = new AdaptiveIconDrawable(aid.getBackground(), 157 aid.getForeground(), td.loadPaddedDrawable()); 158 } 159 } 160 } 161 return icon; 162 } 163 getThemeDataForPackage(String packageName)164 protected ThemeData getThemeDataForPackage(String packageName) { 165 return null; 166 } 167 loadPackageIcon(PackageItemInfo info, ApplicationInfo appInfo, int density)168 private Drawable loadPackageIcon(PackageItemInfo info, ApplicationInfo appInfo, int density) { 169 Drawable icon = null; 170 if (BuildCompat.isAtLeastV() && info.isArchived) { 171 // Icons for archived apps com from system service, let the default impl handle that 172 icon = info.loadIcon(mContext.getPackageManager()); 173 } 174 if (icon == null && density != 0 && (info.icon != 0 || appInfo.icon != 0)) { 175 try { 176 final Resources resources = mContext.getPackageManager() 177 .getResourcesForApplication(appInfo); 178 // Try to load the package item icon first 179 if (info != appInfo && info.icon != 0) { 180 try { 181 icon = resources.getDrawableForDensity(info.icon, density); 182 } catch (Resources.NotFoundException exc) { } 183 } 184 if (icon == null && appInfo.icon != 0) { 185 // Load the fallback app icon 186 icon = loadAppInfoIcon(appInfo, resources, density); 187 } 188 } catch (NameNotFoundException | Resources.NotFoundException exc) { } 189 } 190 return icon != null ? icon : getFullResDefaultActivityIcon(density); 191 } 192 193 @Nullable loadAppInfoIcon(ApplicationInfo info, Resources resources, int density)194 protected Drawable loadAppInfoIcon(ApplicationInfo info, Resources resources, int density) { 195 try { 196 return resources.getDrawableForDensity(info.icon, density); 197 } catch (Resources.NotFoundException exc) { } 198 return null; 199 } 200 201 @TargetApi(Build.VERSION_CODES.TIRAMISU) loadCalendarDrawable(int iconDpi, @Nullable ThemeData td)202 private Drawable loadCalendarDrawable(int iconDpi, @Nullable ThemeData td) { 203 PackageManager pm = mContext.getPackageManager(); 204 try { 205 final Bundle metadata = pm.getActivityInfo( 206 mCalendar, 207 PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA) 208 .metaData; 209 final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName()); 210 final int id = getDynamicIconId(metadata, resources); 211 if (id != ID_NULL) { 212 if (DEBUG) Log.d(TAG, "Got icon #" + id); 213 Drawable drawable = resources.getDrawableForDensity(id, iconDpi, null /* theme */); 214 if (ATLEAST_T && drawable instanceof AdaptiveIconDrawable && td != null) { 215 AdaptiveIconDrawable aid = (AdaptiveIconDrawable) drawable; 216 if (aid.getMonochrome() != null) { 217 return drawable; 218 } 219 if ("array".equals(td.mResources.getResourceTypeName(td.mResID))) { 220 TypedArray ta = td.mResources.obtainTypedArray(td.mResID); 221 int monoId = ta.getResourceId(IconProvider.getDay(), ID_NULL); 222 ta.recycle(); 223 return monoId == ID_NULL ? drawable 224 : new AdaptiveIconDrawable(aid.getBackground(), aid.getForeground(), 225 new ThemeData(td.mResources, monoId).loadPaddedDrawable()); 226 } 227 } 228 return drawable; 229 } 230 } catch (PackageManager.NameNotFoundException e) { 231 if (DEBUG) { 232 Log.d(TAG, "Could not get activityinfo or resources for package: " 233 + mCalendar.getPackageName()); 234 } 235 } 236 return null; 237 } 238 239 /** 240 * Returns the default activity icon 241 */ 242 @NonNull getFullResDefaultActivityIcon(final int iconDpi)243 public Drawable getFullResDefaultActivityIcon(final int iconDpi) { 244 return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity( 245 android.R.drawable.sym_def_app_icon, iconDpi)); 246 } 247 248 /** 249 * @param metadata metadata of the default activity of Calendar 250 * @param resources from the Calendar package 251 * @return the resource id for today's Calendar icon; 0 if resources cannot be found. 252 */ getDynamicIconId(Bundle metadata, Resources resources)253 private int getDynamicIconId(Bundle metadata, Resources resources) { 254 if (metadata == null) { 255 return ID_NULL; 256 } 257 String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX; 258 final int arrayId = metadata.getInt(key, ID_NULL); 259 if (arrayId == ID_NULL) { 260 return ID_NULL; 261 } 262 try { 263 return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL); 264 } catch (Resources.NotFoundException e) { 265 if (DEBUG) { 266 Log.d(TAG, "package defines '" + key + "' but corresponding array not found"); 267 } 268 return ID_NULL; 269 } 270 } 271 272 /** 273 * Refreshes the system state definition used to check the validity of an app icon. It 274 * incorporates all the properties that can affect the app icon like the list of enabled locale 275 * and system-version. 276 */ updateSystemState()277 public void updateSystemState() { 278 mSystemState = mContext.getResources().getConfiguration().getLocales().toLanguageTags() 279 + "," + Build.VERSION.SDK_INT; 280 } 281 282 /** 283 * @return Today's day of the month, zero-indexed. 284 */ getDay()285 private static int getDay() { 286 return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1; 287 } 288 parseComponentOrNull(Context context, int resId)289 private static ComponentName parseComponentOrNull(Context context, int resId) { 290 String cn = context.getString(resId); 291 return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn); 292 } 293 294 /** 295 * Registers a callback to listen for various system dependent icon changes. 296 */ registerIconChangeListener(IconChangeListener listener, Handler handler)297 public SafeCloseable registerIconChangeListener(IconChangeListener listener, Handler handler) { 298 return new IconChangeReceiver(listener, handler); 299 } 300 301 public static class ThemeData { 302 303 final Resources mResources; 304 final int mResID; 305 ThemeData(Resources resources, int resID)306 public ThemeData(Resources resources, int resID) { 307 mResources = resources; 308 mResID = resID; 309 } 310 loadPaddedDrawable()311 Drawable loadPaddedDrawable() { 312 if (!"drawable".equals(mResources.getResourceTypeName(mResID))) { 313 return null; 314 } 315 Drawable d = mResources.getDrawable(mResID).mutate(); 316 d = new InsetDrawable(d, .2f); 317 float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction()); 318 Drawable fg = new InsetDrawable(d, inset); 319 return fg; 320 } 321 } 322 323 private class IconChangeReceiver extends BroadcastReceiver implements SafeCloseable { 324 325 private final IconChangeListener mCallback; 326 IconChangeReceiver(IconChangeListener callback, Handler handler)327 IconChangeReceiver(IconChangeListener callback, Handler handler) { 328 mCallback = callback; 329 if (mCalendar != null || mClock != null) { 330 final IntentFilter filter = new IntentFilter(ACTION_TIMEZONE_CHANGED); 331 if (mCalendar != null) { 332 filter.addAction(Intent.ACTION_TIME_CHANGED); 333 filter.addAction(ACTION_DATE_CHANGED); 334 } 335 mContext.registerReceiver(this, filter, null, handler); 336 } 337 } 338 339 @Override onReceive(Context context, Intent intent)340 public void onReceive(Context context, Intent intent) { 341 switch (intent.getAction()) { 342 case ACTION_TIMEZONE_CHANGED: 343 if (mClock != null) { 344 mCallback.onAppIconChanged(mClock.getPackageName(), Process.myUserHandle()); 345 } 346 // follow through 347 case ACTION_DATE_CHANGED: 348 case ACTION_TIME_CHANGED: 349 if (mCalendar != null) { 350 for (UserHandle user 351 : context.getSystemService(UserManager.class).getUserProfiles()) { 352 mCallback.onAppIconChanged(mCalendar.getPackageName(), user); 353 } 354 } 355 break; 356 } 357 } 358 359 @Override close()360 public void close() { 361 try { 362 mContext.unregisterReceiver(this); 363 } catch (Exception ignored) { } 364 } 365 } 366 367 /** 368 * Listener for receiving icon changes 369 */ 370 public interface IconChangeListener { 371 372 /** 373 * Called when the icon for a particular app changes 374 */ onAppIconChanged(String packageName, UserHandle user)375 void onAppIconChanged(String packageName, UserHandle user); 376 } 377 } 378