Add education tip to widget picker.

A tip is shown on the first widget/shortcut in the recommended table.
If there are no recommended widgets, a tip is shown on first widget
in an expanded header.

There is a delay of few milliseconds, to let the WidgetCells be
completely rendered on screen before getting their location.

Test: Manually tested
Bug: 184920163
Change-Id: I2637e84e7fc467b27888023434e3578a4b8ed4d6
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 51dddab..638eec7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -140,6 +140,9 @@
     <dimen name="widget_row_padding">8dp</dimen>
     <dimen name="widget_row_divider">2dp</dimen>
 
+    <dimen name="widget_picker_education_tip_width">120dp</dimen>
+    <dimen name="widget_picker_education_tip_min_margin">4dp</dimen>
+
     <!-- Padding applied to shortcut previews -->
     <dimen name="shortcut_preview_padding_left">0dp</dimen>
     <dimen name="shortcut_preview_padding_right">0dp</dimen>
diff --git a/src/com/android/launcher3/views/ArrowTipView.java b/src/com/android/launcher3/views/ArrowTipView.java
index 1f12a2f..89ff821 100644
--- a/src/com/android/launcher3/views/ArrowTipView.java
+++ b/src/com/android/launcher3/views/ArrowTipView.java
@@ -21,6 +21,7 @@
 import android.graphics.Paint;
 import android.graphics.drawable.ShapeDrawable;
 import android.os.Handler;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.MotionEvent;
@@ -29,6 +30,8 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
 import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -43,6 +46,7 @@
  */
 public class ArrowTipView extends AbstractFloatingView {
 
+    private static final String TAG = ArrowTipView.class.getSimpleName();
     private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
     private static final long SHOW_DELAY_MS = 200;
     private static final long SHOW_DURATION_MS = 300;
@@ -105,7 +109,8 @@
                 arrowLp.width, arrowLp.height, false));
         Paint arrowPaint = arrowDrawable.getPaint();
         TypedValue typedValue = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        context.getTheme()
+                .resolveAttribute(android.R.attr.colorAccent, typedValue, true);
         arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
         // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
         arrowPaint.setPathEffect(new CornerPathEffect(
@@ -165,6 +170,60 @@
     }
 
     /**
+     * Show the ArrowTipView (tooltip) custom aligned.
+     *
+     * @param text The text to be shown in the tooltip.
+     * @param arrowXCoord The X coordinate for the arrow on the tip. The arrow is usually in the
+     *                    center of ArrowTipView unless the ArrowTipView goes beyond screen margin.
+     * @param yCoord The Y coordinate of the bottom of the tooltip.
+     * @return The tool tip view.
+     */
+    @Nullable public ArrowTipView showAtLocation(String text, int arrowXCoord, int yCoord) {
+        ViewGroup parent = mActivity.getDragLayer();
+        @Px int parentViewWidth = parent.getWidth();
+        @Px int textViewWidth = getContext().getResources()
+                .getDimensionPixelSize(R.dimen.widget_picker_education_tip_width);
+        @Px int minViewMargin = getContext().getResources()
+                .getDimensionPixelSize(R.dimen.widget_picker_education_tip_min_margin);
+        if (parentViewWidth < textViewWidth + 2 * minViewMargin) {
+            Log.w(TAG, "Cannot display tip on a small screen of size: " + parentViewWidth);
+            return null;
+        }
+
+        TextView textView = findViewById(R.id.text);
+        textView.setText(text);
+        textView.setWidth(textViewWidth);
+        parent.addView(this);
+        requestLayout();
+
+        post(() -> setY(yCoord - getHeight()));
+        post(() -> {
+            float halfWidth = getWidth() / 2f;
+            float xCoord;
+            if (arrowXCoord - halfWidth < minViewMargin) {
+                xCoord = minViewMargin;
+            } else if (arrowXCoord + halfWidth > parentViewWidth - minViewMargin) {
+                xCoord = parentViewWidth - minViewMargin - getWidth();
+            } else {
+                xCoord = arrowXCoord - halfWidth;
+            }
+            setX(xCoord);
+            findViewById(R.id.arrow).setX(arrowXCoord - xCoord);
+            requestLayout();
+        });
+
+        setAlpha(0);
+        animate()
+                .alpha(1f)
+                .withLayer()
+                .setStartDelay(SHOW_DELAY_MS)
+                .setDuration(SHOW_DURATION_MS)
+                .setInterpolator(Interpolators.DEACCEL)
+                .start();
+        return this;
+    }
+
+    /**
      * Register a callback fired when toast is hidden
      */
     public ArrowTipView setOnClosedCallback(Runnable runnable) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index a4257a2..5d9a2e2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -41,6 +41,7 @@
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.core.view.ViewCompat;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.DeviceProfile;
@@ -51,6 +52,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 import com.android.launcher3.widget.BaseWidgetSheet;
@@ -66,6 +68,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Predicate;
+import java.util.stream.IntStream;
 
 /**
  * Popup for showing the full list of available widgets
@@ -78,11 +81,13 @@
 
     private static final long DEFAULT_OPEN_DURATION = 267;
     private static final long FADE_IN_DURATION = 150;
+    private static final long EDUCATION_TIP_DELAY_MS = 200;
     private static final float VERTICAL_START_POSITION = 0.3f;
     // The widget recommendation table can easily take over the entire screen on devices with small
     // resolution or landscape on phone. This ratio defines the max percentage of content area that
     // the table can display.
     private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
+    private static final String WIDGETS_EDUCATION_TIP_SEEN = "launcher.widgets_education_tip_seen";
 
     private final Rect mInsets = new Rect();
     private final boolean mHasWorkProfile;
@@ -92,6 +97,35 @@
             mCurrentUser.equals(entry.mPkgItem.user);
     private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
             mPrimaryWidgetsFilter.negate();
+    private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
+            new OnLayoutChangeListener() {
+                @Override
+                public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                    if (hasSeenEducationTip()) {
+                        removeOnLayoutChangeListener(this);
+                        return;
+                    }
+
+                    // Widgets are loaded asynchronously, We are adding a delay because we only want
+                    // to show the tip when the widget preview has finished loading and rendering in
+                    // this view.
+                    removeCallbacks(mShowEducationTipTask);
+                    postDelayed(mShowEducationTipTask, EDUCATION_TIP_DELAY_MS);
+                }
+            };
+
+    private final Runnable mShowEducationTipTask = () -> {
+        if (hasSeenEducationTip()) {
+            removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+            return;
+        }
+        View viewForTip = getViewToShowEducationTip();
+        if (viewForTip != null && ViewCompat.isLaidOut(viewForTip)) {
+            removeOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+            showEducationTipOnView(viewForTip);
+        }
+    };
     private final int mTabsHeight;
     private final int mWidgetCellHorizontalPadding;
 
@@ -170,6 +204,10 @@
 
         mSearchAndRecommendationViewHolder.mSearchBar.initialize(
                 mLauncher.getPopupDataProvider(), /* searchModeListener= */ this);
