blob: 74ddc8f6b8fc4451785a450ca8e0e08eb8bfdc98 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.bubbles;
import android.annotation.Nullable;
import android.app.ActivityView;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.graphics.ColorUtils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
/**
* A floating object on the screen that can post message updates.
*/
public class BubbleView extends FrameLayout implements BubbleTouchHandler.FloatingView {
private static final String TAG = "BubbleView";
// Same value as Launcher3 badge code
private static final float WHITE_SCRIM_ALPHA = 0.54f;
private Context mContext;
private BadgedImageView mBadgedImageView;
private TextView mMessageView;
private int mPadding;
private int mIconInset;
private NotificationEntry mEntry;
private PendingIntent mAppOverlayIntent;
private BubbleController mBubbleController;
private ActivityView mActivityView;
private boolean mActivityViewReady;
private boolean mActivityViewStarted;
public BubbleView(Context context) {
this(context, null);
}
public BubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mContext = context;
// XXX: can this padding just be on the view and we look it up?
mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding);
mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
mBubbleController = Dependency.get(BubbleController.class);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBadgedImageView = (BadgedImageView) findViewById(R.id.bubble_image);
mMessageView = (TextView) findViewById(R.id.message_view);
mMessageView.setVisibility(GONE);
mMessageView.setPivotX(0);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
updateViews();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
measureChild(mBadgedImageView, widthSpec, heightSpec);
measureChild(mMessageView, widthSpec, heightSpec);
boolean messageGone = mMessageView.getVisibility() == GONE;
int imageHeight = mBadgedImageView.getMeasuredHeight();
int imageWidth = mBadgedImageView.getMeasuredWidth();
int messageHeight = messageGone ? 0 : mMessageView.getMeasuredHeight();
int messageWidth = messageGone ? 0 : mMessageView.getMeasuredWidth();
setMeasuredDimension(
getPaddingStart() + imageWidth + mPadding + messageWidth + getPaddingEnd(),
getPaddingTop() + Math.max(imageHeight, messageHeight) + getPaddingBottom());
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
left = getPaddingStart();
top = getPaddingTop();
int imageWidth = mBadgedImageView.getMeasuredWidth();
int imageHeight = mBadgedImageView.getMeasuredHeight();
int messageWidth = mMessageView.getMeasuredWidth();
int messageHeight = mMessageView.getMeasuredHeight();
mBadgedImageView.layout(left, top, left + imageWidth, top + imageHeight);
mMessageView.layout(left + imageWidth + mPadding, top,
left + imageWidth + mPadding + messageWidth, top + messageHeight);
}
/**
* Populates this view with a notification.
* <p>
* This should only be called when a new notification is being set on the view, updates to the
* current notification should use {@link #update(NotificationEntry)}.
*
* @param entry the notification to display as a bubble.
*/
public void setNotif(NotificationEntry entry) {
mEntry = entry;
updateViews();
}
/**
* The {@link NotificationEntry} associated with this view, if one exists.
*/
@Nullable
public NotificationEntry getEntry() {
return mEntry;
}
/**
* The key for the {@link NotificationEntry} associated with this view, if one exists.
*/
@Nullable
public String getKey() {
return (mEntry != null) ? mEntry.key : null;
}
/**
* Updates the UI based on the entry, updates badge and animates messages as needed.
*/
public void update(NotificationEntry entry) {
mEntry = entry;
updateViews();
}
/**
* @return the {@link ExpandableNotificationRow} view to display notification content when the
* bubble is expanded.
*/
@Nullable
public ExpandableNotificationRow getRowView() {
return (mEntry != null) ? mEntry.getRow() : null;
}
/**
* Marks this bubble as "read", i.e. no badge should show.
*/
public void updateDotVisibility() {
boolean showDot = getEntry().showInShadeWhenBubble();
animateDot(showDot);
}
/**
* Animates the badge to show or hide.
*/
private void animateDot(boolean showDot) {
if (mBadgedImageView.isShowingDot() != showDot) {
mBadgedImageView.setShowDot(showDot);
mBadgedImageView.clearAnimation();
mBadgedImageView.animate().setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
fraction = showDot ? fraction : 1 - fraction;
mBadgedImageView.setDotScale(fraction);
}).withEndAction(() -> {
if (!showDot) {
mBadgedImageView.setShowDot(false);
}
}).start();
}
}
private void updateViews() {
if (mEntry == null) {
return;
}
Notification n = mEntry.notification.getNotification();
boolean isLarge = n.getLargeIcon() != null;
Icon ic = isLarge ? n.getLargeIcon() : n.getSmallIcon();
Drawable iconDrawable = ic.loadDrawable(mContext);
if (!isLarge) {
// Center icon on coloured background
iconDrawable.setTint(Color.WHITE); // TODO: dark mode
Drawable bg = new ColorDrawable(n.color);
InsetDrawable d = new InsetDrawable(iconDrawable, mIconInset);
Drawable[] layers = {bg, d};
mBadgedImageView.setImageDrawable(new LayerDrawable(layers));
} else {
mBadgedImageView.setImageDrawable(iconDrawable);
}
int badgeColor = determineDominateColor(iconDrawable, n.color);
mBadgedImageView.setDotColor(badgeColor);
animateDot(mEntry.showInShadeWhenBubble() /* showDot */);
}
private int determineDominateColor(Drawable d, int defaultTint) {
// XXX: should we pull from the drawable, app icon, notif tint?
return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
}
/**
* @return a view used to display app overlay content when expanded.
*/
public ActivityView getActivityView() {
if (mActivityView == null) {
mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
true /* singleTaskInstance */);
Log.d(TAG, "[getActivityView] created: " + mActivityView);
mActivityView.setCallback(new ActivityView.StateCallback() {
@Override
public void onActivityViewReady(ActivityView view) {
mActivityViewReady = true;
mActivityView.startActivity(mAppOverlayIntent);
}
@Override
public void onActivityViewDestroyed(ActivityView view) {
mActivityViewReady = false;
}
/**
* This is only called for tasks on this ActivityView, which is also set to
* single-task mode -- meaning never more than one task on this display. If a task
* is being removed, it's the top Activity finishing and this bubble should
* be removed or collapsed.
*/
@Override
public void onTaskRemovalStarted(int taskId) {
if (mEntry != null) {
// Must post because this is called from a binder thread.
post(() -> mBubbleController.removeBubble(mEntry.key));
}
}
});
mActivityView.setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
ActivityView activityView = (ActivityView) view;
// Here we assume that the position of the ActivityView on the screen
// remains regardless of IME status. When we move ActivityView, the
// forwardedInsets should be computed not against the current location
// and size, but against the post-moved location and size.
Point displaySize = new Point();
view.getContext().getDisplay().getSize(displaySize);
int[] windowLocation = view.getLocationOnScreen();
final int windowBottom = windowLocation[1] + view.getHeight();
final int keyboardHeight = insets.getSystemWindowInsetBottom()
- insets.getStableInsetBottom();
final int insetsBottom = Math.max(0,
windowBottom + keyboardHeight - displaySize.y);
activityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom));
return view.onApplyWindowInsets(insets);
});
}
return mActivityView;
}
/**
* Removes and releases an ActivityView if one was previously created for this bubble.
*/
public void destroyActivityView(ViewGroup tmpParent) {
if (mActivityView == null) {
return;
}
if (!mActivityViewReady) {
// release not needed, never initialized?
mActivityView = null;
return;
}
// HACK: release() will crash if the view is not attached.
if (!mActivityView.isAttachedToWindow()) {
mActivityView.setVisibility(View.GONE);
tmpParent.addView(mActivityView, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
}
mActivityView.release();
((ViewGroup) mActivityView.getParent()).removeView(mActivityView);
mActivityView = null;
}
@Override
public void setPosition(float x, float y) {
setPositionX(x);
setPositionY(y);
}
@Override
public void setPositionX(float x) {
setTranslationX(x);
}
@Override
public void setPositionY(float y) {
setTranslationY(y);
}
@Override
public PointF getPosition() {
return new PointF(getTranslationX(), getTranslationY());
}
/**
* @return whether an ActivityView should be used to display the content of this Bubble
*/
public boolean hasAppOverlayIntent() {
return mAppOverlayIntent != null;
}
public PendingIntent getAppOverlayIntent() {
return mAppOverlayIntent;
}
public void setBubbleIntent(PendingIntent intent) {
mAppOverlayIntent = intent;
}
}