blob: de887ff6ff86d28344ac147bf41b49fa3728224d [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 Mellor7af771a2019-03-07 15:04:54 -080046import android.os.UserHandle;
Mady Mellor9801e852019-01-22 14:50:28 -080047import android.provider.Settings;
Steven Wub00225b2019-02-08 14:27:42 -050048import android.service.notification.StatusBarNotification;
Mady Mellordea7ecf2018-12-10 15:47:40 -080049import android.util.AttributeSet;
Mady Mellor9801e852019-01-22 14:50:28 -080050import android.util.Log;
Steven Wub00225b2019-02-08 14:27:42 -050051import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080052import android.view.View;
Mady Mellor5029fa62019-03-05 12:16:21 -080053import android.view.ViewGroup;
Mady Mellor3dff9e62019-02-05 18:12:53 -080054import android.view.WindowInsets;
Mady Mellore8e07712019-01-23 12:45:33 -080055import android.widget.FrameLayout;
Mady Mellor9801e852019-01-22 14:50:28 -080056import android.widget.ImageButton;
Mady Mellore8e07712019-01-23 12:45:33 -080057import android.widget.ImageView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080058import android.widget.LinearLayout;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050059import android.widget.TextView;
Mady Mellordea7ecf2018-12-10 15:47:40 -080060
Mady Mellor3dff9e62019-02-05 18:12:53 -080061import com.android.systemui.Dependency;
Mady Mellore8e07712019-01-23 12:45:33 -080062import com.android.systemui.Interpolators;
Mady Mellordea7ecf2018-12-10 15:47:40 -080063import com.android.systemui.R;
64import com.android.systemui.recents.TriangleShape;
Mady Mellor9801e852019-01-22 14:50:28 -080065import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellor3dff9e62019-02-05 18:12:53 -080066import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
67import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
Mady Mellordea7ecf2018-12-10 15:47:40 -080068
69/**
70 * Container for the expanded bubble view, handles rendering the caret and header of the view.
71 */
Mady Mellor3d82e682019-02-05 13:34:48 -080072public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
Mady Mellor9801e852019-01-22 14:50:28 -080073 private static final String TAG = "BubbleExpandedView";
Mady Mellordea7ecf2018-12-10 15:47:40 -080074
Mady Mellor44ee2fe2019-01-30 17:51:16 -080075 // Configurable via bubble settings; just for testing
76 private boolean mUseFooter;
77 private boolean mShowOnTop;
78
Mady Mellordea7ecf2018-12-10 15:47:40 -080079 // The triangle pointing to the expanded view
80 private View mPointerView;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080081 private int mPointerMargin;
Mady Mellore8e07712019-01-23 12:45:33 -080082
83 // Header
84 private View mHeaderView;
Mady Mellor9801e852019-01-22 14:50:28 -080085 private ImageButton mDeepLinkIcon;
Mady Mellor9801e852019-01-22 14:50:28 -080086 private ImageButton mSettingsIcon;
Mady Mellore8e07712019-01-23 12:45:33 -080087
88 // Permission view
89 private View mPermissionView;
90
Mady Mellor3dff9e62019-02-05 18:12:53 -080091 // Views for expanded state
92 private ExpandableNotificationRow mNotifRow;
93 private ActivityView mActivityView;
94
95 private boolean mActivityViewReady = false;
96 private PendingIntent mBubbleIntent;
97
Mady Mellor5d8f1402019-02-21 18:23:52 -080098 private boolean mKeyboardVisible;
99 private boolean mNeedsNewHeight;
100
Mady Mellorfe7ec032019-01-30 17:32:49 -0800101 private int mMinHeight;
102 private int mHeaderHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800103 private int mBubbleHeight;
104 private int mPermissionHeight;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800105
Mady Mellor9801e852019-01-22 14:50:28 -0800106 private NotificationEntry mEntry;
107 private PackageManager mPm;
108 private String mAppName;
Mady Mellore8e07712019-01-23 12:45:33 -0800109 private Drawable mAppIcon;
110
111 private INotificationManager mNotificationManagerService;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800112 private BubbleController mBubbleController = Dependency.get(BubbleController.class);
Mady Mellor9801e852019-01-22 14:50:28 -0800113
Mady Mellor9801e852019-01-22 14:50:28 -0800114 private BubbleStackView mStackView;
115
Mady Mellor3dff9e62019-02-05 18:12:53 -0800116 private BubbleExpandedView.OnBubbleBlockedListener mOnBubbleBlockedListener;
117
118 private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
119 @Override
120 public void onActivityViewReady(ActivityView view) {
Mady Mellor6d002032019-02-13 13:45:17 -0800121 if (!mActivityViewReady) {
122 mActivityViewReady = true;
123 mActivityView.startActivity(mBubbleIntent);
124 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800125 }
126
127 @Override
128 public void onActivityViewDestroyed(ActivityView view) {
129 mActivityViewReady = false;
130 }
131
132 /**
133 * This is only called for tasks on this ActivityView, which is also set to
134 * single-task mode -- meaning never more than one task on this display. If a task
135 * is being removed, it's the top Activity finishing and this bubble should
136 * be removed or collapsed.
137 */
138 @Override
139 public void onTaskRemovalStarted(int taskId) {
140 if (mEntry != null) {
141 // Must post because this is called from a binder thread.
Mark Renouf08bc42a2019-03-07 13:01:59 -0500142 post(() -> mBubbleController.removeBubble(mEntry.key,
143 BubbleController.DISMISS_TASK_FINISHED));
Mady Mellor3dff9e62019-02-05 18:12:53 -0800144 }
145 }
146 };
Mady Mellore8e07712019-01-23 12:45:33 -0800147
Mady Mellor3d82e682019-02-05 13:34:48 -0800148 public BubbleExpandedView(Context context) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800149 this(context, null);
150 }
151
Mady Mellor3d82e682019-02-05 13:34:48 -0800152 public BubbleExpandedView(Context context, AttributeSet attrs) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800153 this(context, attrs, 0);
154 }
155
Mady Mellor3d82e682019-02-05 13:34:48 -0800156 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800157 this(context, attrs, defStyleAttr, 0);
158 }
159
Mady Mellor3d82e682019-02-05 13:34:48 -0800160 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
Mady Mellordea7ecf2018-12-10 15:47:40 -0800161 int defStyleRes) {
162 super(context, attrs, defStyleAttr, defStyleRes);
Mady Mellor9801e852019-01-22 14:50:28 -0800163 mPm = context.getPackageManager();
Mady Mellorfe7ec032019-01-30 17:32:49 -0800164 mMinHeight = getResources().getDimensionPixelSize(
Mady Mellor3dff9e62019-02-05 18:12:53 -0800165 R.dimen.bubble_expanded_default_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800166 mPointerMargin = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_margin);
Mady Mellore8e07712019-01-23 12:45:33 -0800167 try {
168 mNotificationManagerService = INotificationManager.Stub.asInterface(
169 ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE));
170 } catch (ServiceManager.ServiceNotFoundException e) {
171 Log.w(TAG, e);
172 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800173 }
174
175 @Override
176 protected void onFinishInflate() {
177 super.onFinishInflate();
178
179 Resources res = getResources();
180 mPointerView = findViewById(R.id.pointer_view);
181 int width = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
182 int height = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
Mady Mellordd497052019-01-30 17:23:48 -0800183
184 TypedArray ta = getContext().obtainStyledAttributes(
185 new int[] {android.R.attr.colorBackgroundFloating});
186 int bgColor = ta.getColor(0, Color.WHITE);
187 ta.recycle();
188
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800189 mShowOnTop = BubbleController.showBubblesAtTop(getContext());
190 mUseFooter = BubbleController.useFooter(getContext());
191
Mady Mellordea7ecf2018-12-10 15:47:40 -0800192 ShapeDrawable triangleDrawable = new ShapeDrawable(
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800193 TriangleShape.create(width, height, mShowOnTop /* pointUp */));
Mady Mellordd497052019-01-30 17:23:48 -0800194 triangleDrawable.setTint(bgColor);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800195 mPointerView.setBackground(triangleDrawable);
Mady Mellor9801e852019-01-22 14:50:28 -0800196
Mady Mellore8e07712019-01-23 12:45:33 -0800197 FrameLayout viewWrapper = findViewById(R.id.header_permission_wrapper);
Mady Mellor310c7c62019-02-21 17:03:08 -0800198 viewWrapper.setBackground(createHeaderPermissionBackground(bgColor));
199
Mady Mellore8e07712019-01-23 12:45:33 -0800200 LayoutTransition transition = new LayoutTransition();
201 transition.setDuration(200);
202
203 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
204 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
205 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
206
207 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
208 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
209 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
210
211 transition.setAnimateParentHierarchy(false);
212 viewWrapper.setLayoutTransition(transition);
213 viewWrapper.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
214
Mady Mellor310c7c62019-02-21 17:03:08 -0800215
Mady Mellorfe7ec032019-01-30 17:32:49 -0800216 mHeaderHeight = getContext().getResources().getDimensionPixelSize(
217 R.dimen.bubble_expanded_header_height);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800218 mPermissionHeight = getContext().getResources().getDimensionPixelSize(
219 R.dimen.bubble_permission_height);
Mady Mellore8e07712019-01-23 12:45:33 -0800220 mHeaderView = findViewById(R.id.header_layout);
Mady Mellor9801e852019-01-22 14:50:28 -0800221 mDeepLinkIcon = findViewById(R.id.deep_link_button);
222 mSettingsIcon = findViewById(R.id.settings_button);
223 mDeepLinkIcon.setOnClickListener(this);
224 mSettingsIcon.setOnClickListener(this);
Mady Mellore8e07712019-01-23 12:45:33 -0800225
226 mPermissionView = findViewById(R.id.permission_layout);
227 findViewById(R.id.no_bubbles_button).setOnClickListener(this);
228 findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800229
230 mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
231 true /* singleTaskInstance */);
232 addView(mActivityView);
233
Mady Mellor5d8f1402019-02-21 18:23:52 -0800234 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
235 // Keep track of IME displaying because we should not make any adjustments that might
236 // cause a config change while the IME is displayed otherwise it'll loose focus.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800237 final int keyboardHeight = insets.getSystemWindowInsetBottom()
238 - insets.getStableInsetBottom();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800239 mKeyboardVisible = keyboardHeight != 0;
240 if (!mKeyboardVisible && mNeedsNewHeight) {
241 updateHeight();
242 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800243 return view.onApplyWindowInsets(insets);
244 });
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800245
246 if (!mShowOnTop) {
247 removeView(mPointerView);
248 if (mUseFooter) {
Mady Mellor310c7c62019-02-21 17:03:08 -0800249 View divider = findViewById(R.id.divider);
250 viewWrapper.removeView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800251 removeView(viewWrapper);
Mady Mellor310c7c62019-02-21 17:03:08 -0800252 addView(divider);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800253 addView(viewWrapper);
254 }
255 addView(mPointerView);
256 }
Mady Mellore8e07712019-01-23 12:45:33 -0800257 }
258
Mady Mellor5d8f1402019-02-21 18:23:52 -0800259 @Override
260 protected void onDetachedFromWindow() {
261 super.onDetachedFromWindow();
262 mKeyboardVisible = false;
263 mNeedsNewHeight = false;
264 if (mActivityView != null) {
265 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
266 }
267 }
268
269 /**
270 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
271 * This should be done post-move and post-animation.
272 */
273 void updateInsets(WindowInsets insets) {
274 if (usingActivityView()) {
275 Point displaySize = new Point();
276 mActivityView.getContext().getDisplay().getSize(displaySize);
277 int[] windowLocation = mActivityView.getLocationOnScreen();
278 final int windowBottom = windowLocation[1] + mActivityView.getHeight();
279 final int keyboardHeight = insets.getSystemWindowInsetBottom()
280 - insets.getStableInsetBottom();
281 final int insetsBottom = Math.max(0,
282 windowBottom + keyboardHeight - displaySize.y);
283 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
284 }
285 }
286
Mady Mellore8e07712019-01-23 12:45:33 -0800287 /**
Mady Mellor310c7c62019-02-21 17:03:08 -0800288 * Creates a background with corners rounded based on how the view is configured to display
289 */
290 private Drawable createHeaderPermissionBackground(int bgColor) {
291 TypedArray ta2 = getContext().obtainStyledAttributes(
292 new int[] {android.R.attr.dialogCornerRadius});
293 final float cr = ta2.getDimension(0, 0f);
294 ta2.recycle();
295
296 float[] radii = mUseFooter
297 ? new float[] {0, 0, 0, 0, cr, cr, cr, cr}
298 : new float[] {cr, cr, cr, cr, 0, 0, 0, 0};
299 GradientDrawable chromeBackground = new GradientDrawable();
300 chromeBackground.setShape(GradientDrawable.RECTANGLE);
301 chromeBackground.setCornerRadii(radii);
302 chromeBackground.setColor(bgColor);
303 return chromeBackground;
304 }
305
306 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800307 * Sets the listener to notify when a bubble has been blocked.
308 */
309 public void setOnBlockedListener(OnBubbleBlockedListener listener) {
310 mOnBubbleBlockedListener = listener;
Mady Mellor9801e852019-01-22 14:50:28 -0800311 }
312
313 /**
314 * Sets the notification entry used to populate this view.
315 */
316 public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
317 mStackView = stackView;
318 mEntry = entry;
319
320 ApplicationInfo info;
321 try {
322 info = mPm.getApplicationInfo(
323 entry.notification.getPackageName(),
324 PackageManager.MATCH_UNINSTALLED_PACKAGES
325 | PackageManager.MATCH_DISABLED_COMPONENTS
326 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
327 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
328 if (info != null) {
329 mAppName = String.valueOf(mPm.getApplicationLabel(info));
Mady Mellore8e07712019-01-23 12:45:33 -0800330 mAppIcon = mPm.getApplicationIcon(info);
Mady Mellor9801e852019-01-22 14:50:28 -0800331 }
332 } catch (PackageManager.NameNotFoundException e) {
333 // Ahh... just use package name
334 mAppName = entry.notification.getPackageName();
335 }
Mady Mellore8e07712019-01-23 12:45:33 -0800336 if (mAppIcon == null) {
337 mAppIcon = mPm.getDefaultActivityIcon();
338 }
Mady Mellor9801e852019-01-22 14:50:28 -0800339 updateHeaderView();
Mady Mellore8e07712019-01-23 12:45:33 -0800340 updatePermissionView();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800341 updateExpandedView();
Mady Mellor6d002032019-02-13 13:45:17 -0800342 }
343
344 /**
345 * Lets activity view know it should be shown / populated.
346 */
Mady Mellor5029fa62019-03-05 12:16:21 -0800347 public void populateExpandedView() {
348 if (usingActivityView()) {
349 mActivityView.setCallback(mStateCallback);
350 } else {
351 // We're using notification template
352 ViewGroup parent = (ViewGroup) mNotifRow.getParent();
353 if (parent == this) {
354 // Already added
355 return;
356 } else if (parent != null) {
357 // Still in the shade... remove it
358 parent.removeView(mNotifRow);
359 }
360 if (mShowOnTop) {
361 addView(mNotifRow);
362 } else {
363 addView(mNotifRow, mUseFooter ? 0 : 1);
364 }
365 }
Mady Mellor9801e852019-01-22 14:50:28 -0800366 }
367
Mady Mellorfe7ec032019-01-30 17:32:49 -0800368 /**
369 * Updates the entry backing this view. This will not re-populate ActivityView, it will
370 * only update the deep-links in the header, the title, and the height of the view.
371 */
372 public void update(NotificationEntry entry) {
373 if (entry.key.equals(mEntry.key)) {
374 mEntry = entry;
375 updateHeaderView();
376 updateHeight();
377 } else {
378 Log.w(TAG, "Trying to update entry with different key, new entry: "
379 + entry.key + " old entry: " + mEntry.key);
380 }
381 }
382
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700383 /**
Lyn Han76e803d2019-03-26 17:31:33 -0700384 * Update header color when user toggles dark mode.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700385 */
386 void updateHeaderColor() {
Lyn Han76e803d2019-03-26 17:31:33 -0700387 TypedArray ta = mContext.obtainStyledAttributes(
388 new int[] {android.R.attr.colorBackgroundFloating, android.R.attr.colorForeground});
389 int bgColor = ta.getColor(0, Color.WHITE /* default */);
390 int btnColor = ta.getColor(1, Color.BLACK /* default */);
391 ta.recycle();
392
393 mHeaderView.setBackgroundColor(bgColor);
394 mSettingsIcon.setColorFilter(btnColor);
395 mDeepLinkIcon.setColorFilter(btnColor);
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700396 }
397
Mady Mellor9801e852019-01-22 14:50:28 -0800398 private void updateHeaderView() {
399 mSettingsIcon.setContentDescription(getResources().getString(
400 R.string.bubbles_settings_button_description, mAppName));
401 mDeepLinkIcon.setContentDescription(getResources().getString(
402 R.string.bubbles_deep_link_button_description, mAppName));
Mady Mellore8e07712019-01-23 12:45:33 -0800403 }
404
405 private void updatePermissionView() {
406 boolean hasUserApprovedBubblesForPackage = false;
407 try {
408 hasUserApprovedBubblesForPackage =
409 mNotificationManagerService.hasUserApprovedBubblesForPackage(
410 mEntry.notification.getPackageName(), mEntry.notification.getUid());
411 } catch (RemoteException e) {
412 Log.w(TAG, e);
413 }
414 if (hasUserApprovedBubblesForPackage) {
415 mHeaderView.setVisibility(VISIBLE);
416 mPermissionView.setVisibility(GONE);
417 } else {
418 mHeaderView.setVisibility(GONE);
419 mPermissionView.setVisibility(VISIBLE);
420 ((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();
516 if (id == R.id.deep_link_button) {
517 mStackView.collapseStack(() -> {
518 try {
519 n.contentIntent.send();
Steven Wu45e38ae2019-03-25 16:16:59 -0400520 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500521 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_APP);
Mady Mellor9801e852019-01-22 14:50:28 -0800522 } catch (PendingIntent.CanceledException e) {
523 Log.w(TAG, "Failed to send intent for bubble with key: "
524 + (mEntry != null ? mEntry.key : " null entry"));
525 }
526 });
527 } else if (id == R.id.settings_button) {
528 Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
529 mEntry.notification.getUid());
Steven Wub00225b2019-02-08 14:27:42 -0500530 mStackView.collapseStack(() -> {
531 mContext.startActivity(intent);
Steven Wu45e38ae2019-03-25 16:16:59 -0400532 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500533 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
534 });
Mady Mellore8e07712019-01-23 12:45:33 -0800535 } else if (id == R.id.no_bubbles_button) {
536 setBubblesAllowed(false);
537 } else if (id == R.id.yes_bubbles_button) {
538 setBubblesAllowed(true);
539 }
540 }
541
542 private void setBubblesAllowed(boolean allowed) {
543 try {
544 mNotificationManagerService.setBubblesAllowed(
545 mEntry.notification.getPackageName(),
546 mEntry.notification.getUid(),
547 allowed);
548 if (allowed) {
549 mPermissionView.setVisibility(GONE);
550 mHeaderView.setVisibility(VISIBLE);
551 } else if (mOnBubbleBlockedListener != null) {
552 mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
553 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800554 mStackView.onExpandedHeightChanged();
Steven Wu45e38ae2019-03-25 16:16:59 -0400555 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500556 allowed ? StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_IN :
557 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_OUT);
Mady Mellore8e07712019-01-23 12:45:33 -0800558 } catch (RemoteException e) {
559 Log.w(TAG, e);
Mady Mellor9801e852019-01-22 14:50:28 -0800560 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800561 }
562
563 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800564 * Update appearance of the expanded view being displayed.
565 */
566 public void updateView() {
567 if (usingActivityView()
568 && mActivityView.getVisibility() == VISIBLE
569 && mActivityView.isAttachedToWindow()) {
570 mActivityView.onLocationChanged();
571 } else if (mNotifRow != null) {
572 applyRowState(mNotifRow);
573 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800574 updateHeight();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800575 }
576
577 /**
Mady Mellordea7ecf2018-12-10 15:47:40 -0800578 * Set the x position that the tip of the triangle should point to.
579 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800580 public void setPointerPosition(float x) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800581 // Adjust for the pointer size
Mady Mellor3dff9e62019-02-05 18:12:53 -0800582 x -= (mPointerView.getWidth() / 2f);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800583 mPointerView.setTranslationX(x);
584 }
585
586 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800587 * Removes and releases an ActivityView if one was previously created for this bubble.
Mady Mellordea7ecf2018-12-10 15:47:40 -0800588 */
Mady Mellor94d94a72019-03-05 18:16:59 -0800589 public void cleanUpExpandedState() {
590 removeView(mNotifRow);
591
Mady Mellor3dff9e62019-02-05 18:12:53 -0800592 if (mActivityView == null) {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500593 return;
594 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500595 if (mActivityViewReady) {
596 mActivityView.release();
Mady Mellordea7ecf2018-12-10 15:47:40 -0800597 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500598 removeView(mActivityView);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800599 mActivityView = null;
600 mActivityViewReady = false;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800601 }
602
Mady Mellor3dff9e62019-02-05 18:12:53 -0800603 private boolean usingActivityView() {
Mady Mellor323fb0b2019-03-25 12:15:22 -0700604 return mBubbleIntent != null && mActivityView != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800605 }
606
607 private void applyRowState(ExpandableNotificationRow view) {
608 view.reset();
609 view.setHeadsUp(false);
610 view.resetTranslation();
611 view.setOnKeyguard(false);
612 view.setOnAmbient(false);
613 view.setClipBottomAmount(0);
614 view.setClipTopAmount(0);
615 view.setContentTransformationAmount(0, false);
616 view.setIconsVisible(true);
617
618 // TODO - Need to reset this (and others) when view goes back in shade, leave for now
619 // view.setTopRoundness(1, false);
620 // view.setBottomRoundness(1, false);
621
622 ExpandableViewState viewState = view.getViewState();
623 viewState = viewState == null ? new ExpandableViewState() : viewState;
624 viewState.height = view.getIntrinsicHeight();
625 viewState.gone = false;
626 viewState.hidden = false;
627 viewState.dimmed = false;
628 viewState.dark = false;
629 viewState.alpha = 1f;
630 viewState.notGoneIndex = -1;
631 viewState.xTranslation = 0;
632 viewState.yTranslation = 0;
633 viewState.zTranslation = 0;
634 viewState.scaleX = 1;
635 viewState.scaleY = 1;
636 viewState.inShelf = true;
637 viewState.headsUpIsVisible = false;
638 viewState.applyToView(view);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800639 }
Mady Mellor9801e852019-01-22 14:50:28 -0800640
641 private Intent getSettingsIntent(String packageName, final int appUid) {
642 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
643 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
644 intent.putExtra(Settings.EXTRA_APP_UID, appUid);
645 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
646 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
647 return intent;
648 }
Mady Mellore8e07712019-01-23 12:45:33 -0800649
Mady Mellor3dff9e62019-02-05 18:12:53 -0800650 @Nullable
651 private PendingIntent getBubbleIntent(NotificationEntry entry) {
652 Notification notif = entry.notification.getNotification();
653 String packageName = entry.notification.getPackageName();
654 Notification.BubbleMetadata data = notif.getBubbleMetadata();
655 if (data != null && canLaunchInActivityView(data.getIntent(), true /* enableLogging */,
656 packageName)) {
657 return data.getIntent();
658 } else if (BubbleController.shouldUseContentIntent(mContext)
659 && canLaunchInActivityView(notif.contentIntent, false /* enableLogging */,
660 packageName)) {
661 return notif.contentIntent;
662 }
663 return null;
664 }
665
666 /**
667 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
668 *
669 * @param intent the pending intent of the bubble.
670 * @param enableLogging whether bubble developer error should be logged.
671 * @param packageName the notification package name for this bubble.
672 * @return
673 */
674 private boolean canLaunchInActivityView(PendingIntent intent, boolean enableLogging,
675 String packageName) {
676 if (intent == null) {
677 return false;
678 }
679 ActivityInfo info =
680 intent.getIntent().resolveActivityInfo(mContext.getPackageManager(), 0);
681 if (info == null) {
682 if (enableLogging) {
683 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
684 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
685 }
686 return false;
687 }
688 if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
689 if (enableLogging) {
690 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
691 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
692 }
693 return false;
694 }
695 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
696 if (enableLogging) {
697 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
698 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
699 }
700 return false;
701 }
702 return (info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
703 }
704
Mady Mellore8e07712019-01-23 12:45:33 -0800705 /**
706 * Listener that is notified when a bubble is blocked.
707 */
708 public interface OnBubbleBlockedListener {
709 /**
710 * Called when a bubble is blocked for the provided entry.
711 */
712 void onBubbleBlocked(NotificationEntry entry);
713 }
Steven Wub00225b2019-02-08 14:27:42 -0500714
715 /**
716 * Logs bubble UI click event.
717 *
Steven Wu45e38ae2019-03-25 16:16:59 -0400718 * @param entry the bubble notification entry that user is interacting with.
Steven Wub00225b2019-02-08 14:27:42 -0500719 * @param action the user interaction enum.
720 */
Steven Wu45e38ae2019-03-25 16:16:59 -0400721 private void logBubbleClickEvent(NotificationEntry entry, int action) {
722 StatusBarNotification notification = entry.notification;
Steven Wub00225b2019-02-08 14:27:42 -0500723 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
724 notification.getPackageName(),
725 notification.getNotification().getChannelId(),
726 notification.getId(),
727 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
728 mStackView.getBubbleCount(),
729 action,
730 mStackView.getNormalizedXPosition(),
Steven Wu45e38ae2019-03-25 16:16:59 -0400731 mStackView.getNormalizedYPosition(),
732 entry.showInShadeWhenBubble());
Steven Wub00225b2019-02-08 14:27:42 -0500733 }
Mady Mellor7af771a2019-03-07 15:04:54 -0800734
735 private int getDimenForPackageUser(int resId, String pkg, int userId) {
736 Resources r;
737 if (pkg != null) {
738 try {
739 if (userId == UserHandle.USER_ALL) {
740 userId = UserHandle.USER_SYSTEM;
741 }
742 r = mPm.getResourcesForApplicationAsUser(pkg, userId);
743 return r.getDimensionPixelSize(resId);
744 } catch (PackageManager.NameNotFoundException ex) {
745 // Uninstalled, don't care
746 } catch (Resources.NotFoundException e) {
747 // Invalid res id, return 0 and user our default
748 Log.e(TAG, "Couldn't find desired height res id", e);
749 }
750 }
751 return 0;
752 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800753}