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