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 android.content.BroadcastReceiver; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.LauncherActivityInfo; 26 import android.content.pm.PackageManager; 27 import android.content.res.Resources; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.Process; 32 import android.os.UserHandle; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import com.android.launcher3.R; 37 import com.android.launcher3.icons.BitmapInfo.Extender; 38 import com.android.launcher3.pm.UserCache; 39 import com.android.launcher3.util.ComponentKey; 40 import com.android.launcher3.util.SafeCloseable; 41 42 import java.util.Calendar; 43 import java.util.function.BiConsumer; 44 import java.util.function.BiFunction; 45 46 /** 47 * Class to handle icon loading from different packages 48 */ 49 public class IconProvider { 50 51 private static final String TAG = "IconProvider"; 52 private static final boolean DEBUG = false; 53 54 private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons"; 55 56 private static final String SYSTEM_STATE_SEPARATOR = " "; 57 58 // Default value returned if there are problems getting resources. 59 private static final int NO_ID = 0; 60 61 private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER = 62 LauncherActivityInfo::getIcon; 63 64 private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER = 65 ActivityInfo::loadUnbadgedIcon; 66 67 68 private final Context mContext; 69 private final ComponentName mCalendar; 70 private final ComponentName mClock; 71 IconProvider(Context context)72 public IconProvider(Context context) { 73 mContext = context; 74 mCalendar = parseComponentOrNull(context, R.string.calendar_component_name); 75 mClock = parseComponentOrNull(context, R.string.clock_component_name); 76 } 77 78 /** 79 * Adds any modification to the provided systemState for dynamic icons. This system state 80 * is used by caches to check for icon invalidation. 81 */ getSystemStateForPackage(String systemState, String packageName)82 public String getSystemStateForPackage(String systemState, String packageName) { 83 if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { 84 return systemState + SYSTEM_STATE_SEPARATOR + getDay(); 85 } else { 86 return systemState; 87 } 88 } 89 90 /** 91 * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly 92 * on the UI 93 */ getIconForUI(LauncherActivityInfo info, int iconDpi)94 public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) { 95 Drawable icon = getIcon(info, iconDpi); 96 if (icon instanceof BitmapInfo.Extender) { 97 ((Extender) icon).prepareToDrawOnUi(); 98 } 99 return icon; 100 } 101 102 /** 103 * Loads the icon for the provided LauncherActivityInfo 104 */ getIcon(LauncherActivityInfo info, int iconDpi)105 public Drawable getIcon(LauncherActivityInfo info, int iconDpi) { 106 return getIcon(info.getApplicationInfo().packageName, info.getUser(), 107 info, iconDpi, LAI_LOADER); 108 } 109 110 /** 111 * Loads the icon for the provided activity info 112 */ getIcon(ActivityInfo info, UserHandle user)113 public Drawable getIcon(ActivityInfo info, UserHandle user) { 114 return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(), 115 AI_LOADER); 116 } 117 getIcon(String packageName, UserHandle user, T obj, P param, BiFunction<T, P, Drawable> loader)118 private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param, 119 BiFunction<T, P, Drawable> loader) { 120 Drawable icon = null; 121 if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) { 122 icon = loadCalendarDrawable(0); 123 } else if (mClock != null 124 && mClock.getPackageName().equals(packageName) 125 && Process.myUserHandle().equals(user)) { 126 icon = loadClockDrawable(0); 127 } 128 return icon == null ? loader.apply(obj, param) : icon; 129 } 130 loadCalendarDrawable(int iconDpi)131 private Drawable loadCalendarDrawable(int iconDpi) { 132 PackageManager pm = mContext.getPackageManager(); 133 try { 134 final Bundle metadata = pm.getActivityInfo( 135 mCalendar, 136 PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA) 137 .metaData; 138 final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName()); 139 final int id = getDynamicIconId(metadata, resources); 140 if (id != NO_ID) { 141 if (DEBUG) Log.d(TAG, "Got icon #" + id); 142 return resources.getDrawableForDensity(id, iconDpi, null /* theme */); 143 } 144 } catch (PackageManager.NameNotFoundException e) { 145 if (DEBUG) { 146 Log.d(TAG, "Could not get activityinfo or resources for package: " 147 + mCalendar.getPackageName()); 148 } 149 } 150 return null; 151 } 152 loadClockDrawable(int iconDpi)153 private Drawable loadClockDrawable(int iconDpi) { 154 return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi); 155 } 156 isClockIcon(ComponentKey key)157 protected boolean isClockIcon(ComponentKey key) { 158 return mClock != null && mClock.equals(key.componentName) 159 && Process.myUserHandle().equals(key.user); 160 } 161 162 /** 163 * @param metadata metadata of the default activity of Calendar 164 * @param resources from the Calendar package 165 * @return the resource id for today's Calendar icon; 0 if resources cannot be found. 166 */ getDynamicIconId(Bundle metadata, Resources resources)167 private int getDynamicIconId(Bundle metadata, Resources resources) { 168 if (metadata == null) { 169 return NO_ID; 170 } 171 String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX; 172 final int arrayId = metadata.getInt(key, NO_ID); 173 if (arrayId == NO_ID) { 174 return NO_ID; 175 } 176 try { 177 return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID); 178 } catch (Resources.NotFoundException e) { 179 if (DEBUG) { 180 Log.d(TAG, "package defines '" + key + "' but corresponding array not found"); 181 } 182 return NO_ID; 183 } 184 } 185 186 /** 187 * @return Today's day of the month, zero-indexed. 188 */ getDay()189 private int getDay() { 190 return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1; 191 } 192 193 194 /** 195 * Registers a callback to listen for calendar icon changes. 196 * The callback receives the packageName for the calendar icon 197 */ registerIconChangeListener(Context context, BiConsumer<String, UserHandle> callback, Handler handler)198 public static SafeCloseable registerIconChangeListener(Context context, 199 BiConsumer<String, UserHandle> callback, Handler handler) { 200 ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name); 201 ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name); 202 203 if (calendar == null && clock == null) { 204 return () -> { }; 205 } 206 207 BroadcastReceiver receiver = new DateTimeChangeReceiver(callback); 208 final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); 209 if (calendar != null) { 210 filter.addAction(Intent.ACTION_TIME_CHANGED); 211 filter.addAction(Intent.ACTION_DATE_CHANGED); 212 } 213 context.registerReceiver(receiver, filter, null, handler); 214 215 return () -> context.unregisterReceiver(receiver); 216 } 217 218 private static class DateTimeChangeReceiver extends BroadcastReceiver { 219 220 private final BiConsumer<String, UserHandle> mCallback; 221 DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback)222 DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) { 223 mCallback = callback; 224 } 225 226 @Override onReceive(Context context, Intent intent)227 public void onReceive(Context context, Intent intent) { 228 if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { 229 ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name); 230 if (clock != null) { 231 mCallback.accept(clock.getPackageName(), Process.myUserHandle()); 232 } 233 } 234 235 ComponentName calendar = 236 parseComponentOrNull(context, R.string.calendar_component_name); 237 if (calendar != null) { 238 for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) { 239 mCallback.accept(calendar.getPackageName(), user); 240 } 241 } 242 243 } 244 } 245 parseComponentOrNull(Context context, int resId)246 private static ComponentName parseComponentOrNull(Context context, int resId) { 247 String cn = context.getString(resId); 248 return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn); 249 250 } 251 } 252