diff --git a/packages/SystemUI/res/layout/quick_settings_header.xml b/packages/SystemUI/res/layout/quick_settings_header.xml new file mode 100644 index 0000000000000..43197c4001399 --- /dev/null +++ b/packages/SystemUI/res/layout/quick_settings_header.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index a5e37d529bee6..13ca11401c03e 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -40,7 +40,7 @@ 27dp 27dp - 16dp + 32dp 6dp 28dp diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d11ab4298b827..bc828ff8efb4e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -290,7 +290,7 @@ 106dp 19dp - 16dp + 32dp 48dp 12dp 16dp @@ -309,6 +309,7 @@ 16dp 4dp 0dp + 32dp 56dp 0dp 56dp @@ -333,6 +334,9 @@ 32dp 0dp 20dp + 16dp + 24dp + 32dp 16dp 24dp 16dp diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 8c59e75315a1f..86cab22a27565 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -666,6 +666,8 @@ Ethernet + + Press & hold on the icons for more options Do not disturb diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index adb4e33d1a191..8b577400357d0 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -47,6 +47,7 @@ public final class Prefs { 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 @@ public final class Prefs { 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 222c6e8274f53..fccd9ceb5c97e 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 class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha public static final float EXPANDED_TILE_DELAY = .86f; + private final ArrayList mAllViews = new ArrayList<>(); /** * List of {@link View}s representing Quick Settings that are being animated from the quick QS @@ -65,6 +66,11 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha private TouchAnimator mNonfirstPageDelayedAnimator; private TouchAnimator mBrightnessAnimator; + /** + * Whether the animation is stable and not in the middle of animating between the collapsed and + * expanded states. + */ + private boolean mIsInStableState; private boolean mOnKeyguard; private boolean mAllowFancy; @@ -89,6 +95,10 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha Log.w(TAG, "QS Not using page layout"); } panel.setPageListener(this); + + // At time of creation, the QS panel is always considered stable as it's not in the middle + // of collapse/expanded. + mIsInStableState = true; } public void onRtlChanged() { @@ -243,6 +253,11 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha } else { mBrightnessAnimator = null; } + View headerView = mQsPanel.getHeaderView(); + if (headerView!= null) { + firstPageBuilder.addFloat(headerView, "translationY", heightDiff, 0); + mAllViews.add(headerView); + } mFirstPageAnimator = firstPageBuilder .setListener(this) .build(); @@ -326,11 +341,21 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha @Override public void onAnimationAtStart() { + if (!mIsInStableState) { + mQsPanel.onCollapse(); + } + mIsInStableState = true; + mQuickQsPanel.setVisibility(View.VISIBLE); } @Override public void onAnimationAtEnd() { + if (!mIsInStableState) { + mQsPanel.onExpanded(); + } + mIsInStableState = true; + mQuickQsPanel.setVisibility(View.INVISIBLE); final int N = mQuickQsViews.size(); for (int i = 0; i < N; i++) { @@ -340,6 +365,11 @@ public class QSAnimator implements Callback, PageListener, Listener, OnLayoutCha @Override public void onAnimationStarted() { + if (mIsInStableState) { + mQsPanel.onAnimating(); + } + mIsInStableState = false; + 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 f7c388db0840b..5640be55d2d24 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 @@ package com.android.systemui.qs; 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; @@ -58,6 +59,7 @@ import java.util.Collection; 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 mRecords = new ArrayList(); @@ -72,6 +74,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private BrightnessController mBrightnessController; protected QSTileHost mHost; + protected QSTooltipView mTooltipView; protected QSSecurityFooter mFooter; private boolean mGridContentVisible = true; @@ -94,6 +97,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne setOrientation(VERTICAL); + mTooltipView = (QSTooltipView) LayoutInflater.from(mContext) + .inflate(R.layout.quick_settings_header, this, false); + mBrightnessView = LayoutInflater.from(mContext).inflate( R.layout.quick_settings_brightness_dialog, this, false); mTileLayout = new TileLayout(mContext); @@ -101,7 +107,12 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne Space space = new Space(mContext); space.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, mContext.getResources().getDimensionPixelSize(R.dimen.qs_footer_height))); - mScrollLayout = new QSScrollLayout(mContext, mBrightnessView, (View) mTileLayout, space); + mScrollLayout = new QSScrollLayout( + mContext, + mTooltipView, + mBrightnessView, + (View) mTileLayout, + space); addView(mScrollLayout); addDivider(); @@ -134,7 +145,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne @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()); } @@ -166,11 +180,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne @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}); @@ -205,6 +224,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return mBrightnessView; } + View getHeaderView() { + return mTooltipView; + } + public void setCallback(QSDetail.Callback callback) { mCallback = callback; } @@ -266,11 +289,27 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne 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) { if (mExpanded == expanded) return; mExpanded = expanded; + if (!mExpanded) { if (mTileLayout instanceof PagedTileLayout) { ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); 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 0000000000000..d1f9741ba6c2d --- /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 83148558ea1d1..1b4b7dfb310f8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -124,9 +124,8 @@ public class QuickQSPanel extends QSPanel { @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 65135ab142d71..9fa7bebf6ee0f 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 @@ public class TileLayout extends ViewGroup implements QSTileLayout { 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 37f2528205f70..6263efa2c7114 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 @@ public class TouchAnimator { 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 7259282935a04..016cbd6f6675b 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.internal.logging.MetricsLogger; 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; 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 abstract class QSTileImpl implements QSTile { 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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index 1c9c7949a971f..676463407f3ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -37,6 +37,7 @@ import static java.lang.Thread.sleep; import android.content.Intent; import android.metrics.LogMaker; import android.support.test.filters.SmallTest; +import android.support.test.InstrumentationRegistry; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -73,6 +74,7 @@ public class QSTileImplTest extends SysuiTestCase { mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class); mHost = mock(QSTileHost.class); when(mHost.indexOf(spec)).thenReturn(POSITION); + when(mHost.getContext()).thenReturn(mContext.getBaseContext()); mTile = spy(new TileImpl(mHost)); mTile.mHandler = mTile.new H(mTestableLooper.getLooper());