/* * 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 android.animation.AnimatorSet import android.animation.LayoutTransition import android.content.Context import android.graphics.PointF import android.graphics.Rect import android.graphics.Typeface import android.os.Handler import android.os.Looper import android.view.MotionEvent import android.view.View import android.view.View.OnClickListener import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.LayoutRes import com.android.launcher3.BubbleTextView import com.android.launcher3.DragSource import com.android.launcher3.DropTarget.DragObject import com.android.launcher3.Flags import com.android.launcher3.Launcher import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.dragndrop.DragController import com.android.launcher3.dragndrop.DragOptions.PreDragCondition import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.model.data.ItemInfoWithIcon import com.android.launcher3.shortcuts.DeepShortcutTextView import com.android.launcher3.shortcuts.DeepShortcutView import com.android.launcher3.util.Executors import com.android.launcher3.util.ShortcutUtil import com.android.launcher3.views.ActivityContext import java.util.Optional import java.util.stream.Collectors import kotlin.math.max /** * A container for shortcuts to deep links associated with an app. * * @param The activity on with the popup shows */ class PopupContainerWithArrow private constructor( context: Context?, originalView: View, itemInfo: ItemInfo, private val updateIconUi: Boolean, ) : PopupContainer(context, originalView, itemInfo), DragSource, DragController.DragListener, Popup where T : Context, T : ActivityContext { private val deepShortcuts: MutableList = ArrayList() private val interceptTouchDown = PointF() private val shortcutHeight: Float = resources.getDimension(R.dimen.system_shortcut_header_height) val itemClickListener: OnClickListener get() = OnClickListener { view: View? -> mActivityContext?.itemOnClickListener?.onClick(view) } private var containerWidth: Int = resources.getDimensionPixelSize(R.dimen.bg_popup_item_width) private var deepShortcutContainer: ViewGroup? = null private var currentHeight = 0f val originalIcon = originalView as BubbleTextView var itemDragHandler: PopupItemDragHandler? = null private set var widgetContainer: ViewGroup? = null override fun getAccessibilityInitialFocusView(): View { return systemShortcutContainer?.getChildAt(0) ?: super.getAccessibilityInitialFocusView() } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (ev.action == MotionEvent.ACTION_DOWN) { interceptTouchDown[ev.x] = ev.y } // Stop sending touch events to deep shortcut views if user moved beyond touch slop. return (Utilities.squaredHypot(interceptTouchDown.x - ev.x, interceptTouchDown.y - ev.y) > Utilities.squaredTouchSlop(context)) } fun setPopupItemDragHandler(popupItemDragHandler: PopupItemDragHandler?) { itemDragHandler = popupItemDragHandler } fun configureForLauncher(launcher: Launcher, itemInfo: ItemInfo) { addOnAttachStateChangeListener( LauncherPopupLiveUpdateHandler(launcher, this as PopupContainerWithArrow) ) if ( !Flags.privateSpaceRestrictItemDrag() || (itemInfo !is ItemInfoWithIcon) || (itemInfo.runtimeStatusFlags and ItemInfoWithIcon.FLAG_NOT_PINNABLE) == 0 ) { itemDragHandler = LauncherPopupItemDragHandler(launcher, this) } launcher.dragController.addDragListener(this) } /** * Populate and show shortcuts for the Launcher U app shortcut design. Will inflate the * container and shortcut View instances for the popup container. * * @param deepShortcutCount Number of DeepShortcutView instances to add to container * @param systemShortcuts List of SystemShortcuts to add to container */ fun populateAndShowRows(deepShortcutCount: Int, systemShortcuts: List>) { populateAndShowRows(itemInfo, deepShortcutCount, systemShortcuts) } /** * Populate and show shortcuts for the Launcher U app shortcut design. Will inflate the * container and shortcut View instances for the popup container. * * @param itemInfo The info that is used to load app shortcuts * @param deepShortcutCount Number of DeepShortcutView instances to add to container * @param systemShortcuts List of SystemShortcuts to add to container */ private fun populateAndShowRows( itemInfo: ItemInfo, deepShortcutCount: Int, systemShortcuts: List>, ) { containerWidth = resources.getDimensionPixelSize(R.dimen.bg_popup_item_width) if (deepShortcutCount > 0) { addAllShortcuts(deepShortcutCount, systemShortcuts) } else if (systemShortcuts.isNotEmpty()) { addSystemShortcuts( systemShortcuts, R.layout.system_shortcut_rows_container, R.layout.system_shortcut, ) } show() loadAppShortcuts(itemInfo) } /** Animates and loads shortcuts on background thread for this popup container */ private fun loadAppShortcuts(originalItemInfo: ItemInfo) { accessibilityPaneTitle = context.getString(R.string.action_deep_shortcut) originalIcon.forceHideDot = true // All views are added. Animate layout from now on. layoutTransition = LayoutTransition() // Load the shortcuts on a background thread and update the container as it animates. Executors.MODEL_EXECUTOR.handler.postAtFrontOfQueue( PopupPopulator.createUpdateRunnable( mActivityContext, originalItemInfo, Handler(Looper.getMainLooper()), this, deepShortcuts, ) ) } /** * Adds any Deep Shortcuts, System Shortcuts and the Widget Shortcut to their respective * containers * * @param deepShortcutCount number of DeepShortcutView instances * @param systemShortcuts List of SystemShortcuts */ private fun addAllShortcuts(deepShortcutCount: Int, systemShortcuts: List>) { if (deepShortcutCount + systemShortcuts.size <= SHORTCUT_COLLAPSE_THRESHOLD) { // add all system shortcuts including widgets shortcut to same container addSystemShortcuts( systemShortcuts, R.layout.system_shortcut_rows_container, R.layout.system_shortcut, ) val startingHeight = ((shortcutHeight * systemShortcuts.size) + mChildContainerMargin) addDeepShortcuts(deepShortcutCount, startingHeight) return } currentHeight = shortcutHeight + mChildContainerMargin collapseEligibleSystemShortcutsIfOverThreshold(systemShortcuts) addDeepShortcuts(deepShortcutCount, currentHeight) } /** * If the total amount of shortcuts is over threshold, we collapse the shortcuts that are * eligible to be collapsible, and make sure the non-collapsible ones get their own container. * * @param systemShortcuts List of SystemShortcuts */ private fun collapseEligibleSystemShortcutsIfOverThreshold( systemShortcuts: List> ) { val collapsibleSystemShortcuts = getCollapsibleSystemShortcuts(systemShortcuts) // If total shortcuts over threshold, collapse system shortcuts to single row addSystemShortcutsIconsOnly(collapsibleSystemShortcuts) // May need to recalculate row width containerWidth = max( containerWidth.toDouble(), (collapsibleSystemShortcuts.size * resources.getDimensionPixelSize( R.dimen.system_shortcut_header_icon_touch_size )) .toDouble(), ) .toInt() val nonCollapsibleSystemShortcuts = systemShortcuts .stream() .filter { shortcut: SystemShortcut<*> -> !shortcut.mIsCollapsible } .toList() if (nonCollapsibleSystemShortcuts.isNotEmpty()) { addSystemShortcuts( nonCollapsibleSystemShortcuts, R.layout.system_shortcut_rows_container, R.layout.system_shortcut, ) currentHeight += ((shortcutHeight * nonCollapsibleSystemShortcuts.size) + mChildContainerMargin) } } /** * Inflates the given systemShortcutContainerLayout as a container, and populates with the * systemShortcuts as views using the systemShortcutLayout * * @param systemShortcuts List of SystemShortcut to inflate as Views * @param systemShortcutContainerLayout Layout Resource for the Container of shortcut Views * @param systemShortcutLayout Layout Resource for the individual shortcut Views */ private fun addSystemShortcuts( systemShortcuts: List>, @LayoutRes systemShortcutContainerLayout: Int, @LayoutRes systemShortcutLayout: Int, ) { if (systemShortcuts.isEmpty()) { return } systemShortcutContainer = inflateAndAdd(systemShortcutContainerLayout, this) widgetContainer = systemShortcutContainer for (i in systemShortcuts.indices) { initializeSystemShortcut( systemShortcutLayout, systemShortcutContainer, systemShortcuts[i], i < systemShortcuts.size - 1, ) } } private fun addSystemShortcutsIconsOnly(systemShortcuts: List>) { if (systemShortcuts.isEmpty()) { return } systemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons_container, this) for (i in systemShortcuts.indices) { @LayoutRes var shortcutIconLayout = R.layout.system_shortcut_icon_only var shouldAppendSpacer = true if (i == 0) { shortcutIconLayout = R.layout.system_shortcut_icon_only_start } else if (i == systemShortcuts.size - 1) { shortcutIconLayout = R.layout.system_shortcut_icon_only_end shouldAppendSpacer = false } initializeSystemShortcut( shortcutIconLayout, systemShortcutContainer, systemShortcuts[i], shouldAppendSpacer, ) } } /** * Inflates and adds [deepShortcutCount] number of DeepShortcutView for the to a new container * * @param deepShortcutCount number of DeepShortcutView instances to add * @param startingHeight height of popup before adding deep shortcuts */ private fun addDeepShortcuts(deepShortcutCount: Int, startingHeight: Float) { var height = startingHeight deepShortcutContainer = inflateAndAdd(R.layout.deep_shortcut_container, this) for (i in deepShortcutCount downTo 1) { height += shortcutHeight // when there is limited vertical screen space, limit total popup rows to fit if ( height >= (mActivityContext?.deviceProfile?.deviceProperties?.availableHeightPx ?: 0) ) break val v = inflateAndAdd(R.layout.deep_shortcut, deepShortcutContainer) v.layoutParams.width = containerWidth deepShortcuts.add(v) } updateHiddenShortcuts() } override fun getTargetObjectLocation(outPos: Rect) { super.getTargetObjectLocation(outPos) outPos.bottom = outPos.top + (originalIcon.icon?.bounds?.height() ?: originalView.height) } private fun updateHiddenShortcuts() { val total = deepShortcuts.size for (i in 0..= PopupPopulator.MAX_SHORTCUTS) GONE else VISIBLE } } fun initializeWidgetShortcut(container: ViewGroup?, info: SystemShortcut<*>) { val view = initializeSystemShortcut(R.layout.system_shortcut, container, info, false) view.layoutParams.width = containerWidth } /** * Initializes and adds View for given SystemShortcut to a container. * * @param resId Resource id to use for SystemShortcut View. * @param container ViewGroup to add the shortcut View to as a parent * @param info The SystemShortcut instance to create a View for. * @param shouldAppendSpacer If True, will add a spacer after the shortcut, when showing the * SystemShortcut as an icon only. Used to space the shortcut icons evenly. * @return The view inflated for the SystemShortcut */ private fun initializeSystemShortcut( resId: Int, container: ViewGroup?, info: SystemShortcut<*>, shouldAppendSpacer: Boolean, ): View { val view = inflateAndAdd(resId, container) if (view is DeepShortcutView) { // System shortcut takes entire row with icon and text val shortcutView = view if (com.android.wm.shell.Flags.enableGsf()) { shortcutView.bubbleText.typeface = Typeface.create( DeepShortcutTextView.GOOGLE_SANS_FLEX_LABEL_LARGE, Typeface.NORMAL, ) } info.setIconAndLabelFor(shortcutView.iconView, shortcutView.bubbleText) } else if (view is ImageView) { // System shortcut is just an icon info.setIconAndContentDescriptionFor(view) if (shouldAppendSpacer) inflateAndAdd(R.layout.system_shortcut_spacer, container) view.setTooltipText(view.getContentDescription()) } view.tag = info view.setOnClickListener(info) return view } /** * 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. */ override fun createPreDragCondition(): PreDragCondition { return object : PreDragCondition { override fun shouldStartDrag(distanceDragged: Double): Boolean { return distanceDragged > startDragThreshold } override fun onPreDragStart(dragObject: DragObject) { if (!updateIconUi) { return } if (mIsAboveIcon) { // Hide only the icon, keep the text visible. originalIcon.setIconVisible(false) originalIcon.visibility = VISIBLE } else { // Hide both the icon and text. originalIcon.visibility = INVISIBLE } } override fun onPreDragEnd(dragObject: DragObject, dragStarted: Boolean) { if (!updateIconUi) { return } originalIcon.setIconVisible(true) if (dragStarted) { // Make sure we keep the original icon hidden while it is being dragged. originalIcon.visibility = INVISIBLE } else { // TODO: add WW logging if want to add logging for long press on popup // container. // mLauncher.getUserEventDispatcher().logDeepShortcutsOpen(mOriginalIcon); if (!mIsAboveIcon) { // Show the icon but keep the text hidden. originalIcon.visibility = VISIBLE originalIcon.setTextVisibility(false) } } } } } override fun onCreateCloseAnimation(anim: AnimatorSet) { // Animate original icon's text back in. anim.play(originalIcon.createTextAlphaAnimator(true /* fadeIn */)) originalIcon.forceHideDot = false } override fun closeComplete() { super.closeComplete() mActivityContext?.getDragController>()?.removeDragListener(this) val openPopup = getOpen(mActivityContext) if (openPopup == null || openPopup.originalView !== originalIcon) { originalIcon.setTextVisibility(originalIcon.shouldTextBeVisible()) originalIcon.forceHideDot = false } } companion object { private const val SHORTCUT_COLLAPSE_THRESHOLD = 6 /** Returns true if we can show the container. */ @Deprecated("Left here since some dependent projects are using this method") fun canShow(icon: View?, item: ItemInfo?): Boolean { return icon is BubbleTextView && ShortcutUtil.supportsShortcuts(item) } /** * Shows the popup specifically for the Private Space app. This is specifically special in * which no system shortcuts are shown for this icon. * * @param icon the app icon to show the popup for */ @JvmStatic fun showForPrivateSpaceApp(icon: BubbleTextView) { val activityContext: ActivityContext = ActivityContext.lookupContext(icon.context) if (getOpen(ActivityContext.lookupContext(icon.context)) != null) { // There is already an items container open, so don't open this one. icon.clearFocus() return } val item = icon.tag as ItemInfo val deepShortcutCount = activityContext.activityComponent.popupDataProvider.getShortcutCountForItem(item) val container = create(context = icon.context, originalView = icon, itemInfo = item) container.populateAndShowRows(deepShortcutCount, emptyList()) container.requestFocus() } /** * Finds the first instance of the Widgets Shortcut from the SystemShortcut List * * @param systemShortcuts List of SystemShortcut instances to search * @return Optional Widgets SystemShortcut */ private fun getWidgetShortcut( systemShortcuts: List> ): Optional> { return systemShortcuts .stream() .filter { shortcut: SystemShortcut<*>? -> shortcut is SystemShortcut.Widgets<*> } .map { it as SystemShortcut.Widgets } .findFirst() } /** * Returns list of [systemShortcuts] without the non-collapsible shortcuts. * * @param systemShortcuts list of SystemShortcuts to filter from. * @return systemShortcuts without the Widgets Shortcut. */ private fun getCollapsibleSystemShortcuts( systemShortcuts: List> ): List> { return systemShortcuts .stream() .filter { shortcut: SystemShortcut<*> -> shortcut.mIsCollapsible } .collect(Collectors.toList()) } /** * Returns list of [systemShortcuts] without the non-collapsible shortcuts. * * @param systemShortcuts list of SystemShortcuts to filter from. * @return systemShortcuts without the Widgets Shortcut. */ private fun getNonWidgetSystemShortcuts( systemShortcuts: List> ): List> { return systemShortcuts .stream() .filter { shortcut: SystemShortcut<*>? -> shortcut !is SystemShortcut.Widgets<*> } .collect(Collectors.toList()) } /** * Creates a new instance of [PopupContainerWithArrow]. * * @param context The context in which the popup will be created. * @param originalView The view that the popup is associated with. * @param updateIconUi Whether to update the icon UI during drag and drop. * @return A new instance of [PopupContainerWithArrow]. */ @JvmStatic fun create( context: Context, originalView: View, itemInfo: ItemInfo, updateIconUi: Boolean = true, ): PopupContainerWithArrow where T : Context, T : ActivityContext { val container = PopupContainerWithArrow(context, originalView, itemInfo, updateIconUi) container.id = R.id.popup_container container.clipChildren = false container.clipToPadding = false container.orientation = VERTICAL container.layoutParams = LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) return container } } }