1 /*
2  * Copyright (C) 2024 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.settings.notification.modes;
18 
19 import static com.google.common.util.concurrent.Futures.immediateFuture;
20 
21 import static java.util.Objects.requireNonNull;
22 
23 import android.annotation.Nullable;
24 import android.app.AutomaticZenRule;
25 import android.content.Context;
26 import android.graphics.drawable.AdaptiveIconDrawable;
27 import android.graphics.drawable.ColorDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.graphics.drawable.InsetDrawable;
30 import android.service.notification.SystemZenRules;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.LruCache;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.VisibleForTesting;
37 import androidx.appcompat.content.res.AppCompatResources;
38 
39 import com.google.common.util.concurrent.FluentFuture;
40 import com.google.common.util.concurrent.Futures;
41 import com.google.common.util.concurrent.ListenableFuture;
42 import com.google.common.util.concurrent.ListeningExecutorService;
43 import com.google.common.util.concurrent.MoreExecutors;
44 
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Executors;
47 
48 class IconLoader {
49 
50     private static final String TAG = "ZenIconLoader";
51 
52     private static final Drawable MISSING = new ColorDrawable();
53 
54     @Nullable // Until first usage
55     private static IconLoader sInstance;
56 
57     private final LruCache<String, Drawable> mCache;
58     private final ListeningExecutorService mBackgroundExecutor;
59 
getInstance()60     static IconLoader getInstance() {
61         if (sInstance == null) {
62             sInstance = new IconLoader();
63         }
64         return sInstance;
65     }
66 
IconLoader()67     private IconLoader() {
68         this(Executors.newFixedThreadPool(4));
69     }
70 
71     @VisibleForTesting
IconLoader(ExecutorService backgroundExecutor)72     IconLoader(ExecutorService backgroundExecutor) {
73         mCache = new LruCache<>(50);
74         mBackgroundExecutor =
75                 MoreExecutors.listeningDecorator(backgroundExecutor);
76     }
77 
78     @NonNull
getIcon(Context context, @NonNull AutomaticZenRule rule)79     ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
80         if (rule.getIconResId() == 0) {
81             return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
82         }
83 
84         return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
85                 .transform(icon ->
86                         icon != null ? icon : getFallbackIcon(context, rule.getType()),
87                         MoreExecutors.directExecutor());
88     }
89 
90     @NonNull
loadIcon(Context context, String pkg, int iconResId)91     private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
92             int iconResId) {
93         String cacheKey = pkg + ":" + iconResId;
94         synchronized (mCache) {
95             Drawable cachedValue = mCache.get(cacheKey);
96             if (cachedValue != null) {
97                 return immediateFuture(cachedValue != MISSING ? cachedValue : null);
98             }
99         }
100 
101         return FluentFuture.from(mBackgroundExecutor.submit(() -> {
102             if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
103                 return context.getDrawable(iconResId);
104             } else {
105                 Context appContext = context.createPackageContext(pkg, 0);
106                 Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
107                 return getMonochromeIconIfPresent(appDrawable);
108             }
109         })).catching(Exception.class, ex -> {
110             // If we cannot resolve the icon, then store MISSING in the cache below, so
111             // we don't try again.
112             Log.e(TAG, "Error while loading icon " + cacheKey, ex);
113             return null;
114         }, MoreExecutors.directExecutor()).transform(drawable -> {
115             synchronized (mCache) {
116                 mCache.put(cacheKey, drawable != null ? drawable : MISSING);
117             }
118             return drawable;
119         }, MoreExecutors.directExecutor());
120     }
121 
122     private static Drawable getFallbackIcon(Context context, int ruleType) {
123         int iconResIdFromType = switch (ruleType) {
124             case AutomaticZenRule.TYPE_UNKNOWN ->
125                     com.android.internal.R.drawable.ic_zen_mode_type_unknown;
126             case AutomaticZenRule.TYPE_OTHER ->
127                     com.android.internal.R.drawable.ic_zen_mode_type_other;
128             case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
129                     com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
130             case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
131                     com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
132             case AutomaticZenRule.TYPE_BEDTIME ->
133                     com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
134             case AutomaticZenRule.TYPE_DRIVING ->
135                     com.android.internal.R.drawable.ic_zen_mode_type_driving;
136             case AutomaticZenRule.TYPE_IMMERSIVE ->
137                     com.android.internal.R.drawable.ic_zen_mode_type_immersive;
138             case AutomaticZenRule.TYPE_THEATER ->
139                     com.android.internal.R.drawable.ic_zen_mode_type_theater;
140             case AutomaticZenRule.TYPE_MANAGED ->
141                     com.android.internal.R.drawable.ic_zen_mode_type_managed;
142             default ->
143                     com.android.internal.R.drawable.ic_zen_mode_type_unknown;
144         };
145         return requireNonNull(context.getDrawable(iconResIdFromType));
146     }
147 
148     private static Drawable getMonochromeIconIfPresent(Drawable icon) {
149         // For created rules, the app should've provided a monochrome Drawable. However, implicit
150         // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
151         // we choose it, if present.
152         if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
153             if (adaptiveIcon.getMonochrome() != null) {
154                 // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
155                 return new InsetDrawable(adaptiveIcon.getMonochrome(),
156                         -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
157             }
158         }
159         return icon;
160     }
161 }
162