[QS] Add header tooltip for long press

Added some hooks in animator to pass along expanded/animating/collapsed
state in a more clear manner. Added tooltip view with the teaser text to
animate in at the top of the view (and fade out either when we start
animating or automatically after 5 seconds).

Added prefs for tracking how often tooltip is shown/hiding it based on
that number.

Updated dimensions for correct spacing (based on redlines).

Test: Visually
Bug: 72528203

Change-Id: I70e4654ed95057fac6d8dbb890d575c2a5d9f215
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index adb4e33..8b57740 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -47,6 +47,7 @@
         Key.QS_INVERT_COLORS_ADDED,
         Key.QS_WORK_ADDED,
         Key.QS_NIGHTDISPLAY_ADDED,
+        Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
         Key.SEEN_MULTI_USER,
         Key.NUM_APPS_LAUNCHED,
         Key.HAS_SEEN_RECENTS_ONBOARDING,
@@ -76,6 +77,11 @@
         String QS_WORK_ADDED = "QsWorkAdded";
         @Deprecated
         String QS_NIGHTDISPLAY_ADDED = "QsNightDisplayAdded";
+        /**
+         * Used for tracking how many times the user has seen the long press tooltip in the Quick
+         * Settings panel.
+         */
+        String QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT = "QsLongPressTooltipShownCount";
         String SEEN_MULTI_USER = "HasSeenMultiUser";
         String NUM_APPS_LAUNCHED = "NumAppsLaunched";
         String HAS_SEEN_RECENTS_ONBOARDING = "HasSeenRecentsOnboarding";
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index c3f7eb1..175cddc 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -44,6 +44,7 @@
 
     public static final float EXPANDED_TILE_DELAY = .86f;
 
+
     private final ArrayList<View> mAllViews = new ArrayList<>();
     /**
      * List of {@link View}s representing Quick Settings that are being animated from the quick QS
@@ -65,6 +66,10 @@
     private TouchAnimator mNonfirstPageDelayedAnimator;
     private TouchAnimator mBrightnessAnimator;
 
+    /**
+     * Whether we're in the middle of animating between the collapsed and expanded states.
+     */
+    private boolean mIsAnimating;
     private boolean mOnKeyguard;
 
     private boolean mAllowFancy;
@@ -89,6 +94,9 @@
             Log.w(TAG, "QS Not using page layout");
         }
         panel.setPageListener(this);
+
+        // At time of creation, the QS panel is never animating.
+        mIsAnimating = false;
     }
 
     public void onRtlChanged() {
@@ -243,6 +251,11 @@
             } else {
                 mBrightnessAnimator = null;
             }
+            View headerView = mQsPanel.getHeaderView();
+            if (headerView!= null) {
+                firstPageBuilder.addFloat(headerView, "translationY", heightDiff, 0);
+                mAllViews.add(headerView);
+            }
             mFirstPageAnimator = firstPageBuilder
                     .setListener(this)
                     .build();
