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 package com.android.car.notification.template;
17 
18 import static android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME;
19 
20 import android.annotation.ColorInt;
21 import android.annotation.Nullable;
22 import android.app.Notification;
23 import android.content.Context;
24 import android.content.pm.PackageManager;
25 import android.content.res.TypedArray;
26 import android.graphics.drawable.Drawable;
27 import android.os.Bundle;
28 import android.service.notification.StatusBarNotification;
29 import android.text.BidiFormatter;
30 import android.text.TextDirectionHeuristics;
31 import android.text.TextUtils;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.View;
35 import android.widget.DateTimeView;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.TextView;
39 
40 
41 import com.android.car.notification.AlertEntry;
42 import com.android.car.notification.R;
43 
44 /**
45  * Notification header view that contains the issuer app icon and name, and extra information.
46  */
47 public class CarNotificationHeaderView extends LinearLayout {
48 
49     private static final String TAG = "car_notification_header";
50 
51     private final int mDefaultTextColor;
52     private final String mSeparatorText;
53     private final boolean mUseLauncherIcon;
54 
55     private boolean mIsHeadsUp;
56     @Nullable
57     private ImageView mIconView;
58     @Nullable
59     private TextView mHeaderTextView;
60     @Nullable
61     private DateTimeView mTimeView;
62 
CarNotificationHeaderView(Context context)63     public CarNotificationHeaderView(Context context) {
64         super(context);
65     }
66 
CarNotificationHeaderView(Context context, AttributeSet attrs)67     public CarNotificationHeaderView(Context context, AttributeSet attrs) {
68         super(context, attrs);
69         init(attrs);
70     }
71 
CarNotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr)72     public CarNotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
73         super(context, attrs, defStyleAttr);
74         init(attrs);
75     }
76 
CarNotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)77     public CarNotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr,
78             int defStyleRes) {
79         super(context, attrs, defStyleAttr, defStyleRes);
80         init(attrs);
81     }
82 
83     {
84         mDefaultTextColor = getContext().getColor(R.color.primary_text_color);
85         mSeparatorText = getContext().getString(R.string.header_text_separator);
86         mUseLauncherIcon = getResources().getBoolean(R.bool.config_useLauncherIcon);
87     }
88 
init(AttributeSet attrs)89     private void init(AttributeSet attrs) {
90         TypedArray attributes =
91                 getContext().obtainStyledAttributes(attrs, R.styleable.CarNotificationHeaderView);
92         mIsHeadsUp =
93                 attributes.getBoolean(R.styleable.CarNotificationHeaderView_isHeadsUp,
94                         /* defValue= */ false);
95         inflate(getContext(), mIsHeadsUp ? R.layout.car_headsup_notification_header_view
96                 : R.layout.car_notification_header_view, this);
97         attributes.recycle();
98     }
99 
100     @Override
onFinishInflate()101     protected void onFinishInflate() {
102         super.onFinishInflate();
103         mIconView = findViewById(R.id.app_icon);
104         mHeaderTextView = findViewById(R.id.header_text);
105         mTimeView = findViewById(R.id.time);
106         if (mTimeView != null) {
107             mTimeView.setShowRelativeTime(true);
108         }
109     }
110 
111     /**
112      * Binds the notification header that contains the issuer app icon and name.
113      *
114      * @param alertEntry the notification to be bound.
115      * @param isInGroup  whether this notification is part of a grouped notification.
116      */
bind(AlertEntry alertEntry, boolean isInGroup)117     public void bind(AlertEntry alertEntry, boolean isInGroup) {
118         if (mUseLauncherIcon || isInGroup) {
119             // If the notification is part of a group, individual headers are not shown
120             // instead, there is a header for the entire group in the group notification template
121             // OR
122             // If launcher icon is used then hide header
123             setVisibility(View.GONE);
124             return;
125         }
126 
127         setVisibility(View.VISIBLE);
128 
129         Notification notification = alertEntry.getNotification();
130         StatusBarNotification sbn = alertEntry.getStatusBarNotification();
131 
132         Context packageContext = sbn.getPackageContext(getContext());
133 
134         // App icon
135         if (mIconView != null) {
136             mIconView.setVisibility(View.VISIBLE);
137             Drawable drawable = notification.getSmallIcon().loadDrawable(packageContext);
138             mIconView.setImageDrawable(drawable);
139         }
140 
141         StringBuilder stringBuilder = new StringBuilder();
142 
143         // App name
144         if (mHeaderTextView != null) {
145             mHeaderTextView.setVisibility(View.VISIBLE);
146         }
147         String appName = loadHeaderAppName(sbn);
148 
149         if (mIsHeadsUp) {
150             if (mHeaderTextView != null) {
151                 mHeaderTextView.setText(appName);
152             }
153             if (mTimeView != null) {
154                 mTimeView.setVisibility(View.GONE);
155             }
156             return;
157         }
158 
159         stringBuilder.append(appName);
160         Bundle extras = notification.extras;
161 
162         // Optional field: sub text
163         if (!TextUtils.isEmpty(extras.getCharSequence(Notification.EXTRA_SUB_TEXT))) {
164             stringBuilder.append(mSeparatorText);
165             stringBuilder.append(extras.getCharSequence(Notification.EXTRA_SUB_TEXT));
166         }
167 
168         // Optional field: content info
169         if (!TextUtils.isEmpty(extras.getCharSequence(Notification.EXTRA_INFO_TEXT))) {
170             stringBuilder.append(mSeparatorText);
171             stringBuilder.append(extras.getCharSequence(Notification.EXTRA_INFO_TEXT));
172         }
173 
174         // Optional field: time
175         if (notification.showsTime()) {
176             stringBuilder.append(mSeparatorText);
177             if (mTimeView != null) {
178                 mTimeView.setVisibility(View.VISIBLE);
179                 mTimeView.setTime(notification.when);
180             }
181         }
182 
183         mHeaderTextView.setText(BidiFormatter.getInstance().unicodeWrap(stringBuilder,
184                 TextDirectionHeuristics.LOCALE));
185     }
186 
187     /**
188      * Sets the color for the small icon.
189      */
setSmallIconColor(@olorInt int color)190     public void setSmallIconColor(@ColorInt int color) {
191         if (mIconView != null) {
192             mIconView.setColorFilter(color);
193         }
194     }
195 
196     /**
197      * Sets the header text color.
198      */
setHeaderTextColor(@olorInt int color)199     public void setHeaderTextColor(@ColorInt int color) {
200         if (mHeaderTextView != null) {
201             mHeaderTextView.setTextColor(color);
202         }
203     }
204 
205     /**
206      * Sets the text color for the time field.
207      */
setTimeTextColor(@olorInt int color)208     public void setTimeTextColor(@ColorInt int color) {
209         if (mTimeView != null) {
210             mTimeView.setTextColor(color);
211         }
212     }
213 
214     /**
215      * Resets the notification header empty.
216      */
reset()217     public void reset() {
218         if (mIconView != null) {
219             mIconView.setVisibility(View.GONE);
220             mIconView.setImageDrawable(null);
221             setSmallIconColor(mDefaultTextColor);
222         }
223 
224         if (mHeaderTextView != null) {
225             mHeaderTextView.setVisibility(View.GONE);
226             mHeaderTextView.setText(null);
227             setHeaderTextColor(mDefaultTextColor);
228         }
229 
230         if (mTimeView != null) {
231             mTimeView.setVisibility(View.GONE);
232             mTimeView.setTime(0);
233             setTimeTextColor(mDefaultTextColor);
234         }
235     }
236 
237     /**
238      * Fetches the application label given the notification. If the notification is a system
239      * generated message notification that is posting on behalf of another application, that
240      * application's name is used.
241      *
242      * The system permission {@link android.Manifest.permission#SUBSTITUTE_NOTIFICATION_APP_NAME}
243      * is required to post on behalf of another application. The notification extra should also
244      * contain a key {@link Notification#EXTRA_SUBSTITUTE_APP_NAME} with the value of
245      * the appropriate application name.
246      *
247      * @return application label. Returns {@code null} when application name is not found.
248      */
249     @Nullable
loadHeaderAppName(StatusBarNotification sbn)250     private String loadHeaderAppName(StatusBarNotification sbn) {
251         final Context packageContext = sbn.getPackageContext(mContext);
252         final PackageManager pm = packageContext.getPackageManager();
253         final Notification notification = sbn.getNotification();
254         CharSequence name = pm.getApplicationLabel(packageContext.getApplicationInfo());
255         if (notification.extras.containsKey(EXTRA_SUBSTITUTE_APP_NAME)) {
256             // Only system packages which lump together a bunch of unrelated stuff may substitute a
257             // different name to make the purpose of the notification more clear
258             // The correct package label should always be accessible via SystemUI
259             final String subName = notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME);
260             final String pkg = sbn.getPackageName();
261             if (PackageManager.PERMISSION_GRANTED == pm.checkPermission(
262                     android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME, pkg)) {
263                 name = subName;
264             } else {
265                 Log.w(TAG, "warning: pkg "
266                         + pkg + " attempting to substitute app name '" + subName
267                         + "' without holding perm "
268                         + android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME);
269             }
270         }
271         if (TextUtils.isEmpty(name)) {
272             return null;
273         }
274         return String.valueOf(name);
275     }
276 }
277