blob: cc5de65293581cfc1876286636eb4568baef95dc [file] [log] [blame]
Julia Reynolds8582df52020-04-24 18:30:59 -04001/*
2 * Copyright (C) 2020 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
17package com.android.systemui.statusbar.notification.row;
18
19import static android.app.Notification.EXTRA_IS_GROUP_CONVERSATION;
Julia Reynolds8582df52020-04-24 18:30:59 -040020
21import static java.lang.annotation.RetentionPolicy.SOURCE;
22
23import android.annotation.IntDef;
24import android.app.INotificationManager;
25import android.app.Notification;
26import android.app.NotificationChannel;
27import android.app.NotificationChannelGroup;
28import android.content.Context;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.graphics.drawable.Drawable;
32import android.graphics.drawable.Icon;
33import android.os.Bundle;
34import android.os.Parcelable;
35import android.os.RemoteException;
36import android.service.notification.StatusBarNotification;
37import android.text.TextUtils;
Julia Reynolds8582df52020-04-24 18:30:59 -040038import android.util.AttributeSet;
39import android.view.View;
40import android.view.accessibility.AccessibilityEvent;
41import android.widget.ImageView;
42import android.widget.LinearLayout;
43import android.widget.TextView;
44
45import com.android.internal.annotations.VisibleForTesting;
Julia Reynolds8582df52020-04-24 18:30:59 -040046import com.android.systemui.R;
47import com.android.systemui.statusbar.notification.collection.NotificationEntry;
48
49import java.lang.annotation.Retention;
50import java.util.List;
51import java.util.Set;
52
53/**
54 * The guts of a conversation notification that doesn't use valid shortcuts that is revealed when
55 * performing a long press.
56 */
57public class PartialConversationInfo extends LinearLayout implements
58 NotificationGuts.GutsContent {
59 private static final String TAG = "PartialConvoGuts";
60
61 private INotificationManager mINotificationManager;
62 private PackageManager mPm;
63 private String mPackageName;
64 private String mAppName;
65 private int mAppUid;
66 private String mDelegatePkg;
67 private NotificationChannel mNotificationChannel;
68 private StatusBarNotification mSbn;
69 private boolean mIsDeviceProvisioned;
70 private boolean mIsNonBlockable;
71 private Set<NotificationChannel> mUniqueChannelsInRow;
72 private Drawable mPkgIcon;
73
74 private @Action int mSelectedAction = -1;
75 private boolean mPressedApply;
76 private boolean mPresentingChannelEditorDialog = false;
77
78 private NotificationInfo.OnSettingsClickListener mOnSettingsClickListener;
79 private NotificationGuts mGutsContainer;
80 private ChannelEditorDialogController mChannelEditorDialogController;
81
82 @VisibleForTesting
83 boolean mSkipPost = false;
84
85 @Retention(SOURCE)
86 @IntDef({ACTION_SETTINGS})
87 private @interface Action {}
88 static final int ACTION_SETTINGS = 5;
89
90 private OnClickListener mOnDone = v -> {
91 mPressedApply = true;
92 closeControls(v, true);
93 };
94
95 public PartialConversationInfo(Context context, AttributeSet attrs) {
96 super(context, attrs);
97 }
98
99 public void bindNotification(
100 PackageManager pm,
101 INotificationManager iNotificationManager,
Evan Laird18bd6e62019-08-30 16:39:25 -0400102 ChannelEditorDialogController channelEditorDialogController,
Julia Reynolds8582df52020-04-24 18:30:59 -0400103 String pkg,
104 NotificationChannel notificationChannel,
105 Set<NotificationChannel> uniqueChannelsInRow,
106 NotificationEntry entry,
107 NotificationInfo.OnSettingsClickListener onSettingsClick,
108 boolean isDeviceProvisioned,
109 boolean isNonBlockable) {
110 mSelectedAction = -1;
111 mINotificationManager = iNotificationManager;
112 mPackageName = pkg;
113 mSbn = entry.getSbn();
114 mPm = pm;
115 mAppName = mPackageName;
116 mOnSettingsClickListener = onSettingsClick;
117 mNotificationChannel = notificationChannel;
118 mAppUid = mSbn.getUid();
119 mDelegatePkg = mSbn.getOpPkg();
120 mIsDeviceProvisioned = isDeviceProvisioned;
121 mIsNonBlockable = isNonBlockable;
Evan Laird18bd6e62019-08-30 16:39:25 -0400122 mChannelEditorDialogController = channelEditorDialogController;
Julia Reynolds8582df52020-04-24 18:30:59 -0400123 mUniqueChannelsInRow = uniqueChannelsInRow;
124
125 bindHeader();
126 bindActions();
127
128 View turnOffButton = findViewById(R.id.turn_off_notifications);
129 turnOffButton.setOnClickListener(getTurnOffNotificationsClickListener());
130 turnOffButton.setVisibility(turnOffButton.hasOnClickListeners() && !mIsNonBlockable
131 ? VISIBLE : GONE);
132
133 View done = findViewById(R.id.done);
134 done.setOnClickListener(mOnDone);
135 }
136
137 private void bindActions() {
138 final View settingsButton = findViewById(R.id.info);
139 settingsButton.setOnClickListener(getSettingsOnClickListener());
140 settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE);
141
142 TextView msg = findViewById(R.id.non_configurable_text);
143 msg.setText(getResources().getString(R.string.no_shortcut, mAppName));
144 }
145
146 private void bindHeader() {
147 bindConversationDetails();
148
149 // Delegate
150 bindDelegate();
151 }
152
153 private OnClickListener getSettingsOnClickListener() {
154 if (mAppUid >= 0 && mOnSettingsClickListener != null && mIsDeviceProvisioned) {
155 final int appUidF = mAppUid;
156 return ((View view) -> {
157 mOnSettingsClickListener.onClick(view, mNotificationChannel, appUidF);
158 });
159 }
160 return null;
161 }
162
163 private OnClickListener getTurnOffNotificationsClickListener() {
164 return ((View view) -> {
165 if (!mPresentingChannelEditorDialog && mChannelEditorDialogController != null) {
166 mPresentingChannelEditorDialog = true;
167
168 mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid,
169 mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener);
170 mChannelEditorDialogController.setOnFinishListener(() -> {
171 mPresentingChannelEditorDialog = false;
172 closeControls(this, false);
173 });
174 mChannelEditorDialogController.show();
175 }
176 });
177 }
178
179 private void bindConversationDetails() {
180 final TextView channelName = findViewById(R.id.parent_channel_name);
181 channelName.setText(mNotificationChannel.getName());
182
183 bindGroup();
184 bindName();
185 bindPackage();
186 bindIcon();
187 }
188
189 private void bindName() {
190 TextView name = findViewById(R.id.name);
191 Bundle extras = mSbn.getNotification().extras;
192 String nameString = extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
193 if (TextUtils.isEmpty(nameString)) {
194 nameString = extras.getString(Notification.EXTRA_TITLE);
195 }
196 name.setText(nameString);
197 }
198
199 private void bindIcon() {
200 ImageView image = findViewById(R.id.conversation_icon);
201 if (mSbn.getNotification().extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION, false)) {
202 // TODO: maybe use a generic group icon, or a composite of recent senders
203 image.setImageDrawable(mPkgIcon);
204 } else {
205 final List<Notification.MessagingStyle.Message> messages =
206 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
207 (Parcelable[]) mSbn.getNotification().extras.get(
208 Notification.EXTRA_MESSAGES));
209
210 final Notification.MessagingStyle.Message latestMessage =
211 Notification.MessagingStyle.findLatestIncomingMessage(messages);
212 Icon personIcon = null;
213 if (latestMessage != null && latestMessage.getSenderPerson() != null) {
214 personIcon = latestMessage.getSenderPerson().getIcon();
215 }
216 if (personIcon != null) {
217 image.setImageIcon(latestMessage.getSenderPerson().getIcon());
218 } else {
219 image.setImageDrawable(mPkgIcon);
220 }
221 }
222 }
223
224 private void bindPackage() {
225 ApplicationInfo info;
226 try {
227 info = mPm.getApplicationInfo(
228 mPackageName,
229 PackageManager.MATCH_UNINSTALLED_PACKAGES
230 | PackageManager.MATCH_DISABLED_COMPONENTS
231 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
232 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
233 if (info != null) {
234 mAppName = String.valueOf(mPm.getApplicationLabel(info));
235 mPkgIcon = mPm.getApplicationIcon(info);
236 }
237 } catch (PackageManager.NameNotFoundException e) {
238 mPkgIcon = mPm.getDefaultActivityIcon();
239 }
240 ((TextView) findViewById(R.id.pkg_name)).setText(mAppName);
241 }
242
243 private void bindDelegate() {
244 TextView delegateView = findViewById(R.id.delegate_name);
245
246 if (!TextUtils.equals(mPackageName, mDelegatePkg)) {
247 // this notification was posted by a delegate!
248 delegateView.setVisibility(View.VISIBLE);
249 } else {
250 delegateView.setVisibility(View.GONE);
251 }
252 }
253
254 private void bindGroup() {
255 // Set group information if this channel has an associated group.
256 CharSequence groupName = null;
257 if (mNotificationChannel != null && mNotificationChannel.getGroup() != null) {
258 try {
259 final NotificationChannelGroup notificationChannelGroup =
260 mINotificationManager.getNotificationChannelGroupForPackage(
261 mNotificationChannel.getGroup(), mPackageName, mAppUid);
262 if (notificationChannelGroup != null) {
263 groupName = notificationChannelGroup.getName();
264 }
265 } catch (RemoteException e) {
266 }
267 }
268 TextView groupNameView = findViewById(R.id.group_name);
269 View groupDivider = findViewById(R.id.group_divider);
270 if (groupName != null) {
271 groupNameView.setText(groupName);
272 groupNameView.setVisibility(VISIBLE);
273 groupDivider.setVisibility(VISIBLE);
274 } else {
275 groupNameView.setVisibility(GONE);
276 groupDivider.setVisibility(GONE);
277 }
278 }
279
280 @Override
281 public boolean post(Runnable action) {
282 if (mSkipPost) {
283 action.run();
284 return true;
285 } else {
286 return super.post(action);
287 }
288 }
289
290 @Override
291 protected void onFinishInflate() {
292 super.onFinishInflate();
293 }
294
295 @Override
296 public void onFinishedClosing() {
297 // TODO: do we need to do anything here?
298 }
299
300 @Override
301 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
302 super.onInitializeAccessibilityEvent(event);
303 if (mGutsContainer != null &&
304 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
305 if (mGutsContainer.isExposed()) {
306 event.getText().add(mContext.getString(
307 R.string.notification_channel_controls_opened_accessibility, mAppName));
308 } else {
309 event.getText().add(mContext.getString(
310 R.string.notification_channel_controls_closed_accessibility, mAppName));
311 }
312 }
313 }
314
315 /**
316 * Closes the controls and commits the updated importance values (indirectly).
317 *
318 * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
319 * user does not have the ability to undo the action anymore.
320 */
321 @VisibleForTesting
322 void closeControls(View v, boolean save) {
323 int[] parentLoc = new int[2];
324 int[] targetLoc = new int[2];
325 mGutsContainer.getLocationOnScreen(parentLoc);
326 v.getLocationOnScreen(targetLoc);
327 final int centerX = v.getWidth() / 2;
328 final int centerY = v.getHeight() / 2;
329 final int x = targetLoc[0] - parentLoc[0] + centerX;
330 final int y = targetLoc[1] - parentLoc[1] + centerY;
331 mGutsContainer.closeControls(x, y, save, false /* force */);
332 }
333
334 @Override
335 public void setGutsParent(NotificationGuts guts) {
336 mGutsContainer = guts;
337 }
338
339 @Override
340 public boolean willBeRemoved() {
341 return false;
342 }
343
344 @Override
345 public boolean shouldBeSaved() {
346 return mPressedApply;
347 }
348
349 @Override
350 public View getContentView() {
351 return this;
352 }
353
354 @Override
355 public boolean handleCloseControls(boolean save, boolean force) {
356 return false;
357 }
358
359 @Override
360 public int getActualHeight() {
361 return getHeight();
362 }
363
364 @VisibleForTesting
365 public boolean isAnimating() {
366 return false;
367 }
368}