blob: 6c2db76e19e177c18e65ca0b6bacf14ae815960f [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;
Lyn Hanc26ff122019-03-29 16:46:07 -070041import android.graphics.drawable.AdaptiveIconDrawable;
42import android.graphics.drawable.ColorDrawable;
Mady Mellore8e07712019-01-23 12:45:33 -080043import android.graphics.drawable.Drawable;
Mady Mellor310c7c62019-02-21 17:03:08 -080044import android.graphics.drawable.GradientDrawable;
Lyn Hanc26ff122019-03-29 16:46:07 -070045import android.graphics.drawable.InsetDrawable;
Mady Mellordea7ecf2018-12-10 15:47:40 -080046import android.graphics.drawable.ShapeDrawable;
Mady Mellore8e07712019-01-23 12:45:33 -080047import android.os.RemoteException;
48import android.os.ServiceManager;
Mady Mellor7af771a2019-03-07 15:04:54 -080049import android.os.UserHandle;
Mady Mellor9801e852019-01-22 14:50:28 -080050import android.provider.Settings;
Steven Wub00225b2019-02-08 14:27:42 -050051import android.service.notification.StatusBarNotification;
Mady Mellordea7ecf2018-12-10 15:47:40 -080052import android.util.AttributeSet;
Mady Mellor9801e852019-01-22 14:50:28 -080053import android.util.Log;
Steven Wub00225b2019-02-08 14:27:42 -050054import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080055import android.view.View;
Mady Mellor5029fa62019-03-05 12:16:21 -080056import android.view.ViewGroup;
Mady Mellor3dff9e62019-02-05 18:12:53 -080057import android.view.WindowInsets;
Mady Mellore8e07712019-01-23 12:45:33 -080058import android.widget.FrameLayout;
Mady Mellore8e07712019-01-23 12:45:33 -080059import android.widget.ImageView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080060import android.widget.LinearLayout;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050061import android.widget.TextView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080062
Mady Mellor3dff9e62019-02-05 18:12:53 -080063import com.android.systemui.Dependency;
Mady Mellore8e07712019-01-23 12:45:33 -080064import com.android.systemui.Interpolators;
Mady Mellordea7ecf2018-12-10 15:47:40 -080065import com.android.systemui.R;
66import com.android.systemui.recents.TriangleShape;
Mady Mellor9801e852019-01-22 14:50:28 -080067import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellor3dff9e62019-02-05 18:12:53 -080068import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
69import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
Mady Mellordea7ecf2018-12-10 15:47:40 -080070
71/**
72 * Container for the expanded bubble view, handles rendering the caret and header of the view.
73 */
Mady Mellor3d82e682019-02-05 13:34:48 -080074public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
Mady Mellor9801e852019-01-22 14:50:28 -080075 private static final String TAG = "BubbleExpandedView";
Mady Mellordea7ecf2018-12-10 15:47:40 -080076
Mady Mellor44ee2fe2019-01-30 17:51:16 -080077 // Configurable via bubble settings; just for testing
78 private boolean mUseFooter;
79 private boolean mShowOnTop;
80
Mady Mellordea7ecf2018-12-10 15:47:40 -080081 // The triangle pointing to the expanded view
82 private View mPointerView;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080083 private int mPointerMargin;
Mady Mellore8e07712019-01-23 12:45:33 -080084
Lyn Hanc26ff122019-03-29 16:46:07 -070085 private ImageView 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
Mady Mellore8e07712019-01-23 12:45:33 -0800198 LayoutTransition transition = new LayoutTransition();
199 transition.setDuration(200);
200
201 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
202 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
203 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
204
205 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
206 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
207 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
208
209 transition.setAnimateParentHierarchy(false);
210 viewWrapper.setLayoutTransition(transition);
211 viewWrapper.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
212
Mady Mellorfe7ec032019-01-30 17:32:49 -0800213 mHeaderHeight = getContext().getResources().getDimensionPixelSize(
214 R.dimen.bubble_expanded_header_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800215 mPermissionHeight = getContext().getResources().getDimensionPixelSize(
216 R.dimen.bubble_permission_height);
Mady Mellore8e07712019-01-23 12:45:33 -0800217
218 mPermissionView = findViewById(R.id.permission_layout);
Lyn Hanc26ff122019-03-29 16:46:07 -0700219 mSettingsIcon = findViewById(R.id.settings_button);
220 mSettingsIcon.setOnClickListener(this);
221 updateHeaderColor();
222
Mady Mellore8e07712019-01-23 12:45:33 -0800223 findViewById(R.id.no_bubbles_button).setOnClickListener(this);
224 findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800225
226 mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
227 true /* singleTaskInstance */);
228 addView(mActivityView);
229
Mady Mellor5d8f1402019-02-21 18:23:52 -0800230 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
231 // Keep track of IME displaying because we should not make any adjustments that might
232 // cause a config change while the IME is displayed otherwise it'll loose focus.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800233 final int keyboardHeight = insets.getSystemWindowInsetBottom()
234 - insets.getStableInsetBottom();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800235 mKeyboardVisible = keyboardHeight != 0;
236 if (!mKeyboardVisible && mNeedsNewHeight) {
237 updateHeight();
238 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800239 return view.onApplyWindowInsets(insets);
240 });
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800241
242 if (!mShowOnTop) {
243 removeView(mPointerView);
244 if (mUseFooter) {
Mady Mellor310c7c62019-02-21 17:03:08 -0800245 View divider = findViewById(R.id.divider);
246 viewWrapper.removeView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800247 removeView(viewWrapper);
Mady Mellor310c7c62019-02-21 17:03:08 -0800248 addView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800249 addView(viewWrapper);
250 }
251 addView(mPointerView);
252 }
Mady Mellore8e07712019-01-23 12:45:33 -0800253 }
254
Mady Mellor5d8f1402019-02-21 18:23:52 -0800255 @Override
256 protected void onDetachedFromWindow() {
257 super.onDetachedFromWindow();
258 mKeyboardVisible = false;
259 mNeedsNewHeight = false;
260 if (mActivityView != null) {
261 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
262 }
263 }
264
265 /**
266 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
267 * This should be done post-move and post-animation.
268 */
269 void updateInsets(WindowInsets insets) {
270 if (usingActivityView()) {
271 Point displaySize = new Point();
272 mActivityView.getContext().getDisplay().getSize(displaySize);
273 int[] windowLocation = mActivityView.getLocationOnScreen();
274 final int windowBottom = windowLocation[1] + mActivityView.getHeight();
275 final int keyboardHeight = insets.getSystemWindowInsetBottom()
276 - insets.getStableInsetBottom();
277 final int insetsBottom = Math.max(0,
278 windowBottom + keyboardHeight - displaySize.y);
279 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
280 }
281 }
282
Mady Mellore8e07712019-01-23 12:45:33 -0800283 /**
Mady Mellor310c7c62019-02-21 17:03:08 -0800284 * Creates a background with corners rounded based on how the view is configured to display
285 */
286 private Drawable createHeaderPermissionBackground(int bgColor) {
287 TypedArray ta2 = getContext().obtainStyledAttributes(
288 new int[] {android.R.attr.dialogCornerRadius});
289 final float cr = ta2.getDimension(0, 0f);
290 ta2.recycle();
291
292 float[] radii = mUseFooter
293 ? new float[] {0, 0, 0, 0, cr, cr, cr, cr}
294 : new float[] {cr, cr, cr, cr, 0, 0, 0, 0};
295 GradientDrawable chromeBackground = new GradientDrawable();
296 chromeBackground.setShape(GradientDrawable.RECTANGLE);
297 chromeBackground.setCornerRadii(radii);
298 chromeBackground.setColor(bgColor);
299 return chromeBackground;
300 }
301
302 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800303 * Sets the listener to notify when a bubble has been blocked.
304 */
305 public void setOnBlockedListener(OnBubbleBlockedListener listener) {
306 mOnBubbleBlockedListener = listener;
Mady Mellor9801e852019-01-22 14:50:28 -0800307 }
308
309 /**
310 * Sets the notification entry used to populate this view.
311 */
312 public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
313 mStackView = stackView;
314 mEntry = entry;
315
316 ApplicationInfo info;
317 try {
318 info = mPm.getApplicationInfo(
319 entry.notification.getPackageName(),
320 PackageManager.MATCH_UNINSTALLED_PACKAGES
321 | PackageManager.MATCH_DISABLED_COMPONENTS
322 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
323 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
324 if (info != null) {
325 mAppName = String.valueOf(mPm.getApplicationLabel(info));
Mady Mellore8e07712019-01-23 12:45:33 -0800326 mAppIcon = mPm.getApplicationIcon(info);
Mady Mellor9801e852019-01-22 14:50:28 -0800327 }
328 } catch (PackageManager.NameNotFoundException e) {
329 // Ahh... just use package name
330 mAppName = entry.notification.getPackageName();
331 }
Mady Mellore8e07712019-01-23 12:45:33 -0800332 if (mAppIcon == null) {
333 mAppIcon = mPm.getDefaultActivityIcon();
334 }
Mady Mellor9801e852019-01-22 14:50:28 -0800335 updateHeaderView();
Mady Mellore8e07712019-01-23 12:45:33 -0800336 updatePermissionView();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800337 updateExpandedView();
Mady Mellor6d002032019-02-13 13:45:17 -0800338 }
339
340 /**
341 * Lets activity view know it should be shown / populated.
342 */
Mady Mellor5029fa62019-03-05 12:16:21 -0800343 public void populateExpandedView() {
344 if (usingActivityView()) {
345 mActivityView.setCallback(mStateCallback);
346 } else {
347 // We're using notification template
348 ViewGroup parent = (ViewGroup) mNotifRow.getParent();
349 if (parent == this) {
350 // Already added
351 return;
352 } else if (parent != null) {
353 // Still in the shade... remove it
354 parent.removeView(mNotifRow);
355 }
356 if (mShowOnTop) {
357 addView(mNotifRow);
358 } else {
359 addView(mNotifRow, mUseFooter ? 0 : 1);
360 }
361 }
Mady Mellor9801e852019-01-22 14:50:28 -0800362 }
363
Mady Mellorfe7ec032019-01-30 17:32:49 -0800364 /**
365 * Updates the entry backing this view. This will not re-populate ActivityView, it will
366 * only update the deep-links in the header, the title, and the height of the view.
367 */
368 public void update(NotificationEntry entry) {
369 if (entry.key.equals(mEntry.key)) {
370 mEntry = entry;
371 updateHeaderView();
372 updateHeight();
373 } else {
374 Log.w(TAG, "Trying to update entry with different key, new entry: "
375 + entry.key + " old entry: " + mEntry.key);
376 }
377 }
378
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700379 /**
Lyn Hanc26ff122019-03-29 16:46:07 -0700380 * Update header color and icon shape when theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700381 */
382 void updateHeaderColor() {
Lyn Han76e803d2019-03-26 17:31:33 -0700383 TypedArray ta = mContext.obtainStyledAttributes(
384 new int[] {android.R.attr.colorBackgroundFloating, android.R.attr.colorForeground});
Lyn Hanc26ff122019-03-29 16:46:07 -0700385 int backgroundColor = ta.getColor(0, Color.WHITE /* default */);
386 int foregroundColor = ta.getColor(1, Color.BLACK /* default */);
Lyn Han76e803d2019-03-26 17:31:33 -0700387 ta.recycle();
388
Lyn Hanc26ff122019-03-29 16:46:07 -0700389 mPermissionView.setBackground(createHeaderPermissionBackground(backgroundColor));
390
391 Drawable settingsIcon = mSettingsIcon.getDrawable();
392 settingsIcon.setTint(foregroundColor);
393
394 int mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
395 InsetDrawable foreground = new InsetDrawable(settingsIcon, mIconInset);
396 ColorDrawable background = new ColorDrawable(backgroundColor);
397 AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(background,
398 foreground);
399 mSettingsIcon.setImageDrawable(adaptiveIcon);
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700400 }
401
Mady Mellor9801e852019-01-22 14:50:28 -0800402 private void updateHeaderView() {
403 mSettingsIcon.setContentDescription(getResources().getString(
404 R.string.bubbles_settings_button_description, mAppName));
Mady Mellore8e07712019-01-23 12:45:33 -0800405 }
406
407 private void updatePermissionView() {
408 boolean hasUserApprovedBubblesForPackage = false;
409 try {
410 hasUserApprovedBubblesForPackage =
411 mNotificationManagerService.hasUserApprovedBubblesForPackage(
412 mEntry.notification.getPackageName(), mEntry.notification.getUid());
413 } catch (RemoteException e) {
414 Log.w(TAG, e);
415 }
416 if (hasUserApprovedBubblesForPackage) {
Lyn Hanc26ff122019-03-29 16:46:07 -0700417 showSettingsIcon();
Mady Mellore8e07712019-01-23 12:45:33 -0800418 } else {
Lyn Hanc26ff122019-03-29 16:46:07 -0700419 showPermissionView();
Mady Mellore8e07712019-01-23 12:45:33 -0800420 ((ImageView) mPermissionView.findViewById(R.id.pkgicon)).setImageDrawable(mAppIcon);
421 ((TextView) mPermissionView.findViewById(R.id.pkgname)).setText(mAppName);
Mady Mellor81f734c2019-03-20 18:42:50 -0700422 ((TextView) mPermissionView.findViewById(R.id.prompt)).setText(
423 getResources().getString(R.string.bubbles_prompt, mAppName));
Steven Wu45e38ae2019-03-25 16:16:59 -0400424 logBubbleClickEvent(mEntry,
Steven Wua62cb6a2019-02-15 17:12:51 -0500425 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_DIALOG_SHOWN);
Mady Mellor9801e852019-01-22 14:50:28 -0800426 }
427 }
428
Mady Mellor3dff9e62019-02-05 18:12:53 -0800429 private void updateExpandedView() {
430 mBubbleIntent = getBubbleIntent(mEntry);
431 if (mBubbleIntent != null) {
432 if (mNotifRow != null) {
433 // Clear out the row if we had it previously
434 removeView(mNotifRow);
435 mNotifRow = null;
436 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800437 mActivityView.setVisibility(VISIBLE);
438 } else {
439 // Hide activity view if we had it previously
440 mActivityView.setVisibility(GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800441 mNotifRow = mEntry.getRow();
Mady Mellor5029fa62019-03-05 12:16:21 -0800442
Mady Mellor3dff9e62019-02-05 18:12:53 -0800443 }
444 updateView();
445 }
446
Mark Renouf041d7262019-02-06 12:09:41 -0500447 boolean performBackPressIfNeeded() {
Mady Mellor323fb0b2019-03-25 12:15:22 -0700448 if (!usingActivityView()) {
Mark Renouf041d7262019-02-06 12:09:41 -0500449 return false;
450 }
451 mActivityView.performBackPress();
452 return true;
453 }
454
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800455 /**
456 * @return total height that the expanded view occupies.
457 */
458 int getExpandedSize() {
459 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
460 ? mHeaderHeight
461 : mPermissionHeight;
462 return mBubbleHeight + mPointerView.getHeight() + mPointerMargin
463 + chromeHeight;
464 }
465
Mady Mellorfe7ec032019-01-30 17:32:49 -0800466 void updateHeight() {
467 if (usingActivityView()) {
468 Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
Mady Mellor7af771a2019-03-07 15:04:54 -0800469 float desiredHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800470 if (data == null) {
471 // This is a contentIntent based bubble, lets allow it to be the max height
472 // as it was forced into this mode and not prepared to be small
473 desiredHeight = mStackView.getMaxExpandedHeight();
474 } else {
Mady Mellor7af771a2019-03-07 15:04:54 -0800475 boolean useRes = data.getDesiredHeightResId() != 0;
476 float desiredPx;
477 if (useRes) {
478 desiredPx = getDimenForPackageUser(data.getDesiredHeightResId(),
479 mEntry.notification.getPackageName(),
480 mEntry.notification.getUser().getIdentifier());
481 } else {
482 desiredPx = data.getDesiredHeight()
483 * getContext().getResources().getDisplayMetrics().density;
484 }
485 desiredHeight = desiredPx > 0 ? desiredPx : mMinHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800486 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800487 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
488 ? mHeaderHeight
489 : mPermissionHeight;
490 int max = mStackView.getMaxExpandedHeight() - chromeHeight - mPointerView.getHeight()
491 - mPointerMargin;
Mady Mellor7af771a2019-03-07 15:04:54 -0800492 float height = Math.min(desiredHeight, max);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800493 height = Math.max(height, mMinHeight);
494 LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800495 mNeedsNewHeight = lp.height != height;
496 if (!mKeyboardVisible) {
497 // If the keyboard is visible... don't adjust the height because that will cause
498 // a configuration change and the keyboard will be lost.
Mady Mellor7af771a2019-03-07 15:04:54 -0800499 lp.height = (int) height;
500 mBubbleHeight = (int) height;
Mady Mellor5d8f1402019-02-21 18:23:52 -0800501 mActivityView.setLayoutParams(lp);
502 mNeedsNewHeight = false;
503 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800504 } else {
505 mBubbleHeight = mNotifRow != null ? mNotifRow.getIntrinsicHeight() : mMinHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800506 }
507 }
508
Mady Mellor9801e852019-01-22 14:50:28 -0800509 @Override
510 public void onClick(View view) {
511 if (mEntry == null) {
512 return;
513 }
514 Notification n = mEntry.notification.getNotification();
515 int id = view.getId();
Lyn Hanc26ff122019-03-29 16:46:07 -0700516 if (id == R.id.settings_button) {
Mady Mellor9801e852019-01-22 14:50:28 -0800517 Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
518 mEntry.notification.getUid());
Steven Wub00225b2019-02-08 14:27:42 -0500519 mStackView.collapseStack(() -> {
520 mContext.startActivity(intent);
Steven Wu45e38ae2019-03-25 16:16:59 -0400521 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500522 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
523 });
Mady Mellore8e07712019-01-23 12:45:33 -0800524 } else if (id == R.id.no_bubbles_button) {
525 setBubblesAllowed(false);
526 } else if (id == R.id.yes_bubbles_button) {
527 setBubblesAllowed(true);
528 }
529 }
530
531 private void setBubblesAllowed(boolean allowed) {
532 try {
533 mNotificationManagerService.setBubblesAllowed(
534 mEntry.notification.getPackageName(),
535 mEntry.notification.getUid(),
536 allowed);
537 if (allowed) {
Lyn Hanc26ff122019-03-29 16:46:07 -0700538 showSettingsIcon();
Mady Mellore8e07712019-01-23 12:45:33 -0800539 } else if (mOnBubbleBlockedListener != null) {
540 mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
541 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800542 mStackView.onExpandedHeightChanged();
Steven Wu45e38ae2019-03-25 16:16:59 -0400543 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500544 allowed ? StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_IN :
545 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_OUT);
Mady Mellore8e07712019-01-23 12:45:33 -0800546 } catch (RemoteException e) {
547 Log.w(TAG, e);
Mady Mellor9801e852019-01-22 14:50:28 -0800548 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800549 }
550
Lyn Hanc26ff122019-03-29 16:46:07 -0700551 void showSettingsIcon() {
552 mPermissionView.setVisibility(GONE);
553 mSettingsIcon.setVisibility(VISIBLE);
554 }
555
556 void showPermissionView() {
557 mSettingsIcon.setVisibility(GONE);
558 mPermissionView.setVisibility(VISIBLE);
559
560 }
561
Mady Mellordea7ecf2018-12-10 15:47:40 -0800562 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800563 * Update appearance of the expanded view being displayed.
564 */
565 public void updateView() {
566 if (usingActivityView()
567 && mActivityView.getVisibility() == VISIBLE
568 && mActivityView.isAttachedToWindow()) {
569 mActivityView.onLocationChanged();
570 } else if (mNotifRow != null) {
571 applyRowState(mNotifRow);
572 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800573 updateHeight();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800574 }
575
576 /**
Mady Mellordea7ecf2018-12-10 15:47:40 -0800577 * Set the x position that the tip of the triangle should point to.
578 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800579 public void setPointerPosition(float x) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800580 // Adjust for the pointer size
Mady Mellor3dff9e62019-02-05 18:12:53 -0800581 x -= (mPointerView.getWidth() / 2f);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800582 mPointerView.setTranslationX(x);
583 }
584
585 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800586 * Removes and releases an ActivityView if one was previously created for this bubble.
Mady Mellordea7ecf2018-12-10 15:47:40 -0800587 */
Mady Mellor94d94a72019-03-05 18:16:59 -0800588 public void cleanUpExpandedState() {
589 removeView(mNotifRow);
590
Mady Mellor3dff9e62019-02-05 18:12:53 -0800591 if (mActivityView == null) {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500592 return;
593 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500594 if (mActivityViewReady) {
595 mActivityView.release();
Mady Mellordea7ecf2018-12-10 15:47:40 -0800596 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500597 removeView(mActivityView);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800598 mActivityView = null;
599 mActivityViewReady = false;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800600 }
601
Mady Mellor3dff9e62019-02-05 18:12:53 -0800602 private boolean usingActivityView() {
Mady Mellor323fb0b2019-03-25 12:15:22 -0700603 return mBubbleIntent != null && mActivityView != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800604 }
605
606 private void applyRowState(ExpandableNotificationRow view) {
607 view.reset();
608 view.setHeadsUp(false);
609 view.resetTranslation();
610 view.setOnKeyguard(false);
611 view.setOnAmbient(false);
612 view.setClipBottomAmount(0);
613 view.setClipTopAmount(0);
614 view.setContentTransformationAmount(0, false);
615 view.setIconsVisible(true);
616
617 // TODO - Need to reset this (and others) when view goes back in shade, leave for now
618 // view.setTopRoundness(1, false);
619 // view.setBottomRoundness(1, false);
620
621 ExpandableViewState viewState = view.getViewState();
622 viewState = viewState == null ? new ExpandableViewState() : viewState;
623 viewState.height = view.getIntrinsicHeight();
624 viewState.gone = false;
625 viewState.hidden = false;
626 viewState.dimmed = false;
627 viewState.dark = false;
628 viewState.alpha = 1f;
629 viewState.notGoneIndex = -1;
630 viewState.xTranslation = 0;
631 viewState.yTranslation = 0;
632 viewState.zTranslation = 0;
633 viewState.scaleX = 1;
634 viewState.scaleY = 1;
635 viewState.inShelf = true;
636 viewState.headsUpIsVisible = false;
637 viewState.applyToView(view);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800638 }
Mady Mellor9801e852019-01-22 14:50:28 -0800639
640 private Intent getSettingsIntent(String packageName, final int appUid) {
641 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
642 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
643 intent.putExtra(Settings.EXTRA_APP_UID, appUid);
644 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
645 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
646 return intent;
647 }
Mady Mellore8e07712019-01-23 12:45:33 -0800648
Mady Mellor3dff9e62019-02-05 18:12:53 -0800649 @Nullable
650 private PendingIntent getBubbleIntent(NotificationEntry entry) {
651 Notification notif = entry.notification.getNotification();
652 String packageName = entry.notification.getPackageName();
653 Notification.BubbleMetadata data = notif.getBubbleMetadata();
654 if (data != null && canLaunchInActivityView(data.getIntent(), true /* enableLogging */,
655 packageName)) {
656 return data.getIntent();
657 } else if (BubbleController.shouldUseContentIntent(mContext)
658 && canLaunchInActivityView(notif.contentIntent, false /* enableLogging */,
659 packageName)) {
660 return notif.contentIntent;
661 }
662 return null;
663 }
664
665 /**
666 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
667 *
668 * @param intent the pending intent of the bubble.
669 * @param enableLogging whether bubble developer error should be logged.
670 * @param packageName the notification package name for this bubble.
671 * @return
672 */
673 private boolean canLaunchInActivityView(PendingIntent intent, boolean enableLogging,
674 String packageName) {
675 if (intent == null) {
676 return false;
677 }
678 ActivityInfo info =
679 intent.getIntent().resolveActivityInfo(mContext.getPackageManager(), 0);
680 if (info == null) {
681 if (enableLogging) {
682 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
683 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
684 }
685 return false;
686 }
687 if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
688 if (enableLogging) {
689 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
690 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
691 }
692 return false;
693 }
694 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
695 if (enableLogging) {
696 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
697 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
698 }
699 return false;
700 }
701 return (info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
702 }
703
Mady Mellore8e07712019-01-23 12:45:33 -0800704 /**
705 * Listener that is notified when a bubble is blocked.
706 */
707 public interface OnBubbleBlockedListener {
708 /**
709 * Called when a bubble is blocked for the provided entry.
710 */
711 void onBubbleBlocked(NotificationEntry entry);
712 }
Steven Wub00225b2019-02-08 14:27:42 -0500713
714 /**
715 * Logs bubble UI click event.
716 *
Steven Wu45e38ae2019-03-25 16:16:59 -0400717 * @param entry the bubble notification entry that user is interacting with.
Steven Wub00225b2019-02-08 14:27:42 -0500718 * @param action the user interaction enum.
719 */
Steven Wu45e38ae2019-03-25 16:16:59 -0400720 private void logBubbleClickEvent(NotificationEntry entry, int action) {
721 StatusBarNotification notification = entry.notification;
Steven Wub00225b2019-02-08 14:27:42 -0500722 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
723 notification.getPackageName(),
724 notification.getNotification().getChannelId(),
725 notification.getId(),
726 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
727 mStackView.getBubbleCount(),
728 action,
729 mStackView.getNormalizedXPosition(),
Steven Wu45e38ae2019-03-25 16:16:59 -0400730 mStackView.getNormalizedYPosition(),
731 entry.showInShadeWhenBubble());
Steven Wub00225b2019-02-08 14:27:42 -0500732 }
Mady Mellor7af771a2019-03-07 15:04:54 -0800733
734 private int getDimenForPackageUser(int resId, String pkg, int userId) {
735 Resources r;
736 if (pkg != null) {
737 try {
738 if (userId == UserHandle.USER_ALL) {
739 userId = UserHandle.USER_SYSTEM;
740 }
741 r = mPm.getResourcesForApplicationAsUser(pkg, userId);
742 return r.getDimensionPixelSize(resId);
743 } catch (PackageManager.NameNotFoundException ex) {
744 // Uninstalled, don't care
745 } catch (Resources.NotFoundException e) {
746 // Invalid res id, return 0 and user our default
747 Log.e(TAG, "Couldn't find desired height res id", e);
748 }
749 }
750 return 0;
751 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800752}