Snap for 7450397 from d88f572864 to sc-release

Change-Id: I6bbfaab8acf2435504c6fb8aee99984ced70d5e9
This commit is contained in:
android-build-team Robot
2021-06-12 03:07:51 +00:00
25 changed files with 775 additions and 174 deletions
@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="@dimen/work_fab_radius" />
<solid android:color="?android:attr/colorControlHighlight" />
<padding android:left="@dimen/work_fab_radius" android:right="@dimen/work_fab_radius" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/work_fab_radius" />
<solid android:color="?android:attr/colorAccent" />
<padding android:left="@dimen/work_fab_radius" android:right="@dimen/work_fab_radius" />
</shape>
</item>
</selector>
+1 -1
View File
@@ -15,7 +15,7 @@
-->
<com.android.launcher3.views.OptionsPopupView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/deep_shortcuts_container"
android:id="@+id/popup_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
+10 -1
View File
@@ -16,12 +16,21 @@
<com.android.launcher3.popup.PopupContainerWithArrow
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/deep_shortcuts_container"
android:id="@+id/popup_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:orientation="vertical">
<LinearLayout
android:id="@+id/deep_shortcuts_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tag="@string/popup_container_iterate_children"
android:elevation="@dimen/deep_shortcuts_elevation"
android:orientation="vertical"/>
<LinearLayout
android:id="@+id/notification_container"
android:layout_width="match_parent"
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.allapps.WorkModeSwitch
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/work_mode_toggle"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_height="@dimen/work_fab_height"
android:layout_width="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:drawableTint="@android:color/white"
android:textColor="@android:color/white"
android:background="@drawable/work_apps_toggle_background"
android:drawablePadding="16dp"
android:drawableStart="@drawable/ic_corp_off"
android:elevation="10dp"
android:layout_marginBottom="@dimen/work_fab_margin"
android:layout_marginEnd="@dimen/work_fab_margin"
android:text="@string/work_apps_pause_btn_text" />
+6 -3
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2017 The Android Open Source Project
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
@@ -12,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.launcher3.allapps.WorkModeSwitch xmlns:android="http://schemas.android.com/apk/res/android"
<com.android.launcher3.allapps.WorkModeSwitch
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/PrimaryHeadline"
@@ -33,4 +35,5 @@
android:paddingBottom="@dimen/work_profile_footer_padding"
android:paddingLeft="@dimen/work_profile_footer_padding"
android:paddingRight="@dimen/work_profile_footer_padding"
android:paddingTop="@dimen/work_profile_footer_padding" />
android:paddingTop="@dimen/work_profile_footer_padding"
/>
+61
View File
@@ -0,0 +1,61 @@
<?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.views.WorkEduView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="@drawable/bottom_sheet_top_border"
android:backgroundTint="?attr/eduHalfSheetBGColor" />
<LinearLayout
android:id="@+id/view_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/eduHalfSheetBGColor"
android:orientation="vertical"
android:paddingLeft="@dimen/bottom_sheet_edu_padding"
android:paddingRight="@dimen/bottom_sheet_edu_padding">
<TextView
android:id="@+id/content_text"
style="@style/TextHeadline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="48dp"
android:gravity="center"
android:text="@string/work_profile_edu_personal_apps"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
<Button
android:id="@+id/proceed"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="end"
android:background="?android:attr/selectableItemBackground"
android:gravity="center"
android:text="@string/work_profile_edu_next"
android:textAlignment="center"
android:textColor="@android:color/white" />
</LinearLayout>
</com.android.launcher3.views.WorkEduView>
+2 -2
View File
@@ -17,11 +17,11 @@
*/
-->
<resources>
<color name="popup_color_primary_light">@android:color/system_neutral1_0</color>
<color name="popup_color_primary_light">@android:color/system_accent2_50</color>
<color name="popup_color_secondary_light">@android:color/system_neutral2_100</color>
<color name="popup_color_tertiary_light">@android:color/system_neutral2_300</color>
<color name="popup_color_neutral_dark">@android:color/system_neutral1_1000</color>
<color name="popup_color_primary_dark">@android:color/system_neutral1_800</color>
<color name="popup_color_primary_dark">@android:color/system_neutral2_800</color>
<color name="popup_color_secondary_dark">@android:color/system_neutral1_900</color>
<color name="popup_color_tertiary_dark">@android:color/system_neutral2_700</color>
+3
View File
@@ -31,6 +31,9 @@
<!-- View tag key used to store SpringAnimation data. -->
<item type="id" name="spring_animation_tag" />
<!-- View tag key used to determine if we should fade in the child views.. -->
<string name="popup_container_iterate_children" translatable="false">popup_container_iterate_children</string>
<!-- Workspace -->
<!-- The duration (in ms) of the fade animation on the object outlines, used when
we are dragging objects around on the home screen. -->
-4
View File
@@ -116,10 +116,6 @@
<dimen name="all_apps_divider_margin_vertical">8dp</dimen>
<!-- Floating action button inside work tab to toggle work profile -->
<dimen name="work_fab_height">48dp</dimen>
<dimen name="work_fab_radius">24dp</dimen>
<dimen name="work_fab_margin">18dp</dimen>
<dimen name="work_profile_footer_padding">20dp</dimen>
<dimen name="work_profile_footer_text_size">16sp</dimen>
+8 -5
View File
@@ -398,24 +398,27 @@
<string name="work_profile_edu_accept">Got it</string>
<!--- heading shown when user opens work apps tab while work apps are paused -->
<string name="work_apps_paused_title">Work apps are off</string>
<string name="work_apps_paused_title">Work profile is paused</string>
<!--- body shown when user opens work apps tab while work apps are paused -->
<string name="work_apps_paused_body">Your work apps cant send you notifications, use your battery, or access your location</string>
<string name="work_apps_paused_body">Work apps cant send you notifications, use your battery, or access your location</string>
<!-- content description for paused work apps list -->
<string name="work_apps_paused_content_description">Work apps are off. Your work apps cant send you notifications, use your battery, or access your location</string>
<string name="work_apps_paused_content_description">Work profile is paused. Work apps cant send you notifications, use your battery, or access your location</string>
<!-- string shown in educational banner about work profile -->
<string name="work_apps_paused_edu_banner">Work apps are badged and visible to your IT admin</string>
<!-- button string shown to dismiss work tab education -->
<string name="work_apps_paused_edu_accept">Got it</string>
<!-- button string shown pause work profile -->
<string name="work_apps_pause_btn_text">Turn off work apps</string>
<string name="work_apps_pause_btn_text">Pause work apps</string>
<!-- button string shown enable work profile -->
<string name="work_apps_enable_btn_text">Turn on work apps</string>
<string name="work_apps_enable_btn_text">Turn on</string>
<!-- A hint shown in launcher settings develop options filter box -->
<string name="developer_options_filter_hint">Filter</string>
<!-- A tip shown pointing at work toggle -->
<string name="work_switch_tip">Pause work apps and notifications</string>
<!-- Failed action error message: e.g. Failed: Pause -->
<string name="remote_action_failed">Failed: <xliff:g id="what" example="Pause">%1$s</xliff:g></string>
</resources>
@@ -228,7 +228,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
}
private void resetWorkProfile() {
mWorkModeSwitch.updateCurrentState(!mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED));
mWorkModeSwitch.update(!mAllAppsStore.hasModelFlag(FLAG_QUIET_MODE_ENABLED));
mAH[AdapterHolder.WORK].setupOverlay();
mAH[AdapterHolder.WORK].applyPadding();
}
@@ -482,7 +482,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
private void setupWorkToggle() {
if (Utilities.ATLEAST_P) {
mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
R.layout.work_mode_fab, this, false);
R.layout.work_mode_switch, this, false);
this.addView(mWorkModeSwitch);
mWorkModeSwitch.setInsets(mInsets);
mWorkModeSwitch.post(this::resetWorkProfile);
@@ -190,8 +190,6 @@ public class AllAppsRecyclerView extends BaseRecyclerView {
case SCROLL_STATE_DRAGGING:
mgr.logger().sendToInteractionJankMonitor(
LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
getApplicationWindowToken());
break;
case SCROLL_STATE_IDLE:
mgr.logger().sendToInteractionJankMonitor(
@@ -207,6 +205,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView {
&& mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
mEmptySearchBackground.setHotspot(e.getX(), e.getY());
}
hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
getApplicationWindowToken());
return result;
}
@@ -25,6 +25,7 @@ import android.view.MotionEvent;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.views.WorkEduView;
/**
* AllAppsContainerView with launcher specific callbacks
@@ -87,6 +88,13 @@ public class LauncherAllAppsContainerView extends AllAppsContainerView {
@Override
public void onActivePageChanged(int currentActivePage) {
super.onActivePageChanged(currentActivePage);
if (mUsingTabs) {
if (currentActivePage == AdapterHolder.WORK) {
WorkEduView.showWorkEduIfNeeded(mLauncher);
} else {
mWorkTabListener = WorkEduView.showEduFlowIfNeeded(mLauncher, mWorkTabListener);
}
}
}
@Override
@@ -15,57 +15,108 @@
*/
package com.android.launcher3.allapps;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.Switch;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.views.ArrowTipView;
import java.lang.ref.WeakReference;
/**
* Work profile toggle switch shown at the bottom of AllApps work tab
*/
public class WorkModeSwitch extends Button implements Insettable, View.OnClickListener {
public class WorkModeSwitch extends Switch implements Insettable {
private static final int WORK_TIP_THRESHOLD = 2;
public static final String KEY_WORK_TIP_COUNTER = "worked_tip_counter";
private Rect mInsets = new Rect();
private boolean mWorkEnabled;
private final float[] mTouch = new float[2];
private int mTouchSlop;
public WorkModeSwitch(Context context) {
this(context, null, 0);
super(context);
init();
}
public WorkModeSwitch(Context context, AttributeSet attrs) {
this(context, attrs, 0);
super(context, attrs);
init();
}
public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
mTouchSlop = viewConfiguration.getScaledTouchSlop();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setOnClickListener(this);
public void setChecked(boolean checked) { }
@Override
public void toggle() {
// don't show tip if user uses toggle
Utilities.getPrefs(getContext()).edit().putInt(KEY_WORK_TIP_COUNTER, -1).apply();
trySetQuietModeEnabledToAllProfilesAsync(isChecked());
}
/**
* Sets the enabled or disabled state of the button
* @param isChecked
*/
public void update(boolean isChecked) {
super.setChecked(isChecked);
setCompoundDrawablesRelativeWithIntrinsicBounds(
isChecked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
setEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mTouch[0] = ev.getX();
mTouch[1] = ev.getY();
} else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
if (Math.abs(mTouch[0] - ev.getX()) > mTouchSlop
|| Math.abs(mTouch[1] - ev.getY()) > mTouchSlop) {
int action = ev.getAction();
ev.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(ev);
ev.setAction(action);
return false;
}
}
return super.onTouchEvent(ev);
}
private void trySetQuietModeEnabledToAllProfilesAsync(boolean enabled) {
new SetQuietModeEnabledAsyncTask(enabled, new WeakReference<>(this)).execute();
}
@Override
public void setInsets(Rect insets) {
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
ViewGroup.MarginLayoutParams marginLayoutParams =
(ViewGroup.MarginLayoutParams) getLayoutParams();
if (marginLayoutParams != null) {
marginLayoutParams.bottomMargin = bottomInset + marginLayoutParams.bottomMargin;
}
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
getPaddingBottom() + bottomInset);
}
/**
@@ -74,41 +125,78 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi
public void setWorkTabVisible(boolean workTabVisible) {
clearAnimation();
if (workTabVisible) {
setEnabled(true);
setVisibility(VISIBLE);
setAlpha(0);
animate().alpha(1).start();
showTipIfNeeded();
} else {
animate().alpha(0).withEndAction(() -> this.setVisibility(GONE)).start();
}
}
@Override
public void onClick(View view) {
setEnabled(false);
UI_HELPER_EXECUTOR.post(() -> setToState(!mWorkEnabled));
private static final class SetQuietModeEnabledAsyncTask
extends AsyncTask<Void, Void, Boolean> {
private final boolean enabled;
private final WeakReference<WorkModeSwitch> switchWeakReference;
SetQuietModeEnabledAsyncTask(boolean enabled,
WeakReference<WorkModeSwitch> switchWeakReference) {
this.enabled = enabled;
this.switchWeakReference = switchWeakReference;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
WorkModeSwitch workModeSwitch = switchWeakReference.get();
if (workModeSwitch != null) {
workModeSwitch.setEnabled(false);
}
}
@Override
protected Boolean doInBackground(Void... voids) {
WorkModeSwitch workModeSwitch = switchWeakReference.get();
if (workModeSwitch == null || !Utilities.ATLEAST_P) {
return false;
}
Context context = workModeSwitch.getContext();
UserManager userManager = context.getSystemService(UserManager.class);
boolean showConfirm = false;
for (UserHandle userProfile : UserCache.INSTANCE.get(context).getUserProfiles()) {
if (Process.myUserHandle().equals(userProfile)) {
continue;
}
showConfirm |= !userManager.requestQuietModeEnabled(enabled, userProfile);
}
return showConfirm;
}
@Override
protected void onPostExecute(Boolean showConfirm) {
if (showConfirm) {
WorkModeSwitch workModeSwitch = switchWeakReference.get();
if (workModeSwitch != null) {
workModeSwitch.setEnabled(true);
}
}
}
}
/**
* Sets the enabled or disabled state of the button
* Shows a work tip on the Nth work tab open
*/
public void updateCurrentState(boolean active) {
mWorkEnabled = active;
setEnabled(true);
setCompoundDrawablesRelativeWithIntrinsicBounds(
active ? R.drawable.ic_corp_off : R.drawable.ic_corp, 0, 0, 0);
setText(active ? R.string.work_apps_pause_btn_text : R.string.work_apps_enable_btn_text);
}
protected Boolean setToState(boolean toState) {
UserManager userManager = getContext().getSystemService(UserManager.class);
boolean showConfirm = false;
for (UserHandle userProfile : UserCache.INSTANCE.get(getContext()).getUserProfiles()) {
if (Process.myUserHandle().equals(userProfile)) {
continue;
}
showConfirm |= !userManager.requestQuietModeEnabled(!toState, userProfile);
public void showTipIfNeeded() {
Context context = getContext();
SharedPreferences prefs = Utilities.getPrefs(context);
int tipCounter = prefs.getInt(KEY_WORK_TIP_COUNTER, WORK_TIP_THRESHOLD);
if (tipCounter < 0) return;
if (tipCounter == 0) {
new ArrowTipView(context)
.show(context.getString(R.string.work_switch_tip), getTop());
}
return showConfirm;
prefs.edit().putInt(KEY_WORK_TIP_COUNTER, tipCounter - 1).apply();
}
}
@@ -18,6 +18,7 @@ package com.android.launcher3.notification;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
import android.animation.AnimatorSet;
import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
@@ -92,6 +93,15 @@ public class NotificationItemView {
});
}
/**
* Animates the background color to a new color.
* @param color The color to change to.
* @param animatorSetOut The AnimatorSet where we add the color animator to.
*/
public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
mMainView.updateBackgroundColor(color, animatorSetOut);
}
public void addGutter() {
if (mGutter == null) {
mGutter = mPopupContainer.inflateAndAdd(R.layout.notification_gutter, mRootView);
@@ -20,10 +20,13 @@ import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelo
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DISMISSED;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
@@ -78,6 +81,9 @@ public class NotificationMainView extends FrameLayout implements SingleAxisSwipe
private SingleAxisSwipeDetector mSwipeDetector;
private final ColorDrawable mColorDrawable;
private final RippleDrawable mRippleDrawable;
public NotificationMainView(Context context) {
this(context, null, 0);
}
@@ -90,6 +96,10 @@ public class NotificationMainView extends FrameLayout implements SingleAxisSwipe
super(context, attrs, defStyle);
mContentTranslateAnimator = ObjectAnimator.ofFloat(this, CONTENT_TRANSLATION, 0);
mColorDrawable = new ColorDrawable(Color.TRANSPARENT);
mRippleDrawable = new RippleDrawable(ColorStateList.valueOf(
Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
mColorDrawable, null);
}
@Override
@@ -105,18 +115,31 @@ public class NotificationMainView extends FrameLayout implements SingleAxisSwipe
updateBackgroundColor(colorBackground.getColor());
}
public void updateBackgroundColor(int color) {
private void updateBackgroundColor(int color) {
mBackgroundColor = color;
RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf(
Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
new ColorDrawable(color), null);
mTextAndBackground.setBackground(rippleBackground);
mColorDrawable.setColor(color);
mTextAndBackground.setBackground(mRippleDrawable);
if (mNotificationInfo != null) {
mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
mBackgroundColor));
}
}
/**
* Animates the background color to a new color.
* @param color The color to change to.
* @param animatorSetOut The AnimatorSet where we add the color animator to.
*/
public void updateBackgroundColor(int color, AnimatorSet animatorSetOut) {
int oldColor = mBackgroundColor;
ValueAnimator colors = ValueAnimator.ofArgb(oldColor, color);
colors.addUpdateListener(valueAnimator -> {
int newColor = (int) valueAnimator.getAnimatedValue();
updateBackgroundColor(newColor);
});
animatorSetOut.play(colors);
}
public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
mSwipeDetector = swipeDetector;
}
+194 -22
View File
@@ -25,35 +25,48 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.LocalColorExtractor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* A container for shortcuts to deep links and notifications associated with an app.
@@ -76,6 +89,10 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
private static final int CLOSE_CHILD_FADE_START_DELAY = 0;
private static final int CLOSE_CHILD_FADE_DURATION = 140;
// Index used to get background color when using local wallpaper color extraction,
private static final int DARK_COLOR_EXTRACTION_INDEX = android.R.color.system_neutral2_800;
private static final int LIGHT_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_50;
private final Rect mTempRect = new Rect();
protected final LayoutInflater mInflater;
@@ -104,9 +121,18 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
private Runnable mOnCloseCallback = () -> { };
// The rect string of the view that the arrow is attached to, in screen reference frame.
protected String mArrowColorRectString;
private int mArrowColor;
protected final HashMap<String, View> mViewForRect = new HashMap<>();
@Nullable protected LocalColorExtractor mColorExtractor;
private final float mElevation;
private final int mBackgroundColor;
private final String mIterateChildrenTag;
public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mInflater = LayoutInflater.from(context);
@@ -115,6 +141,7 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
mIsRtl = Utilities.isRtl(getResources());
mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
mArrowColor = mBackgroundColor;
mElevation = getResources().getDimension(R.dimen.deep_shortcuts_elevation);
// Initialize arrow view
@@ -139,6 +166,14 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
mRoundedBottom.setColor(mBackgroundColor);
mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius,
smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children);
boolean isAboveAnotherSurface = getTopOpenViewWithType(mLauncher, TYPE_FOLDER) != null
|| mLauncher.getStateManager().getState() == LauncherState.ALL_APPS;
if (!isAboveAnotherSurface && Utilities.ATLEAST_S) {
setupColorExtraction();
}
}
public ArrowPopup(Context context, AttributeSet attrs) {
@@ -184,11 +219,11 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
/**
* Set the margins and radius of backgrounds after views are properly ordered.
*/
protected void assignMarginsAndBackgrounds() {
int count = getChildCount();
public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
int count = viewGroup.getChildCount();
int totalVisibleShortcuts = 0;
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
View view = viewGroup.getChildAt(i);
if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
totalVisibleShortcuts++;
}
@@ -197,8 +232,7 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
int numVisibleShortcut = 0;
View lastView = null;
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
boolean isShortcut = view instanceof DeepShortcutView;
View view = viewGroup.getChildAt(i);
if (view.getVisibility() == VISIBLE) {
if (lastView != null) {
MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
@@ -208,7 +242,12 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
mlp.bottomMargin = 0;
if (isShortcut) {
if (view instanceof ViewGroup && mIterateChildrenTag.equals(view.getTag())) {
assignMarginsAndBackgrounds((ViewGroup) view);
continue;
}
if (view instanceof DeepShortcutView) {
if (totalVisibleShortcuts == 1) {
view.setBackgroundResource(R.drawable.single_item_primary);
} else if (totalVisibleShortcuts > 1) {
@@ -227,6 +266,118 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
}
@TargetApi(Build.VERSION_CODES.S)
private int getExtractedColor(SparseIntArray colors) {
int index = Utilities.isDarkTheme(getContext())
? DARK_COLOR_EXTRACTION_INDEX
: LIGHT_COLOR_EXTRACTION_INDEX;
return colors.get(index, mBackgroundColor);
}
@TargetApi(Build.VERSION_CODES.S)
private void setupColorExtraction() {
Workspace workspace = mLauncher.findViewById(R.id.workspace);
if (workspace == null) {
return;
}
mColorExtractor = LocalColorExtractor.newInstance(mLauncher);
mColorExtractor.setListener((rect, extractedColors) -> {
String rectString = rect.toShortString();
View v = mViewForRect.get(rectString);
AnimatorSet colors = new AnimatorSet();
if (v != null) {
int newColor = getExtractedColor(extractedColors);
setChildColor(v, newColor, colors);
int numChildren = v instanceof ViewGroup ? ((ViewGroup) v).getChildCount() : 0;
for (int i = 0; i < numChildren; ++i) {
View childView = ((ViewGroup) v).getChildAt(i);
setChildColor(childView, newColor, colors);
}
if (rectString.equals(mArrowColorRectString)) {
mArrowColor = newColor;
updateArrowColor();
}
}
colors.setDuration(150);
v.post(colors::start);
});
}
protected void addPreDrawForColorExtraction(Launcher launcher) {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
initColorExtractionLocations(launcher);
return true;
}
});
}
/**
* Returns list of child views that will receive local color extraction treatment.
* Note: Order should match the view hierarchy.
*/
protected List<View> getChildrenForColorExtraction() {
return Collections.emptyList();
}
private void initColorExtractionLocations(Launcher launcher) {
if (mColorExtractor == null) {
return;
}
ArrayList<RectF> locations = new ArrayList<>();
boolean firstVisibleChild = true;
// Order matters here, since we need the arrow to match the color of its adjacent view.
for (View view : getChildrenForColorExtraction()) {
if (view != null && view.getVisibility() == VISIBLE) {
RectF rf = new RectF();
mColorExtractor.getExtractedRectForView(launcher,
launcher.getWorkspace().getCurrentPage(), view, rf);
if (!rf.isEmpty()) {
locations.add(rf);
String rectString = rf.toShortString();
mViewForRect.put(rectString, view);
if (mIsAboveIcon) {
mArrowColorRectString = rectString;
} else {
if (firstVisibleChild) {
mArrowColorRectString = rectString;
}
}
if (firstVisibleChild) {
firstVisibleChild = false;
}
}
}
}
if (!locations.isEmpty()) {
mColorExtractor.addLocation(locations);
}
}
/**
* Sets the background color of the child.
*/
protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
Drawable bg = view.getBackground();
if (bg instanceof GradientDrawable) {
GradientDrawable gd = (GradientDrawable) bg.mutate();
int oldColor = ((GradientDrawable) bg).getColor().getDefaultColor();
animatorSetOut.play(ObjectAnimator.ofArgb(gd, "color", oldColor, color));
} else if (bg instanceof ColorDrawable) {
ColorDrawable cd = (ColorDrawable) bg.mutate();
int oldColor = ((ColorDrawable) bg).getColor();
animatorSetOut.play(ObjectAnimator.ofArgb(cd, "color", oldColor, color));
}
}
/**
* Shows the popup at the desired location, optionally reversing the children.
* @param viewsToFlip number of views from the top to to flip in case of reverse order
@@ -238,7 +389,7 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
reverseOrder(viewsToFlip);
}
onInflationComplete(reverseOrder);
assignMarginsAndBackgrounds();
assignMarginsAndBackgrounds(this);
if (shouldAddArrow()) {
addArrow();
}
@@ -251,7 +402,7 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
protected void show() {
setupForDisplay();
onInflationComplete(false);
assignMarginsAndBackgrounds();
assignMarginsAndBackgrounds(this);
if (shouldAddArrow()) {
addArrow();
}
@@ -297,18 +448,24 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
// so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
mArrow.setVisibility(INVISIBLE);
} else {
updateArrowColor();
}
mArrow.setPivotX(mArrowWidth / 2.0f);
mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
}
private void updateArrowColor() {
if (!Gravity.isVertical(mGravity)) {
mArrow.setBackground(new RoundedArrowDrawable(
mArrowWidth, mArrowHeight, mArrowPointRadius,
mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
mArrowOffsetHorizontal, -mArrowOffsetVertical,
!mIsAboveIcon, mIsLeftAligned,
mBackgroundColor));
mArrowColor));
// TODO: Remove elevation when arrow is above as it casts a shadow on the container
mArrow.setElevation(mIsAboveIcon ? mElevation : 0);
}
mArrow.setPivotX(mArrowWidth / 2.0f);
mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
}
/**
@@ -506,7 +663,7 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
private AnimatorSet getOpenCloseAnimator(boolean isOpening, int totalDuration,
int fadeStartDelay, int fadeDuration, int childFadeStartDelay,
int childFadeDuration, Interpolator interpolator) {
final AnimatorSet openAnim = new AnimatorSet();
final AnimatorSet animatorSet = new AnimatorSet();
float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0};
float[] scaleValues = isOpening ? new float[] {0.5f, 1} : new float[] {1, 0.5f};
@@ -519,32 +676,41 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
mArrow.setAlpha(alpha);
setAlpha(alpha);
});
openAnim.play(fade);
animatorSet.play(fade);
setPivotX(mIsLeftAligned ? 0 : getMeasuredWidth());
setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0);
Animator scale = ObjectAnimator.ofFloat(this, View.SCALE_Y, scaleValues);
scale.setDuration(totalDuration);
scale.setInterpolator(interpolator);
openAnim.play(scale);
animatorSet.play(scale);
for (int i = getChildCount() - 1; i >= 0; --i) {
View view = getChildAt(i);
fadeInChildViews(this, alphaValues, childFadeStartDelay, childFadeDuration, animatorSet);
return animatorSet;
}
private void fadeInChildViews(ViewGroup group, float[] alphaValues, long startDelay,
long duration, AnimatorSet out) {
for (int i = group.getChildCount() - 1; i >= 0; --i) {
View view = group.getChildAt(i);
if (view.getVisibility() == VISIBLE && view instanceof ViewGroup) {
if (mIterateChildrenTag.equals(view.getTag())) {
fadeInChildViews((ViewGroup) view, alphaValues, startDelay, duration, out);
continue;
}
for (int j = ((ViewGroup) view).getChildCount() - 1; j >= 0; --j) {
View childView = ((ViewGroup) view).getChildAt(j);
childView.setAlpha(alphaValues[0]);
ValueAnimator childFade = ObjectAnimator.ofFloat(childView, ALPHA, alphaValues);
childFade.setStartDelay(childFadeStartDelay);
childFade.setDuration(childFadeDuration);
childFade.setStartDelay(startDelay);
childFade.setDuration(duration);
childFade.setInterpolator(LINEAR);
openAnim.play(childFade);
out.play(childFade);
}
}
}
return openAnim;
}
@@ -593,6 +759,12 @@ public abstract class ArrowPopup<T extends StatefulActivity<LauncherState>>
getPopupContainer().removeView(this);
getPopupContainer().removeView(mArrow);
mOnCloseCallback.run();
mArrowColorRectString = null;
mViewForRect.clear();
if (mColorExtractor != null) {
mColorExtractor.removeLocations();
mColorExtractor.setListener(null);
}
}
/**
@@ -71,6 +71,7 @@ import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.views.BaseDragLayer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -95,6 +96,8 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
private int mNumNotifications;
private ViewGroup mNotificationContainer;
private ViewGroup mDeepShortcutContainer;
private ViewGroup mSystemShortcutContainer;
protected PopupItemDragHandler mPopupItemDragHandler;
@@ -172,6 +175,14 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
return false;
}
@Override
protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
super.setChildColor(view, color, animatorSetOut);
if (view.getId() == R.id.notification_container && mNotificationItemView != null) {
mNotificationItemView.updateBackgroundColor(color, animatorSetOut);
}
}
/**
* Returns true if we can show the container.
*/
@@ -218,6 +229,13 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
launcher.getDragController().addDragListener(this);
addPreDrawForColorExtraction(launcher);
}
@Override
protected List<View> getChildrenForColorExtraction() {
return Arrays.asList(mSystemShortcutContainer, mDeepShortcutContainer,
mNotificationContainer);
}
@Override
@@ -262,13 +280,18 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
}
int viewsToFlip = getChildCount();
mSystemShortcutContainer = this;
if (mDeepShortcutContainer == null) {
mDeepShortcutContainer = findViewById(R.id.deep_shortcuts_container);
}
if (hasDeepShortcuts) {
mDeepShortcutContainer.setVisibility(View.VISIBLE);
if (mNotificationItemView != null) {
mNotificationItemView.addGutter();
}
for (int i = shortcutCount; i > 0; i--) {
DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, this);
DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, mDeepShortcutContainer);
v.getLayoutParams().width = containerWidth;
mShortcuts.add(v);
}
@@ -281,13 +304,16 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut);
}
}
} else if (!systemShortcuts.isEmpty()) {
if (mNotificationItemView != null) {
mNotificationItemView.addGutter();
}
} else {
mDeepShortcutContainer.setVisibility(View.GONE);
if (!systemShortcuts.isEmpty()) {
if (mNotificationItemView != null) {
mNotificationItemView.addGutter();
}
for (SystemShortcut shortcut : systemShortcuts) {
initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
for (SystemShortcut shortcut : systemShortcuts) {
initializeSystemShortcut(R.layout.system_shortcut, this, shortcut);
}
}
}
@@ -563,7 +589,7 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
mNotificationItemView = null;
mNotificationContainer.setVisibility(GONE);
updateHiddenShortcuts();
assignMarginsAndBackgrounds();
assignMarginsAndBackgrounds(PopupContainerWithArrow.this);
} else {
mNotificationItemView.trimNotifications(
NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
@@ -146,13 +146,25 @@ public class OptionsPopupView extends ArrowPopup
view.setOnLongClickListener(popup);
popup.mItemMap.put(view, item);
}
popup.addPreDrawForColorExtraction(launcher);
popup.show();
return popup;
}
@Override
protected List<View> getChildrenForColorExtraction() {
int childCount = getChildCount();
ArrayList<View> children = new ArrayList<>(childCount);
for (int i = 0; i < childCount; ++i) {
children.add(getChildAt(i));
}
return children;
}
@VisibleForTesting
public static ArrowPopup getOptionsPopup(Launcher launcher) {
return launcher.findViewById(R.id.deep_shortcuts_container);
return launcher.findViewById(R.id.popup_container);
}
public static void showDefaultOptions(Launcher launcher, float x, float y) {
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
@@ -32,6 +33,7 @@ import android.util.Property;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowInsets;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -51,6 +53,7 @@ import java.util.List;
*/
public class RecyclerViewFastScroller extends View {
private static final int FASTSCROLL_THRESHOLD_MILLIS = 200;
private static final int SCROLL_DELTA_THRESHOLD_DP = 4;
private static final Rect sTempRect = new Rect();
@@ -101,6 +104,7 @@ public class RecyclerViewFastScroller extends View {
private final boolean mCanThumbDetach;
private boolean mIgnoreDragGesture;
private boolean mIsRecyclerViewFirstChildInParent = true;
private long mDownTimeStampMillis;
// This is the offset from the top of the scrollbar when the user first starts touching. To
// prevent jumping, this offset is applied as the user scrolls.
@@ -112,6 +116,7 @@ public class RecyclerViewFastScroller extends View {
private TextView mPopupView;
private boolean mPopupVisible;
private String mPopupSectionName;
private Insets mSystemGestureInsets;
protected BaseRecyclerView mRv;
private RecyclerView.OnScrollListener mOnScrollListener;
@@ -237,6 +242,7 @@ public class RecyclerViewFastScroller extends View {
// Keep track of the down positions
mDownX = x;
mDownY = mLastY = y;
mDownTimeStampMillis = ev.getDownTime();
if ((Math.abs(mDy) < mDeltaThreshold &&
mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) {
@@ -246,22 +252,27 @@ public class RecyclerViewFastScroller extends View {
}
if (isNearThumb(x, y)) {
mTouchOffsetY = mDownY - mThumbOffsetY;
} else if (mRv.supportsFastScrolling()
&& isNearScrollBar(mDownX)) {
calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
updateFastScrollSectionNameAndThumbOffset(y);
}
break;
case MotionEvent.ACTION_MOVE:
mLastY = y;
int absDeltaY = Math.abs(y - mDownY);
int absDeltaX = Math.abs(x - mDownX);
// Check if we should start scrolling, but ignore this fastscroll gesture if we have
// exceeded some fixed movement
mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop();
if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
isNearThumb(mDownX, mLastY) &&
Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) {
calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
mIgnoreDragGesture |= absDeltaY > mConfig.getScaledPagingTouchSlop();
if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling()) {
// condition #1: triggering thumb is distance, angle based
if ((isNearThumb(mDownX, mLastY)
&& absDeltaY > mConfig.getScaledPagingTouchSlop()
&& absDeltaY > absDeltaX)
// condition#2: Fastscroll function is now time based
|| (isNearScrollBar(mDownX) && ev.getEventTime() - mDownTimeStampMillis
> FASTSCROLL_THRESHOLD_MILLIS)) {
calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY);
}
}
if (mIsDragging) {
updateFastScrollSectionNameAndThumbOffset(y);
@@ -328,12 +339,22 @@ public class RecyclerViewFastScroller extends View {
canvas.drawRoundRect(mThumbBounds, r, r, mThumbPaint);
if (Utilities.ATLEAST_Q) {
mThumbBounds.roundOut(SYSTEM_GESTURE_EXCLUSION_RECT.get(0));
// swiping very close to the thumb area (not just within it's bound)
// will also prevent back gesture
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).offset(mThumbDrawOffset.x, mThumbDrawOffset.y);
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).left = SYSTEM_GESTURE_EXCLUSION_RECT.get(0).right
- mSystemGestureInsets.right;
setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
}
canvas.restoreToCount(saveCount);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mSystemGestureInsets = insets.getSystemGestureInsets();
return super.onApplyWindowInsets(insets);
}
private float getScrollThumbRadius() {
return mWidth + mThumbPadding + mThumbPadding;
}
@@ -0,0 +1,230 @@
/*
* 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.
*/
package com.android.launcher3.views;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsPagedView;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.statemanager.StateManager.StateListener;
/**
* On boarding flow for users right after setting up work profile
*/
public class WorkEduView extends AbstractSlideInView<Launcher>
implements Insettable, StateListener<LauncherState> {
private static final int DEFAULT_CLOSE_DURATION = 200;
public static final String KEY_WORK_EDU_STEP = "showed_work_profile_edu";
public static final String KEY_LEGACY_WORK_EDU_SEEN = "showed_bottom_user_education";
private static final int WORK_EDU_NOT_STARTED = 0;
private static final int WORK_EDU_PERSONAL_APPS = 1;
private static final int WORK_EDU_WORK_APPS = 2;
protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
private Rect mInsets = new Rect();
private View mViewWrapper;
private Button mProceedButton;
private TextView mContentText;
private int mNextWorkEduStep = WORK_EDU_PERSONAL_APPS;
public WorkEduView(Context context, AttributeSet attr) {
this(context, attr, 0);
}
public WorkEduView(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContent = this;
}
@Override
protected void handleClose(boolean animate) {
mActivityContext.getSharedPrefs().edit()
.putInt(KEY_WORK_EDU_STEP, mNextWorkEduStep).apply();
handleClose(true, DEFAULT_CLOSE_DURATION);
}
@Override
protected void onCloseComplete() {
super.onCloseComplete();
mActivityContext.getStateManager().removeStateListener(this);
}
@Override
protected boolean isOfType(int type) {
return (type & TYPE_ON_BOARD_POPUP) != 0;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mViewWrapper = findViewById(R.id.view_wrapper);
mProceedButton = findViewById(R.id.proceed);
mContentText = findViewById(R.id.content_text);
// make sure layout does not shrink when we change the text
mContentText.post(() -> mContentText.setMinLines(mContentText.getLineCount()));
mProceedButton.setOnClickListener(view -> {
if (getAllAppsPagedView() != null) {
getAllAppsPagedView().snapToPage(AllAppsContainerView.AdapterHolder.WORK);
}
goToWorkTab(true);
});
}
private void goToWorkTab(boolean animate) {
mProceedButton.setText(R.string.work_profile_edu_accept);
if (animate) {
ObjectAnimator animator = ObjectAnimator.ofFloat(mContentText, ALPHA, 0);
animator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationSuccess(Animator animator) {
mContentText.setText(
mActivityContext.getString(R.string.work_profile_edu_work_apps));
ObjectAnimator.ofFloat(mContentText, ALPHA, 1).start();
}
});
animator.start();
} else {
mContentText.setText(mActivityContext.getString(R.string.work_profile_edu_work_apps));
}
mNextWorkEduStep = WORK_EDU_WORK_APPS;
mProceedButton.setOnClickListener(v -> handleClose(true));
}
@Override
public void setInsets(Rect insets) {
int leftInset = insets.left - mInsets.left;
int rightInset = insets.right - mInsets.right;
int bottomInset = insets.bottom - mInsets.bottom;
mInsets.set(insets);
setPadding(leftInset, getPaddingTop(), rightInset, 0);
mViewWrapper.setPaddingRelative(mViewWrapper.getPaddingStart(),
mViewWrapper.getPaddingTop(), mViewWrapper.getPaddingEnd(), bottomInset);
}
private void show() {
attachToContainer();
animateOpen();
mActivityContext.getStateManager().addStateListener(this);
}
@Override
protected int getScrimColor(Context context) {
return FINAL_SCRIM_BG_COLOR;
}
private void goToFirstPage() {
if (getAllAppsPagedView() != null) {
getAllAppsPagedView().snapToPageImmediately(AllAppsContainerView.AdapterHolder.MAIN);
}
}
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.start();
}
private AllAppsPagedView getAllAppsPagedView() {
View v = mActivityContext.getAppsView().getContentView();
return (v instanceof AllAppsPagedView) ? (AllAppsPagedView) v : null;
}
/**
* Checks if user has not seen onboarding UI yet and shows it when user navigates to all apps
*/
public static StateListener<LauncherState> showEduFlowIfNeeded(Launcher launcher,
@Nullable StateListener<LauncherState> oldListener) {
if (oldListener != null) {
launcher.getStateManager().removeStateListener(oldListener);
}
if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
WORK_EDU_NOT_STARTED) != WORK_EDU_NOT_STARTED) {
return null;
}
StateListener<LauncherState> listener = new StateListener<LauncherState>() {
@Override
public void onStateTransitionComplete(LauncherState finalState) {
if (finalState != LauncherState.ALL_APPS) return;
LayoutInflater layoutInflater = LayoutInflater.from(launcher);
WorkEduView v = (WorkEduView) layoutInflater.inflate(
R.layout.work_profile_edu, launcher.getDragLayer(),
false);
v.show();
v.goToFirstPage();
launcher.getStateManager().removeStateListener(this);
}
};
launcher.getStateManager().addStateListener(listener);
return listener;
}
/**
* Shows work apps edu if user had dismissed full edu flow
*/
public static void showWorkEduIfNeeded(Launcher launcher) {
if (hasSeenLegacyEdu(launcher) || launcher.getSharedPrefs().getInt(KEY_WORK_EDU_STEP,
WORK_EDU_NOT_STARTED) != WORK_EDU_PERSONAL_APPS) {
return;
}
LayoutInflater layoutInflater = LayoutInflater.from(launcher);
WorkEduView v = (WorkEduView) layoutInflater.inflate(
R.layout.work_profile_edu, launcher.getDragLayer(), false);
v.show();
v.goToWorkTab(false);
}
private static boolean hasSeenLegacyEdu(Launcher launcher) {
return launcher.getSharedPrefs().getBoolean(KEY_LEGACY_WORK_EDU_SEEN, false);
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
close(false);
}
}
@@ -47,7 +47,7 @@ public final class AppIcon extends Launchable {
public AppIconMenu openMenu() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
mObject, "deep_shortcuts_container", LONG_CLICK_EVENT));
mObject, "popup_container", LONG_CLICK_EVENT));
}
}
@@ -58,7 +58,7 @@ public final class AppIcon extends Launchable {
@Override
protected String getLongPressIndicator() {
return "deep_shortcuts_container";
return "popup_container";
}
@Override
@@ -155,7 +155,7 @@ public final class LauncherInstrumentation {
private static final String APPS_RES_ID = "apps_view";
private static final String OVERVIEW_RES_ID = "overview_panel";
private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
private static final String CONTEXT_MENU_RES_ID = "popup_container";
public static final int WAIT_TIME_MS = 60000;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
private static final String ANDROID_PACKAGE = "android";
@@ -26,7 +26,7 @@ public class OptionsPopupMenu {
OptionsPopupMenu(LauncherInstrumentation launcher) {
mLauncher = launcher;
mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
mDeepShortcutsContainer = launcher.waitForLauncherObject("popup_container");
}
/**
@@ -179,7 +179,7 @@ public final class Workspace extends Home {
getHotseatAppIcon("Chrome"),
new Point(mLauncher.getDevice().getDisplayWidth(),
mLauncher.getVisibleBounds(workspace).centerY()),
"deep_shortcuts_container",
"popup_container",
false,
false,
() -> mLauncher.expectEvent(