+
+        if (!hasSeenEducationTip()) {
+            addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
+        }
     }
 
     @Override
@@ -563,6 +601,49 @@
         mSearchAndRecommendationViewHolder.mSearchBar.clearSearchBarFocus();
     }
 
+    private void showEducationTipOnView(View view) {
+        mLauncher.getSharedPrefs().edit().putBoolean(WIDGETS_EDUCATION_TIP_SEEN, true).apply();
+        int[] coords = new int[2];
+        view.getLocationOnScreen(coords);
+        ArrowTipView arrowTipView = new ArrowTipView(mLauncher);
+        arrowTipView.showAtLocation(
+                getContext().getString(R.string.long_press_widget_to_add),
+                /* arrowXCoord= */coords[0] + view.getWidth() / 2,
+                /* yCoord= */coords[1]);
+    }
+
+    @Nullable private View getViewToShowEducationTip() {
+        if (mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getVisibility() == VISIBLE
+                && mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable.getChildCount() > 0
+        ) {
+            return ((ViewGroup) mSearchAndRecommendationViewHolder.mRecommendedWidgetsTable
+                    .getChildAt(0)).getChildAt(0);
+        }
+
+        AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
+                ? AdapterHolder.SEARCH
+                : mViewPager == null
+                        ? AdapterHolder.PRIMARY
+                        : mViewPager.getCurrentPage());
+        WidgetsRowViewHolder viewHolderForTip =
+                (WidgetsRowViewHolder) IntStream.range(
+                                0, adapterHolder.mWidgetsListAdapter.getItemCount())
+                        .mapToObj(adapterHolder.mWidgetsRecyclerView::
+                                findViewHolderForAdapterPosition)
+                        .filter(viewHolder -> viewHolder instanceof WidgetsRowViewHolder)
+                        .findFirst()
+                        .orElse(null);
+        if (viewHolderForTip != null) {
+            return ((ViewGroup) viewHolderForTip.mTableContainer.getChildAt(0)).getChildAt(0);
+        }
+
+        return null;
+    }
+
+    private boolean hasSeenEducationTip() {
+        return mLauncher.getSharedPrefs().getBoolean(WIDGETS_EDUCATION_TIP_SEEN, false);
+    }
+
     /** A holder class for holding adapters & their corresponding recycler view. */
     private final class AdapterHolder {
         static final int PRIMARY = 0;