Introduce feature education for AllApps Search

Preview: https://drive.google.com/file/d/1eXf3K6kFh0bHcYlpwW_voSRjY9RQalJN/view?usp=sharing&resourcekey=0-IABjrtXM5JhHvSf-7yc4tg

Edu can be dismissed permanently by pressing "Got it" button or typing. Swiping down defers edu until next visit to all apps

- Move fallback search to quickstep
Bug: 178100472
Test: Manual

Change-Id: I920aab366330758e81f8b9fa62736abf82fe5cac
This commit is contained in:
Samuel Fufa 2021-01-19 13:12:10 -06:00
parent 867b62a2d0
commit 062a8fd979
10 changed files with 520 additions and 9 deletions

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 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.
-->
<com.android.launcher3.search.FallbackSearchInputView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginLeft="48dp"
android:layout_marginRight="48dp"
android:background="@android:color/transparent"
android:focusableInTouchMode="true"
android:gravity="start|center_vertical"
android:inputType="textNoSuggestions"
android:imeOptions="actionSearch|flagNoExtractUi"
android:maxLines="1"
android:privateImeOptions="bc_search"
android:scrollHorizontally="true"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textColorHint="?android:attr/textColorTertiary"
android:textSize="16sp" />

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 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.
-->
<com.android.launcher3.search.DeviceSearchEdu
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:layout_height="wrap_content"
android:id="@+id/search_box_wrapper"
android:layout_width="match_parent">
<include
layout="@layout/fallback_search_input"
android:id="@+id/mock_search_box" />
</FrameLayout>
<LinearLayout
android:layout_height="wrap_content"
android:id="@+id/edu_wrapper"
android:padding="24dp"
android:layout_marginTop="40dp"
android:orientation="vertical"
android:layout_width="match_parent">
<TextView
style="@style/TextHeadline"
android:layout_width="match_parent"
android:gravity="center"
android:textSize="24sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_height="wrap_content"
android:text="@string/search_edu_primary" />
<TextView
style="@style/TextHeadline"
android:layout_width="match_parent"
android:gravity="center"
android:textSize="18sp"
android:layout_marginTop="30dp"
android:textColor="?android:attr/textColorPrimary"
android:layout_height="wrap_content"
android:text="@string/search_edu_secondary" />
<Button
android:id="@+id/dismiss_edu"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/dynamic_grid_edge_margin"
android:background="?android:attr/selectableItemBackground"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_gravity="center"
android:text="@string/search_edu_dismiss" />
</LinearLayout>
</com.android.launcher3.search.DeviceSearchEdu>

View File

@ -93,6 +93,15 @@
<!-- content description for hotseat items -->
<string name="hotseat_prediction_content_description">Predicted app: <xliff:g id="title" example="Chrome">%1$s</xliff:g></string>
<!-- primary educational text shown for first time search users -->
<string name="search_edu_primary">Search your phone for apps, people, settings and more!</string>
<!-- secondary educational text shown for first time search users -->
<string name="search_edu_secondary">Tap keyboard search button to launch the first search
result.</string>
<!-- Dismiss button string for search education view -->
<string name="search_edu_dismiss">Got it.</string>
<!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
<string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
<!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->

View File

