blob: 4435afb8f6042ac011b65658fda41da458835d19 [file] [log] [blame]
/*
* Copyright (C) 2016 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.launcher3.popup;
import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.R;
import com.android.launcher3.anim.PropertyResetListener;
import com.android.launcher3.badge.BadgeInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.graphics.IconPalette;
import com.android.launcher3.notification.NotificationItemView;
import com.android.launcher3.notification.NotificationKeyData;
import com.android.launcher3.popup.PopupPopulator.Item;
import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutsItemView;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Themes;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A container for shortcuts to deep links and notifications associated with an app.
*/
@TargetApi(Build.VERSION_CODES.N)
public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> implements DragSource,
DragController.DragListener {
private final int mStartDragThreshold;
private NotificationItemView mNotificationItemView;
private AnimatorSet mReduceHeightAnimatorSet;
private int mNumNotifications;
public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mStartDragThreshold = getResources().getDimensionPixelSize(
R.dimen.deep_shortcuts_start_drag_threshold);
}
public PopupContainerWithArrow(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PopupContainerWithArrow(Context context) {
this(context, null, 0);
}
/**
* Shows the notifications and deep shortcuts associated with {@param icon}.
* @return the container if shown or null.
*/
public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
Launcher launcher = Launcher.getLauncher(icon.getContext());
if (getOpen(launcher) != null) {
// There is already an items container open, so don't open this one.
icon.clearFocus();
return null;
}
ItemInfo itemInfo = (ItemInfo) icon.getTag();
if (!DeepShortcutManager.supportsShortcuts(itemInfo)) {
return null;
}
PopupDataProvider popupDataProvider = launcher.getPopupDataProvider();
List<String> shortcutIds = popupDataProvider.getShortcutIdsForItem(itemInfo);
List<NotificationKeyData> notificationKeys = popupDataProvider
.getNotificationKeysForItem(itemInfo);
List<SystemShortcut> systemShortcuts = popupDataProvider
.getEnabledSystemShortcutsForItem(itemInfo);
final PopupContainerWithArrow container =
(PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
R.layout.popup_container, launcher.getDragLayer(), false);
container.populateAndShow(icon, shortcutIds, notificationKeys, systemShortcuts);
return container;
}
private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
mNumNotifications = notificationKeys.size();
PopupPopulator.Item[] itemsToPopulate = PopupPopulator
.getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
populateAndShow(originalIcon, itemsToPopulate);
ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
? Collections.EMPTY_LIST
: mShortcutsItemView.getDeepShortcutViews(mIsAboveIcon);
List<View> systemShortcutViews = mShortcutsItemView == null
? Collections.EMPTY_LIST
: mShortcutsItemView.getSystemShortcutViews(mIsAboveIcon);
if (mNotificationItemView != null) {
updateNotificationHeader();
}
int numShortcuts = shortcutViews.size() + systemShortcutViews.size();
int numNotifications = notificationKeys.size();
if (numNotifications == 0) {
setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
numShortcuts, originalIcon.getContentDescription().toString()));
} else {
setContentDescription(getContext().getString(
R.string.shortcuts_menu_with_notifications_description, numShortcuts,
numNotifications, originalIcon.getContentDescription().toString()));
}
mLauncher.getDragController().addDragListener(this);
mOriginalIcon.forceHideBadge(true);
// Load the shortcuts on a background thread and update the container as it animates.
final Looper workerLooper = LauncherModel.getWorkerLooper();
new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
systemShortcuts, systemShortcutViews));
}
@Override
protected void addDummyViews(Item[] itemTypesToPopulate) {
mNotificationItemView = null;
super.addDummyViews(itemTypesToPopulate);
if (mNumNotifications > 0) {
mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
}
}
@Override
protected void onViewInflated(View view, Item itemType,
boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) {
if (itemType == PopupPopulator.Item.NOTIFICATION) {
mNotificationItemView = (NotificationItemView) view;
boolean notificationFooterHasIcons = mNumNotifications > 1;
int footerHeight = getResources().getDimensionPixelSize(
notificationFooterHasIcons ? R.dimen.notification_footer_height
: R.dimen.notification_empty_footer_height);
view.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
if (notificationFooterHasIcons) {
mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
}
int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
if (shouldUnroundTopCorners) {
roundedCorners &= ~ROUNDED_TOP_CORNERS;
mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
}
if (shouldUnroundBottomCorners) {
roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
}
int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
} else if (itemType == PopupPopulator.Item.SHORTCUT) {
view.setAccessibilityDelegate(mAccessibilityDelegate);
}
if (itemType != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON && itemType.isShortcut
&& mNumNotifications > 0) {
int prevHeight = view.getLayoutParams().height;
// Condense shortcuts height when there are notifications.
view.getLayoutParams().height = getResources().getDimensionPixelSize(
R.dimen.bg_popup_item_condensed_height);
if (view instanceof DeepShortcutView) {
float iconScale = (float) view.getLayoutParams().height / prevHeight;
((DeepShortcutView) view).getIconView().setScaleX(iconScale);
((DeepShortcutView) view).getIconView().setScaleY(iconScale);
}
}
}
private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) {
final Resources res = getResources();
final LayoutInflater inflater = mLauncher.getLayoutInflater();
int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
int numItems = itemTypesToPopulate.length;
for (int i = 0; i < numItems; i++) {
PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
PopupPopulator.Item prevItemTypeToPopulate =
i > 0 ? itemTypesToPopulate[i - 1] : null;
PopupPopulator.Item nextItemTypeToPopulate =
i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null
&& itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut;
boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null
&& itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
mNotificationItemView = (NotificationItemView) item;
boolean notificationFooterHasIcons = numNotifications > 1;
int footerHeight = res.getDimensionPixelSize(
notificationFooterHasIcons ? R.dimen.notification_footer_height
: R.dimen.notification_empty_footer_height);
item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
if (notificationFooterHasIcons) {
mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
}
int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
if (shouldUnroundTopCorners) {
roundedCorners &= ~ROUNDED_TOP_CORNERS;
mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
}
if (shouldUnroundBottomCorners) {
roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
}
int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
} else if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
item.setAccessibilityDelegate(mAccessibilityDelegate);
}
if (itemTypeToPopulate.isShortcut) {
if (mShortcutsItemView == null) {
mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
R.layout.shortcuts_item, this, false);
addView(mShortcutsItemView);
if (shouldUnroundTopCorners) {
shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS;
}
}
if (itemTypeToPopulate != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON
&& numNotifications > 0) {
int prevHeight = item.getLayoutParams().height;
// Condense shortcuts height when there are notifications.
item.getLayoutParams().height = res.getDimensionPixelSize(
R.dimen.bg_popup_item_condensed_height);
if (item instanceof DeepShortcutView) {
float iconScale = (float) item.getLayoutParams().height / prevHeight;
((DeepShortcutView) item).getIconView().setScaleX(iconScale);
((DeepShortcutView) item).getIconView().setScaleY(iconScale);
}
}
mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
if (shouldUnroundBottomCorners) {
shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
}
} else {
addView(item);
}
}
int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners);
if (numNotifications > 0) {
mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
}
}
@Override
protected void onWidgetsBound() {
if (mShortcutsItemView != null) {
mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
}
}
@Override
protected int getIconHeightForPopupPlacement() {
return mOriginalIcon.getIcon() != null
? mOriginalIcon.getIcon().getBounds().height()
: mOriginalIcon.getHeight();
}
/**
* Determines when the deferred drag should be started.
*
* Current behavior:
* - Start the drag if the touch passes a certain distance from the original touch down.
*/
public DragOptions.PreDragCondition createPreDragCondition() {
return new DragOptions.PreDragCondition() {
@Override
public boolean shouldStartDrag(double distanceDragged) {
return distanceDragged > mStartDragThreshold;
}
@Override
public void onPreDragStart(DropTarget.DragObject dragObject) {
if (mIsAboveIcon) {
// Hide only the icon, keep the text visible.
mOriginalIcon.setIconVisible(false);
mOriginalIcon.setVisibility(VISIBLE);
} else {
// Hide both the icon and text.
mOriginalIcon.setVisibility(INVISIBLE);
}
}
@Override
public void onPreDragEnd(DropTarget.DragObject dragObject, boolean dragStarted) {
mOriginalIcon.setIconVisible(true);
if (dragStarted) {
// Make sure we keep the original icon hidden while it is being dragged.
mOriginalIcon.setVisibility(INVISIBLE);
} else {
mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon);
if (!mIsAboveIcon) {
// Show the icon but keep the text hidden.
mOriginalIcon.setVisibility(VISIBLE);
mOriginalIcon.setTextVisibility(false);
}
}
}
};
}
/**
* Updates the notification header if the original icon's badge updated.
*/
public void updateNotificationHeader(Set<PackageUserKey> updatedBadges) {
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
if (updatedBadges.contains(packageUser)) {
updateNotificationHeader();
}
}
private void updateNotificationHeader() {
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
if (mNotificationItemView != null && badgeInfo != null) {
IconPalette palette = mOriginalIcon.getBadgePalette();
mNotificationItemView.updateHeader(badgeInfo.getNotificationCount(), palette);
}
}
public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
if (mNotificationItemView == null) {
return;
}
ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) {
// There are no more notifications, so create an animation to remove
// the notifications view and expand the shortcuts view (if possible).
AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
int hiddenShortcutsHeight = 0;
if (mShortcutsItemView != null) {
hiddenShortcutsHeight = mShortcutsItemView.getHiddenShortcutsHeight();
int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
// With notifications gone, all corners of shortcuts item should be rounded.
mShortcutsItemView.setBackgroundWithCorners(backgroundColor,
ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS);
removeNotification.play(mShortcutsItemView.showAllShortcuts(mIsAboveIcon));
}
final int duration = getResources().getInteger(
R.integer.config_removeNotificationViewDuration);
removeNotification.play(adjustItemHeights(mNotificationItemView.getHeightMinusFooter(),
hiddenShortcutsHeight, duration));
Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
.setDuration(duration);
fade.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
removeView(mNotificationItemView);
mNotificationItemView = null;
if (getItemCount() == 0) {
close(false);
}
}
});
removeNotification.play(fade);
final long arrowScaleDuration = getResources().getInteger(
R.integer.config_popupArrowOpenDuration);
Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
hideArrow.setStartDelay(0);
Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
removeNotification.playSequentially(hideArrow, showArrow);
removeNotification.start();
return;
}
mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
badgeInfo.getNotificationKeys()));
}
public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
return adjustItemHeights(heightToRemove, 0, duration);
}
/**
* Animates the height of the notification item and the translationY of other items accordingly.
*/
public Animator adjustItemHeights(int notificationHeightToRemove, int shortcutHeightToAdd,
int duration) {
if (mReduceHeightAnimatorSet != null) {
mReduceHeightAnimatorSet.cancel();
}
final int translateYBy = mIsAboveIcon ? notificationHeightToRemove - shortcutHeightToAdd
: -notificationHeightToRemove;
mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet();
boolean removingNotification =
notificationHeightToRemove == mNotificationItemView.getHeightMinusFooter();
boolean shouldRemoveNotificationHeightFromTop = mIsAboveIcon && removingNotification;
mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval(
notificationHeightToRemove, shouldRemoveNotificationHeightFromTop));
PropertyResetListener<View, Float> resetTranslationYListener
= new PropertyResetListener<>(TRANSLATION_Y, 0f);
boolean itemIsAfterShortcuts = false;
for (int i = 0; i < getItemCount(); i++) {
final PopupItemView itemView = getItemViewAt(i);
if (itemIsAfterShortcuts) {
// Every item after the shortcuts item needs to adjust for the new height.
itemView.setTranslationY(itemView.getTranslationY() - shortcutHeightToAdd);
}
if (itemView == mNotificationItemView && (!mIsAboveIcon || removingNotification)) {
// The notification view is already in the right place.
continue;
}
ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
itemView.getTranslationY() + translateYBy).setDuration(duration);
translateItem.addListener(resetTranslationYListener);
mReduceHeightAnimatorSet.play(translateItem);
if (itemView == mShortcutsItemView) {
itemIsAfterShortcuts = true;
}
}
if (mIsAboveIcon) {
// We also need to adjust the arrow position to account for the new shortcuts height.
mArrow.setTranslationY(mArrow.getTranslationY() - shortcutHeightToAdd);
}
mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mIsAboveIcon) {
// All the items, including the notification item, translated down, but the
// container itself did not. This means the items would jump back to their
// original translation unless we update the container's translationY here.
setTranslationY(getTranslationY() + translateYBy);
mArrow.setTranslationY(0);
}
mReduceHeightAnimatorSet = null;
}
});
return mReduceHeightAnimatorSet;
}
@Override
public void onDropCompleted(View target, DragObject d, boolean success) { }
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
// Either the original icon or one of the shortcuts was dragged.
// Hide the container, but don't remove it yet because that interferes with touch events.
mDeferContainerRemoval = true;
animateClose();
}
@Override
public void onDragEnd() {
if (!mIsOpen) {
if (mOpenCloseAnimator != null) {
// Close animation is running.
mDeferContainerRemoval = false;
} else {
// Close animation is not running.
if (mDeferContainerRemoval) {
closeComplete();
}
}
}
}
@Override
public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
target.itemType = ItemType.DEEPSHORTCUT;
targetParent.containerType = ContainerType.DEEPSHORTCUTS;
}
@Override
protected void prepareCloseAnimator(AnimatorSet closeAnim) {
// Animate original icon's text back in.
closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
mOriginalIcon.forceHideBadge(false);
super.prepareCloseAnimator(closeAnim);
}
@Override
protected void closeComplete() {
mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
mOriginalIcon.forceHideBadge(false);
mLauncher.getDragController().removeDragListener(this);
super.closeComplete();
}
}