Snap for 7464903 from d2ad97989c to sc-release
Change-Id: Idb65589542d289db602b4194e5a8c607cb1e9c29
This commit is contained in:
@@ -422,8 +422,8 @@ public abstract class BaseQuickstepLauncher extends Launcher
|
||||
|
||||
@Override
|
||||
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
|
||||
return Stream.concat(super.getSupportedShortcuts(),
|
||||
Stream.of(WellbeingModel.SHORTCUT_FACTORY));
|
||||
return Stream.concat(Stream.of(WellbeingModel.SHORTCUT_FACTORY),
|
||||
super.getSupportedShortcuts());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -192,7 +192,7 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
|
||||
@Override
|
||||
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
|
||||
return Stream.concat(
|
||||
super.getSupportedShortcuts(), Stream.of(mHotseatPredictionController));
|
||||
Stream.of(mHotseatPredictionController), super.getSupportedShortcuts());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,8 +45,6 @@ import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
|
||||
import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
|
||||
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
|
||||
import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
|
||||
import static com.android.quickstep.util.SwipePipToHomeAnimator.FRACTION_END;
|
||||
import static com.android.quickstep.util.SwipePipToHomeAnimator.FRACTION_START;
|
||||
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
|
||||
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
|
||||
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
|
||||
@@ -248,7 +246,6 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
|
||||
private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
|
||||
|
||||
private static final long SWIPE_PIP_TO_HOME_DURATION = 425;
|
||||
private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
|
||||
protected boolean mIsSwipingPipToHome;
|
||||
|
||||
@@ -1134,17 +1131,14 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
createHomeAnimationFactory(cookies, duration, isTranslucent, appCanEnterPip,
|
||||
runningTaskTarget);
|
||||
mIsSwipingPipToHome = homeAnimFactory.supportSwipePipToHome() && appCanEnterPip;
|
||||
final RectFSpringAnim windowAnim;
|
||||
if (mIsSwipingPipToHome) {
|
||||
mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
|
||||
mSwipePipToHomeAnimator = createWindowAnimationToPip(
|
||||
homeAnimFactory, runningTaskTarget, start);
|
||||
mSwipePipToHomeAnimator.setDuration(SWIPE_PIP_TO_HOME_DURATION);
|
||||
mSwipePipToHomeAnimator.setInterpolator(interpolator);
|
||||
mSwipePipToHomeAnimator.setFloatValues(FRACTION_START, FRACTION_END);
|
||||
mSwipePipToHomeAnimator.start();
|
||||
mRunningWindowAnim = RunningWindowAnim.wrap(mSwipePipToHomeAnimator);
|
||||
windowAnim = mSwipePipToHomeAnimator;
|
||||
} else {
|
||||
mSwipePipToHomeAnimator = null;
|
||||
RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
|
||||
windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
|
||||
windowAnim.addAnimatorListener(new AnimationSuccessListener() {
|
||||
@Override
|
||||
public void onAnimationSuccess(Animator animator) {
|
||||
@@ -1158,9 +1152,9 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
|
||||
}
|
||||
});
|
||||
windowAnim.start(mContext, velocityPxPerMs);
|
||||
mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
|
||||
}
|
||||
windowAnim.start(mContext, velocityPxPerMs);
|
||||
mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
|
||||
homeAnimFactory.setSwipeVelocity(velocityPxPerMs.y);
|
||||
homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
|
||||
mLauncherTransitionController = null;
|
||||
@@ -1216,7 +1210,7 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
}
|
||||
}
|
||||
|
||||
private SwipePipToHomeAnimator getSwipePipToHomeAnimator(HomeAnimationFactory homeAnimFactory,
|
||||
private SwipePipToHomeAnimator createWindowAnimationToPip(HomeAnimationFactory homeAnimFactory,
|
||||
RemoteAnimationTargetCompat runningTaskTarget, float startProgress) {
|
||||
// Directly animate the app to PiP (picture-in-picture) mode
|
||||
final ActivityManager.RunningTaskInfo taskInfo = mGestureState.getRunningTask();
|
||||
@@ -1229,16 +1223,15 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
runningTaskTarget.taskInfo.pictureInPictureParams,
|
||||
homeRotation,
|
||||
mDp.hotseatBarSizePx);
|
||||
final Rect startBounds = new Rect();
|
||||
updateProgressForStartRect(new Matrix(), startProgress).round(startBounds);
|
||||
final SwipePipToHomeAnimator swipePipToHomeAnimator = new SwipePipToHomeAnimator(
|
||||
mContext,
|
||||
runningTaskTarget.taskId,
|
||||
taskInfo.topActivity,
|
||||
runningTaskTarget.leash.getSurfaceControl(),
|
||||
TaskInfoCompat.getPipSourceRectHint(
|
||||
runningTaskTarget.taskInfo.pictureInPictureParams),
|
||||
TaskInfoCompat.getWindowConfigurationBounds(taskInfo),
|
||||
startBounds,
|
||||
updateProgressForStartRect(new Matrix(), startProgress),
|
||||
destinationBounds,
|
||||
mRecentsView.getPipCornerRadius(),
|
||||
mRecentsView);
|
||||
@@ -1250,7 +1243,7 @@ public abstract class AbsSwipeUpHandler<T extends StatefulActivity<S>,
|
||||
}
|
||||
AnimatorPlaybackController activityAnimationToHome =
|
||||
homeAnimFactory.createActivityAnimationToHome();
|
||||
swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
swipePipToHomeAnimator.addAnimatorListener(new AnimatorListenerAdapter() {
|
||||
private boolean mHasAnimationEnded;
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
|
||||
@@ -27,8 +27,11 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH
|
||||
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED;
|
||||
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED;
|
||||
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED;
|
||||
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_DISABLED;
|
||||
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_THEMED_ICON_ENABLED;
|
||||
import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
|
||||
import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
|
||||
import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -40,6 +43,7 @@ import android.util.Xml;
|
||||
|
||||
import com.android.launcher3.AutoInstallsLayout;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.logging.InstanceIdSequence;
|
||||
import com.android.launcher3.logging.StatsLogManager;
|
||||
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
|
||||
@@ -170,6 +174,13 @@ public class SettingsChangeLogger implements
|
||||
if (gridSizeChangedEvent != null) {
|
||||
logger.log(gridSizeChangedEvent);
|
||||
}
|
||||
|
||||
if (FeatureFlags.ENABLE_THEMED_ICONS.get()) {
|
||||
logger.log(prefs.getBoolean(KEY_THEMED_ICONS, false)
|
||||
? LAUNCHER_THEMED_ICON_ENABLED
|
||||
: LAUNCHER_THEMED_ICON_DISABLED);
|
||||
}
|
||||
|
||||
mLoggablePrefs.forEach((key, lp) -> logger.log(() ->
|
||||
prefs.getBoolean(key, lp.defaultValue) ? lp.eventIdOn : lp.eventIdOff));
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.C
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.RectEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
@@ -44,30 +44,24 @@ import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
|
||||
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
|
||||
|
||||
/**
|
||||
* An {@link Animator} that animates an Activity to PiP (picture-in-picture) window when
|
||||
* swiping up (in gesture navigation mode). Note that this class is derived from
|
||||
* {@link com.android.wm.shell.pip.PipAnimationController.PipTransitionAnimator}.
|
||||
*
|
||||
* TODO: consider sharing this class including the animator and leash operations between
|
||||
* Launcher and SysUI. Also, there should be one source of truth for the corner radius of the
|
||||
* PiP window, which would ideally be on SysUI side as well.
|
||||
* Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
|
||||
* when swiping up (in gesture navigation mode).
|
||||
*/
|
||||
public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
public class SwipePipToHomeAnimator extends RectFSpringAnim {
|
||||
private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName();
|
||||
|
||||
public static final float FRACTION_START = 0f;
|
||||
public static final float FRACTION_END = 1f;
|
||||
private static final float END_PROGRESS = 1.0f;
|
||||
|
||||
private final int mTaskId;
|
||||
private final ComponentName mComponentName;
|
||||
private final SurfaceControl mLeash;
|
||||
private final Rect mAppBounds = new Rect();
|
||||
private final Rect mStartBounds = new Rect();
|
||||
private final Rect mCurrentBounds = new Rect();
|
||||
private final Rect mDestinationBounds = new Rect();
|
||||
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
|
||||
|
||||
/** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */
|
||||
private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
|
||||
/** for calculating transform in {@link #onAnimationUpdate(AppCloseConfig, RectF, float)} */
|
||||
private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
|
||||
private final Rect mSourceHintRectInsets;
|
||||
private final Rect mSourceInsets = new Rect();
|
||||
@@ -90,6 +84,7 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
private SurfaceControl mContentOverlay;
|
||||
|
||||
/**
|
||||
* @param context {@link Context} provides Launcher resources
|
||||
* @param taskId Task id associated with this animator, see also {@link #getTaskId()}
|
||||
* @param componentName Component associated with this animator,
|
||||
* see also {@link #getComponentName()}
|
||||
@@ -102,20 +97,22 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
* @param destinationBounds Bounds of the destination this animator ends to
|
||||
* @param cornerRadius Corner radius in pixel value for PiP window
|
||||
*/
|
||||
public SwipePipToHomeAnimator(int taskId,
|
||||
public SwipePipToHomeAnimator(@NonNull Context context,
|
||||
int taskId,
|
||||
@NonNull ComponentName componentName,
|
||||
@NonNull SurfaceControl leash,
|
||||
@Nullable Rect sourceRectHint,
|
||||
@NonNull Rect appBounds,
|
||||
@NonNull Rect startBounds,
|
||||
@NonNull RectF startBounds,
|
||||
@NonNull Rect destinationBounds,
|
||||
int cornerRadius,
|
||||
@NonNull View view) {
|
||||
super(startBounds, new RectF(destinationBounds), context);
|
||||
mTaskId = taskId;
|
||||
mComponentName = componentName;
|
||||
mLeash = leash;
|
||||
mAppBounds.set(appBounds);
|
||||
mStartBounds.set(startBounds);
|
||||
startBounds.round(mStartBounds);
|
||||
mDestinationBounds.set(destinationBounds);
|
||||
mDestinationBoundsTransformed.set(mDestinationBounds);
|
||||
mDestinationBoundsAnimation.set(mDestinationBounds);
|
||||
@@ -151,10 +148,10 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
t.reparent(mContentOverlay, mLeash);
|
||||
t.apply();
|
||||
|
||||
addUpdateListener(valueAnimator -> {
|
||||
float alpha = valueAnimator.getAnimatedFraction() < 0.5f
|
||||
addOnUpdateListener((values, currentRect, progress) -> {
|
||||
float alpha = progress < 0.5f
|
||||
? 0
|
||||
: Utilities.mapToRange(valueAnimator.getAnimatedFraction(), 0.5f, 1f,
|
||||
: Utilities.mapToRange(Math.min(progress, 1f), 0.5f, 1f,
|
||||
0f, 1f, Interpolators.FAST_OUT_SLOW_IN);
|
||||
t.setAlpha(mContentOverlay, alpha);
|
||||
t.apply();
|
||||
@@ -166,7 +163,7 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
appBounds.bottom - sourceRectHint.bottom);
|
||||
}
|
||||
|
||||
addListener(new AnimationSuccessListener() {
|
||||
addAnimatorListener(new AnimationSuccessListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP);
|
||||
@@ -191,7 +188,7 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
mHasAnimationEnded = true;
|
||||
}
|
||||
});
|
||||
addUpdateListener(this::onAnimationUpdate);
|
||||
addOnUpdateListener(this::onAnimationUpdate);
|
||||
}
|
||||
|
||||
/** sets the from rotation if it's different from the target rotation. */
|
||||
@@ -219,34 +216,34 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
mAppBounds.top + mDestinationBounds.height());
|
||||
}
|
||||
|
||||
private void onAnimationUpdate(ValueAnimator animator) {
|
||||
private void onAnimationUpdate(@Nullable AppCloseConfig values, RectF currentRect,
|
||||
float progress) {
|
||||
if (mHasAnimationEnded) return;
|
||||
final SurfaceControl.Transaction tx =
|
||||
PipSurfaceTransactionHelper.newSurfaceControlTransaction();
|
||||
onAnimationUpdate(tx, animator.getAnimatedFraction());
|
||||
onAnimationUpdate(tx, currentRect, progress);
|
||||
tx.apply();
|
||||
}
|
||||
|
||||
private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx,
|
||||
float fraction) {
|
||||
final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds,
|
||||
mDestinationBoundsAnimation);
|
||||
RectF currentRect, float progress) {
|
||||
currentRect.round(mCurrentBounds);
|
||||
final PictureInPictureSurfaceTransaction op;
|
||||
if (mSourceHintRectInsets == null) {
|
||||
// no source rect hint been set, directly scale the window down
|
||||
op = onAnimationScale(fraction, tx, bounds);
|
||||
op = onAnimationScale(progress, tx, mCurrentBounds);
|
||||
} else {
|
||||
// scale and crop according to the source rect hint
|
||||
op = onAnimationScaleAndCrop(fraction, tx, bounds);
|
||||
op = onAnimationScaleAndCrop(progress, tx, mCurrentBounds);
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
/** scale the window directly with no source rect hint being set */
|
||||
private PictureInPictureSurfaceTransaction onAnimationScale(
|
||||
float fraction, SurfaceControl.Transaction tx, Rect bounds) {
|
||||
float progress, SurfaceControl.Transaction tx, Rect bounds) {
|
||||
if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
|
||||
final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
|
||||
final RotatedPosition rotatedPosition = getRotatedPosition(progress);
|
||||
return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds,
|
||||
rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
|
||||
} else {
|
||||
@@ -256,12 +253,12 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
|
||||
/** scale and crop the window with source rect hint */
|
||||
private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop(
|
||||
float fraction, SurfaceControl.Transaction tx,
|
||||
float progress, SurfaceControl.Transaction tx,
|
||||
Rect bounds) {
|
||||
final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
|
||||
final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets,
|
||||
mSourceHintRectInsets);
|
||||
if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
|
||||
final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
|
||||
final RotatedPosition rotatedPosition = getRotatedPosition(progress);
|
||||
return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
|
||||
rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
|
||||
} else {
|
||||
@@ -291,22 +288,22 @@ public class SwipePipToHomeAnimator extends ValueAnimator {
|
||||
// get the final leash operations but do not apply to the leash.
|
||||
final SurfaceControl.Transaction tx =
|
||||
PipSurfaceTransactionHelper.newSurfaceControlTransaction();
|
||||
return onAnimationUpdate(tx, FRACTION_END);
|
||||
return onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS);
|
||||
}
|
||||
|
||||
private RotatedPosition getRotatedPosition(float fraction) {
|
||||
private RotatedPosition getRotatedPosition(float progress) {
|
||||
final float degree, positionX, positionY;
|
||||
if (mFromRotation == Surface.ROTATION_90) {
|
||||
degree = -90 * fraction;
|
||||
positionX = fraction * (mDestinationBoundsTransformed.left - mStartBounds.left)
|
||||
degree = -90 * progress;
|
||||
positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
|
||||
+ mStartBounds.left;
|
||||
positionY = fraction * (mDestinationBoundsTransformed.bottom - mStartBounds.top)
|
||||
positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top)
|
||||
+ mStartBounds.top;
|
||||
} else {
|
||||
degree = 90 * fraction;
|
||||
positionX = fraction * (mDestinationBoundsTransformed.right - mStartBounds.left)
|
||||
degree = 90 * progress;
|
||||
positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left)
|
||||
+ mStartBounds.left;
|
||||
positionY = fraction * (mDestinationBoundsTransformed.top - mStartBounds.top)
|
||||
positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
|
||||
+ mStartBounds.top;
|
||||
}
|
||||
return new RotatedPosition(degree, positionX, positionY);
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportHeight="20.0"
|
||||
android:viewportWidth="20.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:tint="?android:attr/textColorPrimary">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
android:height="@dimen/deep_shortcut_drag_handle_size"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorHint" >
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
|
||||
<path
|
||||
android:pathData="M20,9H4v2h16V9z M4,15h16v-2H4V15z"
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20.0"
|
||||
android:viewportHeight="20.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/textColorPrimary" >
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
android:layout_height="@dimen/system_shortcut_icon_size"
|
||||
android:layout_marginStart="@dimen/system_shortcut_margin_start"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:backgroundTint="?android:attr/textColorTertiary"/>
|
||||
android:backgroundTint="?android:attr/textColorPrimary"/>
|
||||
|
||||
</com.android.launcher3.shortcuts.DeepShortcutView>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/system_shortcut_full"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/system_shortcut_header_height"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end|center_vertical"
|
||||
android:elevation="@dimen/deep_shortcuts_elevation"
|
||||
android:tag="@string/popup_container_iterate_children"
|
||||
android:clipToPadding="true">
|
||||
</LinearLayout>
|
||||
@@ -13,7 +13,7 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<TableLayout
|
||||
<com.android.launcher3.widget.picker.WidgetsListTableView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widgets_table"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -16,30 +16,38 @@
|
||||
<com.android.launcher3.allapps.WorkEduCard xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/work_edu_card_margin"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/work_card"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
style="@style/PrimaryHeadline"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:id="@+id/work_apps_paused_title"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/work_profile_edu_work_apps"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp" />
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/work_edu_card_margin"
|
||||
android:background="@drawable/work_card"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center"
|
||||
android:id="@+id/wrapper">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/action_btn"
|
||||
android:textColor="?attr/workProfileOverlayTextColor"
|
||||
android:text="@string/work_profile_edu_accept"
|
||||
android:textAlignment="center"
|
||||
android:background="@drawable/work_card_btn"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
style="@style/PrimaryHeadline"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:id="@+id/work_apps_paused_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/work_profile_edu_work_apps"
|
||||
android:textAlignment="center"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/action_btn"
|
||||
android:textColor="?attr/workProfileOverlayTextColor"
|
||||
android:text="@string/work_profile_edu_accept"
|
||||
android:textAlignment="center"
|
||||
android:background="@drawable/work_card_btn"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</com.android.launcher3.allapps.WorkEduCard>
|
||||
@@ -12,11 +12,10 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.android.launcher3.allapps.WorkPausedCard xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/allAppsScrimColor"
|
||||
android:padding="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/work_edu_card_margin"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
@@ -39,5 +38,16 @@
|
||||
android:textColor="?attr/workProfileOverlayTextColor"
|
||||
android:text="@string/work_apps_paused_body"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/enable_work_apps"
|
||||
android:textColor="?attr/workProfileOverlayTextColor"
|
||||
android:text="@string/work_apps_enable_btn_text"
|
||||
android:textAlignment="center"
|
||||
android:background="@drawable/work_card_btn"
|
||||
android:textSize="14sp" />
|
||||
</com.android.launcher3.allapps.WorkPausedCard>
|
||||
@@ -94,11 +94,11 @@
|
||||
<dimen name="all_apps_background_canvas_height">475dp</dimen>
|
||||
<dimen name="all_apps_header_pill_height">48dp</dimen>
|
||||
<dimen name="all_apps_header_pill_corner_radius">18dp</dimen>
|
||||
<dimen name="all_apps_header_pills_width">320dp</dimen>
|
||||
<dimen name="all_apps_header_pills_width">348dp</dimen>
|
||||
<dimen name="all_apps_header_tab_height">48dp</dimen>
|
||||
<dimen name="all_apps_tabs_indicator_height">2dp</dimen>
|
||||
<dimen name="all_apps_header_top_padding">36dp</dimen>
|
||||
<dimen name="all_apps_header_bottom_padding">16dp</dimen>
|
||||
<dimen name="all_apps_header_bottom_padding">6dp</dimen>
|
||||
<dimen name="all_apps_work_profile_tab_footer_top_padding">16dp</dimen>
|
||||
<dimen name="all_apps_work_profile_tab_footer_bottom_padding">20dp</dimen>
|
||||
<dimen name="all_apps_tabs_vertical_padding">6dp</dimen>
|
||||
@@ -140,7 +140,6 @@
|
||||
|
||||
<dimen name="widget_list_top_bottom_corner_radius">28dp</dimen>
|
||||
<dimen name="widget_list_content_corner_radius">4dp</dimen>
|
||||
<dimen name="widget_list_content_joined_corner_radius">0dp</dimen>
|
||||
|
||||
<dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
|
||||
<dimen name="widget_list_entry_bottom_margin">2dp</dimen>
|
||||
|
||||
+4
-1
@@ -107,7 +107,10 @@ public final class WidgetsListHeaderViewHolderBinderTest {
|
||||
/* iconClickListener= */ view -> {},
|
||||
/* iconLongClickListener= */ view -> false);
|
||||
mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
|
||||
LayoutInflater.from(mTestActivity), mOnHeaderClickListener, widgetsListAdapter);
|
||||
LayoutInflater.from(mTestActivity),
|
||||
mOnHeaderClickListener,
|
||||
new WidgetsListDrawableFactory(mTestActivity),
|
||||
widgetsListAdapter);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
+4
-1
@@ -107,7 +107,10 @@ public final class WidgetsListSearchHeaderViewHolderBinderTest {
|
||||
/* iconClickListener= */ view -> {},
|
||||
/* iconLongClickListener= */ view -> false);
|
||||
mViewHolderBinder = new WidgetsListSearchHeaderViewHolderBinder(
|
||||
LayoutInflater.from(mTestActivity), mOnHeaderClickListener, widgetsListAdapter);
|
||||
LayoutInflater.from(mTestActivity),
|
||||
mOnHeaderClickListener,
|
||||
new WidgetsListDrawableFactory(mTestActivity),
|
||||
widgetsListAdapter);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
+1
@@ -118,6 +118,7 @@ public final class WidgetsListTableViewHolderBinderTest {
|
||||
mOnIconClickListener,
|
||||
mOnLongClickListener,
|
||||
mWidgetPreviewLoader,
|
||||
new WidgetsListDrawableFactory(mTestActivity),
|
||||
widgetsListAdapter);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import com.android.launcher3.anim.Interpolators;
|
||||
import com.android.launcher3.dragndrop.DragController;
|
||||
import com.android.launcher3.dragndrop.DragLayer;
|
||||
@@ -82,6 +80,8 @@ public abstract class ButtonDropTarget extends TextView
|
||||
private boolean mAccessibleDrag;
|
||||
/** An item must be dragged at least this many pixels before this drop target is enabled. */
|
||||
private final int mDragDistanceThreshold;
|
||||
/** The size of the drawable shown in the drop target. */
|
||||
private final int mDrawableSize;
|
||||
|
||||
protected CharSequence mText;
|
||||
protected ColorStateList mOriginalTextColor;
|
||||
@@ -103,6 +103,7 @@ public abstract class ButtonDropTarget extends TextView
|
||||
|
||||
Resources resources = getResources();
|
||||
mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold);
|
||||
mDrawableSize = resources.getDimensionPixelSize(R.dimen.drop_target_text_size);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,14 +121,18 @@ public abstract class ButtonDropTarget extends TextView
|
||||
}
|
||||
|
||||
protected void setDrawable(int resId) {
|
||||
mDrawable = getContext().getDrawable(resId).mutate();
|
||||
mDrawable.setBounds(0, 0, mDrawableSize, mDrawableSize);
|
||||
setDrawable(mDrawable);
|
||||
}
|
||||
|
||||
private void setDrawable(Drawable drawable) {
|
||||
// We do not set the drawable in the xml as that inflates two drawables corresponding to
|
||||
// drawableLeft and drawableStart.
|
||||
if (mTextVisible) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0, 0, 0);
|
||||
mDrawable = getCompoundDrawablesRelative()[0];
|
||||
setCompoundDrawablesRelative(drawable, null, null, null);
|
||||
} else {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, resId, 0, 0);
|
||||
mDrawable = getCompoundDrawablesRelative()[1];
|
||||
setCompoundDrawablesRelative(null, drawable, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,11 +334,7 @@ public abstract class ButtonDropTarget extends TextView
|
||||
if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) {
|
||||
mTextVisible = isVisible;
|
||||
setText(newText);
|
||||
if (mTextVisible) {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
|
||||
} else {
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(null, mDrawable, null, null);
|
||||
}
|
||||
setDrawable(mDrawable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,10 @@ import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCU
|
||||
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
|
||||
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
@@ -82,10 +83,7 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
ScrimView.ScrimDrawingController {
|
||||
|
||||
public static final float PULL_MULTIPLIER = .02f;
|
||||
public static final float FLING_VELOCITY_MULTIPLIER = 2000f;
|
||||
|
||||
// Starts the springs after at least 25% of the animation has passed.
|
||||
public static final float FLING_ANIMATION_THRESHOLD = 0.25f;
|
||||
public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
|
||||
|
||||
private final Paint mHeaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
@@ -494,15 +492,6 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
View overlay = mAH[AdapterHolder.WORK].getOverlayView();
|
||||
int v = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? GONE : VISIBLE;
|
||||
overlay.findViewById(R.id.work_apps_paused_title).setVisibility(v);
|
||||
overlay.findViewById(R.id.work_apps_paused_content).setVisibility(v);
|
||||
}
|
||||
|
||||
private void replaceRVContainer(boolean showTabs) {
|
||||
for (int i = 0; i < mAH.length; i++) {
|
||||
if (mAH[i].recyclerView != null) {
|
||||
@@ -544,9 +533,6 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
&& mAllAppsStore.hasModelFlag(
|
||||
FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
|
||||
}
|
||||
if (mSearchUiManager != null && mSearchUiManager.getEditText() != null) {
|
||||
mSearchUiManager.getEditText().hideKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
// Used by tests only
|
||||
@@ -658,20 +644,18 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
/**
|
||||
* Adds an update listener to {@param animator} that adds springs to the animation.
|
||||
*/
|
||||
public void addSpringFromFlingUpdateListener(ValueAnimator animator, float velocity) {
|
||||
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
boolean shouldSpring = true;
|
||||
|
||||
public void addSpringFromFlingUpdateListener(ValueAnimator animator,
|
||||
float velocity /* release velocity */,
|
||||
float progress /* portion of the distance to travel*/) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
if (shouldSpring
|
||||
&& valueAnimator.getAnimatedFraction() >= FLING_ANIMATION_THRESHOLD) {
|
||||
absorbSwipeUpVelocity(Math.max(100, Math.abs(
|
||||
Math.round(velocity * FLING_VELOCITY_MULTIPLIER))));
|
||||
// calculate the velocity of using the not user controlled interpolator
|
||||
// of when the container reach the end.
|
||||
shouldSpring = false;
|
||||
}
|
||||
public void onAnimationStart(Animator animator) {
|
||||
float distance = (float) ((1 - progress) * getHeight()); // px
|
||||
float settleVelocity = Math.min(0, distance
|
||||
/ (AllAppsTransitionController.INTERP_COEFF * animator.getDuration())
|
||||
+ velocity);
|
||||
absorbSwipeUpVelocity(Math.max(1000, Math.abs(
|
||||
Math.round(settleVelocity * FLING_VELOCITY_MULTIPLIER))));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -704,7 +688,9 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
mHeaderPaint.setColor(mHeaderColor);
|
||||
mHeaderPaint.setAlpha((int) (getAlpha() * Color.alpha(mHeaderColor)));
|
||||
if (mHeaderPaint.getColor() != mScrimColor && mHeaderPaint.getColor() != 0) {
|
||||
canvas.drawRect(0, 0, getWidth(), mSearchContainer.getTop() + getTranslationY(),
|
||||
int bottom = mUsingTabs && mHeader.mHeaderCollapsed ? mHeader.getVisibleBottomBound()
|
||||
: mSearchContainer.getBottom();
|
||||
canvas.drawRect(0, 0, getWidth(), bottom + getTranslationY(),
|
||||
mHeaderPaint);
|
||||
}
|
||||
}
|
||||
@@ -781,13 +767,6 @@ public class AllAppsContainerView extends SpringRelativeLayout implements DragSo
|
||||
mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
|
||||
&& verticalFadingEdge);
|
||||
}
|
||||
|
||||
private View getOverlayView() {
|
||||
if (mOverlay == null) {
|
||||
mOverlay = mLauncher.getLayoutInflater().inflate(R.layout.work_apps_paused, null);
|
||||
}
|
||||
return mOverlay;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -447,4 +447,14 @@ public class AllAppsRecyclerView extends BaseRecyclerView {
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns distance between left and right app icons
|
||||
*/
|
||||
public int getTabWidth() {
|
||||
DeviceProfile grid = BaseDraggingActivity.fromContext(getContext()).getDeviceProfile();
|
||||
int totalWidth = (grid.availableWidthPx - getPaddingLeft() - getPaddingRight());
|
||||
int iconPadding = totalWidth / grid.numShownAllAppsColumns - grid.allAppsIconSizePx;
|
||||
return totalWidth - iconPadding;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ import com.android.launcher3.views.ScrimView;
|
||||
*/
|
||||
public class AllAppsTransitionController
|
||||
implements StateHandler<LauncherState>, OnDeviceProfileChangeListener {
|
||||
// This constant should match the second derivative of the animator interpolator.
|
||||
public static final float INTERP_COEFF = 1.7f;
|
||||
private static final float CONTENT_VISIBLE_MAX_THRESHOLD = 0.5f;
|
||||
|
||||
public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
|
||||
|
||||
@@ -72,8 +72,13 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
}
|
||||
|
||||
int current = -mCurrentRV.getCurrentScrollY();
|
||||
boolean headerCollapsed = mHeaderCollapsed;
|
||||
moved(current);
|
||||
applyVerticalMove();
|
||||
if (headerCollapsed != mHeaderCollapsed) {
|
||||
AllAppsContainerView parent = (AllAppsContainerView) getParent();
|
||||
parent.invalidateHeader();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,6 +224,8 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
|
||||
mTabsHidden = tabsHidden;
|
||||
mTabLayout.setVisibility(tabsHidden ? View.GONE : View.VISIBLE);
|
||||
mTabLayout.getLayoutParams().width =
|
||||
mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView.getTabWidth();
|
||||
mMainRV = setupRV(mMainRV, mAH[AllAppsContainerView.AdapterHolder.MAIN].recyclerView);
|
||||
mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
|
||||
mParent = (ViewGroup) mMainRV.getParent();
|
||||
@@ -429,6 +436,13 @@ public class FloatingHeaderView extends LinearLayout implements
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns visible height of FloatingHeaderView contents
|
||||
*/
|
||||
public int getVisibleBottomBound() {
|
||||
return getBottom() + mTranslationY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
@@ -29,7 +29,7 @@ import com.android.launcher3.R;
|
||||
/**
|
||||
* Work profile toggle switch shown at the bottom of AllApps work tab
|
||||
*/
|
||||
public class WorkEduCard extends LinearLayout implements View.OnClickListener,
|
||||
public class WorkEduCard extends FrameLayout implements View.OnClickListener,
|
||||
Animation.AnimationListener {
|
||||
|
||||
private final Launcher mLauncher;
|
||||
@@ -52,11 +52,24 @@ public class WorkEduCard extends LinearLayout implements View.OnClickListener,
|
||||
mDismissAnim.setAnimationListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
mDismissAnim.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mDismissAnim.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
findViewById(R.id.action_btn).setOnClickListener(this);
|
||||
MarginLayoutParams lp = ((MarginLayoutParams) findViewById(R.id.wrapper).getLayoutParams());
|
||||
lp.width = mLauncher.getAppsView().getActiveRecyclerView().getTabWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.widget.Button;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.android.launcher3.Insettable;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.pm.UserCache;
|
||||
|
||||
@@ -79,7 +78,9 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi
|
||||
clearAnimation();
|
||||
if (workTabVisible) {
|
||||
setEnabled(true);
|
||||
setVisibility(VISIBLE);
|
||||
if (mWorkEnabled) {
|
||||
setVisibility(VISIBLE);
|
||||
}
|
||||
setAlpha(0);
|
||||
animate().alpha(1).start();
|
||||
} else {
|
||||
@@ -91,7 +92,7 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi
|
||||
public void onClick(View view) {
|
||||
if (Utilities.ATLEAST_P) {
|
||||
setEnabled(false);
|
||||
UI_HELPER_EXECUTOR.post(() -> setToState(!mWorkEnabled));
|
||||
UI_HELPER_EXECUTOR.post(() -> setWorkProfileEnabled(getContext(), false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,20 +102,18 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi
|
||||
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);
|
||||
setVisibility(active ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.P)
|
||||
protected Boolean setToState(boolean toState) {
|
||||
UserManager userManager = getContext().getSystemService(UserManager.class);
|
||||
public static Boolean setWorkProfileEnabled(Context context, boolean enabled) {
|
||||
UserManager userManager = context.getSystemService(UserManager.class);
|
||||
boolean showConfirm = false;
|
||||
for (UserHandle userProfile : UserCache.INSTANCE.get(getContext()).getUserProfiles()) {
|
||||
for (UserHandle userProfile : UserCache.INSTANCE.get(context).getUserProfiles()) {
|
||||
if (Process.myUserHandle().equals(userProfile)) {
|
||||
continue;
|
||||
}
|
||||
showConfirm |= !userManager.requestQuietModeEnabled(!toState, userProfile);
|
||||
showConfirm |= !userManager.requestQuietModeEnabled(!enabled, userProfile);
|
||||
}
|
||||
return showConfirm;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.allapps;
|
||||
|
||||
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Utilities;
|
||||
|
||||
/**
|
||||
* Work profile toggle switch shown at the bottom of AllApps work tab
|
||||
*/
|
||||
public class WorkPausedCard extends LinearLayout implements View.OnClickListener {
|
||||
|
||||
private final Launcher mLauncher;
|
||||
private Button mBtn;
|
||||
|
||||
public WorkPausedCard(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public WorkPausedCard(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public WorkPausedCard(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
mLauncher = Launcher.getLauncher(getContext());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mBtn = findViewById(R.id.enable_work_apps);
|
||||
mBtn.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (Utilities.ATLEAST_P) {
|
||||
setEnabled(false);
|
||||
UI_HELPER_EXECUTOR.post(() -> WorkModeSwitch.setWorkProfileEnabled(getContext(), true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
int orientation = getResources().getConfiguration().orientation;
|
||||
getLayoutParams().height = orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
}
|
||||
}
|
||||
@@ -473,7 +473,13 @@ public class StatsLogManager implements ResourceBasedOverride {
|
||||
LAUNCHER_WIDGET_RESIZE_COMPLETED(824),
|
||||
|
||||
@UiEvent(doc = "User reconfigured a widget on their home screen.")
|
||||
LAUNCHER_WIDGET_RECONFIGURED(821)
|
||||
LAUNCHER_WIDGET_RECONFIGURED(821),
|
||||
|
||||
@UiEvent(doc = "User enabled themed icons option in wallpaper & style settings.")
|
||||
LAUNCHER_THEMED_ICON_ENABLED(836),
|
||||
|
||||
@UiEvent(doc = "User disabled themed icons option in wallpaper & style settings.")
|
||||
LAUNCHER_THEMED_ICON_DISABLED(837)
|
||||
;
|
||||
|
||||
// ADD MORE
|
||||
|
||||
@@ -130,8 +130,9 @@ public class AddWorkspaceItemsTask extends BaseModelUpdateTask {
|
||||
packageName);
|
||||
|
||||
if (!packageInstaller.verifySessionInfo(sessionInfo)) {
|
||||
FileLog.d(LOG, "Item info failed session info verification: "
|
||||
+ workspaceInfo);
|
||||
FileLog.d(LOG, "Item info failed session info verification. "
|
||||
+ "Skipping : " + workspaceInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
List<LauncherActivityInfo> activities = launcherApps
|
||||
|
||||
@@ -217,7 +217,8 @@ public class InstallSessionHelper {
|
||||
void tryQueuePromiseAppIcon(PackageInstaller.SessionInfo sessionInfo) {
|
||||
if (FeatureFlags.PROMISE_APPS_NEW_INSTALLS.get()
|
||||
&& SessionCommitReceiver.isEnabled(mAppContext)
|
||||
&& verifySessionInfo(sessionInfo)) {
|
||||
&& verifySessionInfo(sessionInfo)
|
||||
&& !promiseIconAddedForId(sessionInfo.getSessionId())) {
|
||||
FileLog.d(LOG, "Adding package name to install queue: "
|
||||
+ sessionInfo.getAppPackageName());
|
||||
|
||||
@@ -234,25 +235,28 @@ public class InstallSessionHelper {
|
||||
&& sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER
|
||||
&& sessionInfo.getAppIcon() != null
|
||||
&& !TextUtils.isEmpty(sessionInfo.getAppLabel())
|
||||
&& !promiseIconAddedForId(sessionInfo.getSessionId())
|
||||
&& !new PackageManagerHelper(mAppContext).isAppInstalled(
|
||||
sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
|
||||
|
||||
if (sessionInfo != null) {
|
||||
Bitmap appIcon = sessionInfo.getAppIcon();
|
||||
|
||||
FileLog.d(LOG, String.format(
|
||||
"Verifying session info. Valid: %b, Session verified: %b, Install reason valid:"
|
||||
+ " %b, App icon: %s, App label: %s, Promise icon added: %b, "
|
||||
+ "App installed: %b.",
|
||||
validSessionInfo,
|
||||
verify(sessionInfo) != null,
|
||||
sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER,
|
||||
appIcon == null ? "null" : IOUtils.toBase64String(appIcon),
|
||||
sessionInfo.getAppLabel(),
|
||||
promiseIconAddedForId(sessionInfo.getSessionId()),
|
||||
new PackageManagerHelper(mAppContext).isAppInstalled(
|
||||
sessionInfo.getAppPackageName(), getUserHandle(sessionInfo))));
|
||||
if (Utilities.IS_DEBUG_DEVICE) {
|
||||
FileLog.d(LOG, String.format(
|
||||
"Verifying session info. Valid: %b,"
|
||||
+ " Session verified: %b,"
|
||||
+ " Install reason valid: %b,"
|
||||
+ " App icon: %s,"
|
||||
+ " App label: %s,"
|
||||
+ " App installed: %b.",
|
||||
validSessionInfo,
|
||||
verify(sessionInfo) != null,
|
||||
sessionInfo.getInstallReason() == PackageManager.INSTALL_REASON_USER,
|
||||
appIcon == null ? "null" : IOUtils.toBase64String(appIcon),
|
||||
sessionInfo.getAppLabel(),
|
||||
new PackageManagerHelper(mAppContext).isAppInstalled(
|
||||
sessionInfo.getAppPackageName(), getUserHandle(sessionInfo))));
|
||||
}
|
||||
} else {
|
||||
FileLog.d(LOG, "Verifying session info failed: session info null.");
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
|
||||
private int mNumNotifications;
|
||||
private ViewGroup mNotificationContainer;
|
||||
|
||||
private ViewGroup mWidgetContainer;
|
||||
|
||||
private ViewGroup mDeepShortcutContainer;
|
||||
|
||||
private ViewGroup mSystemShortcutContainer;
|
||||
@@ -234,7 +236,7 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
|
||||
|
||||
@Override
|
||||
protected List<View> getChildrenForColorExtraction() {
|
||||
return Arrays.asList(mSystemShortcutContainer, mDeepShortcutContainer,
|
||||
return Arrays.asList(mSystemShortcutContainer, mWidgetContainer, mDeepShortcutContainer,
|
||||
mNotificationContainer);
|
||||
}
|
||||
|
||||
@@ -298,10 +300,24 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
|
||||
updateHiddenShortcuts();
|
||||
|
||||
if (!systemShortcuts.isEmpty()) {
|
||||
mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
|
||||
for (SystemShortcut shortcut : systemShortcuts) {
|
||||
initializeSystemShortcut(
|
||||
R.layout.system_shortcut_icon_only, mSystemShortcutContainer, shortcut);
|
||||
if (shortcut instanceof SystemShortcut.Widgets) {
|
||||
if (mWidgetContainer == null) {
|
||||
mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
|
||||
this);
|
||||
}
|
||||
initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
|
||||
shortcut);
|
||||
}
|
||||
}
|
||||
mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons, this);
|
||||
|
||||
for (SystemShortcut shortcut : systemShortcuts) {
|
||||
if (!(shortcut instanceof SystemShortcut.Widgets)) {
|
||||
initializeSystemShortcut(
|
||||
R.layout.system_shortcut_icon_only, mSystemShortcutContainer,
|
||||
shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -524,25 +540,34 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
|
||||
mLauncher.getPopupDataProvider().setChangeListener(null);
|
||||
}
|
||||
|
||||
private View getWidgetsView(ViewGroup container) {
|
||||
for (int i = container.getChildCount() - 1; i >= 0; --i) {
|
||||
View systemShortcutView = container.getChildAt(i);
|
||||
if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
|
||||
return systemShortcutView;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWidgetsBound() {
|
||||
ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
|
||||
SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
|
||||
View widgetsView = null;
|
||||
int count = mSystemShortcutContainer.getChildCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
View systemShortcutView = mSystemShortcutContainer.getChildAt(i);
|
||||
if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
|
||||
widgetsView = systemShortcutView;
|
||||
break;
|
||||
}
|
||||
View widgetsView = getWidgetsView(PopupContainerWithArrow.this);
|
||||
if (widgetsView == null && mWidgetContainer != null) {
|
||||
widgetsView = getWidgetsView(mWidgetContainer);
|
||||
}
|
||||
|
||||
if (widgetInfo != null && widgetsView == null) {
|
||||
// We didn't have any widgets cached but now there are some, so enable the shortcut.
|
||||
if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
|
||||
initializeSystemShortcut(R.layout.system_shortcut_icon_only,
|
||||
mSystemShortcutContainer, widgetInfo);
|
||||
if (mWidgetContainer == null) {
|
||||
mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
|
||||
PopupContainerWithArrow.this);
|
||||
}
|
||||
initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
|
||||
widgetInfo);
|
||||
} else {
|
||||
// If using the expanded system shortcut (as opposed to just the icon), we need
|
||||
// to reopen the container to ensure measurements etc. all work out. While this
|
||||
@@ -554,8 +579,10 @@ public class PopupContainerWithArrow<T extends StatefulActivity<LauncherState>>
|
||||
}
|
||||
} else if (widgetInfo == null && widgetsView != null) {
|
||||
// No widgets exist, but we previously added the shortcut so remove it.
|
||||
if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
|
||||
mSystemShortcutContainer.removeView(widgetsView);
|
||||
if (mSystemShortcutContainer
|
||||
!= PopupContainerWithArrow.this
|
||||
&& mWidgetContainer != null) {
|
||||
mWidgetContainer.removeView(widgetsView);
|
||||
} else {
|
||||
close(false);
|
||||
PopupContainerWithArrow.showForIcon(mOriginalIcon);
|
||||
|
||||
@@ -335,12 +335,10 @@ public abstract class AbstractStateChangeTouchController
|
||||
mCurrentAnimation.dispatchOnStart();
|
||||
if (targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
|
||||
if (mAllAppsOvershootStarted) {
|
||||
|
||||
mLauncher.getAppsView().onRelease();
|
||||
mAllAppsOvershootStarted = false;
|
||||
|
||||
} else {
|
||||
mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
|
||||
mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity, progress);
|
||||
}
|
||||
}
|
||||
anim.start();
|
||||
|
||||
@@ -103,18 +103,25 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
|
||||
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
|
||||
mLauncher = Launcher.getLauncher(context);
|
||||
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
|
||||
WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);
|
||||
mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
|
||||
layoutInflater, iconClickListener, iconLongClickListener,
|
||||
widgetPreviewLoader, /* listAdapter= */ this);
|
||||
widgetPreviewLoader, listDrawableFactory, /* listAdapter= */ this);
|
||||
mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
|
||||
mViewHolderBinders.put(
|
||||
VIEW_TYPE_WIDGETS_HEADER,
|
||||
new WidgetsListHeaderViewHolderBinder(
|
||||
layoutInflater, /* onHeaderClickListener= */this, /* listAdapter= */ this));
|
||||
layoutInflater,
|
||||
/* onHeaderClickListener= */ this,
|
||||
listDrawableFactory,
|
||||
/* listAdapter= */ this));
|
||||
mViewHolderBinders.put(
|
||||
VIEW_TYPE_WIDGETS_SEARCH_HEADER,
|
||||
new WidgetsListSearchHeaderViewHolderBinder(
|
||||
layoutInflater, /*onHeaderClickListener=*/ this, /* listAdapter= */ this));
|
||||
layoutInflater,
|
||||
/* onHeaderClickListener= */ this,
|
||||
listDrawableFactory,
|
||||
/* listAdapter= */ this));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.widget.picker;
|
||||
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.FIRST;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.FIRST_EXPANDED;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE_EXPANDED;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.SINGLE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.util.Themes;
|
||||
|
||||
/** Factory for creating drawables to use as background for list elements. */
|
||||
final class WidgetsListDrawableFactory {
|
||||
|
||||
private final float mTopBottomCornerRadius;
|
||||
private final float mMiddleCornerRadius;
|
||||
private final ColorStateList mSurfaceColor;
|
||||
private final ColorStateList mRippleColor;
|
||||
|
||||
WidgetsListDrawableFactory(Context context) {
|
||||
Resources res = context.getResources();
|
||||
mTopBottomCornerRadius = res.getDimension(R.dimen.widget_list_top_bottom_corner_radius);
|
||||
mMiddleCornerRadius = res.getDimension(R.dimen.widget_list_content_corner_radius);
|
||||
mSurfaceColor = context.getColorStateList(R.color.surface);
|
||||
mRippleColor = ColorStateList.valueOf(
|
||||
Themes.getAttrColor(context, android.R.attr.colorControlHighlight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drawable for widget header list items. This drawable supports all positions
|
||||
* in {@link WidgetsListDrawableState}.
|
||||
*/
|
||||
Drawable createHeaderBackgroundDrawable() {
|
||||
StateListDrawable stateList = new StateListDrawable();
|
||||
stateList.addState(
|
||||
SINGLE.mStateSet,
|
||||
createRoundedRectDrawable(mTopBottomCornerRadius, mTopBottomCornerRadius));
|
||||
stateList.addState(
|
||||
FIRST_EXPANDED.mStateSet,
|
||||
createRoundedRectDrawable(mTopBottomCornerRadius, 0));
|
||||
stateList.addState(
|
||||
FIRST.mStateSet,
|
||||
createRoundedRectDrawable(mTopBottomCornerRadius, mMiddleCornerRadius));
|
||||
stateList.addState(
|
||||
MIDDLE_EXPANDED.mStateSet,
|
||||
createRoundedRectDrawable(mMiddleCornerRadius, 0));
|
||||
stateList.addState(
|
||||
MIDDLE.mStateSet,
|
||||
createRoundedRectDrawable(mMiddleCornerRadius, mMiddleCornerRadius));
|
||||
stateList.addState(
|
||||
LAST.mStateSet,
|
||||
createRoundedRectDrawable(mMiddleCornerRadius, mTopBottomCornerRadius));
|
||||
return new RippleDrawable(mRippleColor, /* content= */ stateList, /* mask= */ stateList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drawable for widget content list items. This state list supports the middle and
|
||||
* last states.
|
||||
*/
|
||||
Drawable createContentBackgroundDrawable() {
|
||||
StateListDrawable stateList = new StateListDrawable();
|
||||
stateList.addState(
|
||||
MIDDLE.mStateSet,
|
||||
createRoundedRectDrawable(0, mMiddleCornerRadius));
|
||||
stateList.addState(
|
||||
LAST.mStateSet,
|
||||
createRoundedRectDrawable(0, mTopBottomCornerRadius));
|
||||
return new RippleDrawable(mRippleColor, /* content= */ stateList, /* mask= */ stateList);
|
||||
}
|
||||
|
||||
/** Creates a rounded-rect drawable with the specified radii. */
|
||||
private Drawable createRoundedRectDrawable(float topRadius, float bottomRadius) {
|
||||
GradientDrawable backgroundMask = new GradientDrawable();
|
||||
backgroundMask.setColor(mSurfaceColor);
|
||||
backgroundMask.setShape(GradientDrawable.RECTANGLE);
|
||||
backgroundMask.setCornerRadii(
|
||||
new float[]{
|
||||
topRadius,
|
||||
topRadius,
|
||||
topRadius,
|
||||
topRadius,
|
||||
bottomRadius,
|
||||
bottomRadius,
|
||||
bottomRadius,
|
||||
bottomRadius
|
||||
});
|
||||
return backgroundMask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.widget.picker;
|
||||
|
||||
/**
|
||||
* Different possible list position states for an item in the widgets list to have. Note that only
|
||||
* headers use the expanded state.
|
||||
*/
|
||||
enum WidgetsListDrawableState {
|
||||
FIRST(new int[]{android.R.attr.state_first}),
|
||||
FIRST_EXPANDED(new int[]{android.R.attr.state_first, android.R.attr.state_expanded}),
|
||||
MIDDLE(new int[]{android.R.attr.state_middle}),
|
||||
MIDDLE_EXPANDED(new int[]{android.R.attr.state_middle, android.R.attr.state_expanded}),
|
||||
LAST(new int[]{android.R.attr.state_last}),
|
||||
SINGLE(new int[]{android.R.attr.state_single});
|
||||
|
||||
final int[] mStateSet;
|
||||
|
||||
WidgetsListDrawableState(int[] stateSet) {
|
||||
mStateSet = stateSet;
|
||||
}
|
||||
|
||||
static WidgetsListDrawableState obtain(boolean isFirst, boolean isLast, boolean isExpanded) {
|
||||
if (isFirst && isLast) return SINGLE;
|
||||
if (isFirst && isExpanded) return FIRST_EXPANDED;
|
||||
if (isFirst) return FIRST;
|
||||
if (isLast) return LAST;
|
||||
if (isExpanded) return MIDDLE_EXPANDED;
|
||||
return MIDDLE;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* 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.widget.picker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.util.Themes;
|
||||
|
||||
/** Helper class for creating drawables to use as background for list elements. */
|
||||
final class WidgetsListDrawables {
|
||||
|
||||
private WidgetsListDrawables() {}
|
||||
|
||||
/** Creates a list background drawable with the specified radii. */
|
||||
static Drawable createListBackgroundDrawable(
|
||||
Context context,
|
||||
float topRadius,
|
||||
float bottomRadius) {
|
||||
GradientDrawable backgroundMask = new GradientDrawable();
|
||||
backgroundMask.setColor(context.getColorStateList(R.color.surface));
|
||||
backgroundMask.setShape(GradientDrawable.RECTANGLE);
|
||||
|
||||
backgroundMask.setCornerRadii(
|
||||
new float[]{
|
||||
topRadius,
|
||||
topRadius,
|
||||
topRadius,
|
||||
topRadius,
|
||||
bottomRadius,
|
||||
bottomRadius,
|
||||
bottomRadius,
|
||||
bottomRadius
|
||||
});
|
||||
|
||||
return new RippleDrawable(
|
||||
/* color= */ ColorStateList.valueOf(
|
||||
Themes.getAttrColor(context, android.R.attr.colorControlHighlight)),
|
||||
/* content= */ backgroundMask,
|
||||
/* mask= */ backgroundMask);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -60,9 +60,6 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
@Nullable private Drawable mIconDrawable;
|
||||
private final int mIconSize;
|
||||
private final int mBottomMarginSize;
|
||||
private final float mTopBottomCornerRadius;
|
||||
private final float mMiddleCornerRadius;
|
||||
private final float mJoinedCornerRadius;
|
||||
|
||||
private ImageView mAppIcon;
|
||||
private TextView mTitle;
|
||||
@@ -70,6 +67,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
|
||||
private CheckBox mExpandToggle;
|
||||
private boolean mIsExpanded = false;
|
||||
@Nullable private WidgetsListDrawableState mListDrawableState;
|
||||
|
||||
public WidgetsListHeader(Context context) {
|
||||
this(context, /* attrs= */ null);
|
||||
@@ -90,12 +88,6 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
grid.iconSizePx);
|
||||
mBottomMarginSize =
|
||||
getResources().getDimensionPixelSize(R.dimen.widget_list_entry_bottom_margin);
|
||||
mTopBottomCornerRadius =
|
||||
getResources().getDimension(R.dimen.widget_list_top_bottom_corner_radius);
|
||||
mMiddleCornerRadius =
|
||||
getResources().getDimension(R.dimen.widget_list_content_corner_radius);
|
||||
mJoinedCornerRadius =
|
||||
getResources().getDimension(R.dimen.widget_list_content_joined_corner_radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -163,6 +155,14 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
|
||||
@UiThread
|
||||
public void setListDrawableState(WidgetsListDrawableState state) {
|
||||
if (state == mListDrawableState) return;
|
||||
this.mListDrawableState = state;
|
||||
refreshDrawableState();
|
||||
}
|
||||
|
||||
/** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
|
||||
@UiThread
|
||||
public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
|
||||
@@ -263,20 +263,6 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
verifyHighRes();
|
||||
}
|
||||
|
||||
/** Updates the list to have a background drawable with the appropriate corner radii. */
|
||||
@UiThread
|
||||
public void updateListBackground(boolean isFirst, boolean isLast, boolean isExpanded) {
|
||||
float topRadius = isFirst ? mTopBottomCornerRadius : mMiddleCornerRadius;
|
||||
float bottomRadius = isLast
|
||||
? mTopBottomCornerRadius
|
||||
: isExpanded
|
||||
? mJoinedCornerRadius
|
||||
: mMiddleCornerRadius;
|
||||
setBackground(
|
||||
WidgetsListDrawables.createListBackgroundDrawable(
|
||||
getContext(), topRadius, bottomRadius));
|
||||
}
|
||||
|
||||
private void setTitles(WidgetsListSearchHeaderEntry entry) {
|
||||
mTitle.setText(entry.mPkgItem.title);
|
||||
|
||||
@@ -300,6 +286,17 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] onCreateDrawableState(int extraSpace) {
|
||||
if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace);
|
||||
// Augment the state set from the super implementation with the custom states from
|
||||
// mListDrawableState.
|
||||
int[] drawableState =
|
||||
super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length);
|
||||
mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
/** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
|
||||
public void verifyHighRes() {
|
||||
if (mIconLoadRequest != null) {
|
||||
|
||||
@@ -30,13 +30,16 @@ public final class WidgetsListHeaderViewHolderBinder implements
|
||||
ViewHolderBinder<WidgetsListHeaderEntry, WidgetsListHeaderHolder> {
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
private final OnHeaderClickListener mOnHeaderClickListener;
|
||||
private final WidgetsListDrawableFactory mListDrawableFactory;
|
||||
private final WidgetsListAdapter mWidgetsListAdapter;
|
||||
|
||||
public WidgetsListHeaderViewHolderBinder(LayoutInflater layoutInflater,
|
||||
OnHeaderClickListener onHeaderClickListener,
|
||||
WidgetsListDrawableFactory listDrawableFactory,
|
||||
WidgetsListAdapter listAdapter) {
|
||||
mLayoutInflater = layoutInflater;
|
||||
mOnHeaderClickListener = onHeaderClickListener;
|
||||
mListDrawableFactory = listDrawableFactory;
|
||||
mWidgetsListAdapter = listAdapter;
|
||||
}
|
||||
|
||||
@@ -44,7 +47,7 @@ public final class WidgetsListHeaderViewHolderBinder implements
|
||||
public WidgetsListHeaderHolder newViewHolder(ViewGroup parent) {
|
||||
WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
|
||||
R.layout.widgets_list_row_header, parent, false);
|
||||
|
||||
header.setBackground(mListDrawableFactory.createHeaderBackgroundDrawable());
|
||||
return new WidgetsListHeaderHolder(header);
|
||||
}
|
||||
|
||||
@@ -52,12 +55,13 @@ public final class WidgetsListHeaderViewHolderBinder implements
|
||||
public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data,
|
||||
int position) {
|
||||
WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
|
||||
widgetsListHeader.updateListBackground(
|
||||
/* isFirst= */ position == 0,
|
||||
/* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
|
||||
/* isExpanded= */ data.isWidgetListShown());
|
||||
widgetsListHeader.applyFromItemInfoWithIcon(data);
|
||||
widgetsListHeader.setExpanded(data.isWidgetListShown());
|
||||
widgetsListHeader.setListDrawableState(
|
||||
WidgetsListDrawableState.obtain(
|
||||
/* isFirst= */ position == 0,
|
||||
/* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
|
||||
/* isExpanded= */ data.isWidgetListShown()));
|
||||
widgetsListHeader.setOnExpandChangeListener(isExpanded ->
|
||||
mOnHeaderClickListener.onHeaderClicked(
|
||||
isExpanded,
|
||||
|
||||
+9
-5
@@ -31,13 +31,16 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
|
||||
ViewHolderBinder<WidgetsListSearchHeaderEntry, WidgetsListSearchHeaderHolder> {
|
||||
private final LayoutInflater mLayoutInflater;
|
||||
private final OnHeaderClickListener mOnHeaderClickListener;
|
||||
private final WidgetsListDrawableFactory mListDrawableFactory;
|
||||
private final WidgetsListAdapter mWidgetsListAdapter;
|
||||
|
||||
public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater,
|
||||
OnHeaderClickListener onHeaderClickListener,
|
||||
WidgetsListDrawableFactory listDrawableFactory,
|
||||
WidgetsListAdapter listAdapter) {
|
||||
mLayoutInflater = layoutInflater;
|
||||
mOnHeaderClickListener = onHeaderClickListener;
|
||||
mListDrawableFactory = listDrawableFactory;
|
||||
mWidgetsListAdapter = listAdapter;
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
|
||||
public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) {
|
||||
WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
|
||||
R.layout.widgets_list_row_header, parent, false);
|
||||
|
||||
header.setBackground(mListDrawableFactory.createHeaderBackgroundDrawable());
|
||||
return new WidgetsListSearchHeaderHolder(header);
|
||||
}
|
||||
|
||||
@@ -53,12 +56,13 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
|
||||
public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder,
|
||||
WidgetsListSearchHeaderEntry data, int position) {
|
||||
WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
|
||||
widgetsListHeader.updateListBackground(
|
||||
/* isFirst= */ position == 0,
|
||||
/* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
|
||||
/* isExpanded= */ data.isWidgetListShown());
|
||||
widgetsListHeader.applyFromItemInfoWithIcon(data);
|
||||
widgetsListHeader.setExpanded(data.isWidgetListShown());
|
||||
widgetsListHeader.setListDrawableState(
|
||||
WidgetsListDrawableState.obtain(
|
||||
/* isFirst= */ position == 0,
|
||||
/* isLast= */ position == mWidgetsListAdapter.getItemCount() - 1,
|
||||
/* isExpanded= */ data.isWidgetListShown()));
|
||||
widgetsListHeader.setOnExpandChangeListener(isExpanded ->
|
||||
mOnHeaderClickListener.onHeaderClicked(isExpanded,
|
||||
new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user)));
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.widget.picker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TableLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
/**
|
||||
* Extension of {@link TableLayout} to support the drawable states used by
|
||||
* {@link WidgetsListDrawableState}.
|
||||
*/
|
||||
public class WidgetsListTableView extends TableLayout {
|
||||
|
||||
@Nullable private WidgetsListDrawableState mListDrawableState;
|
||||
|
||||
public WidgetsListTableView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public WidgetsListTableView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
/** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
|
||||
@UiThread
|
||||
public void setListDrawableState(WidgetsListDrawableState state) {
|
||||
if (state == mListDrawableState) return;
|
||||
mListDrawableState = state;
|
||||
refreshDrawableState();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] onCreateDrawableState(int extraSpace) {
|
||||
if (mListDrawableState == null) return super.onCreateDrawableState(extraSpace);
|
||||
// Augment the state set from the super implementation with the custom states from
|
||||
// mListDrawableState.
|
||||
int[] drawableState =
|
||||
super.onCreateDrawableState(extraSpace + mListDrawableState.mStateSet.length);
|
||||
mergeDrawableStates(drawableState, mListDrawableState.mStateSet);
|
||||
return drawableState;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,10 @@
|
||||
*/
|
||||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.LAST;
|
||||
import static com.android.launcher3.widget.picker.WidgetsListDrawableState.MIDDLE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -51,10 +53,8 @@ public final class WidgetsListTableViewHolderBinder
|
||||
private final OnClickListener mIconClickListener;
|
||||
private final OnLongClickListener mIconLongClickListener;
|
||||
private final WidgetPreviewLoader mWidgetPreviewLoader;
|
||||
private final WidgetsListDrawableFactory mListDrawableFactory;
|
||||
private final WidgetsListAdapter mWidgetsListAdapter;
|
||||
private final float mTopBottomCornerRadius;
|
||||
private final float mMiddleCornerRadius;
|
||||
private final float mJoinedCornerRadius;
|
||||
private boolean mApplyBitmapDeferred = false;
|
||||
|
||||
public WidgetsListTableViewHolderBinder(
|
||||
@@ -63,19 +63,14 @@ public final class WidgetsListTableViewHolderBinder
|
||||
OnClickListener iconClickListener,
|
||||
OnLongClickListener iconLongClickListener,
|
||||
WidgetPreviewLoader widgetPreviewLoader,
|
||||
WidgetsListDrawableFactory listDrawableFactory,
|
||||
WidgetsListAdapter listAdapter) {
|
||||
mLayoutInflater = layoutInflater;
|
||||
mIconClickListener = iconClickListener;
|
||||
mIconLongClickListener = iconLongClickListener;
|
||||
mWidgetPreviewLoader = widgetPreviewLoader;
|
||||
mListDrawableFactory = listDrawableFactory;
|
||||
mWidgetsListAdapter = listAdapter;
|
||||
Resources resources = context.getResources();
|
||||
mTopBottomCornerRadius =
|
||||
resources.getDimension(R.dimen.widget_list_top_bottom_corner_radius);
|
||||
mMiddleCornerRadius =
|
||||
resources.getDimension(R.dimen.widget_list_content_corner_radius);
|
||||
mJoinedCornerRadius =
|
||||
resources.getDimension(R.dimen.widget_list_content_joined_corner_radius);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,28 +92,25 @@ public final class WidgetsListTableViewHolderBinder
|
||||
Log.v(TAG, "\nonCreateViewHolder");
|
||||
}
|
||||
|
||||
ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
|
||||
R.layout.widgets_table_container, parent, false);
|
||||
return new WidgetsRowViewHolder(container);
|
||||
WidgetsRowViewHolder viewHolder =
|
||||
new WidgetsRowViewHolder(mLayoutInflater.inflate(
|
||||
R.layout.widgets_table_container, parent, false));
|
||||
viewHolder.mTableContainer.setBackgroundDrawable(
|
||||
mListDrawableFactory.createContentBackgroundDrawable());
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry,
|
||||
int position) {
|
||||
TableLayout table = holder.mTableContainer;
|
||||
WidgetsListTableView table = holder.mTableContainer;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
|
||||
entry.mWidgets.size(), table.getChildCount()));
|
||||
}
|
||||
|
||||
// The content is always joined to an expanded header above.
|
||||
float topRadius = mJoinedCornerRadius;
|
||||
float bottomRadius = position == mWidgetsListAdapter.getItemCount() - 1
|
||||
? mTopBottomCornerRadius
|
||||
: mMiddleCornerRadius;
|
||||
table.setBackgroundDrawable(
|
||||
WidgetsListDrawables.createListBackgroundDrawable(
|
||||
holder.itemView.getContext(), topRadius, bottomRadius));
|
||||
table.setListDrawableState(
|
||||
position == mWidgetsListAdapter.getItemCount() - 1 ? LAST : MIDDLE);
|
||||
|
||||
List<ArrayList<WidgetItem>> widgetItemsTable =
|
||||
WidgetsTableUtils.groupWidgetItemsIntoTable(entry.mWidgets, mMaxSpansPerRow);
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
package com.android.launcher3.widget.picker;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
|
||||
@@ -25,9 +24,9 @@ import com.android.launcher3.R;
|
||||
/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
|
||||
public final class WidgetsRowViewHolder extends ViewHolder {
|
||||
|
||||
public final TableLayout mTableContainer;
|
||||
public final WidgetsListTableView mTableContainer;
|
||||
|
||||
public WidgetsRowViewHolder(ViewGroup v) {
|
||||
public WidgetsRowViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
mTableContainer = v.findViewById(R.id.widgets_table);
|
||||
|
||||
@@ -293,7 +293,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
|
||||
try {
|
||||
final AppIconMenu menu = allApps.
|
||||
getAppIcon(APP_NAME).
|
||||
openMenu();
|
||||
openDeepShortcutMenu();
|
||||
|
||||
executeOnLauncher(
|
||||
launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
|
||||
@@ -341,7 +341,7 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
|
||||
try {
|
||||
final AppIconMenu menu = allApps
|
||||
.getAppIcon(APP_NAME)
|
||||
.openMenu();
|
||||
.openDeepShortcutMenu();
|
||||
final AppIconMenuItem menuItem0 = menu.getMenuItem(0);
|
||||
final AppIconMenuItem menuItem2 = menu.getMenuItem(2);
|
||||
|
||||
|
||||
@@ -51,6 +51,16 @@ public final class AppIcon extends Launchable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Long-clicks the icon to open its menu, and looks at the deep shortcuts container only.
|
||||
*/
|
||||
public AppIconMenu openDeepShortcutMenu() {
|
||||
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
|
||||
return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
|
||||
mObject, "deep_shortcuts_container", LONG_CLICK_EVENT));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addExpectedEventsForLongClick() {
|
||||
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT);
|
||||
|
||||
Reference in New Issue
Block a user