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.util.AttributeSet;
25 import android.view.View;
26 import android.widget.Button;
27 import android.widget.RelativeLayout;
28 
29 import androidx.annotation.Nullable;
30 import androidx.annotation.VisibleForTesting;
31 
32 import com.android.car.assist.client.CarAssistUtils;
33 import com.android.car.notification.AlertEntry;
34 import com.android.car.notification.NotificationClickHandlerFactory;
35 import com.android.car.notification.NotificationDataManager;
36 import com.android.car.notification.PreprocessingManager;
37 import com.android.car.notification.R;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Notification actions view that contains the buttons that fire actions.
44  */
45 public class CarNotificationActionsView extends RelativeLayout implements
46         PreprocessingManager.CallStateListener {
47 
48     private static final String TAG = "CarNotificationAction";
49     // Maximum 3 actions
50     // https://developer.android.com/reference/android/app/Notification.Builder.html#addAction
51     @VisibleForTesting
52     static final int MAX_NUM_ACTIONS = 3;
53     @VisibleForTesting
54     static final int FIRST_MESSAGE_ACTION_BUTTON_INDEX = 0;
55     @VisibleForTesting
56     static final int SECOND_MESSAGE_ACTION_BUTTON_INDEX = 1;
57 
58     private final List<Button> mActionButtons = new ArrayList<>();
59 
60     private boolean mIsCategoryCall;
61     private boolean mIsInCall;
62     private Context mContext;
63 
CarNotificationActionsView(Context context)64     public CarNotificationActionsView(Context context) {
65         super(context);
66         PreprocessingManager.getInstance(context).addCallStateListener(this::onCallStateChanged);
67         init(/* attrs= */ null);
68     }
69 
CarNotificationActionsView(Context context, AttributeSet attrs)70     public CarNotificationActionsView(Context context, AttributeSet attrs) {
71         super(context, attrs);
72         mContext = context;
73         init(attrs);
74     }
75 
CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr)76     public CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr) {
77         super(context, attrs, defStyleAttr);
78         mContext = context;
79         init(attrs);
80     }
81 
CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)82     public CarNotificationActionsView(Context context, AttributeSet attrs, int defStyleAttr,
83             int defStyleRes) {
84         super(context, attrs, defStyleAttr, defStyleRes);
85         mContext = context;
86         init(attrs);
87     }
88 
init(@ullable AttributeSet attrs)89     private void init(@Nullable AttributeSet attrs) {
90         if (attrs != null) {
91             TypedArray attributes =
92                     getContext().obtainStyledAttributes(attrs, R.styleable.CarNotificationActionsView);
93             mIsCategoryCall =
94                     attributes.getBoolean(R.styleable.CarNotificationActionsView_categoryCall,
95                             /* default value= */false);
96             attributes.recycle();
97         }
98         PreprocessingManager.getInstance(getContext()).addCallStateListener(
99                 this::onCallStateChanged);
100         inflate(getContext(), R.layout.car_notification_actions_view, /* root= */ this);
101     }
102 
103     /**
104      * Binds the notification action buttons.
105      *
106      * @param clickHandlerFactory factory class used to generate {@link OnClickListener}s.
107      * @param alertEntry          the notification that contains the actions.
108      */
bind(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)109     public void bind(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry) {
110 
111         Notification notification = alertEntry.getNotification();
112         Notification.Action[] actions = notification.actions;
113         if (actions == null || actions.length == 0) {
114             return;
115         }
116 
117         if (CarAssistUtils.isCarCompatibleMessagingNotification(
118                 alertEntry.getStatusBarNotification())) {
119             createPlayButton(clickHandlerFactory, alertEntry);
120             createMuteButton(clickHandlerFactory, alertEntry);
121             return;
122         }
123 
124         int length = Math.min(actions.length, MAX_NUM_ACTIONS);
125         for (int i = 0; i < length; i++) {
126             Notification.Action action = actions[i];
127             Button button = mActionButtons.get(i);
128             button.setVisibility(View.VISIBLE);
129             // clear spannables and only use the text
130             button.setText(action.title.toString());
131 
132             if (action.actionIntent != null) {
133                 button.setOnClickListener(clickHandlerFactory.getActionClickHandler(alertEntry, i));
134             }
135         }
136 
137         if (mIsCategoryCall) {
138             Drawable acceptButton = mContext.getResources().getDrawable(
139                     R.drawable.call_action_button_background);
140             acceptButton.setColorFilter(
141                     new PorterDuffColorFilter(mContext.getColor(R.color.call_accept_button),
142                             PorterDuff.Mode.SRC_IN));
143             mActionButtons.get(0).setBackground(acceptButton);
144 
145             Drawable declineButton = mContext.getResources().getDrawable(
146                     R.drawable.call_action_button_background);
147             declineButton.setColorFilter(
148                     new PorterDuffColorFilter(mContext.getColor(R.color.call_decline_button),
149                             PorterDuff.Mode.SRC_IN));
150             mActionButtons.get(1).setBackground(declineButton);
151         }
152     }
153 
154     /**
155      * Resets the notification actions empty for recycling.
156      */
reset()157     public void reset() {
158         for (Button button : mActionButtons) {
159             button.setVisibility(View.GONE);
160             button.setText(null);
161             button.setOnClickListener(null);
162         }
163         PreprocessingManager.getInstance(getContext()).removeCallStateListener(
164                 this::onCallStateChanged);
165     }
166 
167     @Override
onFinishInflate()168     protected void onFinishInflate() {
169         super.onFinishInflate();
170         mActionButtons.add(findViewById(R.id.action_1));
171         mActionButtons.add(findViewById(R.id.action_2));
172         mActionButtons.add(findViewById(R.id.action_3));
173     }
174 
175     @VisibleForTesting
getActionButtons()176     List<Button> getActionButtons() {
177         return mActionButtons;
178     }
179 
180     @VisibleForTesting
setCategoryIsCall(boolean isCall)181     void setCategoryIsCall(boolean isCall) {
182         mIsCategoryCall = isCall;
183     }
184 
185     /**
186      * The Play button triggers the assistant to read the message aloud, optionally prompting the
187      * user to reply to the message afterwards.
188      */
createPlayButton(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)189     private void createPlayButton(NotificationClickHandlerFactory clickHandlerFactory,
190             AlertEntry alertEntry) {
191         if (mIsInCall) return;
192 
193         Button button = mActionButtons.get(FIRST_MESSAGE_ACTION_BUTTON_INDEX);
194         button.setText(mContext.getString(R.string.assist_action_play_label));
195         button.setVisibility(View.VISIBLE);
196         button.setOnClickListener(
197                 clickHandlerFactory.getPlayClickHandler(alertEntry));
198     }
199 
200     /**
201      * The Mute button allows users to toggle whether or not incoming notification with the same
202      * statusBarNotification key will be shown with a HUN and trigger a notification sound.
203      */
createMuteButton(NotificationClickHandlerFactory clickHandlerFactory, AlertEntry alertEntry)204     private void createMuteButton(NotificationClickHandlerFactory clickHandlerFactory,
205             AlertEntry alertEntry) {
206         int index = SECOND_MESSAGE_ACTION_BUTTON_INDEX;
207         if (mIsInCall) index = FIRST_MESSAGE_ACTION_BUTTON_INDEX;
208 
209         Button button = mActionButtons.get(index);
210         NotificationDataManager manager = clickHandlerFactory.getNotificationDataManager();
211         button.setText((manager != null && manager.isMessageNotificationMuted(alertEntry))
212                 ? mContext.getString(R.string.action_unmute_long)
213                 : mContext.getString(R.string.action_mute_long));
214         button.setVisibility(View.VISIBLE);
215         button.setOnClickListener(clickHandlerFactory.getMuteClickHandler(button, alertEntry));
216     }
217 
218     /** Implementation of {@link PreprocessingManager.CallStateListener} **/
219     @Override
onCallStateChanged(boolean isInCall)220     public void onCallStateChanged(boolean isInCall) {
221         mIsInCall = isInCall;
222     }
223 }
224