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

Commit a230afa8 authored by Charlie Boutier's avatar Charlie Boutier Committed by Android (Google) Code Review
Browse files

Merge "Revert "Refactor WirelessChargingAnimation.""

parents 6757ccaa 16fafae9
Loading
Loading
Loading
Loading
+254 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.systemui.charging;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Slog;
import android.view.Gravity;
import android.view.WindowManager;

import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.ripple.RippleShader.RippleShape;

/**
 * A WirelessChargingAnimation is a view containing view + animation for wireless charging.
 * @hide
 */
public class WirelessChargingAnimation {
    public static final int UNKNOWN_BATTERY_LEVEL = -1;
    public static final long DURATION = 1500;
    private static final String TAG = "WirelessChargingView";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final WirelessChargingView mCurrentWirelessChargingView;
    private static WirelessChargingView mPreviousWirelessChargingView;

    public interface Callback {
        void onAnimationStarting();
        void onAnimationEnded();
    }

    /**
     * Constructs an empty WirelessChargingAnimation object.  If looper is null,
     * Looper.myLooper() is used.  Must set
     * {@link WirelessChargingAnimation#mCurrentWirelessChargingView}
     * before calling {@link #show} - can be done through {@link #makeWirelessChargingAnimation}.
     * @hide
     */
    private WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper,
            int transmittingBatteryLevel, int batteryLevel, Callback callback, boolean isDozing,
            RippleShape rippleShape, UiEventLogger uiEventLogger) {
        mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
                transmittingBatteryLevel, batteryLevel, callback, isDozing,
                rippleShape, uiEventLogger);
    }

    /**
     * Creates a wireless charging animation object populated with next view.
     *
     * @hide
     */
    public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
            @Nullable Looper looper, int transmittingBatteryLevel, int batteryLevel,
            Callback callback, boolean isDozing, RippleShape rippleShape,
            UiEventLogger uiEventLogger) {
        return new WirelessChargingAnimation(context, looper, transmittingBatteryLevel,
                batteryLevel, callback, isDozing, rippleShape, uiEventLogger);
    }

    /**
     * Creates a charging animation object using mostly default values for non-dozing and unknown
     * battery level without charging number shown.
     */
    public static WirelessChargingAnimation makeChargingAnimationWithNoBatteryLevel(
            @NonNull Context context, RippleShape rippleShape, UiEventLogger uiEventLogger) {
        return makeWirelessChargingAnimation(context, null,
                UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, null, false,
                rippleShape, uiEventLogger);
    }

    /**
     * Show the view for the specified duration.
     */
    public void show(long delay) {
        if (mCurrentWirelessChargingView == null ||
                mCurrentWirelessChargingView.mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        if (mPreviousWirelessChargingView != null) {
            mPreviousWirelessChargingView.hide(0);
        }

        mPreviousWirelessChargingView = mCurrentWirelessChargingView;
        mCurrentWirelessChargingView.show(delay);
        mCurrentWirelessChargingView.hide(delay + DURATION);
    }

    private static class WirelessChargingView {
        private static final int SHOW = 0;
        private static final int HIDE = 1;

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        private final Handler mHandler;
        private final UiEventLogger mUiEventLogger;

        private int mGravity;
        private WirelessChargingLayout mView;
        private WirelessChargingLayout mNextView;
        private WindowManager mWM;
        private Callback mCallback;

        public WirelessChargingView(Context context, @Nullable Looper looper,
                int transmittingBatteryLevel, int batteryLevel, Callback callback,
                boolean isDozing, RippleShape rippleShape, UiEventLogger uiEventLogger) {
            mCallback = callback;
            mNextView = new WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel,
                    isDozing, rippleShape);
            mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
            mUiEventLogger = uiEventLogger;

            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.MATCH_PARENT;
            params.width = WindowManager.LayoutParams.MATCH_PARENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
            params.setTitle("Charging Animation");
            params.layoutInDisplayCutoutMode =
                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
            params.setFitInsetsTypes(0 /* ignore all system bar insets */);
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
            params.setTrustedOverlay();

            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't display wireless animation on a thread that has not called "
                                    + "Looper.prepare()");
                }
            }

            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            handleShow();
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                    }
                }
            };
        }

        public void show(long delay) {
            if (DEBUG) Slog.d(TAG, "SHOW: " + this);
            mHandler.sendMessageDelayed(Message.obtain(mHandler, SHOW), delay);
        }

        public void hide(long duration) {
            mHandler.removeMessages(HIDE);

            if (DEBUG) Slog.d(TAG, "HIDE: " + this);
            mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration);
        }

        private void handleShow() {
            if (DEBUG) {
                Slog.d(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView="
                        + mNextView);
            }

            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = DURATION;

                if (mView.getParent() != null) {
                    if (DEBUG) Slog.d(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (DEBUG) Slog.d(TAG, "ADD! " + mView + " in " + this);

                try {
                    if (mCallback != null) {
                        mCallback.onAnimationStarting();
                    }
                    mWM.addView(mView, mParams);
                    mUiEventLogger.log(WirelessChargingRippleEvent.WIRELESS_RIPPLE_PLAYED);
                } catch (WindowManager.BadTokenException e) {
                    Slog.d(TAG, "Unable to add wireless charging view. " + e);
                }
            }
        }

        private void handleHide() {
            if (DEBUG) Slog.d(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                if (mView.getParent() != null) {
                    if (DEBUG) Slog.d(TAG, "REMOVE! " + mView + " in " + this);
                    if (mCallback != null) {
                        mCallback.onAnimationEnded();
                    }
                    mWM.removeViewImmediate(mView);
                }

                mView = null;
            }
        }

        enum WirelessChargingRippleEvent implements UiEventLogger.UiEventEnum {
            @UiEvent(doc = "Wireless charging ripple effect played")
            WIRELESS_RIPPLE_PLAYED(830);

            private final int mInt;
            WirelessChargingRippleEvent(int id) {
                mInt = id;
            }

            @Override public int getId() {
                return mInt;
            }
        }
    }
}
+22 −24
Original line number Diff line number Diff line
@@ -16,9 +16,6 @@

