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