@ -0,0 +1,221 @@
/*
* Copyright (C) 2021 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.search;
import static com.android.launcher3.util.OnboardingPrefs.SEARCH_EDU_SEEN;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.AbstractSlideInView;
/**
* Feature education for on-device Search. Shown the first time user opens AllApps Search
*/
public class DeviceSearchEdu extends AbstractSlideInView implements
StateManager.StateListener<LauncherState>, TextWatcher, Insettable,
TextView.OnEditorActionListener {
private static final long ANIMATION_DURATION = 350;
private static final int ANIMATION_CONTENT_TRANSLATION = 200;
private EditText mEduInput;
private View mInputWrapper;
private EditText mSearchInput;
private boolean mSwitchFocusOnDismiss;
public DeviceSearchEdu(Context context) {
this(context, null, 0);
}
public DeviceSearchEdu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DeviceSearchEdu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void close(boolean animate, boolean markAsSeen) {
handleClose(animate);
if (markAsSeen) {
mLauncher.getOnboardingPrefs().markChecked(SEARCH_EDU_SEEN);
}
}
@Override
protected void handleClose(boolean animate) {
handleClose(animate, ANIMATION_DURATION);
mLauncher.getStateManager().removeStateListener(this);
}
@Override
protected boolean isOfType(int type) {
return false;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSearchInput = mLauncher.getAppsView().getSearchUiManager().getEditText();
mInputWrapper = findViewById(R.id.search_box_wrapper);
mContent = findViewById(R.id.edu_wrapper);
mEduInput = findViewById(R.id.mock_search_box);
mEduInput.setHint(R.string.all_apps_on_device_search_bar_hint);
mEduInput.addTextChangedListener(this);
if (mSearchInput != null) {
mEduInput.getLayoutParams().height = mSearchInput.getHeight();
mEduInput.setOnEditorActionListener(this);
} else {
mEduInput.setVisibility(INVISIBLE);
}
findViewById(R.id.dismiss_edu).setOnClickListener((view) -> {
mSwitchFocusOnDismiss = true;
close(true, true);
});
}
private void showInternal() {
mLauncher.getStateManager().addStateListener(this);
AbstractFloatingView.closeAllOpenViews(mLauncher);
attachToContainer();
if (mSearchInput != null) {
Rect r = mLauncher.getViewBounds(mSearchInput);
mEduInput.requestFocus();
InputMethodManager imm = mLauncher.getSystemService(InputMethodManager.class);
imm.showSoftInput(mEduInput, InputMethodManager.SHOW_IMPLICIT);
((LayoutParams) mInputWrapper.getLayoutParams()).setMargins(0, r.top, 0, 0);
}
animateOpen();
}
@Override
protected int getScrimColor(Context context) {
return ColorUtils.setAlphaComponent(Themes.getAttrColor(context, R.attr.allAppsScrimColor),
230);
}
protected void setTranslationShift(float translationShift) {
mTranslationShift = translationShift;
mContent.setAlpha(getBoxedProgress(1 - mTranslationShift, .25f, 1));
mContent.setTranslationY(ANIMATION_CONTENT_TRANSLATION * translationShift);
if (mColorScrim != null) {
mColorScrim.setAlpha(getBoxedProgress(1 - mTranslationShift, 0, .75f));
}
}
/**
* Given input [0-1], returns progress within bounds [min,max] allowing for staged animations
*/
private float getBoxedProgress(float input, float min, float max) {
if (input < min) return 0;
if (input > max) return 1;
return (input - min) / (max - min);
}
private void animateOpen() {
if (mIsOpen || mOpenCloseAnimator.isRunning()) {
return;
}
mIsOpen = true;
mOpenCloseAnimator.setValues(
PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mOpenCloseAnimator.setDuration(ANIMATION_DURATION);
mOpenCloseAnimator.start();
}
/**
* Show On-device search education view.
*/
public static void show(Launcher launcher) {
LayoutInflater layoutInflater = LayoutInflater.from(launcher);
((DeviceSearchEdu) layoutInflater.inflate(
R.layout.search_edu_view, launcher.getDragLayer(),
false)).showInternal();
}
@Override
public void onStateTransitionStart(LauncherState toState) {
close(true, false);
}
@Override
protected void onCloseComplete() {
super.onCloseComplete();
if (mSearchInput != null && mSwitchFocusOnDismiss) {
mSearchInput.requestFocus();
mSearchInput.setSelection(mSearchInput.getText().length());
}
}
@Override
public void afterTextChanged(Editable editable) {
//Does nothing
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
//Does nothing
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if (mSearchInput != null) {
mSearchInput.setText(charSequence.toString());
mSwitchFocusOnDismiss = true;
close(true, true);
}
}
@Override
public void setInsets(Rect insets) {
}
@Override
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
mSearchInput.onEditorAction(i);
close(true, true);
return true;
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 2021 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.search;
import static com.android.launcher3.LauncherState.ALL_APPS;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.Launcher;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AlphabeticalAppsList;
import com.android.launcher3.allapps.FloatingHeaderView;
import com.android.launcher3.allapps.search.AllAppsSearchBarController;
import com.android.launcher3.allapps.search.SearchAlgorithm;
import java.util.ArrayList;
/**
* A search view shown in all apps for on device search
*/
public class FallbackSearchInputView extends ExtendedEditText
implements AllAppsSearchBarController.Callbacks, AllAppsStore.OnUpdateListener {
private final AllAppsSearchBarController mSearchBarController;
private AlphabeticalAppsList mApps;
private Runnable mOnResultsChanged;
private AllAppsContainerView mAppsView;
public FallbackSearchInputView(Context context) {
this(context, null);
}
public FallbackSearchInputView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FallbackSearchInputView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSearchBarController = new AllAppsSearchBarController();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Launcher.getLauncher(getContext()).getAppsView().getAppsStore().addUpdateListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Launcher.getLauncher(getContext()).getAppsView().getAppsStore().removeUpdateListener(this);
}
/**
* Initializes SearchInput
*/
public void initialize(AllAppsContainerView appsView, SearchAlgorithm algo, Runnable changed) {
mOnResultsChanged = changed;
mApps = appsView.getApps();
mAppsView = appsView;
mSearchBarController.initialize(algo, this, Launcher.getLauncher(getContext()), this);
}
@Override
public void onSearchResult(String query, ArrayList<AdapterItem> items) {
if (mApps != null && getParent() != null) {
mApps.setSearchResults(items);
notifyResultChanged();
collapseAppsViewHeader(true);
mAppsView.setLastSearchQuery(query);
}
}
@Override
public void onAppendSearchResult(String query, ArrayList<AdapterItem> items) {
if (mApps != null && getParent() != null) {
mApps.appendSearchResults(items);
notifyResultChanged();
}
}
@Override
public void clearSearchResult() {
if (getParent() != null && mApps != null) {
mApps.setSearchResults(null);
notifyResultChanged();
collapseAppsViewHeader(false);
mAppsView.onClearSearchResult();
}
}
@Override
public void onAppsUpdated() {
mSearchBarController.refreshSearchResult();
}
private void collapseAppsViewHeader(boolean collapse) {
FloatingHeaderView header = mAppsView.getFloatingHeaderView();
if (header != null) {
header.setCollapsed(collapse);
}
}
private void notifyResultChanged() {
if (mOnResultsChanged != null) {
mOnResultsChanged.run();
}
mAppsView.onSearchResultsChanged();
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// TODO: Consider animating the state transition here
if (focused) {
// Getting focus will open the keyboard. Go to the all-apps state, so that the input
// box is at the top and there is enough space below to show search results.
Launcher.getLauncher(getContext()).getStateManager().goToState(ALL_APPS, false);
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
}

View File

@ -29,6 +29,7 @@ import com.android.launcher3.LauncherState;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.search.DeviceSearchEdu;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@ -41,6 +42,7 @@ import com.android.quickstep.views.AllAppsEduView;
*/
public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher> {
public QuickstepOnboardingPrefs(QuickstepLauncher launcher, SharedPreferences sharedPrefs) {
super(launcher, sharedPrefs);
@ -131,5 +133,18 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher>
}
});
}
if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !getBoolean(SEARCH_EDU_SEEN)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
@Override
public void onStateTransitionStart(LauncherState toState) {
if (toState == ALL_APPS) {
mLauncher.getAllAppsController().getInsetController().setSearchEduRunnable(
() -> DeviceSearchEdu.show(launcher));
stateManager.removeStateListener(this);
}
}
});
}
}
}

