• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.app.Notification;
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.PorterDuff;
22 import android.graphics.PorterDuffColorFilter;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.Icon;
25 import android.os.Build;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.View;
31 import android.widget.LinearLayout;
32 
33 import androidx.annotation.ColorInt;
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.VisibleForTesting;
37 
38 import com.android.car.assist.client.CarAssistUtils;
39 import com.android.car.notification.AlertEntry;
40 import com.android.car.notification.NotificationClickHandlerFactory;
41 import com.android.car.notification.NotificationDataManager;
42 import com.android.car.notification.PreprocessingManager;
43 import com.android.car.notification.R;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 /**
49  * Notification actions view that contains the buttons that fire actions.
50  */
51 public class CarNotificationActionsView extends LinearLayout implements
52         PreprocessingManager.CallStateListener {
53     private static final boolean DEBUG = Build.IS_DEBUGGABLE;
54     private static final String TAG = "CarNotificationActionsView";
55 
56     // Maximum 3 actions
57     // https://developer.android.com/reference/android/app/Notification.Builder.html#addAction
58     @VisibleForTesting
59     static final int MAX_NUM_ACTIONS = 3;
60     @VisibleForTesting
61     static final int FIRST_MESSAGE_ACTION_BUTTON_INDEX = 0;
62     @VisibleForTesting
63     static final int SECOND_MESSAGE_ACTION_BUTTON_INDEX = 1;
64     @VisibleForTesting
65     static final int THIRD_MESSAGE_ACTION_BUTTON_INDEX = 2;
66 
67     private final List<CarNotificationActionButton> mActionButtons = new ArrayList<>();
68     private final Context mContext;
69     private final CarAssistUtils mCarAssistUtils;
70     private final Drawable mActionButtonBackground;
71     private final Drawable mCallButtonBackground;
72     private final Drawable mDeclineButtonBackground;
73     private final Drawable mUnmuteButtonBackground;
74     private final String mReplyButtonText;
75     private final String mPlayButtonText;
76     private final String mMuteText;
77     private final String mUnmuteText;
78     @ColorInt
79     private final int mUnmuteTextColor;
80     private final boolean mEnableDirectReply;
81     private final boolean mEnablePlay;
82 
83     @VisibleForTesting
84     final Drawable mPlayButtonDrawable;
85     @VisibleForTesting
86     final Drawable mReplyButtonDrawable;
87     @VisibleForTesting
88     final Drawable mMuteButtonDrawable;
89     @VisibleForTesting
90     final Drawable mUnmuteButtonDrawable;
91 
92 
93     private NotificationDataManager mNotificationDataManager;
94     private NotificationClickHandlerFactory mNotificationClickHandlerFactory;
95     private AlertEntry mAlertEntry;
96     private boolean mIsCategoryCall;
97     private boolean mIsInCall;
98 
CarNotificationActionsView(Context context)99     public CarNotificationActionsView(Context context) {
100         this(context, /* attrs= */ null);
101     }
102 
CarNotificationActionsView(Context context, AttributeSet attrs)103     public CarNotificationActionsView(Context context, AttributeSet attrs) {
104         this(context, attrs, /* defStyleAttr= */ 0);
105     }
106 
CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr)107     public CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr) {
108         this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
109     }
110 
CarNotificationActionsView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)111     public CarNotificationActionsView(
112             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
113         this(context, attrs, defStyleAttr, defStyleRes, new CarAssistUtils(context));
114     }
115 
116     @VisibleForTesting
CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, @NonNull CarAssistUtils carAssistUtils)117     CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr,
118             int defStyleRes, @NonNull CarAssistUtils carAssistUtils) {
119         super(context, attrs, defStyleAttr, defStyleRes);
120 
121         mContext = context;
122         mCarAssistUtils = carAssistUtils;
123         mNotificationDataManager = NotificationDataManager.getInstance();
124         mActionButtonBackground = mContext.getDrawable(R.drawable.action_button_background);
125         mCallButtonBackground = mContext.getDrawable(R.drawable.call_action_button_background);
126         mCallButtonBackground.setColorFilter(
127                 new PorterDuffColorFilter(mContext.getColor(R.color.call_accept_button),
128                         PorterDuff.Mode.SRC_IN));
129         mDeclineButtonBackground = mContext.getDrawable(R.drawable.call_action_button_background);
130         mDeclineButtonBackground.setColorFilter(
131                 new PorterDuffColorFilter(mContext.getColor(R.color.call_decline_button),
132                         PorterDuff.Mode.SRC_IN));
133         mUnmuteButtonBackground = mContext.getDrawable(R.drawable.call_action_button_background);
134         mUnmuteButtonBackground.setColorFilter(
135                 new PorterDuffColorFilter(mContext.getColor(R.color.unmute_button),
136                         PorterDuff.Mode.SRC_IN));
137         mPlayButtonText = mContext.getString(R.string.assist_action_play_label);
138         mReplyButtonText = mContext.getString(R.string.assist_action_reply_label);
139         mMuteText = mContext.getString(R.string.action_mute_short);
140         mUnmuteText = mContext.getString(R.string.action_unmute_short);
141         mPlayButtonDrawable = mContext.getDrawable(R.drawable.ic_play_arrow);
142         mReplyButtonDrawable = mContext.getDrawable(R.drawable.ic_reply);
143         mMuteButtonDrawable = mContext.getDrawable(R.drawable.ic_mute);
144         mUnmuteButtonDrawable = mContext.getDrawable(R.drawable.ic_unmute);
145         mEnablePlay =
146                 mContext.getResources().getBoolean(R.bool.config_enableMessageNotificationPlay);
147         mEnableDirectReply = mContext.getResources()
148                 .getBoolean(R.bool.config_enableMessageNotificationDirectReply);
149         mUnmuteTextColor = mContext.getColor(R.color.dark_icon_tint);
150         init(attrs);
151     }
152 
153     @VisibleForTesting
setNotificationDataManager(NotificationDataManager notificationDataManager)154     void setNotificationDataManager(NotificationDataManager notificationDataManager) {
155         mNotificationDataManager = notificationDataManager;
156     }
157 
init(@ullable AttributeSet attrs)158     private void init(@Nullable AttributeSet attrs) {
159         if (attrs != null) {
160             TypedArray attributes =
161                     mContext.obtainStyledAttributes(attrs, R.styleable.CarNotificationActionsView);
162             mIsCategoryCall =
163                     attributes.getBoolean(R.styleable.CarNotificationActionsView_categoryCall,
164                             /* defaultValue= */ false);
165             attributes.recycle();
166         }
167 
168         inflate(mContext, R.layout.car_notification_actions_view, /* root= */ this);
169     }
170 
171     /**
172      * Binds the notification action buttons.
173      *
174      * @param clickHandlerFactory factory class used to generate {@link OnClickListener}s.
175      * @param alertEntry          the notification that contains the actions.
176      */
bind(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)177     public void bind(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry) {
178         Notification notification = alertEntry.getNotification();
179         Notification.Action[] actions = notification.actions;
180         if (actions == null || actions.length == 0) {
181             setVisibility(View.GONE);
182             return;
183         }
184 
185         PreprocessingManager.getInstance(mContext).addCallStateListener(this);
186 
187         mNotificationClickHandlerFactory = clickHandlerFactory;
188         mAlertEntry = alertEntry;
189 
190         setVisibility(View.VISIBLE);
191 
192         if (CarAssistUtils.isCarCompatibleMessagingNotification(
193                 alertEntry.getStatusBarNotification())) {
194             boolean canPlayMessage = mEnablePlay && mCarAssistUtils.hasActiveAssistant()
195                     || mCarAssistUtils.isFallbackAssistantEnabled();
196             boolean canReplyMessage = mEnableDirectReply && mCarAssistUtils.hasActiveAssistant()
197                     && clickHandlerFactory.getReplyAction(alertEntry.getNotification()) != null;
198             if (canPlayMessage) {
199                 createPlayButton(clickHandlerFactory, alertEntry);
200             }
201             if (canReplyMessage) {
202                 createReplyButton(clickHandlerFactory, alertEntry);
203             }
204             createMuteButton(clickHandlerFactory, alertEntry, canReplyMessage);
205             return;
206         }
207 
208         Context packageContext = alertEntry.getStatusBarNotification().getPackageContext(mContext);
209         int length = Math.min(actions.length, MAX_NUM_ACTIONS);
210         for (int i = 0; i < length; i++) {
211             Notification.Action action = actions[i];
212             CarNotificationActionButton button = mActionButtons.get(i);
213             button.setVisibility(View.VISIBLE);
214             // clear spannables and only use the text
215             button.setText(action.title.toString());
216 
217             if (action.actionIntent != null) {
218                 button.setOnClickListener(clickHandlerFactory.getActionClickHandler(alertEntry, i));
219             }
220 
221             Icon icon = action.getIcon();
222             if (icon != null) {
223                 icon.loadDrawableAsync(packageContext, button::setImageDrawable, getAsyncHandler());
224             }
225         }
226 
227         if (mIsCategoryCall) {
228             mActionButtons.get(0).setBackground(mCallButtonBackground);
229             mActionButtons.get(1).setBackground(mDeclineButtonBackground);
230         }
231     }
232 
233     /**
234      * Resets the notification actions empty for recycling.
235      */
reset()236     public void reset() {
237         resetButtons();
238         PreprocessingManager.getInstance(getContext()).removeCallStateListener(this);
239         mAlertEntry = null;
240         mNotificationClickHandlerFactory = null;
241     }
242 
resetButtons()243     private void resetButtons() {
244         for (CarNotificationActionButton button : mActionButtons) {
245             button.setVisibility(View.GONE);
246             button.setText(null);
247             button.setImageDrawable(null);
248             button.setOnClickListener(null);
249         }
250     }
251 
252     @Override
onFinishInflate()253     protected void onFinishInflate() {
254         super.onFinishInflate();
255         mActionButtons.add(findViewById(R.id.action_1));
256         mActionButtons.add(findViewById(R.id.action_2));
257         mActionButtons.add(findViewById(R.id.action_3));
258     }
259 
260     @VisibleForTesting
getActionButtons()261     List<CarNotificationActionButton> getActionButtons() {
262         return mActionButtons;
263     }
264 
265     @VisibleForTesting
setCategoryIsCall(boolean isCall)266     void setCategoryIsCall(boolean isCall) {
267         mIsCategoryCall = isCall;
268     }
269 
270     /**
271      * The Play button triggers the assistant to read the message aloud, optionally prompting the
272      * user to reply to the message afterwards.
273      */
createPlayButton(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)274     private void createPlayButton(NotificationClickHandlerFactory clickHandlerFactory,
275             AlertEntry alertEntry) {
276         if (mIsInCall) return;
277 
278         CarNotificationActionButton button = mActionButtons.get(FIRST_MESSAGE_ACTION_BUTTON_INDEX);
279         button.setText(mPlayButtonText);
280         button.setImageDrawable(mPlayButtonDrawable);
281         button.setVisibility(View.VISIBLE);
282         button.setOnClickListener(
283                 clickHandlerFactory.getPlayClickHandler(alertEntry));
284     }
285 
286     /**
287      * The Reply button triggers the assistant to read the message aloud, optionally prompting the
288      * user to reply to the message afterwards.
289      */
createReplyButton(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)290     private void createReplyButton(NotificationClickHandlerFactory clickHandlerFactory,
291             AlertEntry alertEntry) {
292         if (mIsInCall) return;
293         int index = SECOND_MESSAGE_ACTION_BUTTON_INDEX;
294 
295         CarNotificationActionButton button = mActionButtons.get(index);
296         button.setText(mReplyButtonText);
297         button.setImageDrawable(mReplyButtonDrawable);
298         button.setVisibility(View.VISIBLE);
299         button.setOnClickListener(
300                 clickHandlerFactory.getReplyClickHandler(alertEntry));
301     }
302 
303     /**
304      * The Mute button allows users to toggle whether or not incoming notification with the same
305      * statusBarNotification key will be shown with a HUN and trigger a notification sound.
306      */
createMuteButton(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry, boolean canReply)307     private void createMuteButton(NotificationClickHandlerFactory clickHandlerFactory,
308             AlertEntry alertEntry, boolean canReply) {
309         int index = THIRD_MESSAGE_ACTION_BUTTON_INDEX;
310         if (!canReply) index = SECOND_MESSAGE_ACTION_BUTTON_INDEX;
311         if (mIsInCall) index = FIRST_MESSAGE_ACTION_BUTTON_INDEX;
312 
313         CarNotificationActionButton button = mActionButtons.get(index);
314         setMuteStatus(button, mNotificationDataManager.isMessageNotificationMuted(alertEntry));
315         button.setVisibility(View.VISIBLE);
316         button.setOnClickListener(
317                 clickHandlerFactory.getMuteClickHandler(button, alertEntry, this::setMuteStatus));
318     }
319 
setMuteStatus(CarNotificationActionButton button, boolean isMuted)320     private void setMuteStatus(CarNotificationActionButton button, boolean isMuted) {
321         button.setText(isMuted ? mUnmuteText : mMuteText);
322         button.setTextColor(isMuted ? mUnmuteTextColor : button.getDefaultTextColor());
323         button.setImageDrawable(isMuted ? mUnmuteButtonDrawable : mMuteButtonDrawable);
324         button.setBackground(isMuted ? mUnmuteButtonBackground : mActionButtonBackground);
325     }
326 
327     /** Implementation of {@link PreprocessingManager.CallStateListener} **/
328     @Override
onCallStateChanged(boolean isInCall)329     public void onCallStateChanged(boolean isInCall) {
330         if (mIsInCall == isInCall) {
331             return;
332         }
333 
334         mIsInCall = isInCall;
335 
336         if (mNotificationClickHandlerFactory == null || mAlertEntry == null) {
337             return;
338         }
339 
340         if (DEBUG) {
341             if (isInCall) {
342                 Log.d(TAG, "Call state activated: " + mAlertEntry);
343             } else {
344                 Log.d(TAG, "Call state deactivated: " + mAlertEntry);
345             }
346         }
347 
348         int focusedButtonIndex = getFocusedButtonIndex();
349         resetButtons();
350         bind(mNotificationClickHandlerFactory, mAlertEntry);
351 
352         // If not in touch mode and action button had focus, then have original or preceding button
353         // request focus.
354         if (!isInTouchMode() && focusedButtonIndex != -1) {
355             for (int i = focusedButtonIndex; i != -1; i--) {
356                 CarNotificationActionButton button = getActionButtons().get(i);
357                 if (button.getVisibility() == View.VISIBLE) {
358                     button.requestFocus();
359                     return;
360                 }
361             }
362         }
363     }
364 
getFocusedButtonIndex()365     private int getFocusedButtonIndex() {
366         for (int i = FIRST_MESSAGE_ACTION_BUTTON_INDEX; i <= THIRD_MESSAGE_ACTION_BUTTON_INDEX;
367                 i++) {
368             boolean hasFocus = getActionButtons().get(i).hasFocus();
369             if (hasFocus) {
370                 return i;
371             }
372         }
373         return -1;
374     }
375 
376     /** Will be overwritten by test to return a mock Handler **/
377     @VisibleForTesting
getAsyncHandler()378     Handler getAsyncHandler() {
379         return Handler.createAsync(Looper.myLooper());
380     }
381 }
382