Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df8d239a authored by Wei Sheng Shih's avatar Wei Sheng Shih Committed by Android (Google) Code Review
Browse files

Merge "Improve app launch animation (1/N)"

parents b3e0793a 73e0da3c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -17,4 +17,6 @@
<resources>
    <!-- Bubbles -->
    <color name="bubbles_icon_tint">@color/GM2_grey_200</color>
    <!-- Splash screen-->
    <color name="splash_window_background_default">@color/splash_screen_bg_dark</color>
</resources>
 No newline at end of file
+5 −0
Original line number Diff line number Diff line
@@ -35,4 +35,9 @@
    <color name="GM2_grey_200">#E8EAED</color>
    <color name="GM2_grey_700">#5F6368</color>
    <color name="GM2_grey_800">#3C4043</color>

    <!-- Splash screen -->
    <color name="splash_screen_bg_light">#FFFFFF</color>
    <color name="splash_screen_bg_dark">#000000</color>
    <color name="splash_window_background_default">@color/splash_screen_bg_light</color>
</resources>
 No newline at end of file
+3 −0
Original line number Diff line number Diff line
@@ -167,4 +167,7 @@
    <!-- Size of padding for the user education cling, this should at minimum be larger than
        individual_bubble_size + some padding. -->
    <dimen name="bubble_stack_user_education_side_inset">72dp</dimen>

    <!-- The width/height of the icon view on staring surface. -->
    <dimen name="starting_surface_icon_size">108dp</dimen>
</resources>
+565 −0
Original line number Diff line number Diff line
/*
 * 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.wm.shell.startingsurface;

import android.annotation.NonNull;
import android.app.ActivityThread;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.util.Slog;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;

import com.android.internal.R;
import com.android.internal.graphics.palette.Palette;
import com.android.internal.graphics.palette.Quantizer;
import com.android.internal.graphics.palette.VariationalKMeansQuantizer;
import com.android.internal.policy.PhoneWindow;

import java.util.List;

/**
 * Util class to create the view for a splash screen content.
 */
class SplashscreenContentDrawer {
    private static final String TAG = StartingSurfaceDrawer.TAG;
    private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN;

    // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
    // icon which it's non-transparent foreground area is similar to it's background area, then
    // do not enlarge the foreground drawable.
    // For example, an icon with the foreground 108*108 opaque pixels and it's background
    // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
    private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
    private final Context mContext;
    private int mIconSize;

    SplashscreenContentDrawer(Context context) {
        mContext = context;
    }

    private void updateDensity() {
        mIconSize = mContext.getResources().getDimensionPixelSize(
                com.android.wm.shell.R.dimen.starting_surface_icon_size);
    }

    private int getSystemBGColor() {
        final Context systemContext = ActivityThread.currentApplication();
        if (systemContext == null) {
            Slog.e(TAG, "System context does not exist!");
            return Color.BLACK;
        }
        final Resources res = systemContext.getResources();
        return res.getColor(com.android.wm.shell.R.color.splash_window_background_default);
    }

    private Drawable createDefaultBackgroundDrawable() {
        return new ColorDrawable(getSystemBGColor());
    }

