blob: 17275ad58240b7575b04f597d797916680b50810 [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/**
Lyn Han02cca812019-04-02 16:27:32 -070072 * Container for the expanded bubble view, handles rendering the caret and settings icon.
Mady Mellordea7ecf2018-12-10 15:47:40 -080073 */
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
77 // The triangle pointing to the expanded view
78 private View mPointerView;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080079 private int mPointerMargin;
Mady Mellore8e07712019-01-23 12:45:33 -080080
Lyn Hanc26ff122019-03-29 16:46:07 -070081 private ImageView mSettingsIcon;
Mady Mellore8e07712019-01-23 12:45:33 -080082
83 // Permission view
84 private View mPermissionView;
Lyn Han02cca812019-04-02 16:27:32 -070085 private TextView mPermissionPrompt;
Mady Mellore8e07712019-01-23 12:45:33 -080086
Mady Mellor3dff9e62019-02-05 18:12:53 -080087 // Views for expanded state
88 private ExpandableNotificationRow mNotifRow;
89 private ActivityView mActivityView;
90
91 private boolean mActivityViewReady = false;
92 private PendingIntent mBubbleIntent;
93
Mady Mellor5d8f1402019-02-21 18:23:52 -080094 private boolean mKeyboardVisible;
95 private boolean mNeedsNewHeight;
96
Mady Mellorfe7ec032019-01-30 17:32:49 -080097 private int mMinHeight;
Lyn Han02cca812019-04-02 16:27:32 -070098 private int mSettingsIconHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -080099 private int mBubbleHeight;
100 private int mPermissionHeight;
Lyn Han02cca812019-04-02 16:27:32 -0700101 private int mIconInset;
102 private Drawable mSettingsIconDrawable;
103 private int mPointerWidth;
104 private int mPointerHeight;
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);
Lyn Han02cca812019-04-02 16:27:32 -0700181 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
182 mPointerHeight = 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
Lyn Han02cca812019-04-02 16:27:32 -0700189 ShapeDrawable triangleDrawable = new ShapeDrawable(TriangleShape.create(
190 mPointerWidth, mPointerHeight, false /* pointUp */));
191
Mady Mellordd497052019-01-30 17:23:48 -0800192 triangleDrawable.setTint(bgColor);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800193 mPointerView.setBackground(triangleDrawable);
Mady Mellor9801e852019-01-22 14:50:28 -0800194
Lyn Han02cca812019-04-02 16:27:32 -0700195 FrameLayout permissionOrSettings = findViewById(R.id.permission_or_settings);
Mady Mellor310c7c62019-02-21 17:03:08 -0800196
Mady Mellore8e07712019-01-23 12:45:33 -0800197 LayoutTransition transition = new LayoutTransition();
198 transition.setDuration(200);
199
200 ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 0f, 1f);
201 transition.setAnimator(LayoutTransition.APPEARING, appearAnimator);
202 transition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN);
203
204 ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f);
205 transition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator);
206 transition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT);
207
208 transition.setAnimateParentHierarchy(false);
Lyn Han02cca812019-04-02 16:27:32 -0700209 permissionOrSettings.setLayoutTransition(transition);
210 permissionOrSettings.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
Mady Mellore8e07712019-01-23 12:45:33 -0800211
Lyn Han02cca812019-04-02 16:27:32 -0700212 mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
Mady Mellorfe7ec032019-01-30 17:32:49 -0800213 R.dimen.bubble_expanded_header_height);
Lyn Han02cca812019-04-02 16:27:32 -0700214 mSettingsIcon = findViewById(R.id.settings_button);
215 mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
216 mSettingsIcon.setOnClickListener(this);
217 // Save initial drawable to create adaptive icons that will take its place.
218 mSettingsIconDrawable = mSettingsIcon.getDrawable();
219
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800220 mPermissionHeight = getContext().getResources().getDimensionPixelSize(
221 R.dimen.bubble_permission_height);
Mady Mellore8e07712019-01-23 12:45:33 -0800222 mPermissionView = findViewById(R.id.permission_layout);
Lyn Han02cca812019-04-02 16:27:32 -0700223 mPermissionPrompt = mPermissionView.findViewById(R.id.prompt);
Lyn Hanc26ff122019-03-29 16:46:07 -0700224
Mady Mellore8e07712019-01-23 12:45:33 -0800225 findViewById(R.id.no_bubbles_button).setOnClickListener(this);
226 findViewById(R.id.yes_bubbles_button).setOnClickListener(this);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800227
228 mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
229 true /* singleTaskInstance */);
230 addView(mActivityView);
231
Mady Mellor52b1ac62019-04-10 16:59:03 -0700232 // Make sure pointer is below activity view
233 bringChildToFront(mPointerView);
234
Mady Mellor5d8f1402019-02-21 18:23:52 -0800235 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
236 // Keep track of IME displaying because we should not make any adjustments that might
237 // cause a config change while the IME is displayed otherwise it'll loose focus.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800238 final int keyboardHeight = insets.getSystemWindowInsetBottom()
239 - insets.getStableInsetBottom();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800240 mKeyboardVisible = keyboardHeight != 0;
241 if (!mKeyboardVisible && mNeedsNewHeight) {
242 updateHeight();
243 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800244 return view.onApplyWindowInsets(insets);
245 });
Mady Mellore8e07712019-01-23 12:45:33 -0800246 }
247
Mady Mellor5d8f1402019-02-21 18:23:52 -0800248 @Override
249 protected void onDetachedFromWindow() {
250 super.onDetachedFromWindow();
251 mKeyboardVisible = false;
252 mNeedsNewHeight = false;
253 if (mActivityView != null) {
254 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
255 }
256 }
257
258 /**
259 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
260 * This should be done post-move and post-animation.
261 */
262 void updateInsets(WindowInsets insets) {
263 if (usingActivityView()) {
264 Point displaySize = new Point();
265 mActivityView.getContext().getDisplay().getSize(displaySize);
266 int[] windowLocation = mActivityView.getLocationOnScreen();
267 final int windowBottom = windowLocation[1] + mActivityView.getHeight();
268 final int keyboardHeight = insets.getSystemWindowInsetBottom()
269 - insets.getStableInsetBottom();
270 final int insetsBottom = Math.max(0,
271 windowBottom + keyboardHeight - displaySize.y);
272 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
273 }
274 }
275
Mady Mellore8e07712019-01-23 12:45:33 -0800276 /**
Mady Mellor310c7c62019-02-21 17:03:08 -0800277 * Creates a background with corners rounded based on how the view is configured to display
278 */
Lyn Han02cca812019-04-02 16:27:32 -0700279 private Drawable createPermissionBackground(int bgColor) {
Mady Mellor310c7c62019-02-21 17:03:08 -0800280 TypedArray ta2 = getContext().obtainStyledAttributes(
281 new int[] {android.R.attr.dialogCornerRadius});
282 final float cr = ta2.getDimension(0, 0f);
283 ta2.recycle();
284
Lyn Han3f5c3a42019-04-01 15:59:56 -0700285 float[] radii = new float[] {cr, cr, cr, cr, 0, 0, 0, 0};
Mady Mellor310c7c62019-02-21 17:03:08 -0800286 GradientDrawable chromeBackground = new GradientDrawable();
287 chromeBackground.setShape(GradientDrawable.RECTANGLE);
288 chromeBackground.setCornerRadii(radii);
289 chromeBackground.setColor(bgColor);
290 return chromeBackground;
291 }
292
293 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800294 * Sets the listener to notify when a bubble has been blocked.
295 */
296 public void setOnBlockedListener(OnBubbleBlockedListener listener) {
297 mOnBubbleBlockedListener = listener;
Mady Mellor9801e852019-01-22 14:50:28 -0800298 }
299
300 /**
301 * Sets the notification entry used to populate this view.
302 */
303 public void setEntry(NotificationEntry entry, BubbleStackView stackView) {
304 mStackView = stackView;
305 mEntry = entry;
306
307 ApplicationInfo info;
308 try {
309 info = mPm.getApplicationInfo(
310 entry.notification.getPackageName(),
311 PackageManager.MATCH_UNINSTALLED_PACKAGES
312 | PackageManager.MATCH_DISABLED_COMPONENTS
313 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
314 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
315 if (info != null) {
316 mAppName = String.valueOf(mPm.getApplicationLabel(info));
Mady Mellore8e07712019-01-23 12:45:33 -0800317 mAppIcon = mPm.getApplicationIcon(info);
Mady Mellor9801e852019-01-22 14:50:28 -0800318 }
319 } catch (PackageManager.NameNotFoundException e) {
320 // Ahh... just use package name
321 mAppName = entry.notification.getPackageName();
322 }
Mady Mellore8e07712019-01-23 12:45:33 -0800323 if (mAppIcon == null) {
324 mAppIcon = mPm.getDefaultActivityIcon();
325 }
Lyn Han02cca812019-04-02 16:27:32 -0700326 updateTheme();
327 togglePermissionOrSettings();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800328 updateExpandedView();
Mady Mellor6d002032019-02-13 13:45:17 -0800329 }
330
331 /**
332 * Lets activity view know it should be shown / populated.
333 */
Mady Mellor5029fa62019-03-05 12:16:21 -0800334 public void populateExpandedView() {
335 if (usingActivityView()) {
336 mActivityView.setCallback(mStateCallback);
337 } else {
338 // We're using notification template
339 ViewGroup parent = (ViewGroup) mNotifRow.getParent();
340 if (parent == this) {
341 // Already added
342 return;
343 } else if (parent != null) {
344 // Still in the shade... remove it
345 parent.removeView(mNotifRow);
346 }
Lyn Han3f5c3a42019-04-01 15:59:56 -0700347 addView(mNotifRow, 1 /* index */);
Mady Mellor5029fa62019-03-05 12:16:21 -0800348 }
Mady Mellor9801e852019-01-22 14:50:28 -0800349 }
350
Mady Mellorfe7ec032019-01-30 17:32:49 -0800351 /**
352 * Updates the entry backing this view. This will not re-populate ActivityView, it will
Lyn Han02cca812019-04-02 16:27:32 -0700353 * only update the deep-links in the title, and the height of the view.
Mady Mellorfe7ec032019-01-30 17:32:49 -0800354 */
355 public void update(NotificationEntry entry) {
356 if (entry.key.equals(mEntry.key)) {
357 mEntry = entry;
Lyn Han02cca812019-04-02 16:27:32 -0700358 updateSettingsContentDescription();
Mady Mellorfe7ec032019-01-30 17:32:49 -0800359 updateHeight();
360 } else {
361 Log.w(TAG, "Trying to update entry with different key, new entry: "
362 + entry.key + " old entry: " + mEntry.key);
363 }
364 }
365
Lyn Han02cca812019-04-02 16:27:32 -0700366 void updateTheme() {
367 // Get new colors.
Lyn Han76e803d2019-03-26 17:31:33 -0700368 TypedArray ta = mContext.obtainStyledAttributes(
Lyn Han02cca812019-04-02 16:27:32 -0700369 new int[]{android.R.attr.colorBackgroundFloating, android.R.attr.colorForeground});
Lyn Hanc26ff122019-03-29 16:46:07 -0700370 int backgroundColor = ta.getColor(0, Color.WHITE /* default */);
371 int foregroundColor = ta.getColor(1, Color.BLACK /* default */);
Lyn Han76e803d2019-03-26 17:31:33 -0700372 ta.recycle();
373
Lyn Han02cca812019-04-02 16:27:32 -0700374 // Must clear tint first - otherwise tint updates inconsistently.
375 mSettingsIconDrawable.setTintList(null);
376 mSettingsIconDrawable.setTint(foregroundColor);
Lyn Hanc26ff122019-03-29 16:46:07 -0700377
Lyn Han02cca812019-04-02 16:27:32 -0700378 InsetDrawable foreground = new InsetDrawable(mSettingsIconDrawable, mIconInset);
Lyn Hanc26ff122019-03-29 16:46:07 -0700379 ColorDrawable background = new ColorDrawable(backgroundColor);
380 AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(background,
381 foreground);
382 mSettingsIcon.setImageDrawable(adaptiveIcon);
Lyn Han02cca812019-04-02 16:27:32 -0700383
384 // Update permission prompt color.
385 mPermissionView.setBackground(createPermissionBackground(backgroundColor));
386 mPermissionPrompt.setTextColor(foregroundColor);
387
388 // Update triangle color.
389 ShapeDrawable triangleDrawable = new ShapeDrawable(
390 TriangleShape.create(mPointerWidth, mPointerHeight, false /* pointUp */));
391 triangleDrawable.setTint(backgroundColor);
392 mPointerView.setBackground(triangleDrawable);
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700393 }
394
Lyn Han02cca812019-04-02 16:27:32 -0700395 void togglePermissionOrSettings() {
Mady Mellore8e07712019-01-23 12:45:33 -0800396 boolean hasUserApprovedBubblesForPackage = false;
397 try {
398 hasUserApprovedBubblesForPackage =
399 mNotificationManagerService.hasUserApprovedBubblesForPackage(
400 mEntry.notification.getPackageName(), mEntry.notification.getUid());
401 } catch (RemoteException e) {
402 Log.w(TAG, e);
403 }
404 if (hasUserApprovedBubblesForPackage) {
Lyn Hanc26ff122019-03-29 16:46:07 -0700405 showSettingsIcon();
Mady Mellore8e07712019-01-23 12:45:33 -0800406 } else {
Lyn Hanc26ff122019-03-29 16:46:07 -0700407 showPermissionView();
Mady Mellor9801e852019-01-22 14:50:28 -0800408 }
409 }
410
Mady Mellor3dff9e62019-02-05 18:12:53 -0800411 private void updateExpandedView() {
412 mBubbleIntent = getBubbleIntent(mEntry);
413 if (mBubbleIntent != null) {
414 if (mNotifRow != null) {
415 // Clear out the row if we had it previously
416 removeView(mNotifRow);
417 mNotifRow = null;
418 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800419 mActivityView.setVisibility(VISIBLE);
420 } else {
421 // Hide activity view if we had it previously
422 mActivityView.setVisibility(GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800423 mNotifRow = mEntry.getRow();
Mady Mellor5029fa62019-03-05 12:16:21 -0800424
Mady Mellor3dff9e62019-02-05 18:12:53 -0800425 }
426 updateView();
427 }
428
Mark Renouf041d7262019-02-06 12:09:41 -0500429 boolean performBackPressIfNeeded() {
Mady Mellor323fb0b2019-03-25 12:15:22 -0700430 if (!usingActivityView()) {
Mark Renouf041d7262019-02-06 12:09:41 -0500431 return false;
432 }
433 mActivityView.performBackPress();
434 return true;
435 }
436
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800437 /**
438 * @return total height that the expanded view occupies.
439 */
440 int getExpandedSize() {
441 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
Lyn Han02cca812019-04-02 16:27:32 -0700442 ? mSettingsIconHeight
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800443 : mPermissionHeight;
444 return mBubbleHeight + mPointerView.getHeight() + mPointerMargin
445 + chromeHeight;
446 }
447
Mady Mellorfe7ec032019-01-30 17:32:49 -0800448 void updateHeight() {
449 if (usingActivityView()) {
450 Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
Mady Mellor7af771a2019-03-07 15:04:54 -0800451 float desiredHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800452 if (data == null) {
453 // This is a contentIntent based bubble, lets allow it to be the max height
454 // as it was forced into this mode and not prepared to be small
455 desiredHeight = mStackView.getMaxExpandedHeight();
456 } else {
Mady Mellor7af771a2019-03-07 15:04:54 -0800457 boolean useRes = data.getDesiredHeightResId() != 0;
458 float desiredPx;
459 if (useRes) {
460 desiredPx = getDimenForPackageUser(data.getDesiredHeightResId(),
461 mEntry.notification.getPackageName(),
462 mEntry.notification.getUser().getIdentifier());
463 } else {
464 desiredPx = data.getDesiredHeight()
465 * getContext().getResources().getDisplayMetrics().density;
466 }
467 desiredHeight = desiredPx > 0 ? desiredPx : mMinHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800468 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800469 int chromeHeight = mPermissionView.getVisibility() != View.VISIBLE
Lyn Han02cca812019-04-02 16:27:32 -0700470 ? mSettingsIconHeight
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800471 : mPermissionHeight;
472 int max = mStackView.getMaxExpandedHeight() - chromeHeight - mPointerView.getHeight()
473 - mPointerMargin;
Mady Mellor7af771a2019-03-07 15:04:54 -0800474 float height = Math.min(desiredHeight, max);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800475 height = Math.max(height, mMinHeight);
476 LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800477 mNeedsNewHeight = lp.height != height;
478 if (!mKeyboardVisible) {
479 // If the keyboard is visible... don't adjust the height because that will cause
480 // a configuration change and the keyboard will be lost.
Mady Mellor7af771a2019-03-07 15:04:54 -0800481 lp.height = (int) height;
482 mBubbleHeight = (int) height;
Mady Mellor5d8f1402019-02-21 18:23:52 -0800483 mActivityView.setLayoutParams(lp);
484 mNeedsNewHeight = false;
485 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800486 } else {
487 mBubbleHeight = mNotifRow != null ? mNotifRow.getIntrinsicHeight() : mMinHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800488 }
489 }
490
Mady Mellor9801e852019-01-22 14:50:28 -0800491 @Override
492 public void onClick(View view) {
493 if (mEntry == null) {
494 return;
495 }
496 Notification n = mEntry.notification.getNotification();
497 int id = view.getId();
Lyn Hanc26ff122019-03-29 16:46:07 -0700498 if (id == R.id.settings_button) {
Mady Mellor9801e852019-01-22 14:50:28 -0800499 Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
500 mEntry.notification.getUid());
Steven Wub00225b2019-02-08 14:27:42 -0500501 mStackView.collapseStack(() -> {
502 mContext.startActivity(intent);
Steven Wu45e38ae2019-03-25 16:16:59 -0400503 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500504 StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
505 });
Mady Mellore8e07712019-01-23 12:45:33 -0800506 } else if (id == R.id.no_bubbles_button) {
507 setBubblesAllowed(false);
508 } else if (id == R.id.yes_bubbles_button) {
509 setBubblesAllowed(true);
510 }
511 }
512
513 private void setBubblesAllowed(boolean allowed) {
514 try {
515 mNotificationManagerService.setBubblesAllowed(
516 mEntry.notification.getPackageName(),
517 mEntry.notification.getUid(),
518 allowed);
519 if (allowed) {
Lyn Hanc26ff122019-03-29 16:46:07 -0700520 showSettingsIcon();
Mady Mellore8e07712019-01-23 12:45:33 -0800521 } else if (mOnBubbleBlockedListener != null) {
522 mOnBubbleBlockedListener.onBubbleBlocked(mEntry);
523 }
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800524 mStackView.onExpandedHeightChanged();
Steven Wu45e38ae2019-03-25 16:16:59 -0400525 logBubbleClickEvent(mEntry,
Steven Wub00225b2019-02-08 14:27:42 -0500526 allowed ? StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_IN :
527 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_OPT_OUT);
Mady Mellore8e07712019-01-23 12:45:33 -0800528 } catch (RemoteException e) {
529 Log.w(TAG, e);
Mady Mellor9801e852019-01-22 14:50:28 -0800530 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800531 }
532
Lyn Han02cca812019-04-02 16:27:32 -0700533 private void updateSettingsContentDescription() {
534 mSettingsIcon.setContentDescription(getResources().getString(
535 R.string.bubbles_settings_button_description, mAppName));
536 }
537
Lyn Hanc26ff122019-03-29 16:46:07 -0700538 void showSettingsIcon() {
Lyn Han02cca812019-04-02 16:27:32 -0700539 updateSettingsContentDescription();
540
Lyn Hanc26ff122019-03-29 16:46:07 -0700541 mPermissionView.setVisibility(GONE);
542 mSettingsIcon.setVisibility(VISIBLE);
543 }
544
545 void showPermissionView() {
Lyn Han02cca812019-04-02 16:27:32 -0700546 ((ImageView) mPermissionView.findViewById(R.id.pkgicon)).setImageDrawable(mAppIcon);
547 ((TextView) mPermissionView.findViewById(R.id.pkgname)).setText(mAppName);
548 mPermissionPrompt.setText(
549 getResources().getString(R.string.bubbles_prompt, mAppName));
550 logBubbleClickEvent(mEntry,
551 StatsLog.BUBBLE_UICHANGED__ACTION__PERMISSION_DIALOG_SHOWN);
552
Lyn Hanc26ff122019-03-29 16:46:07 -0700553 mSettingsIcon.setVisibility(GONE);
554 mPermissionView.setVisibility(VISIBLE);
Lyn Hanc26ff122019-03-29 16:46:07 -0700555 }
556
Mady Mellordea7ecf2018-12-10 15:47:40 -0800557 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800558 * Update appearance of the expanded view being displayed.
559 */
560 public void updateView() {
561 if (usingActivityView()
562 && mActivityView.getVisibility() == VISIBLE
563 && mActivityView.isAttachedToWindow()) {
564 mActivityView.onLocationChanged();
565 } else if (mNotifRow != null) {
566 applyRowState(mNotifRow);
567 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800568 updateHeight();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800569 }
570
571 /**
Mady Mellordea7ecf2018-12-10 15:47:40 -0800572 * Set the x position that the tip of the triangle should point to.
573 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800574 public void setPointerPosition(float x) {
Mady Mellordea7ecf2018-12-10 15:47:40 -0800575 // Adjust for the pointer size
Mady Mellor3dff9e62019-02-05 18:12:53 -0800576 x -= (mPointerView.getWidth() / 2f);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800577 mPointerView.setTranslationX(x);
578 }
579
580 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800581 * Removes and releases an ActivityView if one was previously created for this bubble.
Mady Mellordea7ecf2018-12-10 15:47:40 -0800582 */
Mady Mellor94d94a72019-03-05 18:16:59 -0800583 public void cleanUpExpandedState() {
584 removeView(mNotifRow);
585
Mady Mellor3dff9e62019-02-05 18:12:53 -0800586 if (mActivityView == null) {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500587 return;
588 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500589 if (mActivityViewReady) {
590 mActivityView.release();
Mady Mellordea7ecf2018-12-10 15:47:40 -0800591 }
Mark Renouf28c250d2019-02-25 16:47:34 -0500592 removeView(mActivityView);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800593 mActivityView = null;
594 mActivityViewReady = false;
Mady Mellordea7ecf2018-12-10 15:47:40 -0800595 }
596
Mady Mellor3dff9e62019-02-05 18:12:53 -0800597 private boolean usingActivityView() {
Mady Mellor323fb0b2019-03-25 12:15:22 -0700598 return mBubbleIntent != null && mActivityView != null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800599 }
600
601 private void applyRowState(ExpandableNotificationRow view) {
602 view.reset();
603 view.setHeadsUp(false);
604 view.resetTranslation();
605 view.setOnKeyguard(false);
606 view.setOnAmbient(false);
607 view.setClipBottomAmount(0);
608 view.setClipTopAmount(0);
609 view.setContentTransformationAmount(0, false);
610 view.setIconsVisible(true);
611
612 // TODO - Need to reset this (and others) when view goes back in shade, leave for now
613 // view.setTopRoundness(1, false);
614 // view.setBottomRoundness(1, false);
615
616 ExpandableViewState viewState = view.getViewState();
617 viewState = viewState == null ? new ExpandableViewState() : viewState;
618 viewState.height = view.getIntrinsicHeight();
619 viewState.gone = false;
620 viewState.hidden = false;
621 viewState.dimmed = false;
622 viewState.dark = false;
623 viewState.alpha = 1f;
624 viewState.notGoneIndex = -1;
625 viewState.xTranslation = 0;
626 viewState.yTranslation = 0;
627 viewState.zTranslation = 0;
628 viewState.scaleX = 1;
629 viewState.scaleY = 1;
630 viewState.inShelf = true;
631 viewState.headsUpIsVisible = false;
632 viewState.applyToView(view);
Mady Mellordea7ecf2018-12-10 15:47:40 -0800633 }
Mady Mellor9801e852019-01-22 14:50:28 -0800634
635 private Intent getSettingsIntent(String packageName, final int appUid) {
636 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
637 intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
638 intent.putExtra(Settings.EXTRA_APP_UID, appUid);
639 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
640 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
641 return intent;
642 }
Mady Mellore8e07712019-01-23 12:45:33 -0800643
Mady Mellor3dff9e62019-02-05 18:12:53 -0800644 @Nullable
645 private PendingIntent getBubbleIntent(NotificationEntry entry) {
646 Notification notif = entry.notification.getNotification();
647 String packageName = entry.notification.getPackageName();
648 Notification.BubbleMetadata data = notif.getBubbleMetadata();
649 if (data != null && canLaunchInActivityView(data.getIntent(), true /* enableLogging */,
650 packageName)) {
651 return data.getIntent();
652 } else if (BubbleController.shouldUseContentIntent(mContext)
653 && canLaunchInActivityView(notif.contentIntent, false /* enableLogging */,
654 packageName)) {
655 return notif.contentIntent;
656 }
657 return null;
658 }
659
660 /**
661 * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
662 *
663 * @param intent the pending intent of the bubble.
664 * @param enableLogging whether bubble developer error should be logged.
665 * @param packageName the notification package name for this bubble.
666 * @return
667 */
668 private boolean canLaunchInActivityView(PendingIntent intent, boolean enableLogging,
669 String packageName) {
670 if (intent == null) {
671 return false;
672 }
673 ActivityInfo info =
674 intent.getIntent().resolveActivityInfo(mContext.getPackageManager(), 0);
675 if (info == null) {
676 if (enableLogging) {
677 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
678 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
679 }
680 return false;
681 }
682 if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
683 if (enableLogging) {
684 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
685 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
686 }
687 return false;
688 }
689 if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
690 if (enableLogging) {
691 StatsLog.write(StatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED, packageName,
692 BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__DOCUMENT_LAUNCH_NOT_ALWAYS);
693 }
694 return false;
695 }
696 return (info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) != 0;
697 }
698
Mady Mellore8e07712019-01-23 12:45:33 -0800699 /**
700 * Listener that is notified when a bubble is blocked.
701 */
702 public interface OnBubbleBlockedListener {
703 /**
704 * Called when a bubble is blocked for the provided entry.
705 */
706 void onBubbleBlocked(NotificationEntry entry);
707 }
Steven Wub00225b2019-02-08 14:27:42 -0500708
709 /**
710 * Logs bubble UI click event.
711 *
Steven Wu45e38ae2019-03-25 16:16:59 -0400712 * @param entry the bubble notification entry that user is interacting with.
Steven Wub00225b2019-02-08 14:27:42 -0500713 * @param action the user interaction enum.
714 */
Steven Wu45e38ae2019-03-25 16:16:59 -0400715 private void logBubbleClickEvent(NotificationEntry entry, int action) {
716 StatusBarNotification notification = entry.notification;
Steven Wub00225b2019-02-08 14:27:42 -0500717 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
718 notification.getPackageName(),
719 notification.getNotification().getChannelId(),
720 notification.getId(),
721 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
722 mStackView.getBubbleCount(),
723 action,
724 mStackView.getNormalizedXPosition(),
Steven Wu45e38ae2019-03-25 16:16:59 -0400725 mStackView.getNormalizedYPosition(),
Steven Wu8ba8ca92019-04-11 10:47:42 -0400726 entry.showInShadeWhenBubble(),
727 entry.isForegroundService(),
728 BubbleController.isForegroundApp(mContext, notification.getPackageName()));
Steven Wub00225b2019-02-08 14:27:42 -0500729 }
Mady Mellor7af771a2019-03-07 15:04:54 -0800730
731 private int getDimenForPackageUser(int resId, String pkg, int userId) {
732 Resources r;
733 if (pkg != null) {
734 try {
735 if (userId == UserHandle.USER_ALL) {
736 userId = UserHandle.USER_SYSTEM;
737 }
738 r = mPm.getResourcesForApplicationAsUser(pkg, userId);
739 return r.getDimensionPixelSize(resId);
740 } catch (PackageManager.NameNotFoundException ex) {
741 // Uninstalled, don't care
742 } catch (Resources.NotFoundException e) {
743 // Invalid res id, return 0 and user our default
744 Log.e(TAG, "Couldn't find desired height res id", e);
745 }
746 }
747 return 0;
748 }
Mady Mellordea7ecf2018-12-10 15:47:40 -0800749}