summaryrefslogtreecommitdiff
path: root/java/com/android/dialershared/bubble/Bubble.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialershared/bubble/Bubble.java')
-rw-r--r--java/com/android/dialershared/bubble/Bubble.java667
1 files changed, 0 insertions, 667 deletions
diff --git a/java/com/android/dialershared/bubble/Bubble.java b/java/com/android/dialershared/bubble/Bubble.java
deleted file mode 100644
index 3eb88aa22..000000000
--- a/java/com/android/dialershared/bubble/Bubble.java
+++ /dev/null
@@ -1,667 +0,0 @@
-/*
- * Copyright (C) 2017 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.dialershared.bubble;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.annotation.SuppressLint;
-import android.app.PendingIntent.CanceledException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.PixelFormat;
-import android.graphics.drawable.RippleDrawable;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import android.support.annotation.ColorInt;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.graphics.ColorUtils;
-import android.support.v4.os.BuildCompat;
-import android.support.v4.view.animation.FastOutLinearInInterpolator;
-import android.support.v4.view.animation.LinearOutSlowInInterpolator;
-import android.transition.TransitionManager;
-import android.transition.TransitionValues;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.view.WindowManager;
-import android.view.WindowManager.LayoutParams;
-import android.view.animation.AnticipateInterpolator;
-import android.view.animation.OvershootInterpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.ViewAnimator;
-import com.android.dialershared.bubble.BubbleInfo.Action;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-
-/**
- * Creates and manages a bubble window from information in a {@link BubbleInfo}. Before creating, be
- * sure to check whether bubbles may be shown using {@link #canShowBubbles(Context)} and request
- * permission if necessary ({@link #getRequestPermissionIntent(Context)} is provided for
- * convenience)
- */
-public class Bubble {
-
- // How long text should show after showText(CharSequence) is called
- private static final int SHOW_TEXT_DURATION_MILLIS = 3000;
- // How long the new window should show before destroying the old one during resize operations.
- // This ensures the new window has had time to draw first.
- private static final int WINDOW_REDRAW_DELAY_MILLIS = 50;
-
- private static Boolean canShowBubblesForTesting = null;
-
- private final Context context;
- private final WindowManager windowManager;
-
- private LayoutParams windowParams;
-
- // Initialized in factory method
- @SuppressWarnings("NullableProblems")
- @NonNull
- private BubbleInfo currentInfo;
-
- private boolean isShowing;
- private boolean expanded;
- private boolean textShowing;
- private boolean hideAfterText;
-
- private final Handler handler = new Handler();
-
- private ViewHolder viewHolder;
-
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({CollapseEnd.NOTHING, CollapseEnd.HIDE})
- private @interface CollapseEnd {
- int NOTHING = 0;
- int HIDE = 1;
- }
-
- /**
- * Determines whether bubbles can be shown based on permissions obtained. This should be checked
- * before attempting to create a Bubble.
- *
- * @return true iff bubbles are able to be shown.
- * @see Settings#canDrawOverlays(Context)
- */
- public static boolean canShowBubbles(@NonNull Context context) {
- return canShowBubblesForTesting != null
- ? canShowBubblesForTesting
- : Settings.canDrawOverlays(context);
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.NONE)
- public static void setCanShowBubblesForTesting(boolean canShowBubbles) {
- canShowBubblesForTesting = canShowBubbles;
- }
-
- /** Returns an Intent to request permission to show overlays */
- @NonNull
- public static Intent getRequestPermissionIntent(@NonNull Context context) {
- return new Intent(
- Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- new Uri.Builder().scheme("package").fragment(context.getPackageName()).build());
- }
-
- /** Creates instances of Bubble. The default implementation just calls the constructor. */
- @VisibleForTesting
- public interface BubbleFactory {
- Bubble createBubble(@NonNull Context context);
- }
-
- private static BubbleFactory bubbleFactory = Bubble::new;
-
- public static Bubble createBubble(@NonNull Context context, @NonNull BubbleInfo info) {
- Bubble bubble = bubbleFactory.createBubble(context);
- bubble.setBubbleInfo(info);
- return bubble;
- }
-
- @VisibleForTesting
- public static void setBubbleFactory(@NonNull BubbleFactory bubbleFactory) {
- Bubble.bubbleFactory = bubbleFactory;
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- Bubble(@NonNull Context context) {
- context = new ContextThemeWrapper(context, R.style.Theme_AppCompat);
- this.context = context;
- windowManager = context.getSystemService(WindowManager.class);
-
- viewHolder = new ViewHolder(context);
- }
-
- /**
- * Make the bubble visible. Will show a short entrance animation as it enters. If the bubble is
- * already showing this method does nothing.
- */
- public void show() {
- if (isShowing) {
- return;
- }
-
- hideAfterText = false;
-
- if (windowParams == null) {
- // Apps targeting O+ must use TYPE_APPLICATION_OVERLAY, which is not available prior to O.
- @SuppressWarnings("deprecation")
- @SuppressLint("InlinedApi")
- int type =
- BuildCompat.isAtLeastO()
- ? LayoutParams.TYPE_APPLICATION_OVERLAY
- : LayoutParams.TYPE_PHONE;
-
- windowParams =
- new LayoutParams(
- type,
- LayoutParams.FLAG_NOT_TOUCH_MODAL
- | LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- | LayoutParams.FLAG_NOT_FOCUSABLE,
- PixelFormat.TRANSLUCENT);
- windowParams.gravity = Gravity.TOP | Gravity.LEFT;
- windowParams.x =
- context.getResources().getDimensionPixelOffset(R.dimen.bubble_initial_offset_x);
- windowParams.y =
- context.getResources().getDimensionPixelOffset(R.dimen.bubble_initial_offset_y);
- windowParams.height = LayoutParams.WRAP_CONTENT;
- windowParams.width = LayoutParams.WRAP_CONTENT;
- }
-
- windowManager.addView(viewHolder.getRoot(), windowParams);
- ObjectAnimator showAnimator =
- ObjectAnimator.ofPropertyValuesHolder(
- viewHolder.getPrimaryButton(),
- PropertyValuesHolder.ofFloat(View.SCALE_X, 0, 1),
- PropertyValuesHolder.ofFloat(View.SCALE_Y, 0, 1));
- showAnimator.setInterpolator(new OvershootInterpolator());
- showAnimator.start();
- isShowing = true;
- }
-
- /**
- * Hide the button if visible. Will run a short exit animation before hiding. If the bubble is
- * currently showing text, will hide after the text is done displaying. If the bubble is not
- * visible this method does nothing.
- */
- public void hide() {
- if (!isShowing) {
- return;
- }
-
- if (textShowing) {
- hideAfterText = true;
- return;
- }
-
- if (expanded) {
- startCollapse(CollapseEnd.HIDE);
- return;
- }
-
- viewHolder
- .getPrimaryButton()
- .animate()
- .setInterpolator(new AnticipateInterpolator())
- .scaleX(0)
- .scaleY(0)
- .withEndAction(
- () -> {
- windowManager.removeView(viewHolder.getRoot());
- isShowing = false;
- })
- .start();
- }
-
- /** Returns whether the bubble is currently visible */
- public boolean isShowing() {
- return isShowing;
- }
-
- /**
- * Set the info for this Bubble to display
- *
- * @param bubbleInfo the BubbleInfo to display in this Bubble.
- */
- public void setBubbleInfo(@NonNull BubbleInfo bubbleInfo) {
- currentInfo = bubbleInfo;
- update();
- }
-
- /**
- * Update the state and behavior of actions.
- *
- * @param actions the new state of the bubble's actions
- */
- public void updateActions(@NonNull List<Action> actions) {
- currentInfo = BubbleInfo.from(currentInfo).setActions(actions).build();
- updateButtonStates();
- }
-
- /** Returns the currently displayed BubbleInfo */
- public BubbleInfo getBubbleInfo() {
- return currentInfo;
- }
-
- /**
- * Display text in the main bubble. The bubble's drawer is not expandable while text is showing,
- * and the drawer will be closed if already open.
- *
- * @param text the text to display to the user
- */
- public void showText(@NonNull CharSequence text) {
- textShowing = true;
- if (expanded) {
- startCollapse(CollapseEnd.NOTHING);
- doShowText(text);
- } else {
- // Need to transition from old bounds to new bounds manually
- ChangeOnScreenBounds transition = new ChangeOnScreenBounds();
- // Prepare and capture start values
- TransitionValues startValues = new TransitionValues();
- startValues.view = viewHolder.getPrimaryButton();
- transition.addTarget(startValues.view);
- transition.captureStartValues(startValues);
-
- doResize(
- () -> {
- doShowText(text);
- // Hide the text so we can animate it in
- viewHolder.getPrimaryText().setAlpha(0);
-
- ViewAnimator primaryButton = viewHolder.getPrimaryButton();
- // Cancel the automatic transition scheduled in doShowText
- TransitionManager.endTransitions((ViewGroup) primaryButton.getParent());
- primaryButton
- .getViewTreeObserver()
- .addOnPreDrawListener(
- new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- primaryButton.getViewTreeObserver().removeOnPreDrawListener(this);
-
- // Prepare and capture end values
- TransitionValues endValues = new TransitionValues();
- endValues.view = primaryButton;
- transition.addTarget(endValues.view);
- transition.captureEndValues(endValues);
-
- // animate the primary button bounds change
- Animator bounds =
- transition.createAnimator(primaryButton, startValues, endValues);
-
- // Animate the text in
- Animator alpha =
- ObjectAnimator.ofFloat(viewHolder.getPrimaryText(), View.ALPHA, 1f);
-
- AnimatorSet set = new AnimatorSet();
- set.play(bounds).before(alpha);
- set.start();
- return false;
- }
- });
- });
- }
- handler.removeCallbacks(null);
- handler.postDelayed(
- () -> {
- textShowing = false;
- if (hideAfterText) {
- hide();
- } else {
- doResize(
- () -> viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_ICON));
- }
- },
- SHOW_TEXT_DURATION_MILLIS);
- }
-
- void onMoveStart() {
- startCollapse(CollapseEnd.NOTHING);
- viewHolder
- .getPrimaryButton()
- .animate()
- .translationZ(
- context.getResources().getDimensionPixelOffset(R.dimen.bubble_move_elevation_change));
- }
-
- void onMoveFinish() {
- viewHolder.getPrimaryButton().animate().translationZ(0);
- }
-
- void primaryButtonClick() {
- if (expanded || textShowing || currentInfo.getActions().isEmpty()) {
- try {
- currentInfo.getPrimaryAction().send();
- } catch (CanceledException e) {
- throw new RuntimeException(e);
- }
- return;
- }
-
- boolean onRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
- doResize(
- () -> {
- onLeftRightSwitch(onRight);
- viewHolder.getExpandedView().setVisibility(View.VISIBLE);
- });
- View expandedView = viewHolder.getExpandedView();
- expandedView
- .getViewTreeObserver()
- .addOnPreDrawListener(
- new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- expandedView.getViewTreeObserver().removeOnPreDrawListener(this);
- expandedView.setTranslationX(
- onRight ? expandedView.getWidth() : -expandedView.getWidth());
- expandedView
- .animate()
- .setInterpolator(new LinearOutSlowInInterpolator())
- .translationX(0);
- return false;
- }
- });
- setFocused(true);
- expanded = true;
- }
-
- void onLeftRightSwitch(boolean onRight) {
- viewHolder
- .getRoot()
- .setLayoutDirection(onRight ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
- View primaryContainer = viewHolder.getRoot().findViewById(R.id.bubble_primary_container);
- ViewGroup.LayoutParams layoutParams = primaryContainer.getLayoutParams();
- ((FrameLayout.LayoutParams) layoutParams).gravity = onRight ? Gravity.RIGHT : Gravity.LEFT;
- primaryContainer.setLayoutParams(layoutParams);
-
- viewHolder
- .getExpandedView()
- .setBackgroundResource(
- onRight
- ? R.drawable.bubble_background_pill_rtl
- : R.drawable.bubble_background_pill_ltr);
- }
-
- LayoutParams getWindowParams() {
- return windowParams;
- }
-
- View getRootView() {
- return viewHolder.getRoot();
- }
-
- private void update() {
- RippleDrawable backgroundRipple =
- (RippleDrawable)
- context.getResources().getDrawable(R.drawable.bubble_ripple_circle, context.getTheme());
- int primaryTint =
- ColorUtils.compositeColors(
- context.getColor(R.color.bubble_primary_background_darken),
- currentInfo.getPrimaryColor());
- backgroundRipple.getDrawable(0).setTint(primaryTint);
- viewHolder.getPrimaryButton().setBackground(backgroundRipple);
-
- setBackgroundDrawable(viewHolder.getFirstButton(), primaryTint);
- setBackgroundDrawable(viewHolder.getSecondButton(), primaryTint);
- setBackgroundDrawable(viewHolder.getThirdButton(), primaryTint);
-
- int numButtons = currentInfo.getActions().size();
- viewHolder.getThirdButton().setVisibility(numButtons < 3 ? View.GONE : View.VISIBLE);
- viewHolder.getSecondButton().setVisibility(numButtons < 2 ? View.GONE : View.VISIBLE);
-
- viewHolder.getPrimaryIcon().setImageIcon(currentInfo.getPrimaryIcon());
-
- viewHolder
- .getExpandedView()
- .setBackgroundTintList(ColorStateList.valueOf(currentInfo.getPrimaryColor()));
-
- updateButtonStates();
- }
-
- private void setBackgroundDrawable(CheckableImageButton view, @ColorInt int color) {
- RippleDrawable itemRipple =
- (RippleDrawable)
- context
- .getResources()
- .getDrawable(R.drawable.bubble_ripple_checkable_circle, context.getTheme());
- itemRipple.getDrawable(0).setTint(color);
- view.setBackground(itemRipple);
- }
-
- private void updateButtonStates() {
- int numButtons = currentInfo.getActions().size();
-
- if (numButtons >= 1) {
- configureButton(currentInfo.getActions().get(0), viewHolder.getFirstButton());
- if (numButtons >= 2) {
- configureButton(currentInfo.getActions().get(1), viewHolder.getSecondButton());
- if (numButtons >= 3) {
- configureButton(currentInfo.getActions().get(2), viewHolder.getThirdButton());
- }
- }
- }
- }
-
- private void doShowText(@NonNull CharSequence text) {
- TransitionManager.beginDelayedTransition((ViewGroup) viewHolder.getPrimaryButton().getParent());
- viewHolder.getPrimaryText().setText(text);
- viewHolder.getPrimaryButton().setDisplayedChild(ViewHolder.CHILD_INDEX_TEXT);
- }
-
- private void configureButton(Action action, CheckableImageButton button) {
- action
- .getIcon()
- .loadDrawableAsync(
- context,
- d -> {
- button.setImageIcon(action.getIcon());
- button.setContentDescription(action.getName());
- button.setChecked(action.isChecked());
- button.setEnabled(action.isEnabled());
- },
- handler);
- button.setOnClickListener(v -> doAction(action));
- }
-
- private void doAction(Action action) {
- try {
- action.getAction().send();
- } catch (CanceledException e) {
- throw new RuntimeException(e);
- }
- }
-
- private void doResize(@Nullable Runnable operation) {
- // If we're resizing on the right side of the screen, there is an implicit move operation
- // necessary. The WindowManager does not sync the move and resize operations, so serious jank
- // would occur. To fix this, instead of resizing the window, we create a new one and destroy
- // the old one. There is a short delay before destroying the old view to ensure the new one has
- // had time to draw.
- boolean onRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
- ViewHolder oldViewHolder = viewHolder;
- if (onRight) {
- viewHolder = new ViewHolder(oldViewHolder.getRoot().getContext());
- update();
- viewHolder
- .getPrimaryButton()
- .setDisplayedChild(oldViewHolder.getPrimaryButton().getDisplayedChild());
- viewHolder.getPrimaryText().setText(oldViewHolder.getPrimaryText().getText());
- }
-
- if (operation != null) {
- operation.run();
- }
-
- if (onRight) {
- swapViewHolders(oldViewHolder);
- }
- }
-
- private void swapViewHolders(ViewHolder oldViewHolder) {
- ViewGroup root = viewHolder.getRoot();
- windowManager.addView(root, windowParams);
- root.getViewTreeObserver()
- .addOnPreDrawListener(
- new OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- root.getViewTreeObserver().removeOnPreDrawListener(this);
- // Wait a bit before removing the old view; make sure the new one has drawn over it.
- handler.postDelayed(
- () -> windowManager.removeView(oldViewHolder.getRoot()),
- WINDOW_REDRAW_DELAY_MILLIS);
- return true;
- }
- });
- }
-
- private ViewPropertyAnimator startCollapse(@CollapseEnd int collapseEndAction) {
- setFocused(false);
- boolean onRight = (windowParams.gravity & Gravity.RIGHT) == Gravity.RIGHT;
- View expandedView = viewHolder.getExpandedView();
- return expandedView
- .animate()
- .translationX(onRight ? expandedView.getWidth() : -expandedView.getWidth())
- .setInterpolator(new FastOutLinearInInterpolator())
- .withEndAction(
- () -> {
- expanded = false;
- if (collapseEndAction == CollapseEnd.HIDE) {
- hide();
- } else if (!textShowing) {
- // Don't swap the window while the user is moving it, even if we're on the right.
- // The movement will help hide the jank of the resize.
- boolean swapWindow = onRight && !viewHolder.isMoving();
- if (swapWindow) {
- // We don't actually need to set the drawer to GONE since in the new window it
- // will already be GONE. Just do the resize operation.
- doResize(null);
- } else {
- expandedView.setVisibility(View.GONE);
- }
- }
- });
- }
-
- private void setFocused(boolean focused) {
- if (focused) {
- windowParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
- } else {
- windowParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
- }
- windowManager.updateViewLayout(getRootView(), windowParams);
- }
-
- private class ViewHolder {
-
- public static final int CHILD_INDEX_ICON = 0;
- public static final int CHILD_INDEX_TEXT = 1;
-
- private final MoveHandler moveHandler;
- private final WindowRoot root;
- private final ViewAnimator primaryButton;
- private final ImageView primaryIcon;
- private final TextView primaryText;
-
- private final CheckableImageButton firstButton;
- private final CheckableImageButton secondButton;
- private final CheckableImageButton thirdButton;
- private final View expandedView;
-
- public ViewHolder(Context context) {
- // Window root is not in the layout file so that the inflater has a view to inflate into
- this.root = new WindowRoot(context);
- LayoutInflater inflater = LayoutInflater.from(root.getContext());
- View contentView = inflater.inflate(R.layout.bubble_base, root, true);
- expandedView = contentView.findViewById(R.id.bubble_expanded_layout);
- primaryButton = contentView.findViewById(R.id.bubble_button_primary);
- primaryIcon = contentView.findViewById(R.id.bubble_icon_primary);
- primaryText = contentView.findViewById(R.id.bubble_text);
-
- firstButton = contentView.findViewById(R.id.bubble_icon_first);
- secondButton = contentView.findViewById(R.id.bubble_icon_second);
- thirdButton = contentView.findViewById(R.id.bubble_icon_third);
-
- root.setOnBackPressedListener(
- () -> {
- if (isShowing && expanded) {
- startCollapse(CollapseEnd.NOTHING);
- return true;
- }
- return false;
- });
- root.setOnTouchListener(
- (v, event) -> {
- if (expanded && event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
- startCollapse(CollapseEnd.NOTHING);
- return true;
- }
- return false;
- });
- moveHandler = new MoveHandler(primaryButton, Bubble.this);
- }
-
- public ViewGroup getRoot() {
- return root;
- }
-
- public ViewAnimator getPrimaryButton() {
- return primaryButton;
- }
-
- public ImageView getPrimaryIcon() {
- return primaryIcon;
- }
-
- public TextView getPrimaryText() {
- return primaryText;
- }
-
- public CheckableImageButton getFirstButton() {
- return firstButton;
- }
-
- public CheckableImageButton getSecondButton() {
- return secondButton;
- }
-
- public CheckableImageButton getThirdButton() {
- return thirdButton;
- }
-
- public View getExpandedView() {
- return expandedView;
- }
-
- public boolean isMoving() {
- return moveHandler.isMoving();
- }
- }
-}