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