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