package com.android.systemui.charging;

import static com.android.systemui.charging.WirelessChargingRippleControllerKt.DEFAULT_DURATION;
import static com.android.systemui.charging.WirelessChargingRippleControllerKt.UNKNOWN_BATTERY_LEVEL;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
@@ -44,43 +41,39 @@ import com.android.systemui.ripple.RippleView;
import java.text.NumberFormat;

/**
 * Layout that is used for wireless charging.
 *
 * <p>Wireless charging layout has {@link RippleView} and text view to show the battery level.
 * @hide
 */
final class WirelessChargingLayout extends FrameLayout {
    private static final long CIRCLE_RIPPLE_ANIMATION_DURATION = 1500;
    private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 1750;
    private static final int SCRIM_COLOR = 0x4C000000;
    private static final int SCRIM_FADE_DURATION = 300;
    private RippleView mRippleView;

    WirelessChargingLayout(Context context, int transmittingBatteryLevel, int batteryLevel,
            boolean isDozing, RippleShape rippleShape, long duration) {
            boolean isDozing, RippleShape rippleShape) {
        super(context);
        init(context, null, transmittingBatteryLevel, batteryLevel, isDozing, rippleShape,
                duration);
        init(context, null, transmittingBatteryLevel, batteryLevel, isDozing, rippleShape);
    }

    private WirelessChargingLayout(Context context) {
        super(context);
        init(context, null, /* isDozing= */ false, RippleShape.CIRCLE,
                DEFAULT_DURATION);
        init(context, null, /* isDozing= */ false, RippleShape.CIRCLE);
    }

