• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.bubbles;
18 
19 import static android.view.Display.INVALID_DISPLAY;
20 
21 import static com.android.systemui.bubbles.BubbleController.DEBUG_ENABLE_AUTO_BUBBLE;
22 
23 import android.annotation.Nullable;
24 import android.app.ActivityOptions;
25 import android.app.ActivityView;
26 import android.app.INotificationManager;
27 import android.app.Notification;
28 import android.app.PendingIntent;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.ApplicationInfo;
32 import android.content.pm.PackageManager;
33 import android.content.res.Resources;
34 import android.content.res.TypedArray;
35 import android.graphics.Color;
36 import android.graphics.Insets;
37 import android.graphics.Point;
38 import android.graphics.drawable.Drawable;
39 import android.graphics.drawable.ShapeDrawable;
40 import android.os.ServiceManager;
41 import android.os.UserHandle;
42 import android.provider.Settings;
43 import android.service.notification.StatusBarNotification;
44 import android.util.AttributeSet;
45 import android.util.Log;
46 import android.util.StatsLog;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.view.WindowInsets;
50 import android.widget.LinearLayout;
51 
52 import com.android.internal.policy.ScreenDecorationsUtils;
53 import com.android.systemui.Dependency;
54 import com.android.systemui.R;
55 import com.android.systemui.recents.TriangleShape;
56 import com.android.systemui.statusbar.AlphaOptimizedButton;
57 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
58 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
59 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
60 
61 /**
62  * Container for the expanded bubble view, handles rendering the caret and settings icon.
63  */
64 public class BubbleExpandedView extends LinearLayout implements View.OnClickListener {
65     private static final String TAG = "BubbleExpandedView";
66 
67     // The triangle pointing to the expanded view
68     private View mPointerView;
69     private int mPointerMargin;
70 
71     private AlphaOptimizedButton mSettingsIcon;
72 
73     // Views for expanded state
74     private ExpandableNotificationRow mNotifRow;
75     private ActivityView mActivityView;
76 
77     private boolean mActivityViewReady = false;
78     private PendingIntent mBubbleIntent;
79 
80     private boolean mKeyboardVisible;
81     private boolean mNeedsNewHeight;
82 
83     private int mMinHeight;
84     private int mSettingsIconHeight;
85     private int mBubbleHeight;
86     private int mPointerWidth;
87     private int mPointerHeight;
88     private ShapeDrawable mPointerDrawable;
89 
90     private NotificationEntry mEntry;
91     private PackageManager mPm;
92     private String mAppName;
93     private Drawable mAppIcon;
94 
95     private INotificationManager mNotificationManagerService;
96     private BubbleController mBubbleController = Dependency.get(BubbleController.class);
97 
98     private BubbleStackView mStackView;
99 
100     private BubbleExpandedView.OnBubbleBlockedListener mOnBubbleBlockedListener;
101 
102     private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
103         @Override
104         public void onActivityViewReady(ActivityView view) {
105             if (!mActivityViewReady) {
106                 mActivityViewReady = true;
107                 // Custom options so there is no activity transition animation
108                 ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
109                         0 /* enterResId */, 0 /* exitResId */);
110                 // Post to keep the lifecycle normal
111                 post(() -> mActivityView.startActivity(mBubbleIntent, options));
112             }
113         }
114 
115         @Override
116         public void onActivityViewDestroyed(ActivityView view) {
117             mActivityViewReady = false;
118         }
119 
120         /**
121          * This is only called for tasks on this ActivityView, which is also set to
122          * single-task mode -- meaning never more than one task on this display. If a task
123          * is being removed, it's the top Activity finishing and this bubble should
124          * be removed or collapsed.
125          */
126         @Override
127         public void onTaskRemovalStarted(int taskId) {
128             if (mEntry != null) {
129                 // Must post because this is called from a binder thread.
130                 post(() -> mBubbleController.removeBubble(mEntry.key,
131                         BubbleController.DISMISS_TASK_FINISHED));
132             }
133         }
134     };
135 
BubbleExpandedView(Context context)136     public BubbleExpandedView(Context context) {
137         this(context, null);
138     }
139 
BubbleExpandedView(Context context, AttributeSet attrs)140     public BubbleExpandedView(Context context, AttributeSet attrs) {
141         this(context, attrs, 0);
142     }
143 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)144     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
145         this(context, attrs, defStyleAttr, 0);
146     }
147 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)148     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
149             int defStyleRes) {
150         super(context, attrs, defStyleAttr, defStyleRes);
151         mPm = context.getPackageManager();
152         mMinHeight = getResources().getDimensionPixelSize(
153                 R.dimen.bubble_expanded_default_height);
154         mPointerMargin = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_margin);
155         try {
156             mNotificationManagerService = INotificationManager.Stub.asInterface(
157                     ServiceManager.getServiceOrThrow(Context.NOTIFICATION_SERVICE));
158         } catch (ServiceManager.ServiceNotFoundException e) {
159             Log.w(TAG, e);
160         }
161     }
162 
163     @Override
onFinishInflate()164     protected void onFinishInflate() {
165         super.onFinishInflate();
166 
167         Resources res = getResources();
168         mPointerView = findViewById(R.id.pointer_view);
169         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
170         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
171 
172 
173         mPointerDrawable = new ShapeDrawable(TriangleShape.create(
174                 mPointerWidth, mPointerHeight, true /* pointUp */));
175         mPointerView.setBackground(mPointerDrawable);
176         mPointerView.setVisibility(GONE);
177 
178         mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
179                 R.dimen.bubble_expanded_header_height);
180         mSettingsIcon = findViewById(R.id.settings_button);
181         mSettingsIcon.setOnClickListener(this);
182 
183         mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
184                 true /* singleTaskInstance */);
185         addView(mActivityView);
186 
187         // Expanded stack layout, top to bottom:
188         // Expanded view container
189         // ==> bubble row
190         // ==> expanded view
191         //   ==> activity view
192         //   ==> manage button
193         bringChildToFront(mActivityView);
194         bringChildToFront(mSettingsIcon);
195 
196         applyThemeAttrs();
197 
198         setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
199             // Keep track of IME displaying because we should not make any adjustments that might
200             // cause a config change while the IME is displayed otherwise it'll loose focus.
201             final int keyboardHeight = insets.getSystemWindowInsetBottom()
202                     - insets.getStableInsetBottom();
203             mKeyboardVisible = keyboardHeight != 0;
204             if (!mKeyboardVisible && mNeedsNewHeight) {
205                 updateHeight();
206             }
207             return view.onApplyWindowInsets(insets);
208         });
209     }
210 
applyThemeAttrs()211     void applyThemeAttrs() {
212         TypedArray ta = getContext().obtainStyledAttributes(R.styleable.BubbleExpandedView);
213         int bgColor = ta.getColor(
214                 R.styleable.BubbleExpandedView_android_colorBackgroundFloating, Color.WHITE);
215         float cornerRadius = ta.getDimension(
216                 R.styleable.BubbleExpandedView_android_dialogCornerRadius, 0);
217         ta.recycle();
218 
219         // Update triangle color.
220         mPointerDrawable.setTint(bgColor);
221 
222         // Update ActivityView cornerRadius
223         if (ScreenDecorationsUtils.supportsRoundedCornersOnWindows(mContext.getResources())) {
224             mActivityView.setCornerRadius(cornerRadius);
225         }
226     }
227 
228     @Override
onDetachedFromWindow()229     protected void onDetachedFromWindow() {
230         super.onDetachedFromWindow();
231         mKeyboardVisible = false;
232         mNeedsNewHeight = false;
233         if (mActivityView != null) {
234             mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0));
235         }
236     }
237 
238     /**
239      * Called by {@link BubbleStackView} when the insets for the expanded state should be updated.
240      * This should be done post-move and post-animation.
241      */
updateInsets(WindowInsets insets)242     void updateInsets(WindowInsets insets) {
243         if (usingActivityView()) {
244             Point displaySize = new Point();
245             mActivityView.getContext().getDisplay().getSize(displaySize);
246             int[] windowLocation = mActivityView.getLocationOnScreen();
247             final int windowBottom = windowLocation[1] + mActivityView.getHeight();
248             final int keyboardHeight = insets.getSystemWindowInsetBottom()
249                     - insets.getStableInsetBottom();
250             final int insetsBottom = Math.max(0,
251                     windowBottom + keyboardHeight - displaySize.y);
252             mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
253         }
254     }
255 
256     /**
257      * Sets the listener to notify when a bubble has been blocked.
258      */
setOnBlockedListener(OnBubbleBlockedListener listener)259     public void setOnBlockedListener(OnBubbleBlockedListener listener) {
260         mOnBubbleBlockedListener = listener;
261     }
262 
263     /**
264      * Sets the notification entry used to populate this view.
265      */
setEntry(NotificationEntry entry, BubbleStackView stackView, String appName)266     public void setEntry(NotificationEntry entry, BubbleStackView stackView, String appName) {
267         mStackView = stackView;
268         mEntry = entry;
269         mAppName = appName;
270 
271         ApplicationInfo info;
272         try {
273             info = mPm.getApplicationInfo(
274                     entry.notification.getPackageName(),
275                     PackageManager.MATCH_UNINSTALLED_PACKAGES
276                             | PackageManager.MATCH_DISABLED_COMPONENTS
277                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
278                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
279             if (info != null) {
280                 mAppIcon = mPm.getApplicationIcon(info);
281             }
282         } catch (PackageManager.NameNotFoundException e) {
283             // Do nothing.
284         }
285         if (mAppIcon == null) {
286             mAppIcon = mPm.getDefaultActivityIcon();
287         }
288         applyThemeAttrs();
289         showSettingsIcon();
290         updateExpandedView();
291     }
292 
293     /**
294      * Lets activity view know it should be shown / populated.
295      */
populateExpandedView()296     public void populateExpandedView() {
297         if (usingActivityView()) {
298             mActivityView.setCallback(mStateCallback);
299         } else {
300             // We're using notification template
301             ViewGroup parent = (ViewGroup) mNotifRow.getParent();
302             if (parent == this) {
303                 // Already added
304                 return;
305             } else if (parent != null) {
306                 // Still in the shade... remove it
307                 parent.removeView(mNotifRow);
308             }
309             addView(mNotifRow, 1 /* index */);
310         }
311     }
312 
313     /**
314      * Updates the entry backing this view. This will not re-populate ActivityView, it will
315      * only update the deep-links in the title, and the height of the view.
316      */
update(NotificationEntry entry)317     public void update(NotificationEntry entry) {
318         if (entry.key.equals(mEntry.key)) {
319             mEntry = entry;
320             updateSettingsContentDescription();
321             updateHeight();
322         } else {
323             Log.w(TAG, "Trying to update entry with different key, new entry: "
324                     + entry.key + " old entry: " + mEntry.key);
325         }
326     }
327 
updateExpandedView()328     private void updateExpandedView() {
329         mBubbleIntent = getBubbleIntent(mEntry);
330         if (mBubbleIntent != null) {
331             if (mNotifRow != null) {
332                 // Clear out the row if we had it previously
333                 removeView(mNotifRow);
334                 mNotifRow = null;
335             }
336             mActivityView.setVisibility(VISIBLE);
337         } else if (DEBUG_ENABLE_AUTO_BUBBLE) {
338             // Hide activity view if we had it previously
339             mActivityView.setVisibility(GONE);
340             mNotifRow = mEntry.getRow();
341         }
342         updateView();
343     }
344 
performBackPressIfNeeded()345     boolean performBackPressIfNeeded() {
346         if (!usingActivityView()) {
347             return false;
348         }
349         mActivityView.performBackPress();
350         return true;
351     }
352 
updateHeight()353     void updateHeight() {
354         if (usingActivityView()) {
355             Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
356             float desiredHeight;
357             if (data == null) {
358                 // This is a contentIntent based bubble, lets allow it to be the max height
359                 // as it was forced into this mode and not prepared to be small
360                 desiredHeight = mStackView.getMaxExpandedHeight();
361             } else {
362                 boolean useRes = data.getDesiredHeightResId() != 0;
363                 float desiredPx;
364                 if (useRes) {
365                     desiredPx = getDimenForPackageUser(data.getDesiredHeightResId(),
366                             mEntry.notification.getPackageName(),
367                             mEntry.notification.getUser().getIdentifier());
368                 } else {
369                     desiredPx = data.getDesiredHeight()
370                             * getContext().getResources().getDisplayMetrics().density;
371                 }
372                 desiredHeight = desiredPx > 0 ? desiredPx : mMinHeight;
373             }
374             int max = mStackView.getMaxExpandedHeight() - mSettingsIconHeight - mPointerHeight
375                     - mPointerMargin;
376             float height = Math.min(desiredHeight, max);
377             height = Math.max(height, mMinHeight);
378             LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
379             mNeedsNewHeight =  lp.height != height;
380             if (!mKeyboardVisible) {
381                 // If the keyboard is visible... don't adjust the height because that will cause
382                 // a configuration change and the keyboard will be lost.
383                 lp.height = (int) height;
384                 mBubbleHeight = (int) height;
385                 mActivityView.setLayoutParams(lp);
386                 mNeedsNewHeight = false;
387             }
388         } else {
389             mBubbleHeight = mNotifRow != null ? mNotifRow.getIntrinsicHeight() : mMinHeight;
390         }
391     }
392 
393     @Override
onClick(View view)394     public void onClick(View view) {
395         if (mEntry == null) {
396             return;
397         }
398         Notification n = mEntry.notification.getNotification();
399         int id = view.getId();
400         if (id == R.id.settings_button) {
401             Intent intent = getSettingsIntent(mEntry.notification.getPackageName(),
402                     mEntry.notification.getUid());
403             mStackView.collapseStack(() -> {
404                 mContext.startActivityAsUser(intent, mEntry.notification.getUser());
405                 logBubbleClickEvent(mEntry,
406                         StatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
407             });
408         }
409     }
410 
updateSettingsContentDescription()411     private void updateSettingsContentDescription() {
412         mSettingsIcon.setContentDescription(getResources().getString(
413                 R.string.bubbles_settings_button_description, mAppName));
414     }
415 
showSettingsIcon()416     void showSettingsIcon() {
417         updateSettingsContentDescription();
418         mSettingsIcon.setVisibility(VISIBLE);
419     }
420 
421     /**
422      * Update appearance of the expanded view being displayed.
423      */
updateView()424     public void updateView() {
425         if (usingActivityView()
426                 && mActivityView.getVisibility() == VISIBLE
427                 && mActivityView.isAttachedToWindow()) {
428             mActivityView.onLocationChanged();
429         } else if (mNotifRow != null) {
430             applyRowState(mNotifRow);
431         }
432         updateHeight();
433     }
434 
435     /**
436      * Set the x position that the tip of the triangle should point to.
437      */
setPointerPosition(float x)438     public void setPointerPosition(float x) {
439         float halfPointerWidth = mPointerWidth / 2f;
440         float pointerLeft = x - halfPointerWidth;
441         mPointerView.setTranslationX(pointerLeft);
442         mPointerView.setVisibility(VISIBLE);
443     }
444 
445     /**
446      * Removes and releases an ActivityView if one was previously created for this bubble.
447      */
cleanUpExpandedState()448     public void cleanUpExpandedState() {
449         removeView(mNotifRow);
450 
451         if (mActivityView == null) {
452             return;
453         }
454         if (mActivityViewReady) {
455             mActivityView.release();
456         }
457         removeView(mActivityView);
458         mActivityView = null;
459         mActivityViewReady = false;
460     }
461 
usingActivityView()462     private boolean usingActivityView() {
463         return mBubbleIntent != null && mActivityView != null;
464     }
465 
466     /**
467      * @return the display id of the virtual display.
468      */
getVirtualDisplayId()469     public int getVirtualDisplayId() {
470         if (usingActivityView()) {
471             return mActivityView.getVirtualDisplayId();
472         }
473         return INVALID_DISPLAY;
474     }
475 
applyRowState(ExpandableNotificationRow view)476     private void applyRowState(ExpandableNotificationRow view) {
477         view.reset();
478         view.setHeadsUp(false);
479         view.resetTranslation();
480         view.setOnKeyguard(false);
481         view.setOnAmbient(false);
482         view.setClipBottomAmount(0);
483         view.setClipTopAmount(0);
484         view.setContentTransformationAmount(0, false);
485         view.setIconsVisible(true);
486 
487         // TODO - Need to reset this (and others) when view goes back in shade, leave for now
488         // view.setTopRoundness(1, false);
489         // view.setBottomRoundness(1, false);
490 
491         ExpandableViewState viewState = view.getViewState();
492         viewState = viewState == null ? new ExpandableViewState() : viewState;
493         viewState.height = view.getIntrinsicHeight();
494         viewState.gone = false;
495         viewState.hidden = false;
496         viewState.dimmed = false;
497         viewState.dark = false;
498         viewState.alpha = 1f;
499         viewState.notGoneIndex = -1;
500         viewState.xTranslation = 0;
501         viewState.yTranslation = 0;
502         viewState.zTranslation = 0;
503         viewState.scaleX = 1;
504         viewState.scaleY = 1;
505         viewState.inShelf = true;
506         viewState.headsUpIsVisible = false;
507         viewState.applyToView(view);
508     }
509 
getSettingsIntent(String packageName, final int appUid)510     private Intent getSettingsIntent(String packageName, final int appUid) {
511         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
512         intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
513         intent.putExtra(Settings.EXTRA_APP_UID, appUid);
514         intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
515         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
516         intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
517         return intent;
518     }
519 
520     @Nullable
getBubbleIntent(NotificationEntry entry)521     private PendingIntent getBubbleIntent(NotificationEntry entry) {
522         Notification notif = entry.notification.getNotification();
523         Notification.BubbleMetadata data = notif.getBubbleMetadata();
524         if (BubbleController.canLaunchInActivityView(mContext, entry) && data != null) {
525             return data.getIntent();
526         }
527         return null;
528     }
529 
530     /**
531      * Listener that is notified when a bubble is blocked.
532      */
533     public interface OnBubbleBlockedListener {
534         /**
535          * Called when a bubble is blocked for the provided entry.
536          */
onBubbleBlocked(NotificationEntry entry)537         void onBubbleBlocked(NotificationEntry entry);
538     }
539 
540     /**
541      * Logs bubble UI click event.
542      *
543      * @param entry the bubble notification entry that user is interacting with.
544      * @param action the user interaction enum.
545      */
logBubbleClickEvent(NotificationEntry entry, int action)546     private void logBubbleClickEvent(NotificationEntry entry, int action) {
547         StatusBarNotification notification = entry.notification;
548         StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
549                 notification.getPackageName(),
550                 notification.getNotification().getChannelId(),
551                 notification.getId(),
552                 mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
553                 mStackView.getBubbleCount(),
554                 action,
555                 mStackView.getNormalizedXPosition(),
556                 mStackView.getNormalizedYPosition(),
557                 entry.showInShadeWhenBubble(),
558                 entry.isForegroundService(),
559                 BubbleController.isForegroundApp(mContext, notification.getPackageName()));
560     }
561 
getDimenForPackageUser(int resId, String pkg, int userId)562     private int getDimenForPackageUser(int resId, String pkg, int userId) {
563         Resources r;
564         if (pkg != null) {
565             try {
566                 if (userId == UserHandle.USER_ALL) {
567                     userId = UserHandle.USER_SYSTEM;
568                 }
569                 r = mPm.getResourcesForApplicationAsUser(pkg, userId);
570                 return r.getDimensionPixelSize(resId);
571             } catch (PackageManager.NameNotFoundException ex) {
572                 // Uninstalled, don't care
573             } catch (Resources.NotFoundException e) {
574                 // Invalid res id, return 0 and user our default
575                 Log.e(TAG, "Couldn't find desired height res id", e);
576             }
577         }
578         return 0;
579     }
580 }
581