1 /*
2  * Copyright (C) 2008 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.systemui.statusbar;
18 
19 import android.app.Notification;
20 import android.content.Context;
21 import android.content.pm.ApplicationInfo;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.Icon;
29 import android.os.Parcelable;
30 import android.os.UserHandle;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.util.TypedValue;
35 import android.view.ViewDebug;
36 import android.view.accessibility.AccessibilityEvent;
37 
38 import com.android.internal.statusbar.StatusBarIcon;
39 import com.android.systemui.R;
40 
41 import java.text.NumberFormat;
42 
43 public class StatusBarIconView extends AnimatedImageView {
44     private static final String TAG = "StatusBarIconView";
45     private boolean mAlwaysScaleIcon;
46 
47     private StatusBarIcon mIcon;
48     @ViewDebug.ExportedProperty private String mSlot;
49     private Drawable mNumberBackground;
50     private Paint mNumberPain;
51     private int mNumberX;
52     private int mNumberY;
53     private String mNumberText;
54     private Notification mNotification;
55     private final boolean mBlocked;
56     private int mDensity;
57 
StatusBarIconView(Context context, String slot, Notification notification)58     public StatusBarIconView(Context context, String slot, Notification notification) {
59         this(context, slot, notification, false);
60     }
61 
StatusBarIconView(Context context, String slot, Notification notification, boolean blocked)62     public StatusBarIconView(Context context, String slot, Notification notification,
63             boolean blocked) {
64         super(context);
65         mBlocked = blocked;
66         mSlot = slot;
67         mNumberPain = new Paint();
68         mNumberPain.setTextAlign(Paint.Align.CENTER);
69         mNumberPain.setColor(context.getColor(R.drawable.notification_number_text_color));
70         mNumberPain.setAntiAlias(true);
71         setNotification(notification);
72         maybeUpdateIconScale();
73         setScaleType(ScaleType.CENTER);
74         mDensity = context.getResources().getDisplayMetrics().densityDpi;
75     }
76 
maybeUpdateIconScale()77     private void maybeUpdateIconScale() {
78         // We do not resize and scale system icons (on the right), only notification icons (on the
79         // left).
80         if (mNotification != null || mAlwaysScaleIcon) {
81             updateIconScale();
82         }
83     }
84 
updateIconScale()85     private void updateIconScale() {
86         Resources res = mContext.getResources();
87         final int outerBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_size);
88         final int imageBounds = res.getDimensionPixelSize(R.dimen.status_bar_icon_drawing_size);
89         final float scale = (float)imageBounds / (float)outerBounds;
90         setScaleX(scale);
91         setScaleY(scale);
92     }
93 
94     @Override
onConfigurationChanged(Configuration newConfig)95     protected void onConfigurationChanged(Configuration newConfig) {
96         super.onConfigurationChanged(newConfig);
97         int density = newConfig.densityDpi;
98         if (density != mDensity) {
99             mDensity = density;
100             maybeUpdateIconScale();
101             updateDrawable();
102         }
103     }
104 
setNotification(Notification notification)105     public void setNotification(Notification notification) {
106         mNotification = notification;
107         setContentDescription(notification);
108     }
109 
StatusBarIconView(Context context, AttributeSet attrs)110     public StatusBarIconView(Context context, AttributeSet attrs) {
111         super(context, attrs);
112         mBlocked = false;
113         mAlwaysScaleIcon = true;
114         updateIconScale();
115         mDensity = context.getResources().getDisplayMetrics().densityDpi;
116     }
117 
streq(String a, String b)118     private static boolean streq(String a, String b) {
119         if (a == b) {
120             return true;
121         }
122         if (a == null && b != null) {
123             return false;
124         }
125         if (a != null && b == null) {
126             return false;
127         }
128         return a.equals(b);
129     }
130 
equalIcons(Icon a, Icon b)131     public boolean equalIcons(Icon a, Icon b) {
132         if (a == b) return true;
133         if (a.getType() != b.getType()) return false;
134         switch (a.getType()) {
135             case Icon.TYPE_RESOURCE:
136                 return a.getResPackage().equals(b.getResPackage()) && a.getResId() == b.getResId();
137             case Icon.TYPE_URI:
138                 return a.getUriString().equals(b.getUriString());
139             default:
140                 return false;
141         }
142     }
143     /**
144      * Returns whether the set succeeded.
145      */
set(StatusBarIcon icon)146     public boolean set(StatusBarIcon icon) {
147         final boolean iconEquals = mIcon != null && equalIcons(mIcon.icon, icon.icon);
148         final boolean levelEquals = iconEquals
149                 && mIcon.iconLevel == icon.iconLevel;
150         final boolean visibilityEquals = mIcon != null
151                 && mIcon.visible == icon.visible;
152         final boolean numberEquals = mIcon != null
153                 && mIcon.number == icon.number;
154         mIcon = icon.clone();
155         setContentDescription(icon.contentDescription);
156         if (!iconEquals) {
157             if (!updateDrawable(false /* no clear */)) return false;
158         }
159         if (!levelEquals) {
160             setImageLevel(icon.iconLevel);
161         }
162 
163         if (!numberEquals) {
164             if (icon.number > 0 && getContext().getResources().getBoolean(
165                         R.bool.config_statusBarShowNumber)) {
166                 if (mNumberBackground == null) {
167                     mNumberBackground = getContext().getResources().getDrawable(
168                             R.drawable.ic_notification_overlay);
169                 }
170                 placeNumber();
171             } else {
172                 mNumberBackground = null;
173                 mNumberText = null;
174             }
175             invalidate();
176         }
177         if (!visibilityEquals) {
178             setVisibility(icon.visible && !mBlocked ? VISIBLE : GONE);
179         }
180         return true;
181     }
182 
updateDrawable()183     public void updateDrawable() {
184         updateDrawable(true /* with clear */);
185     }
186 
updateDrawable(boolean withClear)187     private boolean updateDrawable(boolean withClear) {
188         if (mIcon == null) {
189             return false;
190         }
191         Drawable drawable = getIcon(mIcon);
192         if (drawable == null) {
193             Log.w(TAG, "No icon for slot " + mSlot);
194             return false;
195         }
196         if (withClear) {
197             setImageDrawable(null);
198         }
199         setImageDrawable(drawable);
200         return true;
201     }
202 
getIcon(StatusBarIcon icon)203     private Drawable getIcon(StatusBarIcon icon) {
204         return getIcon(getContext(), icon);
205     }
206 
207     /**
208      * Returns the right icon to use for this item
209      *
210      * @param context Context to use to get resources
211      * @return Drawable for this item, or null if the package or item could not
212      *         be found
213      */
getIcon(Context context, StatusBarIcon statusBarIcon)214     public static Drawable getIcon(Context context, StatusBarIcon statusBarIcon) {
215         int userId = statusBarIcon.user.getIdentifier();
216         if (userId == UserHandle.USER_ALL) {
217             userId = UserHandle.USER_SYSTEM;
218         }
219 
220         Drawable icon = statusBarIcon.icon.loadDrawableAsUser(context, userId);
221 
222         TypedValue typedValue = new TypedValue();
223         context.getResources().getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true);
224         float scaleFactor = typedValue.getFloat();
225 
226         // No need to scale the icon, so return it as is.
227         if (scaleFactor == 1.f) {
228             return icon;
229         }
230 
231         return new ScalingDrawableWrapper(icon, scaleFactor);
232     }
233 
getStatusBarIcon()234     public StatusBarIcon getStatusBarIcon() {
235         return mIcon;
236     }
237 
238     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)239     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
240         super.onInitializeAccessibilityEvent(event);
241         if (mNotification != null) {
242             event.setParcelableData(mNotification);
243         }
244     }
245 
246     @Override
onSizeChanged(int w, int h, int oldw, int oldh)247     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
248         super.onSizeChanged(w, h, oldw, oldh);
249         if (mNumberBackground != null) {
250             placeNumber();
251         }
252     }
253 
254     @Override
onRtlPropertiesChanged(int layoutDirection)255     public void onRtlPropertiesChanged(int layoutDirection) {
256         super.onRtlPropertiesChanged(layoutDirection);
257         updateDrawable();
258     }
259 
260     @Override
onDraw(Canvas canvas)261     protected void onDraw(Canvas canvas) {
262         super.onDraw(canvas);
263 
264         if (mNumberBackground != null) {
265             mNumberBackground.draw(canvas);
266             canvas.drawText(mNumberText, mNumberX, mNumberY, mNumberPain);
267         }
268     }
269 
270     @Override
debug(int depth)271     protected void debug(int depth) {
272         super.debug(depth);
273         Log.d("View", debugIndent(depth) + "slot=" + mSlot);
274         Log.d("View", debugIndent(depth) + "icon=" + mIcon);
275     }
276 
placeNumber()277     void placeNumber() {
278         final String str;
279         final int tooBig = getContext().getResources().getInteger(
280                 android.R.integer.status_bar_notification_info_maxnum);
281         if (mIcon.number > tooBig) {
282             str = getContext().getResources().getString(
283                         android.R.string.status_bar_notification_info_overflow);
284         } else {
285             NumberFormat f = NumberFormat.getIntegerInstance();
286             str = f.format(mIcon.number);
287         }
288         mNumberText = str;
289 
290         final int w = getWidth();
291         final int h = getHeight();
292         final Rect r = new Rect();
293         mNumberPain.getTextBounds(str, 0, str.length(), r);
294         final int tw = r.right - r.left;
295         final int th = r.bottom - r.top;
296         mNumberBackground.getPadding(r);
297         int dw = r.left + tw + r.right;
298         if (dw < mNumberBackground.getMinimumWidth()) {
299             dw = mNumberBackground.getMinimumWidth();
300         }
301         mNumberX = w-r.right-((dw-r.right-r.left)/2);
302         int dh = r.top + th + r.bottom;
303         if (dh < mNumberBackground.getMinimumWidth()) {
304             dh = mNumberBackground.getMinimumWidth();
305         }
306         mNumberY = h-r.bottom-((dh-r.top-th-r.bottom)/2);
307         mNumberBackground.setBounds(w-dw, h-dh, w, h);
308     }
309 
setContentDescription(Notification notification)310     private void setContentDescription(Notification notification) {
311         if (notification != null) {
312             String d = contentDescForNotification(mContext, notification);
313             if (!TextUtils.isEmpty(d)) {
314                 setContentDescription(d);
315             }
316         }
317     }
318 
toString()319     public String toString() {
320         return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
321             + " notification=" + mNotification + ")";
322     }
323 
getSlot()324     public String getSlot() {
325         return mSlot;
326     }
327 
328 
contentDescForNotification(Context c, Notification n)329     public static String contentDescForNotification(Context c, Notification n) {
330         String appName = "";
331         try {
332             Notification.Builder builder = Notification.Builder.recoverBuilder(c, n);
333             appName = builder.loadHeaderAppName();
334         } catch (RuntimeException e) {
335             Log.e(TAG, "Unable to recover builder", e);
336             // Trying to get the app name from the app info instead.
337             Parcelable appInfo = n.extras.getParcelable(
338                     Notification.EXTRA_BUILDER_APPLICATION_INFO);
339             if (appInfo instanceof ApplicationInfo) {
340                 appName = String.valueOf(((ApplicationInfo) appInfo).loadLabel(
341                         c.getPackageManager()));
342             }
343         }
344 
345         CharSequence title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
346         CharSequence ticker = n.tickerText;
347 
348         CharSequence desc = !TextUtils.isEmpty(ticker) ? ticker
349                 : !TextUtils.isEmpty(title) ? title : "";
350 
351         return c.getString(R.string.accessibility_desc_notification_icon, appName, desc);
352     }
353 
354 }
355