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