1 /*
2  * Copyright (C) 2018 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.car.notification.template;
18 
19 import android.annotation.ColorInt;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.res.TypedArray;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.Icon;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.service.notification.StatusBarNotification;
30 import android.text.TextUtils;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.view.View;
34 import android.widget.DateTimeView;
35 import android.widget.ImageView;
36 import android.widget.RelativeLayout;
37 import android.widget.TextView;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.car.notification.NotificationUtils;
42 import com.android.car.notification.R;
43 
44 /**
45  * Common notification body that consists of a title line, a content text line, and an image icon on
46  * the end.
47  *
48  * <p> For example, for a messaging notification, the title is the sender's name,
49  * the content is the message, and the image icon is the sender's avatar.
50  */
51 public class CarNotificationBodyView extends RelativeLayout {
52     private static final String TAG = "CarNotificationBodyView";
53     private static final int DEFAULT_MAX_LINES = 3;
54     @ColorInt
55     private final int mDefaultPrimaryTextColor;
56     @ColorInt
57     private final int mDefaultSecondaryTextColor;
58     private final boolean mDefaultUseLauncherIcon;
59 
60     /**
61      * Key that system apps can add to the Notification extras to override the default
62      * {@link R.bool.config_useLauncherIcon} behavior. If this is set to false, a small and a large
63      * icon should be specified to be shown properly in the relevant default configuration.
64      */
65     @VisibleForTesting
66     static final String EXTRA_USE_LAUNCHER_ICON =
67             "com.android.car.notification.EXTRA_USE_LAUNCHER_ICON";
68 
69     private boolean mIsHeadsUp;
70     private boolean mShowBigIcon;
71     private int mMaxLines;
72     @Nullable
73     private TextView mTitleView;
74     @Nullable
75     private TextView mContentView;
76     @Nullable
77     private ImageView mLargeIconView;
78     @Nullable
79     private TextView mCountView;
80     @Nullable
81     private DateTimeView mTimeView;
82     @Nullable
83     private ImageView mTitleIconView;
84 
CarNotificationBodyView(Context context)85     public CarNotificationBodyView(Context context) {
86         super(context);
87         init(/* attrs= */ null);
88     }
89 
CarNotificationBodyView(Context context, AttributeSet attrs)90     public CarNotificationBodyView(Context context, AttributeSet attrs) {
91         super(context, attrs);
92         init(attrs);
93     }
94 
CarNotificationBodyView(Context context, AttributeSet attrs, int defStyleAttr)95     public CarNotificationBodyView(Context context, AttributeSet attrs, int defStyleAttr) {
96         super(context, attrs, defStyleAttr);
97         init(attrs);
98     }
99 
CarNotificationBodyView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)100     public CarNotificationBodyView(
101             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
102         super(context, attrs, defStyleAttr, defStyleRes);
103         init(attrs);
104     }
105 
106     {
107         mDefaultPrimaryTextColor = getContext().getColor(R.color.primary_text_color);
108         mDefaultSecondaryTextColor = getContext().getColor(R.color.secondary_text_color);
109         mDefaultUseLauncherIcon = getResources().getBoolean(R.bool.config_useLauncherIcon);
110     }
111 
init(AttributeSet attrs)112     private void init(AttributeSet attrs) {
113         if (attrs != null) {
114             TypedArray attributes =
115                     getContext().obtainStyledAttributes(attrs, R.styleable.CarNotificationBodyView);
116             mShowBigIcon =
117                     attributes.getBoolean(R.styleable.CarNotificationBodyView_showBigIcon,
118                             /* defValue= */ false);
119             mMaxLines = attributes.getInteger(R.styleable.CarNotificationBodyView_maxLines,
120                     /* defValue= */ DEFAULT_MAX_LINES);
121             mIsHeadsUp =
122                     attributes.getBoolean(R.styleable.CarNotificationBodyView_isHeadsUp,
123                             /* defValue= */ false);
124             attributes.recycle();
125         }
126 
127         inflate(getContext(), mIsHeadsUp ? R.layout.car_headsup_notification_body_view
128                 : R.layout.car_notification_body_view, /* root= */ this);
129     }
130 
131     @Override
onFinishInflate()132     protected void onFinishInflate() {
133         super.onFinishInflate();
134         mTitleView = findViewById(R.id.notification_body_title);
135         mTitleIconView = findViewById(R.id.notification_body_title_icon);
136         mContentView = findViewById(R.id.notification_body_content);
137         mLargeIconView = findViewById(R.id.notification_body_icon);
138         mCountView = findViewById(R.id.message_count);
139         mTimeView = findViewById(R.id.time);
140         if (mTimeView != null) {
141             mTimeView.setShowRelativeTime(true);
142         }
143     }
144 
145     /**
146      * Binds the notification body.
147      *
148      * @param title     the primary text
149      * @param content   the secondary text, if this is null then content view will be hidden
150      * @param launcherIcon  the launcher icon drawable for notification's package.
151      *        If this and largeIcon are null then large icon view will be hidden.
152      * @param largeIcon the large icon, usually used for avatars.
153      *        If this and launcherIcon are null then large icon view will be hidden.
154      * @param countText text signifying the number of messages inside this notification
155      * @param when      wall clock time in milliseconds for the notification
156      */
bind(CharSequence title, @Nullable CharSequence content, StatusBarNotification sbn, @Nullable Icon largeIcon, @Nullable Drawable titleIcon, @Nullable CharSequence countText, @Nullable Long when)157     public void bind(CharSequence title, @Nullable CharSequence content,
158             StatusBarNotification sbn, @Nullable Icon largeIcon, @Nullable Drawable titleIcon,
159             @Nullable CharSequence countText, @Nullable Long when) {
160         setVisibility(View.VISIBLE);
161 
162         boolean useLauncherIcon = setUseLauncherIcon(sbn);
163         Drawable launcherIcon = loadAppLauncherIcon(sbn);
164         if (mLargeIconView != null) {
165             if (useLauncherIcon && launcherIcon != null) {
166                 mLargeIconView.setVisibility(View.VISIBLE);
167                 mLargeIconView.setImageDrawable(launcherIcon);
168             } else if (!useLauncherIcon && (mShowBigIcon || mDefaultUseLauncherIcon)) {
169                 if (largeIcon != null) {
170                     largeIcon.loadDrawableAsync(getContext(), drawable -> {
171                         mLargeIconView.setVisibility(View.VISIBLE);
172                         mLargeIconView.setImageDrawable(drawable);
173                     }, Handler.createAsync(Looper.myLooper()));
174                 } else {
175                     Log.w(TAG, "Notification with title=" + title
176                             + " did not specify a large icon");
177                     mLargeIconView.setVisibility(View.GONE);
178                     mLargeIconView.setImageDrawable(null);
179                 }
180             } else {
181                 mLargeIconView.setVisibility(View.GONE);
182                 mLargeIconView.setImageDrawable(null);
183             }
184         }
185 
186         if (mTitleView != null) {
187             if (!TextUtils.isEmpty(title)) {
188                 mTitleView.setVisibility(View.VISIBLE);
189                 mTitleView.setText(title);
190             } else {
191                 mTitleView.setVisibility(View.GONE);
192             }
193         }
194 
195         if (mTitleIconView != null) {
196             if (titleIcon != null) {
197                 mTitleIconView.setVisibility(View.VISIBLE);
198                 mTitleIconView.setImageDrawable(titleIcon);
199             } else {
200                 mTitleIconView.setVisibility(View.GONE);
201             }
202         }
203 
204         if (mContentView != null) {
205             if (!TextUtils.isEmpty(content)) {
206                 mContentView.setVisibility(View.VISIBLE);
207                 mContentView.setMaxLines(mMaxLines);
208                 mContentView.setText(content);
209             } else {
210                 mContentView.setVisibility(View.GONE);
211             }
212         }
213 
214         // optional field: time
215         if (mTimeView != null) {
216             if (when != null && !mIsHeadsUp) {
217                 mTimeView.setVisibility(View.VISIBLE);
218                 mTimeView.setTime(when);
219             } else {
220                 mTimeView.setVisibility(View.GONE);
221             }
222         }
223 
224         if (mCountView != null) {
225             if (countText != null) {
226                 mCountView.setVisibility(View.VISIBLE);
227                 mCountView.setText(countText);
228             } else {
229                 mCountView.setVisibility(View.GONE);
230             }
231         }
232     }
233 
234     /**
235      * Sets the primary text color.
236      */
setSecondaryTextColor(@olorInt int color)237     public void setSecondaryTextColor(@ColorInt int color) {
238         if (mContentView != null) {
239             mContentView.setTextColor(color);
240         }
241     }
242 
243     /**
244      * Sets max lines for the content view.
245      */
setContentMaxLines(int maxLines)246     public void setContentMaxLines(int maxLines) {
247         if (mContentView != null) {
248             mContentView.setMaxLines(maxLines);
249         }
250     }
251 
252     /**
253      * Sets the secondary text color.
254      */
setPrimaryTextColor(@olorInt int color)255     public void setPrimaryTextColor(@ColorInt int color) {
256         if (mTitleView != null) {
257             mTitleView.setTextColor(color);
258         }
259     }
260 
261     /**
262      * Sets the text color for the count field.
263      */
setCountTextColor(@olorInt int color)264     public void setCountTextColor(@ColorInt int color) {
265         if (mCountView != null) {
266             mCountView.setTextColor(color);
267         }
268     }
269 
270     /**
271      * Sets the alpha for the count field.
272      */
setCountTextAlpha(float alpha)273     public void setCountTextAlpha(float alpha) {
274         if (mCountView != null) {
275             mCountView.setAlpha(alpha);
276         }
277     }
278 
279     /**
280      * Sets the {@link OnClickListener} for the count field.
281      */
setCountOnClickListener(@ullable OnClickListener listener)282     public void setCountOnClickListener(@Nullable OnClickListener listener) {
283         if (mCountView != null) {
284             mCountView.setOnClickListener(listener);
285         }
286     }
287 
288     /**
289      * Sets the text color for the time field.
290      */
setTimeTextColor(@olorInt int color)291     public void setTimeTextColor(@ColorInt int color) {
292         if (mTimeView != null) {
293             mTimeView.setTextColor(color);
294         }
295     }
296 
297     /**
298      * Resets the notification actions empty for recycling.
299      */
reset()300     public void reset() {
301         setVisibility(View.GONE);
302         if (mTitleView != null) {
303             mTitleView.setVisibility(View.GONE);
304         }
305         if (mTitleIconView != null) {
306             mTitleIconView.setVisibility(View.GONE);
307         }
308         if (mContentView != null) {
309             setContentMaxLines(mMaxLines);
310             mContentView.setVisibility(View.GONE);
311         }
312         setPrimaryTextColor(mDefaultPrimaryTextColor);
313         setSecondaryTextColor(mDefaultSecondaryTextColor);
314         if (mTimeView != null) {
315             mTimeView.setVisibility(View.GONE);
316             mTimeView.setTime(0);
317             setTimeTextColor(mDefaultPrimaryTextColor);
318         }
319 
320         if (mCountView != null) {
321             mCountView.setVisibility(View.GONE);
322             mCountView.setText(null);
323             mCountView.setTextColor(mDefaultPrimaryTextColor);
324         }
325     }
326 
327     /**
328      * Returns true if the launcher icon should be used for a given notification.
329      */
setUseLauncherIcon(StatusBarNotification sbn)330     private boolean setUseLauncherIcon(StatusBarNotification sbn) {
331         Bundle notificationExtras = sbn.getNotification().extras;
332         if (notificationExtras == null) {
333             return getContext().getResources().getBoolean(R.bool.config_useLauncherIcon);
334         }
335 
336         if (notificationExtras.containsKey(EXTRA_USE_LAUNCHER_ICON)
337                 && NotificationUtils.isSystemApp(getContext(), sbn)) {
338             return notificationExtras.getBoolean(EXTRA_USE_LAUNCHER_ICON);
339         }
340         return getContext().getResources().getBoolean(R.bool.config_useLauncherIcon);
341     }
342 
343     @Nullable
loadAppLauncherIcon(StatusBarNotification sbn)344     private Drawable loadAppLauncherIcon(StatusBarNotification sbn) {
345         if (!setUseLauncherIcon(sbn)) {
346             return null;
347         }
348         Context packageContext = sbn.getPackageContext(getContext());
349         PackageManager pm = packageContext.getPackageManager();
350         return pm.getApplicationIcon(packageContext.getApplicationInfo());
351     }
352 
353     @VisibleForTesting
getTitleView()354     TextView getTitleView() {
355         return mTitleView;
356     }
357 
358     @VisibleForTesting
getContentView()359     TextView getContentView() {
360         return mContentView;
361     }
362 
363     @VisibleForTesting
getCountView()364     TextView getCountView() {
365         return mCountView;
366     }
367 
368     @VisibleForTesting
getTimeView()369     DateTimeView getTimeView() {
370         return mTimeView;
371     }
372 }
373