    View makeSplashScreenContentView(PhoneWindow win, Context context, int iconRes,
            int splashscreenContentResId) {
        updateDensity();
        win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        // splash screen content will be deprecated after S.
        final View ssc = makeSplashscreenContentDrawable(win, context, splashscreenContentResId);
        if (ssc != null) {
            return ssc;
        }

        final TypedArray typedArray = context.obtainStyledAttributes(
                com.android.internal.R.styleable.Window);
        final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
        typedArray.recycle();
        final Drawable themeBGDrawable;
        if (resId == 0) {
            Slog.w(TAG, "Window background not exist!");
            themeBGDrawable = createDefaultBackgroundDrawable();
        } else {
            themeBGDrawable = context.getDrawable(resId);
        }
        final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes)
                : context.getPackageManager().getDefaultActivityIcon();
        // TODO (b/173975965) Tracking the performance on improved splash screen.
        final StartingWindowViewBuilder builder = new StartingWindowViewBuilder();
        return builder
                .setPhoneWindow(win)
                .setContext(context)
                .setThemeDrawable(themeBGDrawable)
                .setIconDrawable(iconDrawable).build();
    }

    private class StartingWindowViewBuilder {
        // materials
        private Drawable mThemeBGDrawable;
        private Drawable mIconDrawable;
        private PhoneWindow mPhoneWindow;
        private Context mContext;

        // result
        private boolean mBuildComplete = false;
        private View mCachedResult;
        private int mThemeColor;
        private Drawable mFinalIconDrawable;
        private float mScale = 1f;

        StartingWindowViewBuilder setThemeDrawable(Drawable background) {
            mThemeBGDrawable = background;
            mBuildComplete = false;
            return this;
        }

        StartingWindowViewBuilder setIconDrawable(Drawable iconDrawable) {
            mIconDrawable = iconDrawable;
            mBuildComplete = false;
            return this;
        }

        StartingWindowViewBuilder setPhoneWindow(PhoneWindow window) {
            mPhoneWindow = window;
            mBuildComplete = false;
            return this;
        }

        StartingWindowViewBuilder setContext(Context context) {
            mContext = context;
            mBuildComplete = false;
            return this;
        }

        View build() {
            if (mBuildComplete) {
                return mCachedResult;
            }
            if (mPhoneWindow == null || mContext == null) {
                Slog.e(TAG, "Unable to create StartingWindowView, lack of materials!");
                return null;
            }
            if (mThemeBGDrawable == null) {
                Slog.w(TAG, "Theme Background Drawable is null, forget to set Theme Drawable?");
                mThemeBGDrawable = createDefaultBackgroundDrawable();
            }
            processThemeColor();
            if (!processAdaptiveIcon() && mIconDrawable != null) {
                if (DEBUG) {
                    Slog.d(TAG, "The icon is not an AdaptiveIconDrawable");
                }
                mFinalIconDrawable = mIconDrawable;
            }
            final int iconSize = mFinalIconDrawable != null ? (int) (mIconSize * mScale) : 0;
            mCachedResult = fillViewWithIcon(mPhoneWindow, mContext, iconSize, mFinalIconDrawable);
            mBuildComplete = true;
            return mCachedResult;
        }

        private void processThemeColor() {
            final DrawableColorTester themeBGTester =
                    new DrawableColorTester(mThemeBGDrawable, true /* filterTransparent */);
            if (themeBGTester.nonTransparentRatio() == 0) {
                // the window background is transparent, unable to draw
                Slog.w(TAG, "Window background is transparent, fill background with black color");
                mThemeColor = getSystemBGColor();
            } else {
                mThemeColor = themeBGTester.getDominateColor();
            }
        }

        private boolean processAdaptiveIcon() {
            if (!(mIconDrawable instanceof AdaptiveIconDrawable)) {
                return false;
            }

            final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) mIconDrawable;
            final DrawableColorTester backIconTester =
                    new DrawableColorTester(adaptiveIconDrawable.getBackground());

            final Drawable iconForeground = adaptiveIconDrawable.getForeground();
            final DrawableColorTester foreIconTester =
                    new DrawableColorTester(iconForeground, true /* filterTransparent */);

            final boolean foreComplex = foreIconTester.isComplexColor();
            final int foreMainColor = foreIconTester.getDominateColor();

            if (DEBUG) {
                Slog.d(TAG, "foreground complex color? " + foreComplex + " main color: "
                        + Integer.toHexString(foreMainColor));
            }
            final boolean backComplex = backIconTester.isComplexColor();
            final int backMainColor = backIconTester.getDominateColor();
            if (DEBUG) {
                Slog.d(TAG, "background complex color? " + backComplex + " main color: "
                        + Integer.toHexString(backMainColor));
                Slog.d(TAG, "theme color? " + Integer.toHexString(mThemeColor));
            }

            // Only draw the foreground of AdaptiveIcon to the splash screen if below condition
            // meet:
            // A. The background of the adaptive icon is not complicated. If it is complicated,
            // it may contain some information, and
            // B. The background of the adaptive icon is similar to the theme color, or
            // C. The background of the adaptive icon is grayscale, and the foreground of the
            // adaptive icon forms a certain contrast with the theme color.
            if (!backComplex && (isRgbSimilarInHsv(mThemeColor, backMainColor)
                    || (backIconTester.isGrayscale()
                    && !isRgbSimilarInHsv(mThemeColor, foreMainColor)))) {
                if (DEBUG) {
                    Slog.d(TAG, "makeSplashScreenContentView: choose fg icon");
                }
                // Using AdaptiveIconDrawable here can help keep the shape consistent with the
                // current settings.
                mFinalIconDrawable = new AdaptiveIconDrawable(
                        new ColorDrawable(mThemeColor), iconForeground);
                // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
                // should enlarge the size 108/72 if we only draw adaptiveIcon's foreground.
                if (foreIconTester.nonTransparentRatio() < ENLARGE_FOREGROUND_ICON_THRESHOLD) {
                    mScale = 1.5f;
                }
            } else {
                if (DEBUG) {
                    Slog.d(TAG, "makeSplashScreenContentView: draw whole icon");
                }
                mFinalIconDrawable = adaptiveIconDrawable;
            }
            return true;
        }

        private View fillViewWithIcon(PhoneWindow win, Context context,
                int iconSize, Drawable iconDrawable) {
            final StartingSurfaceWindowView surfaceWindowView =
                    new StartingSurfaceWindowView(context, iconSize);
            surfaceWindowView.setBackground(new ColorDrawable(mThemeColor));
            if (iconDrawable != null) {
                surfaceWindowView.setIconDrawable(iconDrawable);
            }
            if (DEBUG) {
                Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + surfaceWindowView);
            }
            win.setContentView(surfaceWindowView);
            makeSystemUIColorsTransparent(win);
            return surfaceWindowView;
        }

        private void makeSystemUIColorsTransparent(PhoneWindow win) {
            win.setStatusBarColor(Color.TRANSPARENT);
            win.setNavigationBarColor(Color.TRANSPARENT);
        }
    }

    private static boolean isRgbSimilarInHsv(int a, int b) {
        if (a == b) {
            return true;
        }
        final float[] aHsv = new float[3];
        final float[] bHsv = new float[3];
        Color.colorToHSV(a, aHsv);
        Color.colorToHSV(b, bHsv);
        // Minimum degree of the hue between two colors, the result range is 0-180.
        int minAngle = (int) Math.abs(aHsv[0] - bHsv[0]);
        minAngle = (minAngle + 180) % 360 - 180;

        // Calculate the difference between two colors based on the HSV dimensions.
        final float normalizeH = minAngle / 180f;
        final double square =  Math.pow(normalizeH, 2)
                + Math.pow(aHsv[1] - bHsv[1], 2)
                + Math.pow(aHsv[2] - bHsv[2], 2);
        final double mean = square / 3;
        final double root = Math.sqrt(mean);
        if (DEBUG) {
            Slog.d(TAG, "hsvDiff " + minAngle + " a: " + Integer.toHexString(a)
                    + " b " + Integer.toHexString(b) + " ah " + aHsv[0] + " bh " + bHsv[0]
                    + " root " + root);
        }
        return root < 0.1;
    }

    private static View makeSplashscreenContentDrawable(PhoneWindow win, Context ctx,
            int splashscreenContentResId) {
        // doesn't support windowSplashscreenContent after S
        // TODO add an allowlist to skip some packages if needed
        final int targetSdkVersion = ctx.getApplicationInfo().targetSdkVersion;
        if (DEBUG) {
            Slog.d(TAG, "target sdk for package: " + targetSdkVersion);
        }
        if (targetSdkVersion >= Build.VERSION_CODES.S) {
            return null;
        }
        if (splashscreenContentResId == 0) {
            return null;
        }
        final Drawable drawable = ctx.getDrawable(splashscreenContentResId);
        if (drawable == null) {
            return null;
        }
        View view = new View(ctx);
        view.setBackground(drawable);
        win.setContentView(view);
        return view;
    }

    private static class DrawableColorTester {
        private final ColorTester mColorChecker;

        DrawableColorTester(Drawable drawable) {
            this(drawable, false /* filterTransparent */);
        }

        DrawableColorTester(Drawable drawable, boolean filterTransparent) {
            // Some applications use LayerDrawable for their windowBackground. To ensure that we
            // only get the real background, so that the color is not affected by the alpha of the
            // upper layer, try to get the lower layer here. This can also speed up the calculation.
            if (drawable instanceof LayerDrawable) {
                LayerDrawable layerDrawable = (LayerDrawable) drawable;
                if (layerDrawable.getNumberOfLayers() > 0) {
                    if (DEBUG) {
                        Slog.d(TAG, "replace drawable with bottom layer drawable");
                    }
                    drawable = layerDrawable.getDrawable(0);
                }
            }
            mColorChecker = drawable instanceof ColorDrawable
                    ? new SingleColorTester((ColorDrawable) drawable)
                    : new ComplexDrawableTester(drawable, filterTransparent);
        }

        public float nonTransparentRatio() {
            return mColorChecker.nonTransparentRatio();
        }

        public boolean isComplexColor() {
            return mColorChecker.isComplexColor();
        }

        public int getDominateColor() {
            return mColorChecker.getDominantColor();
        }

        public boolean isGrayscale() {
            return mColorChecker.isGrayscale();
        }

        /**
         * A help class to check the color information from a Drawable.
         */
        private interface ColorTester {
            float nonTransparentRatio();
            boolean isComplexColor();
            int getDominantColor();
            boolean isGrayscale();
        }

        private static boolean isGrayscaleColor(int color) {
            final int red = Color.red(color);
            final int green = Color.green(color);
            final int blue = Color.blue(color);
            return red == green && green == blue;
        }

        /**
         * For ColorDrawable only.
         * There will be only one color so don't spend too much resource for it.
         */
        private static class SingleColorTester implements ColorTester {
            private final ColorDrawable mColorDrawable;

            SingleColorTester(@NonNull ColorDrawable drawable) {
                mColorDrawable = drawable;
            }

            @Override
            public float nonTransparentRatio() {
                final int alpha = mColorDrawable.getAlpha();
                return (float) (alpha / 255);
            }

            @Override
            public boolean isComplexColor() {
                return false;
            }

            @Override
            public int getDominantColor() {
                return mColorDrawable.getColor();
            }

            @Override
            public boolean isGrayscale() {
                return isGrayscaleColor(mColorDrawable.getColor());
            }
        }

        /**
         * For any other Drawable except ColorDrawable.
         * This will use the Palette API to check the color information and use a quantizer to
         * filter out transparent colors when needed.
         */
        private static class ComplexDrawableTester implements ColorTester {
            private static final int MAX_BITMAP_SIZE = 40;
            private final Palette mPalette;
            private final boolean mFilterTransparent;
            private static final TransparentFilterQuantizer TRANSPARENT_FILTER_QUANTIZER =
                    new TransparentFilterQuantizer();

            ComplexDrawableTester(Drawable drawable, boolean filterTransparent) {
                final Rect initialBounds = drawable.copyBounds();
                int width = drawable.getIntrinsicWidth();
                int height = drawable.getIntrinsicHeight();

                // Some drawables do not have intrinsic dimensions
                if (width <= 0 || height <= 0) {
                    width = MAX_BITMAP_SIZE;
                    height = MAX_BITMAP_SIZE;
                } else {
                    width = Math.min(width, MAX_BITMAP_SIZE);
                    height = Math.min(height, MAX_BITMAP_SIZE);
                }

                final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                final Canvas bmpCanvas = new Canvas(bitmap);
                drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
                drawable.draw(bmpCanvas);
                // restore to original bounds
                drawable.setBounds(initialBounds);

                final Palette.Builder builder = new Palette.Builder(bitmap)
                        .maximumColorCount(5).clearFilters();
                // The Palette API will ignore Alpha, so it cannot handle transparent pixels, but
                // sometimes we will need this information to know if this Drawable object is
                // transparent.
                mFilterTransparent = filterTransparent;
                if (mFilterTransparent) {
                    builder.setQuantizer(TRANSPARENT_FILTER_QUANTIZER);
                }
                mPalette = builder.generate();
                bitmap.recycle();
            }

            @Override
            public float nonTransparentRatio() {
                return mFilterTransparent ? TRANSPARENT_FILTER_QUANTIZER.mNonTransparentRatio : 1;
            }

            @Override
            public boolean isComplexColor() {
                return mPalette.getSwatches().size() > 1;
            }

            @Override
            public int getDominantColor() {
                final Palette.Swatch mainSwatch = mPalette.getDominantSwatch();
                if (mainSwatch != null) {
                    return mainSwatch.getRgb();
                }
                return Color.BLACK;
            }

            @Override
            public boolean isGrayscale() {
                final List<Palette.Swatch> swatches = mPalette.getSwatches();
                if (swatches != null) {
                    for (int i = swatches.size() - 1; i >= 0; i--) {
                        Palette.Swatch swatch = swatches.get(i);
                        if (!isGrayscaleColor(swatch.getRgb())) {
                            return false;
                        }
                    }
                }
                return true;
            }

            private static class TransparentFilterQuantizer implements Quantizer {
                private static final int NON_TRANSPARENT = 0xFF000000;
                private final Quantizer mInnerQuantizer = new VariationalKMeansQuantizer();
                private float mNonTransparentRatio;
                @Override
                public void quantize(final int[] pixels, final int maxColors,
                        final Palette.Filter[] filters) {
                    mNonTransparentRatio = 0;
                    int realSize = 0;
                    for (int i = pixels.length - 1; i > 0; i--) {
                        if ((pixels[i] & NON_TRANSPARENT) != 0) {
                            realSize++;
                        }
                    }
                    if (realSize == 0) {
                        if (DEBUG) {
                            Slog.d(TAG, "quantize: this is pure transparent image");
                        }
                        mInnerQuantizer.quantize(pixels, maxColors, filters);
                        return;
                    }
                    mNonTransparentRatio = (float) realSize / pixels.length;
                    final int[] samplePixels = new int[realSize];
                    int rowIndex = 0;
                    for (int i = pixels.length - 1; i > 0; i--) {
                        if ((pixels[i] & NON_TRANSPARENT) == NON_TRANSPARENT) {
                            samplePixels[rowIndex] = pixels[i];
                            rowIndex++;
                        }
                    }
                    mInnerQuantizer.quantize(samplePixels, maxColors, filters);
                }

                @Override
                public List<Palette.Swatch> getQuantizedColors() {
                    return mInnerQuantizer.getQuantizedColors();
                }
            }
        }
    }

    private static class StartingSurfaceWindowView extends FrameLayout {
        // TODO animate the icon view
        private final View mIconView;

        StartingSurfaceWindowView(Context context, int iconSize) {
            super(context);

            final boolean emptyIcon = iconSize == 0;
            if (emptyIcon) {
                mIconView = null;
            } else {
                mIconView = new View(context);
                FrameLayout.LayoutParams params =
                        new FrameLayout.LayoutParams(iconSize, iconSize);
                params.gravity = Gravity.CENTER;
                addView(mIconView, params);
            }
            setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
        }

        // TODO support animatable icon
        void setIconDrawable(Drawable icon) {
            if (mIconView != null) {
                mIconView.setBackground(icon);
            }
        }
    }
}