| /* |
| * 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.notification.NotificationMainView.NOTIFICATION_ITEM_INFO; |
| import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS; |
| 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.AnimatorSet; |
| import android.animation.LayoutTransition; |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.AttributeSet; |
| import android.util.Pair; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| |
| import com.android.launcher3.AbstractFloatingView; |
| 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.ItemInfoWithIcon; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherModel; |
| import com.android.launcher3.R; |
| import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; |
| import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate; |
| import com.android.launcher3.badge.BadgeInfo; |
| import com.android.launcher3.dragndrop.DragController; |
| import com.android.launcher3.dragndrop.DragOptions; |
| import com.android.launcher3.dragndrop.DragView; |
| import com.android.launcher3.logging.LoggerUtils; |
| import com.android.launcher3.notification.NotificationInfo; |
| import com.android.launcher3.notification.NotificationItemView; |
| import com.android.launcher3.notification.NotificationKeyData; |
| import com.android.launcher3.shortcuts.DeepShortcutManager; |
| import com.android.launcher3.shortcuts.DeepShortcutView; |
| import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; |
| import com.android.launcher3.touch.ItemClickHandler; |
| import com.android.launcher3.touch.ItemLongClickListener; |
| import com.android.launcher3.util.PackageUserKey; |
| import com.android.launcher3.views.BaseDragLayer; |
| |
| import java.util.ArrayList; |
| 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 ArrowPopup implements DragSource, |
| DragController.DragListener, View.OnLongClickListener, |
| View.OnTouchListener { |
| |
| private final List<DeepShortcutView> mShortcuts = new ArrayList<>(); |
| private final PointF mInterceptTouchDown = new PointF(); |
| protected final Point mIconLastTouchPos = new Point(); |
| |
| private final int mStartDragThreshold; |
| private final LauncherAccessibilityDelegate mAccessibilityDelegate; |
| |
| private BubbleTextView mOriginalIcon; |
| private NotificationItemView mNotificationItemView; |
| private int mNumNotifications; |
| |
| private ViewGroup mSystemShortcutContainer; |
| |
| public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mStartDragThreshold = getResources().getDimensionPixelSize( |
| R.dimen.deep_shortcuts_start_drag_threshold); |
| mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher); |
| } |
| |
| public PopupContainerWithArrow(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public PopupContainerWithArrow(Context context) { |
| this(context, null, 0); |
| } |
| |
| public LauncherAccessibilityDelegate getAccessibilityDelegate() { |
| return mAccessibilityDelegate; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| mInterceptTouchDown.set(ev.getX(), ev.getY()); |
| } |
| if (mNotificationItemView != null |
| && mNotificationItemView.onInterceptTouchEvent(ev)) { |
| return true; |
| } |
| // Stop sending touch events to deep shortcut views if user moved beyond touch slop. |
| return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY()) |
| > ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mNotificationItemView != null) { |
| return mNotificationItemView.onTouchEvent(ev) || super.onTouchEvent(ev); |
| } |
| return super.onTouchEvent(ev); |
| } |
| |
| @Override |
| protected boolean isOfType(int type) { |
| return (type & TYPE_ACTION_POPUP) != 0; |
| } |
| |
| @Override |
| public void logActionCommand(int command) { |
| mLauncher.getUserEventDispatcher().logActionCommand( |
| command, mOriginalIcon, ContainerType.DEEPSHORTCUTS); |
| } |
| |
| public OnClickListener getItemClickListener() { |
| return ItemClickHandler.INSTANCE; |
| } |
| |
| @Override |
| public boolean onControllerInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| BaseDragLayer dl = getPopupContainer(); |
| if (!dl.isEventOverView(this, ev)) { |
| mLauncher.getUserEventDispatcher().logActionTapOutside( |
| LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS)); |
| close(true); |
| |
| // We let touches on the original icon go through so that users can launch |
| // the app with one tap if they don't find a shortcut they want. |
| return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| final PopupContainerWithArrow container = |
| (PopupContainerWithArrow) launcher.getLayoutInflater().inflate( |
| R.layout.popup_container, launcher.getDragLayer(), false); |
| container.populateAndShow(icon, itemInfo, SystemShortcutFactory.INSTANCE.get(launcher)); |
| return container; |
| } |
| |
| @Override |
| protected void onInflationComplete(boolean isReversed) { |
| if (isReversed && mNotificationItemView != null) { |
| mNotificationItemView.inverseGutterMargin(); |
| } |
| |
| // Update dividers |
| int count = getChildCount(); |
| DeepShortcutView lastView = null; |
| for (int i = 0; i < count; i++) { |
| View view = getChildAt(i); |
| if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { |
| if (lastView != null) { |
| lastView.setDividerVisibility(VISIBLE); |
| } |
| lastView = (DeepShortcutView) view; |
| lastView.setDividerVisibility(INVISIBLE); |
| } |
| } |
| } |
| |
| protected void populateAndShow( |
| BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory) { |
| PopupDataProvider popupDataProvider = mLauncher.getPopupDataProvider(); |
| populateAndShow(icon, |
| popupDataProvider.getShortcutIdsForItem(item), |
| popupDataProvider.getNotificationKeysForItem(item), |
| factory.getEnabledShortcuts(mLauncher, item)); |
| } |
| |
| @TargetApi(Build.VERSION_CODES.P) |
| protected void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds, |
| final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) { |
| mNumNotifications = notificationKeys.size(); |
| mOriginalIcon = originalIcon; |
| |
| // Add views |
| if (mNumNotifications > 0) { |
| // Add notification entries |
| View.inflate(getContext(), R.layout.notification_content, this); |
| mNotificationItemView = new NotificationItemView(this); |
| if (mNumNotifications == 1) { |
| mNotificationItemView.removeFooter(); |
| } |
| updateNotificationHeader(); |
| } |
| int viewsToFlip = getChildCount(); |
| mSystemShortcutContainer = this; |
| |
| if (!shortcutIds.isEmpty()) { |
| if (mNotificationItemView != null) { |
| mNotificationItemView.addGutter(); |
| } |
| |
| for (int i = shortcutIds.size(); i > 0; i--) { |
| mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this)); |
| } |
| updateHiddenShortcuts(); |
| |
| if (!systemShortcuts.isEmpty()) { |
| mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this); |
| for (SystemShortcut shortcut : systemShortcuts) { |
| initializeSystemShortcut( |
| R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut); |
| } |
| } |
| } else if (!systemShortcuts.isEmpty()) { |
| if (mNotificationItemView != null) { |
| mNotificationItemView.addGutter(); |
| } |
| |
| for (SystemShortcut shortcut : systemShortcuts) { |
| initializeSystemShortcut(R.layout.system_shortcut, this, shortcut); |
| } |
| } |
| |
| reorderAndShow(viewsToFlip); |
| |
| ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag(); |
| if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { |
| setAccessibilityPaneTitle(getTitleForAccessibility()); |
| } |
| |
| mLauncher.getDragController().addDragListener(this); |
| mOriginalIcon.forceHideBadge(true); |
| |
| // All views are added. Animate layout from now on. |
| setLayoutTransition(new LayoutTransition()); |
| |
| // 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, mShortcuts, notificationKeys)); |
| } |
| |
| private String getTitleForAccessibility() { |
| return getContext().getString(mNumNotifications == 0 ? |
| R.string.action_deep_shortcut : |
| R.string.shortcuts_menu_with_notifications_description); |
| } |
| |
| @Override |
| protected Pair<View, String> getAccessibilityTarget() { |
| return Pair.create(this, ""); |
| } |
| |
| @Override |
| protected void getTargetObjectLocation(Rect outPos) { |
| getPopupContainer().getDescendantRectRelativeToSelf(mOriginalIcon, outPos); |
| outPos.top += mOriginalIcon.getPaddingTop(); |
| outPos.left += mOriginalIcon.getPaddingLeft(); |
| outPos.right -= mOriginalIcon.getPaddingRight(); |
| outPos.bottom = outPos.top + (mOriginalIcon.getIcon() != null |
| ? mOriginalIcon.getIcon().getBounds().height() |
| : mOriginalIcon.getHeight()); |
| } |
| |
| public void applyNotificationInfos(List<NotificationInfo> notificationInfos) { |
| mNotificationItemView.applyNotificationInfos(notificationInfos); |
| } |
| |
| private void updateHiddenShortcuts() { |
| int allowedCount = mNotificationItemView != null |
| ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS; |
| int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height); |
| int itemHeight = mNotificationItemView != null ? |
| getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height) |
| : originalHeight; |
| float iconScale = ((float) itemHeight) / originalHeight; |
| |
| int total = mShortcuts.size(); |
| for (int i = 0; i < total; i++) { |
| DeepShortcutView view = mShortcuts.get(i); |
| view.setVisibility(i >= allowedCount ? GONE : VISIBLE); |
| view.getLayoutParams().height = itemHeight; |
| view.getIconView().setScaleX(iconScale); |
| view.getIconView().setScaleY(iconScale); |
| } |
| } |
| |
| private void updateDividers() { |
| int count = getChildCount(); |
| DeepShortcutView lastView = null; |
| for (int i = 0; i < count; i++) { |
| View view = getChildAt(i); |
| if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) { |
| if (lastView != null) { |
| lastView.setDividerVisibility(VISIBLE); |
| } |
| lastView = (DeepShortcutView) view; |
| lastView.setDividerVisibility(INVISIBLE); |
| } |
| } |
| } |
| |
| @Override |
| protected void onWidgetsBound() { |
| ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag(); |
| SystemShortcut widgetInfo = new SystemShortcut.Widgets(); |
| View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo); |
| View widgetsView = null; |
| int count = mSystemShortcutContainer.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| View systemShortcutView = mSystemShortcutContainer.getChildAt(i); |
| if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) { |
| widgetsView = systemShortcutView; |
| break; |
| } |
| } |
| |
| if (onClickListener != null && widgetsView == null) { |
| // We didn't have any widgets cached but now there are some, so enable the shortcut. |
| if (mSystemShortcutContainer != this) { |
| initializeSystemShortcut( |
| R.layout.system_shortcut_icon_only, mSystemShortcutContainer, widgetInfo); |
| } else { |
| // If using the expanded system shortcut (as opposed to just the icon), we need to |
| // reopen the container to ensure measurements etc. all work out. While this could |
| // be quite janky, in practice the user would typically see a small flicker as the |
| // animation restarts partway through, and this is a very rare edge case anyway. |
| close(false); |
| PopupContainerWithArrow.showForIcon(mOriginalIcon); |
| } |
| } else if (onClickListener == null && widgetsView != null) { |
| // No widgets exist, but we previously added the shortcut so remove it. |
| if (mSystemShortcutContainer != this) { |
| mSystemShortcutContainer.removeView(widgetsView); |
| } else { |
| close(false); |
| PopupContainerWithArrow.showForIcon(mOriginalIcon); |
| } |
| } |
| } |
| |
| private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) { |
| View view = inflateAndAdd(resId, container); |
| if (view instanceof DeepShortcutView) { |
| // Expanded system shortcut, with both icon and text shown on white background. |
| final DeepShortcutView shortcutView = (DeepShortcutView) view; |
| shortcutView.getIconView().setBackgroundResource(info.iconResId); |
| shortcutView.getBubbleText().setText(info.labelResId); |
| } else if (view instanceof ImageView) { |
| // Only the system shortcut icon shows on a gray background header. |
| final ImageView shortcutIcon = (ImageView) view; |
| shortcutIcon.setImageResource(info.iconResId); |
| shortcutIcon.setContentDescription(getContext().getText(info.labelResId)); |
| } |
| view.setTag(info); |
| view.setOnClickListener(info.getOnClickListener(mLauncher, |
| (ItemInfo) mOriginalIcon.getTag())); |
| } |
| |
| /** |
| * 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() { |
| ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag(); |
| BadgeInfo badgeInfo = mLauncher.getBadgeInfoForItem(itemInfo); |
| if (mNotificationItemView != null && badgeInfo != null) { |
| mNotificationItemView.updateHeader( |
| badgeInfo.getNotificationCount(), itemInfo.iconColor); |
| } |
| } |
| |
| 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) { |
| // No more notifications, remove the notification views and expand all shortcuts. |
| mNotificationItemView.removeAllViews(); |
| mNotificationItemView = null; |
| updateHiddenShortcuts(); |
| updateDividers(); |
| } else { |
| mNotificationItemView.trimNotifications( |
| NotificationKeyData.extractKeysOnly(badgeInfo.getNotificationKeys())); |
| } |
| } |
| |
| @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) { |
| if (info == NOTIFICATION_ITEM_INFO) { |
| target.itemType = ItemType.NOTIFICATION; |
| } else { |
| target.itemType = ItemType.DEEPSHORTCUT; |
| target.rank = info.rank; |
| } |
| targetParent.containerType = ContainerType.DEEPSHORTCUTS; |
| } |
| |
| @Override |
| protected void onCreateCloseAnimation(AnimatorSet anim) { |
| // Animate original icon's text back in. |
| anim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */)); |
| mOriginalIcon.forceHideBadge(false); |
| } |
| |
| @Override |
| protected void closeComplete() { |
| super.closeComplete(); |
| mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible()); |
| mOriginalIcon.forceHideBadge(false); |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent ev) { |
| // Touched a shortcut, update where it was touched so we can drag from there on long click. |
| switch (ev.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| case MotionEvent.ACTION_MOVE: |
| mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY()); |
| break; |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean onLongClick(View v) { |
| if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; |
| // Return early if not the correct view |
| if (!(v.getParent() instanceof DeepShortcutView)) return false; |
| |
| // Long clicked on a shortcut. |
| DeepShortcutView sv = (DeepShortcutView) v.getParent(); |
| sv.setWillDrawIcon(false); |
| |
| // Move the icon to align with the center-top of the touch point |
| Point iconShift = new Point(); |
| iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x; |
| iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx; |
| |
| DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), |
| this, sv.getFinalInfo(), |
| new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions()); |
| dv.animateShift(-iconShift.x, -iconShift.y); |
| |
| // TODO: support dragging from within folder without having to close it |
| AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER); |
| return false; |
| } |
| |
| /** |
| * Returns a PopupContainerWithArrow which is already open or null |
| */ |
| public static PopupContainerWithArrow getOpen(Launcher launcher) { |
| return getOpenView(launcher, TYPE_ACTION_POPUP); |
| } |
| } |