• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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