@@ -329,11 +342,21 @@
 
     @Override
     public void onAnimationAtStart() {
+        if (mIsAnimating) {
+            mQsPanel.onCollapse();
+        }
+        mIsAnimating = false;
+
         mQuickQsPanel.setVisibility(View.VISIBLE);
     }
 
     @Override
     public void onAnimationAtEnd() {
+        if (mIsAnimating) {
+            mQsPanel.onExpanded();
+        }
+        mIsAnimating = false;
+
         mQuickQsPanel.setVisibility(View.INVISIBLE);
         final int N = mQuickQsViews.size();
         for (int i = 0; i < N; i++) {
@@ -343,6 +366,11 @@
 
     @Override
     public void onAnimationStarted() {
+        if (!mIsAnimating) {
+            mQsPanel.onAnimating();
+        }
+        mIsAnimating = true;
+
         mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE);
         if (mOnFirstPage) {
             final int N = mQuickQsViews.size();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index cdcc5e6..476cb40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -18,6 +18,7 @@
 
 import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -57,6 +58,7 @@
 public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener {
 
     public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
+    public static final String QS_SHOW_LONG_PRESS_TOOLTIP = "qs_show_long_press";
 
     protected final Context mContext;
     protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
@@ -72,6 +74,7 @@
     private BrightnessController mBrightnessController;
     protected QSTileHost mHost;
 
+    protected QSTooltipView mTooltipView;
     protected QSSecurityFooter mFooter;
     private boolean mGridContentVisible = true;
 
@@ -93,6 +96,11 @@
 
         setOrientation(VERTICAL);
 
+        mTooltipView = (QSTooltipView) LayoutInflater.from(mContext)
+                .inflate(R.layout.quick_settings_header, this, false);
+
+        addView(mTooltipView);
+
         mBrightnessView = LayoutInflater.from(mContext).inflate(
             R.layout.quick_settings_brightness_dialog, this, false);
         addView(mBrightnessView);
@@ -142,7 +150,10 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        Dependency.get(TunerService.class).addTunable(this, QS_SHOW_BRIGHTNESS);
+        final TunerService tunerService = Dependency.get(TunerService.class);
+        tunerService.addTunable(this, QS_SHOW_BRIGHTNESS);
+        tunerService.addTunable(this, QS_SHOW_LONG_PRESS_TOOLTIP);
+
         if (mHost != null) {
             setTiles(mHost.getTiles());
         }
@@ -174,11 +185,16 @@
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (QS_SHOW_BRIGHTNESS.equals(key)) {
-            mBrightnessView.setVisibility(newValue == null || Integer.parseInt(newValue) != 0
-                    ? VISIBLE : GONE);
+            updateViewVisibilityForTuningValue(mBrightnessView, newValue);
+        } else if (QS_SHOW_LONG_PRESS_TOOLTIP.equals(key)) {
+            updateViewVisibilityForTuningValue(mTooltipView, newValue);
         }
     }
 
+    private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) {
+        view.setVisibility(newValue == null || Integer.parseInt(newValue) != 0 ? VISIBLE : GONE);
+    }
+
     public void openDetails(String subPanel) {
         QSTile tile = getTile(subPanel);
         showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
@@ -213,6 +229,10 @@
         return mBrightnessView;
     }
 
+    View getHeaderView() {
+        return mTooltipView;
+    }
+
     public void setCallback(QSDetail.Callback callback) {
         mCallback = callback;
     }
@@ -269,6 +289,21 @@
         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
             mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
         }
+
+        // Instantly hide the header here since we don't want it to still be animating.
+        mTooltipView.setVisibility(View.INVISIBLE);
+    }
+
+    /**
+     * Called when the panel is fully animated out/expanded. This is different from the state
+     * tracked by {@link #mExpanded}, which only checks if the panel is even partially pulled out.
+     */
+    public void onExpanded() {
+        mTooltipView.fadeIn();
+    }
+
+    public void onAnimating() {
+        mTooltipView.fadeOut();
     }
 
     public void setExpanded(boolean expanded) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTooltipView.java b/packages/SystemUI/src/com/android/systemui/qs/QSTooltipView.java
