1 /*
2  * Copyright (C) 2021 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 
17 package com.android.systemui.statusbar;
18 
19 import static android.app.Flags.lifetimeExtensionRefactor;
20 
21 import android.annotation.NonNull;
22 import android.app.Notification;
23 import android.app.RemoteInputHistoryItem;
24 import android.content.Context;
25 import android.net.Uri;
26 import android.os.Parcelable;
27 import android.service.notification.StatusBarNotification;
28 import android.text.TextUtils;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.systemui.dagger.SysUISingleton;
32 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
33 
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.stream.Stream;
37 
38 import javax.inject.Inject;
39 
40 /**
41  * A helper class which will augment the notifications using arguments and other information
42  * accessible to the entry in order to provide intermediate remote input states.
43  */
44 @SysUISingleton
45 public class RemoteInputNotificationRebuilder {
46 
47     private final Context mContext;
48 
49     @Inject
RemoteInputNotificationRebuilder(Context context)50     RemoteInputNotificationRebuilder(Context context) {
51         mContext = context;
52     }
53 
54     /**
55      * When a smart reply is sent off to the app, we insert the text into the remote input history,
56      * and show a spinner to indicate that the app has yet to respond.
57      */
58     @NonNull
rebuildForSendingSmartReply(NotificationEntry entry, CharSequence reply)59     public StatusBarNotification rebuildForSendingSmartReply(NotificationEntry entry,
60             CharSequence reply) {
61         return rebuildWithRemoteInputInserted(entry, reply,
62                 true /* showSpinner */,
63                 null /* mimeType */, null /* uri */);
64     }
65 
66     /**
67      * When the app cancels a notification in response to a smart reply, we remove the spinner
68      * and leave the previously-added reply.  This is the lifetime-extended appearance of the
69      * notification.
70      */
71     @NonNull
rebuildForCanceledSmartReplies( NotificationEntry entry)72     public StatusBarNotification rebuildForCanceledSmartReplies(
73             NotificationEntry entry) {
74         return rebuildWithExistingReplies(entry);
75     }
76 
77     /**
78      * Rebuilds to include any previously-added remote input replies.
79      * For when the app cancels a notification that has already been lifetime extended.
80      */
81     @NonNull
rebuildWithExistingReplies(NotificationEntry entry)82     public StatusBarNotification rebuildWithExistingReplies(NotificationEntry entry) {
83         return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
84                 false /* showSpinner */, null /* mimeType */, null /* uri */);
85     }
86 
87     /**
88      * When the app cancels a notification in response to a remote input reply, we update the
89      * notification with the reply text and/or attachment. This is the lifetime-extended
90      * appearance of the notification.
91      */
92     @NonNull
rebuildForRemoteInputReply(NotificationEntry entry)93     public StatusBarNotification rebuildForRemoteInputReply(NotificationEntry entry) {
94         CharSequence remoteInputText = entry.remoteInputText;
95         if (TextUtils.isEmpty(remoteInputText)) {
96             remoteInputText = entry.remoteInputTextWhenReset;
97         }
98         String remoteInputMimeType = entry.remoteInputMimeType;
99         Uri remoteInputUri = entry.remoteInputUri;
100         StatusBarNotification newSbn = rebuildWithRemoteInputInserted(entry,
101                 remoteInputText, false /* showSpinner */, remoteInputMimeType,
102                 remoteInputUri);
103         return newSbn;
104     }
105 
106     /** Inner method for generating the SBN */
107     @VisibleForTesting
108     @NonNull
rebuildWithRemoteInputInserted(NotificationEntry entry, CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri)109     StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
110             CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
111         StatusBarNotification sbn = entry.getSbn();
112         Notification.Builder b = Notification.Builder
113                 .recoverBuilder(mContext, sbn.getNotification().clone());
114 
115         if (lifetimeExtensionRefactor()) {
116             if (entry.remoteInputs == null) {
117                 entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>();
118             }
119 
120             // Append new remote input information to remoteInputs list
121             if (remoteInputText != null || uri != null) {
122                 RemoteInputHistoryItem newItem = uri != null
123                         ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
124                         : new RemoteInputHistoryItem(remoteInputText);
125                 // The list is latest-first, so new elements should be added as the first element.
126                 entry.remoteInputs.add(0, newItem);
127             }
128 
129             // Read the whole remoteInputs list from the entry, then append all of those to the sbn.
130             Parcelable[] oldHistoryItems = sbn.getNotification().extras
131                     .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
132 
133             RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
134                     ? Stream.concat(
135                             entry.remoteInputs.stream(),
136                             Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
137                     .toArray(RemoteInputHistoryItem[]::new)
138                     : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new);
139             b.setRemoteInputHistory(newHistoryItems);
140 
141         } else {
142             if (remoteInputText != null || uri != null) {
143                 RemoteInputHistoryItem newItem = uri != null
144                         ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
145                         : new RemoteInputHistoryItem(remoteInputText);
146                 Parcelable[] oldHistoryItems = sbn.getNotification().extras
147                         .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
148                 RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
149                         ? Stream.concat(
150                                 Stream.of(newItem),
151                                 Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
152                         .toArray(RemoteInputHistoryItem[]::new)
153                         : new RemoteInputHistoryItem[]{newItem};
154                 b.setRemoteInputHistory(newHistoryItems);
155             }
156         }
157         b.setShowRemoteInputSpinner(showSpinner);
158         b.setHideSmartReplies(true);
159 
160         Notification newNotification = b.build();
161 
162         // Undo any compatibility view inflation
163         newNotification.contentView = sbn.getNotification().contentView;
164         newNotification.bigContentView = sbn.getNotification().bigContentView;
165         newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
166 
167         return new StatusBarNotification(
168                 sbn.getPackageName(),
169                 sbn.getOpPkg(),
170                 sbn.getId(),
171                 sbn.getTag(),
172                 sbn.getUid(),
173                 sbn.getInitialPid(),
174                 newNotification,
175                 sbn.getUser(),
176                 sbn.getOverrideGroupKey(),
177                 sbn.getPostTime());
178     }
179 
180 
181 }
182