blob: 49beae6872fd4dd703375d91335b41d024e2d51d [file] [log] [blame]
Mady Mellordea7ecf2018-12-10 15:47:40 -08001/*
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
17package com.android.systemui.bubbles;
18
Mady Mellor3dff9e62019-02-05 18:12:53 -080019import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
20import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING;
21import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE;
22import static android.util.StatsLogInternal.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS;
23
Mady Mellore8e07712019-01-23 12:45:33 -080024import android.animation.LayoutTransition;
25import android.animation.ObjectAnimator;
Mady Mellordea7ecf2018-12-10 15:47:40 -080026import android.annotation.Nullable;
Mady Mellor3dff9e62019-02-05 18:12:53 -080027import android.app.ActivityView;
Mady Mellore8e07712019-01-23 12:45:33 -080028import android.app.INotificationManager;
Mady Mellor9801e852019-01-22 14:50:28 -080029import android.app.Notification;
30import android.app.PendingIntent;
Mady Mellordea7ecf2018-12-10 15:47:40 -080031import android.content.Context;
Mady Mellor9801e852019-01-22 14:50:28 -080032import android.content.Intent;
Mady Mellor3dff9e62019-02-05 18:12:53 -080033import android.content.pm.ActivityInfo;
Mady Mellor9801e852019-01-22 14:50:28 -080034import android.content.pm.ApplicationInfo;
35import android.content.pm.PackageManager;
Mady Mellordea7ecf2018-12-10 15:47:40 -080036import android.content.res.Resources;
Mady Mellordd497052019-01-30 17:23:48 -080037import android.content.res.TypedArray;
Mady Mellordea7ecf2018-12-10 15:47:40 -080038import android.graphics.Color;
Mady Mellor3dff9e62019-02-05 18:12:53 -080039import android.graphics.Insets;
40import android.graphics.Point;
Mady Mellore8e07712019-01-23 12:45:33 -080041import android.graphics.drawable.Drawable;
Mady Mellor310c7c62019-02-21 17:03:08 -080042import android.graphics.drawable.GradientDrawable;
Mady Mellordea7ecf2018-12-10 15:47:40 -080043import android.graphics.drawable.ShapeDrawable;
Mady Mellore8e07712019-01-23 12:45:33 -080044import android.os.RemoteException;
45import android.os.ServiceManager;
Mady Mellor9801e852019-01-22 14:50:28 -080046import android.provider.Settings;
Steven Wub00225b2019-02-08 14:27:42 -050047import android.service.notification.StatusBarNotification;
Mady Mellordea7ecf2018-12-10 15:47:40 -080048import android.util.AttributeSet;
Mady Mellor9801e852019-01-22 14:50:28 -080049import android.util.Log;
Steven Wub00225b2019-02-08 14:27:42 -050050import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080051import android.view.View;
Mady Mellor5029fa62019-03-05 12:16:21 -080052import android.view.ViewGroup;
Mady Mellor3dff9e62019-02-05 18:12:53 -080053import android.view.WindowInsets;
Mady Mellore8e07712019-01-23 12:45:33 -080054import android.widget.FrameLayout;
Mady Mellor9801e852019-01-22 14:50:28 -080055import android.widget.ImageButton;
Mady Mellore8e07712019-01-23 12:45:33 -080056import android.widget.ImageView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080057import android.widget.LinearLayout;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050058import android.widget.TextView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080059
Mady Mellor3dff9e62019-02-05 18:12:53 -080060import com.android.systemui.Dependency;
Mady Mellore8e07712019-01-23 12:45:33 -080061import com.android.systemui.Interpolators;
Mady Mellordea7ecf2018-12-10 15:47:40 -080062import com.android.systemui.R;
63import com.android.systemui.recents.TriangleShape;
Mady Mellor9801e852019-01-22 14:50:28 -080064import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellor3dff9e62019-02-05 18:12:53 -080065import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
66import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
Mady Mellordea7ecf2018-12-10 15:47:40 -080067
68/**
69 * Container for the expanded bubble view, handles rendering the caret and header of the view.
70 */
Mady Mellor3d82e682019-02-05 13:34:48 -080071public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
Mady Mellor9801e852019-01-22 14:50:28 -080072 private static final String TAG = "BubbleExpandedView";
Mady Mellordea7ecf2018-12-10 15:47:40 -080073
Mady Mellor44ee2fe2019-01-30 17:51:16 -080074 // Configurable via bubble settings; just for testing
75 private boolean mUseFooter;
76 private boolean mShowOnTop;
77
Mady Mellordea7ecf2018-12-10 15:47:40 -080078 // The triangle pointing to the expanded view
79 private View mPointerView;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080080 private int mPointerMargin;
Mady Mellore8e07712019-01-23 12:45:33 -080081
82 // Header
83 private View mHeaderView;
Mady Mellor9801e852019-01-22 14:50:28 -080084 private ImageButton mDeepLinkIcon;
Mady Mellor9801e852019-01-22 14:50:28 -080085 private ImageButton mSettingsIcon;
Mady Mellore8e07712019-01-23 12:45:33 -080086
87 // Permission view
88 private View mPermissionView;
89
Mady Mellor3dff9e62019-02-05 18:12:53 -080090 // Views for expanded state
91 private ExpandableNotificationRow mNotifRow;
92 private ActivityView mActivityView;
93
94 private boolean mActivityViewReady = false;
95 private PendingIntent mBubbleIntent;
96
Mady Mellor5d8f1402019-02-21 18:23:52 -080097 private boolean mKeyboardVisible;
98 private boolean mNeedsNewHeight;
99
Mady Mellorfe7ec032019-01-30 17:32:49 -0800100 private int mMinHeight;
101 private int mHeaderHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800102 private int mBubbleHeight;
103 private int mPermissionHeight;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800104
Mady Mellor9801e852019-01-22 14:50:28 -0800105 private NotificationEntry mEntry;
106 private PackageManager mPm;
107 private String mAppName;
Mady Mellore8e07712019-01-23 12:45:33 -0800108 private Drawable mAppIcon;
109
110 private INotificationManager mNotificationManagerService;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800111 private BubbleController mBubbleController = Dependency.get(BubbleController.class);
Mady Mellor9801e852019-01-22 14:50:28 -0800112
Mady Mellor9801e852019-01-22 14:50:28 -0800113 private BubbleStackView mStackView;
114
Mady Mellor3dff9e62019-02-05 18:12:53 -0800115 private BubbleExpandedView.OnBubbleBlockedListener mOnBubbleBlockedListener;
116
117 private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
118 @Override
119 public void onActivityViewReady(ActivityView view) {
Mady Mellor6d002032019-02-13 13:45:17 -0800120 if (!mActivityViewReady) {
121 mActivityViewReady = true;
122 mActivityView.startActivity(mBubbleIntent);
123 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800124 }
125
126 @Override
127 public void onActivityViewDestroyed(ActivityView view) {
128 mActivityViewReady = false;
129 }
130
131 /**
132 * This is only called for tasks on this ActivityView, which is also set to
133 * single-task mode -- meaning never more than one task on this display. If a task
134 * is being removed, it's the top Activity finishing and this bubble should
135 * be removed or collapsed.
136 */
137 @Override
138 public void onTaskRemovalStarted(int taskId) {
139 if (mEntry != null) {
140 // Must post because this is called from a binder thread.
Mark Renouf08bc42a2019-03-07 13:01:59 -0500141 post(() -> mBubbleController.removeBubble(mEntry.key,
142 BubbleController.DISMISS_TASK_FINISHED));
Mady Mellor3dff9e62019-02-05 18:12:53 -0800143 }
144 }
145 };
Mady Mellore8e07712019-01-23 12:45:33 -0800146
Mady Mellor3d82e682019-02-05 13:34:48 -0800147 public BubbleExpandedView(Context context) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800148 this(context, null);
149 }
150
Mady Mellor3d82e682019-02-05 13:34:48 -0800151 public BubbleExpandedView(Context context, AttributeSet attrs) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800152 this(context, attrs, 0);
153 }
154
Mady Mellor3d82e682019-02-05 13:34:48 -0800155 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800156 this(context, attrs, defStyleAttr, 0);
157 }
158
Mady Mellor3d82e682019-02-05 13:34:48 -0800159 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
Mady Mellordea7ecf2018-12-10 15:47:40 -0800160 int defStyleRes) {
161 super(context, attrs, defStyleAttr, defStyleRes);
Mady Mellor9801e852019-01-22 14:50:28 -0800162 mPm = context.getPackageManager();
Mady Mellorfe7ec032019-01-30 17:32:49 -0800163 mMinHeight = getResources().getDimensionPixelSize(
Mady Mellor3dff9e62019-02-05 18:12:53 -0800164 R.dimen.bubble_expanded_default_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800165 mPointerMargin = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_margin);
Mady Mellore8e07712019-01-23 12:45:33 -0800166 try {
167 mNotificationManagerService = INotificationManager.Stub.asInterface(
168 ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE));
169 } catch (ServiceManager.ServiceNotFoundException e) {
170 Log.w(TAG, e);
171 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800172 }
173
174 @Override
175 protected void onFinishInflate() {
176 super.onFinishInflate();
177
178 Resources res = getResources();
179 mPointerView = findViewById(R.id.pointer_view);
180 int width = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
181 int height = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
Mady Mellordd497052019-01-30 17:23:48 -0800182
183 TypedArray ta = getContext().obtainStyledAttributes(
184 new int[] {android.R.attr.colorBackgroundFloating});
185 int bgColor = ta.getColor(0, Color.WHITE);
186 ta.recycle();
187
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800188 mShowOnTop = BubbleController.showBubblesAtTop(getContext());
189 mUseFooter = BubbleController.useFooter(getContext());
190
Mady Mellordea7ecf2018-12-10 15:47:40 -0800191 ShapeDrawable triangleDrawable = new ShapeDrawable(
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800192 TriangleShape.create(width, height, mShowOnTop /* pointUp */));
Mady Mellordd497052019-01-30 17:23:48 -0800193 triangleDrawable.setTint(bgColor);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800194 mPointerView.setBackground(triangleDrawable);
Mady Mellor9801e852019-01-22 14:50:28 -0800195
Mady Mellore8e07712019-01-23 12:45:33 -0800196 FrameLayout viewWrapper = findViewById(R.id.header_permission_wrapper);
Mady Mellor310c7c62019-02-21 17:03:08 -0800197 viewWrapper.setBackground(createHeaderPermissionBackground(bgColor));
198
Mady Mellore8e07712019-01-23 12:45:33 -0800199 LayoutTransition transition = new LayoutTransition();
200 transition.setDuration(200);
201
202 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
203 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
204 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
205
206 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
207 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
208 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
209
210 transition.setAnimateParentHierarchy(false);
211 viewWrapper.setLayoutTransition(transition);
212 viewWrapper.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
213
Mady Mellor310c7c62019-02-21 17:03:08 -0800214
Mady Mellorfe7ec032019-01-30 17:32:49 -0800215 mHeaderHeight = getContext().getResources().getDimensionPixelSize(
216 R.dimen.bubble_expanded_header_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800217 mPermissionHeight = getContext().getResources().getDimensionPixelSize(
218 R.dimen.bubble_permission_height);
Mady Mellore8e07712019-01-23 12:45:33 -0800219 mHeaderView = findViewById(R.id.header_layout);
Mady Mellor9801e852019-01-22 14:50:28 -0800220 mDeepLinkIcon = findViewById(R.id.deep_link_button);
221 mSettingsIcon = findViewById(R.id.settings_button);
222 mDeepLinkIcon.setOnClickListener(this);
223 mSettingsIcon.setOnClickListener(this);
Mady Mellore8e07712019-01-23 12:45:33 -0800224
225 mPermissionView = findViewById(R.id.permission_layout);
226 findViewById(R.id.no_bubbles_button).setOnClickListener(this);
227 findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800228
229 mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
230 true /* singleTaskInstance */);
231 addView(mActivityView);
232
Mady Mellor5d8f1402019-02-21 18:23:52 -0800233 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
234 // Keep track of IME displaying because we should not make any adjustments that might
235 // cause a config change while the IME is displayed otherwise it'll loose focus.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800236 final int keyboardHeight = insets.getSystemWindowInsetBottom()
237 - insets.getStableInsetBottom();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800238 mKeyboardVisible = keyboardHeight != 0;
239 if (!mKeyboardVisible && mNeedsNewHeight) {
240 updateHeight();
241 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800242 return view.onApplyWindowInsets(insets);
243 });
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800244
245 if (!mShowOnTop) {
246 removeView(mPointerView);
247 if (mUseFooter) {
Mady Mellor310c7c62019-02-21 17:03:08 -0800248 View divider = findViewById(R.id.divider);
249 viewWrapper.removeView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800250 removeView(viewWrapper);
Mady Mellor310c7c62019-02-21 17:03:08 -0800251 addView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800252 addView(viewWrapper);
253 }
254 addView(mPointerView);
255 }
Mady Mellore8e07712019-01-23 12:45:33 -0800256 }
257
Mady Mellor5d8f1402019-02-21 18:23:52 -0800258 @Override
259 protected void onDetachedFromWindow() {
260 super.onDetachedFromWindow();
261 mKeyboardVisible = false;
262 mNeedsNewHeight = false;
263 if (mActivityView != null) {
264 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
265 }
266 }
267
268 /**
269 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
270 * This should be done post-move and post-animation.
271 */
272 void updateInsets(WindowInsets insets) {
273 if (usingActivityView()) {
274 Point displaySize = new Point();
275 mActivityView.getContext().getDisplay().getSize(displaySize);
276 int[] windowLocation = mActivityView.getLocationOnScreen();
277 final int windowBottom = windowLocation[1] + mActivityView.getHeight();
278 final int keyboardHeight = insets.getSystemWindowInsetBottom()
279 - insets.getStableInsetBottom();
280 final int insetsBottom = Math.max(0,
281 windowBottom + keyboardHeight - displaySize.y);
282 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
283 }
284 }
285
Mady Mellore8e07712019-01-23 12:45:33 -0800286 /**
Mady Mellor310c7c62019-02-21 17:03:08 -0800287 * Creates a background with corners rounded based on how the view is configured to display
288 */
289 private Drawable createHeaderPermissionBackground(int bgColor) {
290 TypedArray ta2 = getContext().obtainStyledAttributes(
291 new int[] {android.R.attr.dialogCornerRadius});
292 final float cr = ta2.getDimension(0, 0f);
293 ta2.recycle();
294
295 float[] radii = mUseFooter
296 ? new float[] {0, 0, 0, 0, cr, cr, cr, cr}
297 : new float[] {cr, cr, cr, cr, 0, 0, 0, 0};
298 GradientDrawable chromeBackground = new GradientDrawable();
299 chromeBackground.setShape(GradientDrawable.RECTANGLE);
300 chromeBackground.setCornerRadii(radii);
301 chromeBackground.setColor(bgColor);
302 return chromeBackground;
303 }
304
305 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800306 * Sets the listener to notify when a bubble has been blocked.
307 */
308 public void setOnBlockedListener(OnBubbleBlockedListener listener) {
309 mOnBubbleBlockedListener = listener;
Mady Mellor9801e852019-01-22 14:50:28 -0800310 }
311
312 /**
313 * Sets the notification entry used to populate this view.
314 */
315 public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
316 mStackView = stackView;
317 mEntry = entry;
318
319 ApplicationInfo info;
320 try {
321 info = mPm.getApplicationInfo(
322 entry.notification.getPackageName(),
323 PackageManager.MATCH_UNINSTALLED_PACKAGES
324 | PackageManager.MATCH_DISABLED_COMPONENTS
325 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
326 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
327 if (info != null) {
328 mAppName = String.valueOf(mPm.getApplicationLabel(info));
Mady Mellore8e07712019-01-23 12:45:33 -0800329 mAppIcon = mPm.getApplicationIcon(info);
Mady Mellor9801e852019-01-22 14:50:28 -0800330 }
331 } catch (PackageManager.NameNotFoundException e) {
332 // Ahh... just use package name
333 mAppName = entry.notification.getPackageName();
334 }
Mady Mellore8e07712019-01-23 12:45:33 -0800335 if (mAppIcon == null) {
336 mAppIcon = mPm.getDefaultActivityIcon();
337 }
Mady Mellor9801e852019-01-22 14:50:28 -0800338 updateHeaderView();
Mady Mellore8e07712019-01-23 12:45:33 -0800339 updatePermissionView();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800340 updateExpandedView();
Mady Mellor6d002032019-02-13 13:45:17 -0800341 }
342
343 /**
344 * Lets activity view know it should be shown / populated.
345 */
Mady Mellor5029fa62019-03-05 12:16:21 -0800346 public void populateExpandedView() {
347 if (usingActivityView()) {
348 mActivityView.setCallback(mStateCallback);
349 } else {
350 // We're using notification template
351 ViewGroup parent = (ViewGroup) mNotifRow.getParent();
352 if (parent == this) {
353 // Already added
354 return;
355 } else if (parent != null) {
356 // Still in the shade... remove it
357 parent.removeView(mNotifRow);
358 }
359 if (mShowOnTop) {
360 addView(mNotifRow);
361 } else {
362 addView(mNotifRow, mUseFooter ? 0 : 1);
363 }
364 }
Mady Mellor9801e852019-01-22 14:50:28 -0800365 }
366
Mady Mellorfe7ec032019-01-30 17:32:49 -0800367 /**
368 * Updates the entry backing this view. This will not re-populate ActivityView, it will
369 * only update the deep-links in the header, the title, and the height of the view.
370 */
371 public void update(NotificationEntry entry) {
372 if (entry.key.equals(mEntry.key)) {
373 mEntry = entry;
374 updateHeaderView();
375 updateHeight();
376 } else {
377 Log.w(TAG, "Trying to update entry with different key, new entry: "
378 + entry.key + " old entry: " + mEntry.key);
379 }
380 }
381
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700382 /**
383 * Update bubble expanded view header when user toggles dark mode.
384 */
385 void updateHeaderColor() {
386 mHeaderView.setBackgroundColor(mContext.getColor(R.attr.colorAccent));
387 }
388
Mady Mellor9801e852019-01-22 14:50:28 -0800389 private void updateHeaderView() {
390 mSettingsIcon.setContentDescription(getResources().getString(
391 R.string.bubbles_settings_button_description, mAppName));
392 mDeepLinkIcon.setContentDescription(getResources().getString(
393 R.string.bubbles_deep_link_button_description, mAppName));
Mady Mellore8e07712019-01-23 12:45:33 -0800394 }
395
396 private void updatePermissionView() {
397 boolean hasUserApprovedBubblesForPackage = false;
398 try {
399 hasUserApprovedBubblesForPackage =
400 mNotificationManagerService.hasUserApprovedBubblesForPackage(
401 mEntry.notification.getPackageName(), mEntry.notification.getUid());
402 } catch (RemoteException e) {
403 Log.w(TAG, e);
404 }
405 if (hasUserApprovedBubblesForPackage) {
406 mHeaderView.setVisibility(VISIBLE);
407 mPermissionView.setVisibility(GONE);
408 } else {
409 mHeaderView.setVisibility(GONE);
410 mPermissionView.setVisibility(VISIBLE);
411 ((ImageView) mPermissionView.findViewById(R.id.pkgicon)).setImageDrawable(mAppIcon);
412 ((TextView) mPermissionView.findViewById(R.id.pkgname)).setText(mAppName);
Steven Wua62cb6a2019-02-15 17:12:51 -0500413 logBubbleClickEvent(mEntry.notification,
414 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_DIALOG_SHOWN);
Mady Mellor9801e852019-01-22 14:50:28 -0800415 }
416 }
417
Mady Mellor3dff9e62019-02-05 18:12:53 -0800418 private void updateExpandedView() {
419 mBubbleIntent = getBubbleIntent(mEntry);
420 if (mBubbleIntent != null) {
421 if (mNotifRow != null) {
422 // Clear out the row if we had it previously
423 removeView(mNotifRow);
424 mNotifRow = null;
425 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800426 mActivityView.setVisibility(VISIBLE);
427 } else {
428 // Hide activity view if we had it previously
429 mActivityView.setVisibility(GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800430 mNotifRow = mEntry.getRow();
Mady Mellor5029fa62019-03-05 12:16:21 -0800431
Mady Mellor3dff9e62019-02-05 18:12:53 -0800432 }
433 updateView();
434 }
435
Mark Renouf041d7262019-02-06 12:09:41 -0500436 boolean performBackPressIfNeeded() {
437 if (mActivityView == null || !usingActivityView()) {
438 return false;
439 }
440 mActivityView.performBackPress();
441 return true;
442 }
443
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800444 /**
445 * @return total height that the expanded view occupies.
446 */
447 int getExpandedSize() {
448 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
449 ? mHeaderHeight
450 : mPermissionHeight;
451 return mBubbleHeight + mPointerView.getHeight() + mPointerMargin
452 + chromeHeight;
453 }
454
Mady Mellorfe7ec032019-01-30 17:32:49 -0800455 void updateHeight() {
456 if (usingActivityView()) {
457 Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
458 int desiredHeight;
459 if (data == null) {
460 // This is a contentIntent based bubble, lets allow it to be the max height
461 // as it was forced into this mode and not prepared to be small
462 desiredHeight = mStackView.getMaxExpandedHeight();
463 } else {
464 desiredHeight = data.getDesiredHeight() > 0
465 ? data.getDesiredHeight()
466 : mMinHeight;
467 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800468 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
469 ? mHeaderHeight
470 : mPermissionHeight;
471 int max = mStackView.getMaxExpandedHeight() - chromeHeight - mPointerView.getHeight()
472 - mPointerMargin;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800473 int height = Math.min(desiredHeight, max);
474 height = Math.max(height, mMinHeight);
475 LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800476 mNeedsNewHeight = lp.height != height;
477 if (!mKeyboardVisible) {
478 // If the keyboard is visible... don't adjust the height because that will cause
479 // a configuration change and the keyboard will be lost.
480 lp.height = height;
481 mBubbleHeight = height;
482 mActivityView.setLayoutParams(lp);
483 mNeedsNewHeight = false;
484 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800485 } else {
486 mBubbleHeight = mNotifRow != null ? mNotifRow.getIntrinsicHeight() : mMinHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800487 }
488 }
489
Mady Mellor9801e852019-01-22 14:50:28 -0800490 @Override
491 public void onClick(View view) {
492 if (mEntry == null) {
493 return;
494 }
495 Notification n = mEntry.notification.getNotification();
496 int id = view.getId();
497 if (id == R.id.deep_link_button) {
498 mStackView.collapseStack(() -> {
499 try {
500 n.contentIntent.send();
Steven Wub00225b2019-02-08 14:27:42 -0500501 logBubbleClickEvent(mEntry.notification,
502 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_APP);
Mady Mellor9801e852019-01-22 14:50:28 -0800503 } catch (PendingIntent.CanceledException e) {
504 Log.w(TAG, "Failed to send intent for bubble with key: "
505 + (mEntry != null ? mEntry.key : " null entry"));
506 }
507 });
508 } else if (id == R.id.settings_button) {
509 Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
510 mEntry.notification.getUid());
Steven Wub00225b2019-02-08 14:27:42 -0500511 mStackView.collapseStack(() -> {
512 mContext.startActivity(intent);
513 logBubbleClickEvent(mEntry.notification,
514 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
515 });
Mady Mellore8e07712019-01-23 12:45:33 -0800516 } else if (id == R.id.no_bubbles_button) {
517 setBubblesAllowed(false);
518 } else if (id == R.id.yes_bubbles_button) {
519 setBubblesAllowed(true);
520 }
521 }
522
523 private void setBubblesAllowed(boolean allowed) {
524 try {
525 mNotificationManagerService.setBubblesAllowed(
526 mEntry.notification.getPackageName(),
527 mEntry.notification.getUid(),
528 allowed);
529 if (allowed) {
530 mPermissionView.setVisibility(GONE);
531 mHeaderView.setVisibility(VISIBLE);
532 } else if (mOnBubbleBlockedListener != null) {
533 mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
534 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800535 mStackView.onExpandedHeightChanged();
Steven Wub00225b2019-02-08 14:27:42 -0500536 logBubbleClickEvent(mEntry.notification,
537 allowed ? StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_IN :
538 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_OUT);
Mady Mellore8e07712019-01-23 12:45:33 -0800539 } catch (RemoteException e) {
540 Log.w(TAG, e);
Mady Mellor9801e852019-01-22 14:50:28 -0800541 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800542 }
543
544 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800545 * Update appearance of the expanded view being displayed.
546 */
547 public void updateView() {
548 if (usingActivityView()
549 && mActivityView.getVisibility() == VISIBLE
550 && mActivityView.isAttachedToWindow()) {
551 mActivityView.onLocationChanged();
552 } else if (mNotifRow != null) {
553 applyRowState(mNotifRow);
554 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800555 updateHeight();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800556 }
557
558 /**
Mady Mellordea7ecf2018-12-10 15:47:40 -0800559 * Set the x position that the tip of the triangle should point to.
560 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800561 public void setPointerPosition(float x) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800562 // Adjust for the pointer size
Mady Mellor3dff9e62019-02-05 18:12:53 -0800563 x -= (mPointerView.getWidth() / 2f);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800564 mPointerView.setTranslationX(x);
565 }
566
567 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800568 * Removes and releases an ActivityView if one was previously created for this bubble.
Mady Mellordea7ecf2018-12-10 15:47:40 -0800569 */
Mady Mellor94d94a72019-03-05 18:16:59 -0800570 public void cleanUpExpandedState() {
571 removeView(mNotifRow);
572
Mady Mellor3dff9e62019-02-05 18:12:53 -0800573 if (mActivityView == null) {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500574 return;
575 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500576 if (mActivityViewReady) {
577 mActivityView.release();
Mady Mellordea7ecf2018-12-10 15:47:40 -0800578 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500579 removeView(mActivityView);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800580 mActivityView = null;
581 mActivityViewReady = false;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800582 }
583
Mady Mellor3dff9e62019-02-05 18:12:53 -0800584 private boolean usingActivityView() {
585 return mBubbleIntent != null;
586 }
587
588 private void applyRowState(ExpandableNotificationRow view) {
589 view.reset();
590 view.setHeadsUp(false);
591 view.resetTranslation();
592 view.setOnKeyguard(false);
593 view.setOnAmbient(false);
594 view.setClipBottomAmount(0);
595 view.setClipTopAmount(0);
596 view.setContentTransformationAmount(0, false);
597 view.setIconsVisible(true);
598
599 // TODO - Need to reset this (and others) when view goes back in shade, leave for now
600 // view.setTopRoundness(1, false);
601 // view.setBottomRoundness(1, false);
602
603 ExpandableViewState viewState = view.getViewState();
604 viewState = viewState == null ? new ExpandableViewState() : viewState;
605 viewState.height = view.getIntrinsicHeight();
606 viewState.gone = false;
607 viewState.hidden = false;
608 viewState.dimmed = false;
609 viewState.dark = false;
610 viewState.alpha = 1f;
611 viewState.notGoneIndex = -1;
612 viewState.xTranslation = 0;
613 viewState.yTranslation = 0;
614 viewState.zTranslation = 0;
615 viewState.scaleX = 1;
616 viewState.scaleY = 1;
617 viewState.inShelf = true;
618 viewState.headsUpIsVisible = false;
619 viewState.applyToView(view);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800620 }
Mady Mellor9801e852019-01-22 14:50:28 -0800621
622 private Intent getSettingsIntent(String packageName, final int appUid) {
623 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
624 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
625 intent.putExtra(Settings.EXTRA_APP_UID, appUid);
626 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
627 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
628 return intent;
629 }
Mady Mellore8e07712019-01-23 12:45:33 -0800630
Mady Mellor3dff9e62019-02-05 18:12:53 -0800631 @Nullable
632 private PendingIntent getBubbleIntent(NotificationEntry entry) {
633 Notification notif = entry.notification.getNotification();
634 String packageName = entry.notification.getPackageName();
635 Notification.BubbleMetadata data = notif.getBubbleMetadata();
636 if (data != null && canLaunchInActivityView(data.getIntent(), true /* enableLogging */,
637 packageName)) {
638 return data.getIntent();
639 } else if (BubbleController.shouldUseContentIntent(mContext)
640 && canLaunchInActivityView(notif.contentIntent, false /* enableLogging */,
641 packageName)) {
642 return notif.contentIntent;
643 }
644 return null;
645 }
646
647 /**
648 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
649 *
650 * @param intent the pending intent of the bubble.
651 * @param enableLogging whether bubble developer error should be logged.
652 * @param packageName the notification package name for this bubble.
653 * @return
654 */
655 private boolean canLaunchInActivityView(PendingIntent intent, boolean enableLogging,
656 String packageName) {
657 if (intent == null) {
658 return false;
659 }
660 ActivityInfo info =
661 intent.getIntent().resolveActivityInfo(mContext.getPackageManager(), 0);
662 if (info == null) {
663 if (enableLogging) {
664 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
665 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
666 }
667 return false;
668 }
669 if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
670 if (enableLogging) {
671 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
672 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
673 }
674 return false;
675 }
676 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
677 if (enableLogging) {
678 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
679 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
680 }
681 return false;
682 }
683 return (info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
684 }
685
Mady Mellore8e07712019-01-23 12:45:33 -0800686 /**
687 * Listener that is notified when a bubble is blocked.
688 */
689 public interface OnBubbleBlockedListener {
690 /**
691 * Called when a bubble is blocked for the provided entry.
692 */
693 void onBubbleBlocked(NotificationEntry entry);
694 }
Steven Wub00225b2019-02-08 14:27:42 -0500695
696 /**
697 * Logs bubble UI click event.
698 *
699 * @param notification the bubble notification that user is interacting with.
700 * @param action the user interaction enum.
701 */
702 private void logBubbleClickEvent(StatusBarNotification notification, int action) {
703 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
704 notification.getPackageName(),
705 notification.getNotification().getChannelId(),
706 notification.getId(),
707 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
708 mStackView.getBubbleCount(),
709 action,
710 mStackView.getNormalizedXPosition(),
711 mStackView.getNormalizedYPosition());
712 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800713}