View File

@ -44,6 +44,8 @@ public class AllAppsInsetTransitionController {
private WindowInsetsAnimationController mAnimationController;
private WindowInsetsAnimationControlListener mCurrentRequest;
private Runnable mSearchEduRunnable;
private float mAllAppsHeight;
private int mDownInsetBottom;
@ -55,12 +57,28 @@ public class AllAppsInsetTransitionController {
private float mDown, mCurrent;
private View mApps;
/**
*
*/
public boolean showSearchEduIfNecessary() {
if (mSearchEduRunnable == null) {
return false;
}
mSearchEduRunnable.run();
return true;
}
public void setSearchEduRunnable(Runnable eduRunnable) {
mSearchEduRunnable = eduRunnable;
}
// Only purpose of these states is to keep track of fast fling transition
enum State {
RESET, DRAG_START_BOTTOM, DRAG_START_BOTTOM_IME_CANCELLED,
FLING_END_TOP, FLING_END_TOP_IME_CANCELLED,
DRAG_START_TOP, FLING_END_BOTTOM
}
private State mState;
public AllAppsInsetTransitionController(float allAppsHeight, View appsView) {
@ -77,7 +95,7 @@ public class AllAppsInsetTransitionController {
boolean imeVisible = insets.isVisible(WindowInsets.Type.ime());
if (DEBUG) {
Log.d(TAG, "\nhide imeVisible=" + imeVisible);
Log.d(TAG, "\nhide imeVisible=" + imeVisible);
}
if (insets.isVisible(WindowInsets.Type.ime())) {
mApps.getWindowInsetsController().hide(WindowInsets.Type.ime());
@ -107,7 +125,7 @@ public class AllAppsInsetTransitionController {
// mShownAtDown = mApps.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
if (DEBUG) {
Log.d(TAG, "\nonDragStart progress=" + progress
Log.d(TAG, "\nonDragStart progress=" + progress
+ " mDownInsets=" + mDownInsetBottom
+ " mShownAtDown=" + mShownAtDown);
}
@ -123,7 +141,7 @@ public class AllAppsInsetTransitionController {
}
if (controller != null) {
if (mCurrentRequest == this && !handleFinishOnFling(controller)) {
mAnimationController = controller;
mAnimationController = controller;
} else {
controller.finish(false /* just don't show */);
}

View File

@ -270,11 +270,10 @@ public class AllAppsTransitionController implements StateHandler<LauncherState>,
if (Float.compare(mProgress, 0f) == 0) {
mLauncher.getLiveSearchManager().start();
EditText editText = mAppsView.getSearchUiManager().getEditText();
if (editText != null) {
if (editText != null && !mInsetController.showSearchEduIfNecessary()) {
editText.requestFocus();
}
}
else {
} else {
mLauncher.getLiveSearchManager().stop();
}
// TODO: should make the controller hide synchronously

View File

@ -36,13 +36,15 @@ public class OnboardingPrefs<T extends Launcher> {
public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
public static final String SEARCH_EDU_SEEN = "launcher.search_edu";
/**
* Events that either have happened or have not (booleans).
*/
@StringDef(value = {
HOME_BOUNCE_SEEN,
HOTSEAT_LONGPRESS_TIP_SEEN
HOTSEAT_LONGPRESS_TIP_SEEN,
SEARCH_EDU_SEEN
})
@Retention(RetentionPolicy.SOURCE)
public @interface EventBoolKey {}

View File

@ -64,7 +64,7 @@ public abstract class AbstractSlideInView extends AbstractFloatingView
protected final ObjectAnimator mOpenCloseAnimator;
protected View mContent;
private final View mColorScrim;
protected final View mColorScrim;
protected Interpolator mScrollInterpolator;
// range [0, 1], 0=> completely open, 1=> completely closed
@ -216,7 +216,6 @@ public abstract class AbstractSlideInView extends AbstractFloatingView
return mLauncher.getDragLayer();
}
protected static View createColorScrim(Context context, int bgColor) {
View view = new View(context);
view.forceHasOverlappingRendering(false);