new file mode 100644
index 0000000..d1f9741
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTooltipView.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 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.systemui.qs;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.systemui.Prefs;
+
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Tooltip/header view for the Quick Settings panel.
+ */
+public class QSTooltipView extends LinearLayout {
+
+    private static final int FADE_ANIMATION_DURATION_MS = 300;
+    private static final long AUTO_FADE_OUT_DELAY_MS = TimeUnit.SECONDS.toMillis(6);
+    private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
+    public static final int MAX_TOOLTIP_SHOWN_COUNT = 3;
+
+    private final Handler mHandler = new Handler();
+    private final Runnable mAutoFadeOutRunnable = () -> fadeOut();
+
+    private int mShownCount;
+
+    public QSTooltipView(Context context) {
+        this(context, null);
+    }
+
+    public QSTooltipView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mShownCount = getStoredShownCount();
+    }
+
+    /** Returns the latest stored tooltip shown count from SharedPreferences. */
+    private int getStoredShownCount() {
+        return Prefs.getInt(
+                mContext,
+                Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
+                TOOLTIP_NOT_YET_SHOWN_COUNT);
+    }
+
+    /**
+     * Fades in the header view if we can show the tooltip - short circuits any running animation.
+     */
+    public void fadeIn() {
+        if (mShownCount < MAX_TOOLTIP_SHOWN_COUNT) {
+            animate().cancel();
+            setVisibility(View.VISIBLE);
+            animate()
+                    .alpha(1f)
+                    .setDuration(FADE_ANIMATION_DURATION_MS)
+                    .setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mHandler.postDelayed(mAutoFadeOutRunnable, AUTO_FADE_OUT_DELAY_MS);
+                        }
+                    })
+                    .start();
+
+            // Increment and drop the shown count in prefs for the next time we're deciding to
+            // fade in the tooltip. We first sanity check that the tooltip count hasn't changed yet
+            // in prefs (say, from a long press).
+            if (getStoredShownCount() <= mShownCount) {
+                Prefs.putInt(mContext, Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT, ++mShownCount);
+            }
+        }
+    }
+
+    /**
+     * Fades out the header view if it's partially visible - short circuits any running animation.
+     */
+    public void fadeOut() {
+        animate().cancel();
+        if (getVisibility() == View.VISIBLE && getAlpha() != 0f) {
+            mHandler.removeCallbacks(mAutoFadeOutRunnable);
+            animate()
+                    .alpha(0f)
+                    .setDuration(FADE_ANIMATION_DURATION_MS)
+                    .setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            perhapsMakeViewInvisible();
+                        }
+                    })
+                    .start();
+        } else {
+            perhapsMakeViewInvisible();
+        }
+    }
+
+    /**
+     * Only update visibility if the view is currently being shown. Otherwise, it's already been
+     * hidden by some other manner.
+     */
+    private void perhapsMakeViewInvisible() {
+        if (getVisibility() == View.VISIBLE) {
+            setVisibility(View.INVISIBLE);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index bdda3b9..f0684e1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -123,9 +123,8 @@
 
     @Override
     public void onTuningChanged(String key, String newValue) {
-        // No tunings for you.
-        if (key.equals(QS_SHOW_BRIGHTNESS)) {
-            // No Brightness for you.
+        if (QS_SHOW_BRIGHTNESS.equals(key) || QS_SHOW_LONG_PRESS_TOOLTIP.equals(key)) {
+            // No Brightness or Tooltip for you!
             super.onTuningChanged(key, "0");
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index 65135ab..23faa55 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -99,7 +99,11 @@
             record.tileView.measure(exactly(mCellWidth), exactly(mCellHeight));
             previousView = record.tileView.updateAccessibilityOrder(previousView);
         }
-        int height = (mCellHeight + mCellMargin) * rows + (mCellMarginTop - mCellMargin);
+
+        // Only include the top margin in our measurement if we have more than 1 row to show.
+        // Otherwise, don't add the extra margin buffer at top.
+        int height = (mCellHeight + mCellMargin) * rows +
+                (rows != 0 ? (mCellMarginTop - mCellMargin) : 0);
         if (height < 0) height = 0;
         setMeasuredDimension(width, height);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java
index 37f2528..6263efa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java
@@ -107,7 +107,7 @@
         void onAnimationAtStart();
 
         /**
-         * Called when the animator moves into a position of "0". Start and end delays are
+         * Called when the animator moves into a position of "1". Start and end delays are
          * taken into account, so this position may cover a range of fractional inputs.
          */
         void onAnimationAtEnd();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 72592829..016cbd6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -42,6 +42,7 @@
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.Utils;
 import com.android.systemui.Dependency;
+import com.android.systemui.Prefs;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSIconView;
@@ -49,6 +50,7 @@
 import com.android.systemui.plugins.qs.QSTile.State;
 import com.android.systemui.qs.PagedTileLayout.TilePage;
 import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.QSTooltipView;
 
 import java.util.ArrayList;
 
@@ -191,6 +193,11 @@
     public void longClick() {
         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)));
         mHandler.sendEmptyMessage(H.LONG_CLICK);
+
+        Prefs.putInt(
+                mContext,
+                Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
+                QSTooltipView.MAX_TOOLTIP_SHOWN_COUNT);
     }
 
     public LogMaker populate(LogMaker logMaker) {