    private WirelessChargingLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, /* isDozing= */false, RippleShape.CIRCLE,
                DEFAULT_DURATION);
        init(context, attrs, /* isDozing= */false, RippleShape.CIRCLE);
    }

    private void init(Context c, AttributeSet attrs, boolean isDozing, RippleShape rippleShape,
            long duration) {
        init(c, attrs, -1, -1, isDozing, rippleShape, duration);
    private void init(Context c, AttributeSet attrs, boolean isDozing, RippleShape rippleShape) {
        init(c, attrs, -1, -1, isDozing, rippleShape);
    }

    private void init(Context context, AttributeSet attrs, int transmittingBatteryLevel,
            int batteryLevel, boolean isDozing, RippleShape rippleShape, long duration) {
            int batteryLevel, boolean isDozing, RippleShape rippleShape) {
        final boolean showTransmittingBatteryLevel =
                (transmittingBatteryLevel != UNKNOWN_BATTERY_LEVEL);
                (transmittingBatteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL);

        // set style based on background
        int style = R.style.ChargingAnim_WallpaperBackground;
@@ -93,7 +86,7 @@ final class WirelessChargingLayout extends FrameLayout {
        // amount of battery:
        final TextView percentage = findViewById(R.id.wireless_charging_percentage);

        if (batteryLevel != UNKNOWN_BATTERY_LEVEL) {
        if (batteryLevel != WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL) {
            percentage.setText(NumberFormat.getPercentInstance().format(batteryLevel / 100f));
            percentage.setAlpha(0);
        }
@@ -132,6 +125,7 @@ final class WirelessChargingLayout extends FrameLayout {
        // play all animations together
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(textSizeAnimator, textOpacityAnimator, textFadeAnimator);

        ValueAnimator scrimFadeInAnimator = ObjectAnimator.ofArgb(this,
                "backgroundColor", Color.TRANSPARENT, SCRIM_COLOR);
        scrimFadeInAnimator.setDuration(SCRIM_FADE_DURATION);
@@ -140,21 +134,25 @@ final class WirelessChargingLayout extends FrameLayout {
                "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT);
        scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION);
        scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR);
        scrimFadeOutAnimator.setStartDelay(duration - SCRIM_FADE_DURATION);
        scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE
                ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION)
                - SCRIM_FADE_DURATION);
        AnimatorSet animatorSetScrim = new AnimatorSet();
        animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator);
        animatorSetScrim.start();

        mRippleView = findViewById(R.id.wireless_charging_ripple);
        mRippleView.setupShader(rippleShape);
        mRippleView.setDuration(duration);
        int color = Utils.getColorAttr(mRippleView.getContext(),
                android.R.attr.colorAccent).getDefaultColor();
        if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
            mRippleView.setDuration(ROUNDED_BOX_RIPPLE_ANIMATION_DURATION);
            mRippleView.setSparkleStrength(0.22f);
            int color = Utils.getColorAttr(mRippleView.getContext(),
                    android.R.attr.colorAccent).getDefaultColor();
            mRippleView.setColor(ColorUtils.setAlphaComponent(color, 28));
        } else {
            mRippleView.setColor(ColorUtils.setAlphaComponent(color, 45));
            mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
            mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
                    android.R.attr.colorAccent).getDefaultColor());
        }

        OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
+0 −116
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.systemui.charging

import android.content.Context
import android.view.WindowManager
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.UiEventLogger
import com.android.systemui.charging.WirelessChargingView.WirelessChargingRippleEvent
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogLevel
import com.android.systemui.log.dagger.ChargingLog
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject

const val UNKNOWN_BATTERY_LEVEL = -1
const val DEFAULT_DURATION: Long = 1500

/**
 * Controls the wireless charging animation.
 */
