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