| /* |
| * 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.quickstep.interaction; |
| |
| import static com.android.launcher3.Utilities.mapBoundToRange; |
| import static com.android.launcher3.Utilities.mapRange; |
| import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; |
| import static com.android.launcher3.anim.Interpolators.LINEAR; |
| import static com.android.quickstep.OverviewComponentObserver.startHomeIntentSafely; |
| |
| import android.animation.Animator; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.ColorMatrix; |
| import android.graphics.ColorMatrixColorFilter; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.PointF; |
| import android.graphics.RadialGradient; |
| import android.graphics.Rect; |
| import android.graphics.Shader.TileMode; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.os.VibrationEffect; |
| import android.os.Vibrator; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.View.AccessibilityDelegate; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import androidx.annotation.Nullable; |
| import androidx.core.graphics.ColorUtils; |
| |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.InvariantDeviceProfile; |
| import com.android.launcher3.R; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.AnimatedFloat; |
| import com.android.launcher3.anim.AnimatorPlaybackController; |
| import com.android.launcher3.taskbar.TaskbarManager; |
| import com.android.launcher3.util.Executors; |
| import com.android.quickstep.GestureState; |
| import com.android.quickstep.TouchInteractionService.TISBinder; |
| import com.android.quickstep.util.LottieAnimationColorUtils; |
| import com.android.quickstep.util.TISBindHelper; |
| |
| import com.airbnb.lottie.LottieAnimationView; |
| |
| import java.net.URISyntaxException; |
| import java.util.Map; |
| |
| /** |
| * A page shows after SUW flow to hint users to swipe up from the bottom of the screen to go home |
| * for the gestural system navigation. |
| */ |
| public class AllSetActivity extends Activity { |
| |
| private static final String LOG_TAG = "AllSetActivity"; |
| private static final String URI_SYSTEM_NAVIGATION_SETTING = |
| "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S.:settings:fragment_args_key=gesture_system_navigation_input_summary;S.:settings:show_fragment=com.android.settings.gestures.SystemNavigationGestureSettings;end"; |
| private static final String EXTRA_ACCENT_COLOR_DARK_MODE = "suwColorAccentDark"; |
| private static final String EXTRA_ACCENT_COLOR_LIGHT_MODE = "suwColorAccentLight"; |
| private static final String EXTRA_DEVICE_NAME = "suwDeviceName"; |
| |
| private static final String LOTTIE_PRIMARY_COLOR_TOKEN = ".primary"; |
| private static final String LOTTIE_TERTIARY_COLOR_TOKEN = ".tertiary"; |
| |
| private static final float HINT_BOTTOM_FACTOR = 1 - .94f; |
| |
| private static final int MAX_SWIPE_DURATION = 350; |
| |
| private static final float ANIMATION_PAUSE_ALPHA_THRESHOLD = 0.1f; |
| |
| private TISBindHelper mTISBindHelper; |
| private TISBinder mBinder; |
| @Nullable private TaskbarManager mTaskbarManager = null; |
| |
| private final AnimatedFloat mSwipeProgress = new AnimatedFloat(this::onSwipeProgressUpdate); |
| private BgDrawable mBackground; |
| private View mRootView; |
| private float mSwipeUpShift; |
| |
| @Nullable private Vibrator mVibrator; |
| private LottieAnimationView mAnimatedBackground; |
| private Animator.AnimatorListener mBackgroundAnimatorListener; |
| |
| private AnimatorPlaybackController mLauncherStartAnim = null; |
| |
| @Override |
| protected void onCreate(@Nullable Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.activity_allset); |
| mRootView = findViewById(R.id.root_view); |
| mRootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); |
| |
| Resources resources = getResources(); |
| int mode = resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; |
| boolean isDarkTheme = mode == Configuration.UI_MODE_NIGHT_YES; |
| Intent intent = getIntent(); |
| int accentColor = intent.getIntExtra( |
| isDarkTheme ? EXTRA_ACCENT_COLOR_DARK_MODE : EXTRA_ACCENT_COLOR_LIGHT_MODE, |
| isDarkTheme ? Color.WHITE : Color.BLACK); |
| |
| ((ImageView) findViewById(R.id.icon)).getDrawable().mutate().setTint(accentColor); |
| |
| mBackground = new BgDrawable(this); |
| mRootView.setBackground(mBackground); |
| mSwipeUpShift = resources.getDimension(R.dimen.allset_swipe_up_shift); |
| |
| TextView subtitle = findViewById(R.id.subtitle); |
| String suwDeviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); |
| subtitle.setText(getString( |
| R.string.allset_description_generic, |
| !TextUtils.isEmpty(suwDeviceName) |
| ? suwDeviceName : getString(R.string.default_device_name))); |
| |
| TextView settings = findViewById(R.id.navigation_settings); |
| settings.setTextColor(accentColor); |
| settings.setOnClickListener(v -> { |
| try { |
| startActivityForResult( |
| Intent.parseUri(URI_SYSTEM_NAVIGATION_SETTING, 0), 0); |
| } catch (URISyntaxException e) { |
| Log.e(LOG_TAG, "Failed to parse system nav settings intent", e); |
| } |
| }); |
| |
| TextView hint = findViewById(R.id.hint); |
| DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(this).getDeviceProfile(this); |
| if (!dp.isGestureMode) { |
| hint.setText(R.string.allset_button_hint); |
| } |
| hint.setAccessibilityDelegate(new SkipButtonAccessibilityDelegate()); |
| |
| mTISBindHelper = new TISBindHelper(this, this::onTISConnected); |
| |
| mVibrator = getSystemService(Vibrator.class); |
| mAnimatedBackground = findViewById(R.id.animated_background); |
| // There's a bug in the currently used external Lottie library (v5.2.0), and it doesn't load |
| // the correct animation from the raw resources when configuration changes, so we need to |
| // manually load the resource and pass it to Lottie. |
| mAnimatedBackground.setAnimation(resources.openRawResource(R.raw.all_set_page_bg), |
| null); |
| |
| LottieAnimationColorUtils.updateColors( |
| mAnimatedBackground, |
| Map.of(LOTTIE_PRIMARY_COLOR_TOKEN, R.color.all_set_bg_primary, |
| LOTTIE_TERTIARY_COLOR_TOKEN, R.color.all_set_bg_tertiary), |
| getTheme()); |
| |
| startBackgroundAnimation(); |
| } |
| |
| private void runOnUiHelperThread(Runnable runnable) { |
| if (!isResumed() |
| || getContentViewAlphaForSwipeProgress() <= ANIMATION_PAUSE_ALPHA_THRESHOLD) { |
| return; |
| } |
| Executors.UI_HELPER_EXECUTOR.execute(runnable); |
| } |
| |
| private void startBackgroundAnimation() { |
| if (!Utilities.ATLEAST_S || mVibrator == null) { |
| return; |
| } |
| boolean supportsThud = mVibrator.areAllPrimitivesSupported( |
| VibrationEffect.Composition.PRIMITIVE_THUD); |
| |
| if (!supportsThud && !mVibrator.areAllPrimitivesSupported( |
| VibrationEffect.Composition.PRIMITIVE_TICK)) { |
| return; |
| } |
| if (mBackgroundAnimatorListener == null) { |
| VibrationEffect vibrationEffect = VibrationEffect.startComposition() |
| .addPrimitive(supportsThud |
| ? VibrationEffect.Composition.PRIMITIVE_THUD |
| : VibrationEffect.Composition.PRIMITIVE_TICK, |
| /* scale= */ 1.0f, |
| /* delay= */ 50) |
| .compose(); |
| |
| mBackgroundAnimatorListener = |
| new Animator.AnimatorListener() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animation) { |
| runOnUiHelperThread(() -> mVibrator.vibrate(vibrationEffect)); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| runOnUiHelperThread(mVibrator::cancel); |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| runOnUiHelperThread(mVibrator::cancel); |
| } |
| }; |
| } |
| mAnimatedBackground.addAnimatorListener(mBackgroundAnimatorListener); |
| mAnimatedBackground.playAnimation(); |
| } |
| |
| private void setSetupUIVisible(boolean visible) { |
| if (mBinder == null || mTaskbarManager == null) return; |
| mTaskbarManager.setSetupUIVisible(visible); |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| maybeResumeOrPauseBackgroundAnimation(); |
| if (mBinder != null) { |
| setSetupUIVisible(true); |
| mBinder.setSwipeUpProxy(this::createSwipeUpProxy); |
| } |
| } |
| |
| private void onTISConnected(TISBinder binder) { |
| mBinder = binder; |
| mTaskbarManager = mBinder.getTaskbarManager(); |
| setSetupUIVisible(isResumed()); |
| mBinder.setSwipeUpProxy(isResumed() ? this::createSwipeUpProxy : null); |
| mBinder.setOverviewTargetChangeListener(mBinder::preloadOverviewForSUWAllSet); |
| mBinder.preloadOverviewForSUWAllSet(); |
| if (mTaskbarManager != null) { |
| mLauncherStartAnim = mTaskbarManager.createLauncherStartFromSuwAnim(MAX_SWIPE_DURATION); |
| } |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| clearBinderOverride(); |
| maybeResumeOrPauseBackgroundAnimation(); |
| if (mSwipeProgress.value >= 1) { |
| finishAndRemoveTask(); |
| dispatchLauncherAnimStartEnd(); |
| } |
| } |
| |
| private void clearBinderOverride() { |
| if (mBinder != null) { |
| setSetupUIVisible(false); |
| mBinder.setSwipeUpProxy(null); |
| mBinder.setOverviewTargetChangeListener(null); |
| } |
| } |
| |
| /** |
| * Should be called when we have successfully reached Launcher, so we dispatch to animation |
| * listeners to ensure the state matches the visual animation that just occurred. |
| */ |
| private void dispatchLauncherAnimStartEnd() { |
| if (mLauncherStartAnim != null) { |
| mLauncherStartAnim.dispatchOnStart(); |
| mLauncherStartAnim.dispatchOnEnd(); |
| mLauncherStartAnim = null; |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| mTISBindHelper.onDestroy(); |
| clearBinderOverride(); |
| if (mBackgroundAnimatorListener != null) { |
| mAnimatedBackground.removeAnimatorListener(mBackgroundAnimatorListener); |
| } |
| dispatchLauncherAnimStartEnd(); |
| } |
| |
| private AnimatedFloat createSwipeUpProxy(GestureState state) { |
| if (state.getRunningTaskId() != getTaskId()) { |
| return null; |
| } |
| mSwipeProgress.updateValue(0); |
| return mSwipeProgress; |
| } |
| |
| private float getContentViewAlphaForSwipeProgress() { |
| return Utilities.mapBoundToRange( |
| mSwipeProgress.value, 0, HINT_BOTTOM_FACTOR, 1, 0, LINEAR); |
| } |
| |
| private void maybeResumeOrPauseBackgroundAnimation() { |
| boolean shouldPlayAnimation = |
| getContentViewAlphaForSwipeProgress() > ANIMATION_PAUSE_ALPHA_THRESHOLD |
| && isResumed(); |
| if (mAnimatedBackground.isAnimating() && !shouldPlayAnimation) { |
| mAnimatedBackground.pauseAnimation(); |
| } else if (!mAnimatedBackground.isAnimating() && shouldPlayAnimation) { |
| mAnimatedBackground.resumeAnimation(); |
| } |
| } |
| |
| private void onSwipeProgressUpdate() { |
| mBackground.setProgress(mSwipeProgress.value); |
| float alpha = getContentViewAlphaForSwipeProgress(); |
| mRootView.setAlpha(alpha); |
| mRootView.setTranslationY((alpha - 1) * mSwipeUpShift); |
| |
| if (mLauncherStartAnim != null) { |
| mLauncherStartAnim.setPlayFraction( |
| FAST_OUT_SLOW_IN.getInterpolation(mSwipeProgress.value)); |
| } |
| maybeResumeOrPauseBackgroundAnimation(); |
| } |
| |
| /** |
| * Accessibility delegate which exposes a click event without making the view |
| * clickable in touch mode |
| */ |
| private class SkipButtonAccessibilityDelegate extends AccessibilityDelegate { |
| |
| @Override |
| public AccessibilityNodeInfo createAccessibilityNodeInfo(View host) { |
| AccessibilityNodeInfo info = super.createAccessibilityNodeInfo(host); |
| info.addAction(AccessibilityAction.ACTION_CLICK); |
| info.setClickable(true); |
| return info; |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(View host, int action, Bundle args) { |
| if (action == AccessibilityAction.ACTION_CLICK.getId()) { |
| startHomeIntentSafely(AllSetActivity.this, null); |
| finish(); |
| return true; |
| } |
| return super.performAccessibilityAction(host, action, args); |
| } |
| } |
| |
| private static class BgDrawable extends Drawable { |
| |
| private static final float START_SIZE_FACTOR = .5f; |
| private static final float END_SIZE_FACTOR = 2; |
| private static final float GRADIENT_END_PROGRESS = .5f; |
| |
| private final Paint mPaint = new Paint(); |
| private final RadialGradient mMaskGrad; |
| private final Matrix mMatrix = new Matrix(); |
| |
| private final ColorMatrix mColorMatrix = new ColorMatrix(); |
| private final ColorMatrixColorFilter mColorFilter = |
| new ColorMatrixColorFilter(mColorMatrix); |
| |
| private final int mColor; |
| private float mProgress = 0; |
| |
| BgDrawable(Context context) { |
| mColor = context.getColor(R.color.all_set_page_background); |
| mMaskGrad = new RadialGradient(0, 0, 1, |
| new int[] {ColorUtils.setAlphaComponent(mColor, 0), mColor}, |
| new float[]{0, 1}, TileMode.CLAMP); |
| |
| mPaint.setShader(mMaskGrad); |
| mPaint.setColorFilter(mColorFilter); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (mProgress <= 0) { |
| canvas.drawColor(mColor); |
| return; |
| } |
| |
| // Update the progress to half the size only. |
| float progress = mapBoundToRange(mProgress, |
| 0, GRADIENT_END_PROGRESS, 0, 1, LINEAR); |
| Rect bounds = getBounds(); |
| float x = bounds.exactCenterX(); |
| float height = bounds.height(); |
| |
| float size = PointF.length(x, height); |
| float radius = size * mapRange(progress, START_SIZE_FACTOR, END_SIZE_FACTOR); |
| float y = mapRange(progress, height + radius , height / 2); |
| mMatrix.setTranslate(x, y); |
| mMatrix.postScale(radius, radius, x, y); |
| mMaskGrad.setLocalMatrix(mMatrix); |
| |
| // Change the alpha-addition-component (index 19) so that every pixel is updated |
| // accordingly |
| mColorMatrix.getArray()[19] = mapBoundToRange(mProgress, 0, 1, 0, -255, LINEAR); |
| mColorFilter.setColorMatrix(mColorMatrix); |
| |
| canvas.drawPaint(mPaint); |
| } |
| |
| public void setProgress(float progress) { |
| if (mProgress != progress) { |
| mProgress = progress; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| public void setAlpha(int i) { } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { } |
| } |
| } |