@SysUISingleton
class WirelessChargingRippleController @Inject constructor(
        context: Context,
        private val uiEventLogger: UiEventLogger,
        @Main private val delayableExecutor: DelayableExecutor,
        @ChargingLog private val logBuffer: LogBuffer
) {
    private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE)
            as WindowManager

    @VisibleForTesting
    var wirelessChargingView: WirelessChargingView? = null
    private var callback: Callback? = null

    companion object {
        private const val TAG = "WirelessChargingRippleController"
    }

    /**
     * Shows the wireless charging view with the given delay.
     *
     * If there's already the animation is playing, the new request will be disregarded.
     * @param wirelessChargingView WirelessChargingView to display.
     * @param delay the start delay of the WirelessChargingView.
     * @param callback optional callback that is triggered on animations start and end.
     */
    fun show(wirelessChargingView: WirelessChargingView, delay: Long, callback: Callback? = null) {
        // Avoid multiple animation getting triggered.
        if (this.wirelessChargingView != null) {
            logBuffer.log(TAG, LogLevel.INFO, "Already playing animation, disregard " +
                    "$wirelessChargingView")
            return
        }

        this.wirelessChargingView = wirelessChargingView
        this.callback = callback

        logBuffer.log(TAG, LogLevel.DEBUG, "SHOW: $wirelessChargingView")
        delayableExecutor.executeDelayed({ showInternal() }, delay)

        logBuffer.log(TAG, LogLevel.DEBUG, "HIDE: $wirelessChargingView")
        delayableExecutor.executeDelayed({ hideInternal() }, delay + wirelessChargingView.duration)
    }

    private fun showInternal() {
        if (wirelessChargingView == null) {
            return
        }

        val chargingLayout = wirelessChargingView!!.getWirelessChargingLayout()
        try {
            callback?.onAnimationStarting()
            windowManager.addView(chargingLayout, wirelessChargingView!!.wmLayoutParams)
            uiEventLogger.log(WirelessChargingRippleEvent.WIRELESS_RIPPLE_PLAYED)
        } catch (e: WindowManager.BadTokenException) {
            logBuffer.log(TAG, LogLevel.ERROR, "Unable to add wireless charging view. $e")
        }
    }

    private fun hideInternal() {
        wirelessChargingView?.getWirelessChargingLayout().let {
            callback?.onAnimationEnded()
            if (it?.parent != null) {
                windowManager.removeViewImmediate(it)
            }
        }
        wirelessChargingView = null
        callback = null
    }

    /**
     * Callbacks that are triggered on animation events.
     */
    interface Callback {
        /** Triggered when the animation starts playing. */
        fun onAnimationStarting()
        /** Triggered when the animation ends playing. */
        fun onAnimationEnded()
    }
}
+0 −91
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.systemui.charging

import android.content.Context
import android.graphics.PixelFormat
import android.view.View
import android.view.WindowManager
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger.UiEventEnum
import com.android.systemui.ripple.RippleShader.RippleShape

/**
 * WirelessChargingView that encapsulates the current and next [WirelessChargingLayout]s.
 */
class WirelessChargingView (
        context: Context,
        transmittingBatteryLevel: Int,
        batteryLevel: Int,
        isDozing: Boolean,
        rippleShape: RippleShape,
        val duration: Long,
) {
    companion object {
        @JvmStatic
        fun create(
                context: Context,
                transmittingBatteryLevel: Int,
                batteryLevel: Int,
                isDozing: Boolean,
                rippleShape: RippleShape,
                duration: Long = DEFAULT_DURATION
        ): WirelessChargingView {
            return WirelessChargingView(context, transmittingBatteryLevel, batteryLevel, isDozing,
                    rippleShape, duration)
        }

        @JvmStatic
        fun createWithNoBatteryLevel(
                context: Context,
                rippleShape: RippleShape,
                duration: Long = DEFAULT_DURATION
        ): WirelessChargingView {
            return create(context,
                    UNKNOWN_BATTERY_LEVEL, UNKNOWN_BATTERY_LEVEL, false, rippleShape,
                    duration)
        }
    }

    val wmLayoutParams = WindowManager.LayoutParams().apply {
        height = WindowManager.LayoutParams.MATCH_PARENT
        width = WindowManager.LayoutParams.MATCH_PARENT
        format = PixelFormat.TRANSLUCENT
        type = WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
        title = "Charging Animation"
        layoutInDisplayCutoutMode =
                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        fitInsetsTypes = 0
        flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
        packageName = context.applicationContext.opPackageName
        setTrustedOverlay()
    }

    private val wirelessChargingLayout: WirelessChargingLayout =
            WirelessChargingLayout(context, transmittingBatteryLevel, batteryLevel, isDozing,
                    rippleShape, duration)
    fun getWirelessChargingLayout(): View = wirelessChargingLayout

    internal enum class WirelessChargingRippleEvent(private val mInt: Int) : UiEventEnum {
        @UiEvent(doc = "Wireless charging ripple effect played")
        WIRELESS_RIPPLE_PLAYED(830);

        override fun getId(): Int {
            return mInt
        }
    }
}
+0 −35
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.log.dagger;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.android.systemui.log.LogBuffer;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;

import javax.inject.Qualifier;

/**
 * A {@link LogBuffer} for {@link com.android.systemui.charging}
 */
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface ChargingLog {
}
Loading