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

Commit 7700d5c4 authored by Evan Laird's avatar Evan Laird Committed by Automerger Merge Worker
Browse files

Merge "Privacy chip (3/many)" into sc-dev am: cfa1f3d1

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14442084

Change-Id: Icd34f328a4be4012e93ca5188c2a7382254fed6d
parents e13bb344 cfa1f3d1
Loading
Loading
Loading
Loading
+8 −3
Original line number Diff line number Diff line
@@ -91,6 +91,7 @@ import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.concurrency.ThreadFactory;
import com.android.systemui.util.settings.SecureSettings;

import java.util.ArrayList;
@@ -128,6 +129,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
    private CameraAvailabilityListener mCameraListener;
    private final UserTracker mUserTracker;
    private final PrivacyDotViewController mDotViewController;
    private final ThreadFactory mThreadFactory;

    //TODO: These are piecemeal being updated to Points for now to support non-square rounded
    // corners. for now it is only supposed when reading the intrinsic size from the drawables with
@@ -215,7 +217,8 @@ public class ScreenDecorations extends SystemUI implements Tunable {
            BroadcastDispatcher broadcastDispatcher,
            TunerService tunerService,
            UserTracker userTracker,
            PrivacyDotViewController dotViewController) {
            PrivacyDotViewController dotViewController,
            ThreadFactory threadFactory) {
        super(context);
        mMainHandler = handler;
        mSecureSettings = secureSettings;
@@ -223,6 +226,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {
        mTunerService = tunerService;
        mUserTracker = userTracker;
        mDotViewController = dotViewController;
        mThreadFactory = threadFactory;
    }

    @Override
@@ -233,7 +237,8 @@ public class ScreenDecorations extends SystemUI implements Tunable {
        }
        mHandler = startHandlerThread();
        mHandler.post(this::startOnScreenDecorationsThread);
        mDotViewController.setUiExecutor(mHandler::post);
        mDotViewController.setUiExecutor(
                mThreadFactory.buildDelayableExecutorOnLooper(mHandler.getLooper()));
    }

    @VisibleForTesting
@@ -643,7 +648,7 @@ public class ScreenDecorations extends SystemUI implements Tunable {

        int newRotation = mContext.getDisplay().getRotation();
        if (mRotation != newRotation) {
            mDotViewController.updateRotation(newRotation);
            mDotViewController.setNewRotation(newRotation);
        }

        if (mPendingRotationChange) {
+209 −110
Original line number Diff line number Diff line
@@ -17,20 +17,25 @@
package com.android.systemui.statusbar.events

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.annotation.UiThread
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout

import com.android.internal.annotations.GuardedBy
import com.android.systemui.animation.Interpolators
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher
import com.android.systemui.statusbar.phone.StatusBarMarginUpdatedListener
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN

import java.lang.IllegalStateException
import java.util.concurrent.Executor
@@ -54,31 +59,33 @@ import javax.inject.Inject
@SysUISingleton
class PrivacyDotViewController @Inject constructor(
    @Main private val mainExecutor: Executor,
    private val stateController: StatusBarStateController,
    private val locationPublisher: StatusBarLocationPublisher,
    private val animationScheduler: SystemStatusAnimationScheduler
) {
    private var rotation = 0
    private var leftSize = 0
    private var rightSize = 0

    private var sbHeightPortrait = 0
    private var sbHeightLandscape = 0

    private var hasMultipleHeights = false
    private var needsHeightUpdate = false
    private var needsRotationUpdate = false
    private var needsMarginUpdate = false

    private lateinit var tl: View
    private lateinit var tr: View
    private lateinit var bl: View
    private lateinit var br: View

    // Track which corner is active (based on orientation + RTL)
    private var designatedCorner: View? = null
    // Only can be modified on @UiThread
    private var currentViewState: ViewState = ViewState()

    @GuardedBy("lock")
    private var nextViewState: ViewState = currentViewState.copy()
        set(value) {
            field = value
            scheduleUpdate()
        }
    private val lock = Object()
    private var cancelRunnable: Runnable? = null

    // Privacy dots are created in ScreenDecoration's UiThread, which is not the main thread
    private var uiExecutor: Executor? = null
    private var uiExecutor: DelayableExecutor? = null
    private var e: DelayableExecutor? = null

    private val views: Sequence<View>
        get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl)
@@ -89,29 +96,78 @@ class PrivacyDotViewController @Inject constructor(
                setStatusBarMargins(marginLeft, marginRight)
            }
        })

        stateController.addCallback(object : StatusBarStateController.StateListener {
            override fun onExpandedChanged(isExpanded: Boolean) {
                setStatusBarExpanded(isExpanded)
            }
        })
    }

    fun setUiExecutor(e: Executor) {
    fun setUiExecutor(e: DelayableExecutor) {
        uiExecutor = e
    }

    @UiThread
    fun updateRotation(rot: Int) {
        dlog("updateRotation: ")
        if (rot == rotation) {
    fun setNewRotation(rot: Int) {
        dlog("updateRotation: $rot")

        synchronized(lock) {
            if (rot == nextViewState.rotation) {
                return
            }
        }

        // A rotation has started, hide the views to avoid flicker
        // If we rotated, hide all dotes until the next state resolves
        setCornerVisibilities(View.INVISIBLE)

        if (hasMultipleHeights && (rotation % 2) != (rot % 2)) {
            // we've changed from vertical to horizontal; update status bar height
            needsHeightUpdate = true
        val newCorner = selectDesignatedCorner(rot)
        val index = newCorner.cornerIndex()

        val h = when (rot) {
            ROTATION_NONE, ROTATION_UPSIDE_DOWN -> sbHeightPortrait
            ROTATION_LANDSCAPE, ROTATION_SEASCAPE -> sbHeightLandscape
            else -> 0
        }
        synchronized(lock) {
            nextViewState = nextViewState.copy(
                    rotation = rot,
                    height = h,
                    designatedCorner = newCorner,
                    cornerIndex = index)
        }
    }

    @UiThread
    private fun hideDotView(dot: View, animate: Boolean) {
        dot.clearAnimation()
        if (animate) {
            dot.animate()
                    .setDuration(DURATION)
                    .setInterpolator(Interpolators.ALPHA_OUT)
                    .alpha(0f)
                    .withEndAction { dot.visibility = View.INVISIBLE }
                    .start()
        } else {
            dot.visibility = View.INVISIBLE
        }
    }

        rotation = rot
        needsRotationUpdate = true
    @UiThread
    private fun showDotView(dot: View, animate: Boolean) {
        dot.clearAnimation()
        if (animate) {
            dot.visibility = View.VISIBLE
            dot.alpha = 0f
            dot.animate()
                    .alpha(1f)
                    .setDuration(DURATION)
                    .setInterpolator(Interpolators.ALPHA_IN)
                    .start()
        } else {
            dot.visibility = View.VISIBLE
            dot.alpha = 1f
        }
    }

    @UiThread
@@ -127,14 +183,14 @@ class PrivacyDotViewController @Inject constructor(

    // Update the gravity and margins of the privacy views
    @UiThread
    private fun updateRotations() {
    private fun updateRotations(rotation: Int) {
        // To keep a view in the corner, its gravity is always the description of its current corner
        // Therefore, just figure out which view is in which corner. This turns out to be something
        // like (myCorner - rot) mod 4, where topLeft = 0, topRight = 1, etc. and portrait = 0, and
        // rotating the device counter-clockwise increments rotation by 1

        views.forEach { corner ->
            val rotatedCorner = rotatedCorner(cornerForView(corner))
            val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
            (corner.layoutParams as FrameLayout.LayoutParams).apply {
                gravity = rotatedCorner.toGravity()
            }
@@ -147,26 +203,24 @@ class PrivacyDotViewController @Inject constructor(
    }

    @UiThread
    private fun updateCornerSizes() {
    private fun updateCornerSizes(l: Int, r: Int, rotation: Int) {
        views.forEach { corner ->
            val rotatedCorner = rotatedCorner(cornerForView(corner))
            val w = widthForCorner(rotatedCorner)
            Log.d(TAG, "updateCornerSizes: setting (${cornerForView(corner)}) to $w")
            val rotatedCorner = rotatedCorner(cornerForView(corner), rotation)
            val w = widthForCorner(rotatedCorner, l, r)
            (corner.layoutParams as FrameLayout.LayoutParams).width = w
            corner.requestLayout()
        }
    }

    // Designated view will be the one at statusbar's view.END
    @UiThread
    private fun selectDesignatedCorner(): View? {
    private fun selectDesignatedCorner(r: Int): View? {
        if (!this::tl.isInitialized) {
            return null
        }

        val isRtl = tl.isLayoutRtl

        return when (rotation) {
        return when (r) {
            0 -> if (isRtl) tl else tr
            1 -> if (isRtl) tr else br
            2 -> if (isRtl) br else bl
@@ -177,25 +231,19 @@ class PrivacyDotViewController @Inject constructor(

    // Track the current designated corner and maybe animate to a new rotation
    @UiThread
    private fun updateDesignatedCorner(newCorner: View) {
        designatedCorner = newCorner

        if (animationScheduler.hasPersistentDot) {
            fadeInDot()
        }
    }

    @UiThread
    private fun fadeInDot() {
        designatedCorner?.let { dot ->
            dot.visibility = View.VISIBLE
            dot.alpha = 0f
            dot.animate()
    private fun updateDesignatedCorner(newCorner: View?, shouldShowDot: Boolean) {
        if (shouldShowDot) {
            newCorner?.apply {
                clearAnimation()
                visibility = View.VISIBLE
                alpha = 0f
                animate()
                    .alpha(1.0f)
                    .setDuration(300)
                    .start()
            }
        }
    }

    @UiThread
    private fun setCornerVisibilities(vis: Int) {
@@ -214,7 +262,7 @@ class PrivacyDotViewController @Inject constructor(
        }
    }

    private fun rotatedCorner(corner: Int): Int {
    private fun rotatedCorner(corner: Int, rotation: Int): Int {
        var modded = corner - rotation
        if (modded < 0) {
            modded += 4
@@ -223,10 +271,10 @@ class PrivacyDotViewController @Inject constructor(
        return modded
    }

    private fun widthForCorner(corner: Int): Int {
    private fun widthForCorner(corner: Int, left: Int, right: Int): Int {
        return when (corner) {
            TOP_LEFT, BOTTOM_LEFT -> leftSize
            TOP_RIGHT, BOTTOM_RIGHT -> rightSize
            TOP_LEFT, BOTTOM_LEFT -> left
            TOP_RIGHT, BOTTOM_RIGHT -> right
            else -> throw IllegalArgumentException("Unknown corner")
        }
    }
@@ -244,10 +292,16 @@ class PrivacyDotViewController @Inject constructor(
        bl = bottomLeft
        br = bottomRight

        designatedCorner = selectDesignatedCorner()
        val dc = selectDesignatedCorner(0)
        val index = dc.cornerIndex()

        mainExecutor.execute {
            animationScheduler.addCallback(systemStatusAnimationCallback)
        }

        synchronized(lock) {
            nextViewState = nextViewState.copy(designatedCorner = dc, cornerIndex = index)
        }
    }

    /**
@@ -257,8 +311,6 @@ class PrivacyDotViewController @Inject constructor(
    fun setStatusBarHeights(portrait: Int, landscape: Int) {
        sbHeightPortrait = portrait
        sbHeightLandscape = landscape

        hasMultipleHeights = portrait != landscape
    }

    /**
@@ -268,85 +320,109 @@ class PrivacyDotViewController @Inject constructor(
     * @param right space between the status bar contents and the right side of the screen
     */
    private fun setStatusBarMargins(left: Int, right: Int) {
        leftSize = left
        rightSize = right
        dlog("setStatusBarMargins l=$left r=$right")
        synchronized(lock) {
            nextViewState = nextViewState.copy(marginLeft = left, marginRight = right)
        }
    }

    /**
     *  We won't show the dot when quick settings is showing
     */
    private fun setStatusBarExpanded(expanded: Boolean) {
        synchronized(lock) {
            nextViewState = nextViewState.copy(hideDotForQuickSettings = expanded)
        }
    }

        needsMarginUpdate = true
    private fun scheduleUpdate() {
        dlog("scheduleUpdate: ")

        // Margins come after PhoneStatusBarView does a layout pass, and so will always happen
        // after rotation changes. It is safe to execute the updates from here
        uiExecutor?.execute {
            doUpdates(needsRotationUpdate, needsHeightUpdate, needsMarginUpdate)
        cancelRunnable?.run()
        cancelRunnable = uiExecutor?.executeDelayed({
            processNextViewState()
        }, 100)
    }

    @UiThread
    private fun processNextViewState() {
        dlog("processNextViewState: ")

        val newState: ViewState
        synchronized(lock) {
            newState = nextViewState.copy()
        }

    private fun doUpdates(rot: Boolean, height: Boolean, width: Boolean) {
        dlog("doUpdates: ")
        var newDesignatedCorner: View? = null
        resolveState(newState)
    }

        if (rot) {
            needsRotationUpdate = false
            updateRotations()
            newDesignatedCorner = selectDesignatedCorner()
    @UiThread
    private fun resolveState(state: ViewState) {
        dlog("resolveState $state")
        if (state == currentViewState) {
            dlog("resolveState: skipping")
            return
        }

        if (height) {
            needsHeightUpdate = false
            updateHeights(rotation)
        if (state.rotation != currentViewState.rotation) {
            // A rotation has started, hide the views to avoid flicker
            updateRotations(state.rotation)
        }

        if (width) {
            needsMarginUpdate = false
            updateCornerSizes()
        if (state.height != currentViewState.height) {
            updateHeights(state.rotation)
        }

        if (newDesignatedCorner != null && newDesignatedCorner != designatedCorner) {
            updateDesignatedCorner(newDesignatedCorner)
        if (state.marginLeft != currentViewState.marginLeft ||
                state.marginRight != currentViewState.marginRight) {
            updateCornerSizes(state.marginLeft, state.marginRight, state.rotation)
        }

        if (state.designatedCorner != currentViewState.designatedCorner) {
            updateDesignatedCorner(state.designatedCorner, state.shouldShowDot())
        }

    private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
            object : SystemStatusAnimationCallback {
        override fun onSystemStatusAnimationTransitionToPersistentDot(
            showAnimation: Boolean
        ): Animator? {
            if (designatedCorner == null) {
                return null
            } else if (!showAnimation) {
                uiExecutor?.execute { fadeInDot() }
                return null
        if (state.needsLayout(currentViewState)) {
            views.forEach { it.requestLayout() }
        }

            val alpha = ObjectAnimator.ofFloat(
                    designatedCorner, "alpha", 0f, 1f)
            alpha.duration = DURATION
            alpha.interpolator = Interpolators.ALPHA_OUT
            alpha.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animator: Animator) {
                    uiExecutor?.execute { designatedCorner?.visibility = View.VISIBLE }
        val shouldShow = state.shouldShowDot()
        if (shouldShow != currentViewState.shouldShowDot()) {
            if (shouldShow && state.designatedCorner != null) {
                showDotView(state.designatedCorner, true)
            } else if (!shouldShow && state.designatedCorner != null) {
                hideDotView(state.designatedCorner, true)
            }
            })
            return alpha
        }

        override fun onHidePersistentDot(): Animator? {
            if (designatedCorner == null) {
        currentViewState = state
    }

    private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
            object : SystemStatusAnimationCallback {
        override fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? {
            synchronized(lock) {
                nextViewState = nextViewState.copy(systemPrivacyEventIsActive = true)
            }

            return null
        }

            val alpha = ObjectAnimator.ofFloat(
                    designatedCorner, "alpha", 1f, 0f)
            alpha.duration = DURATION
            alpha.interpolator = Interpolators.ALPHA_OUT
            alpha.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animator: Animator) {
                    uiExecutor?.execute { designatedCorner?.visibility = View.INVISIBLE }
        override fun onHidePersistentDot(): Animator? {
            synchronized(lock) {
                nextViewState = nextViewState.copy(systemPrivacyEventIsActive = false)
            }
            })
            alpha.start()

            return null
        }
    }

    private fun View?.cornerIndex(): Int {
        if (this != null) {
            return cornerForView(this)
        }
        return -1
    }
}

private fun dlog(s: String) {
@@ -382,3 +458,26 @@ private fun Int.innerGravity(): Int {
        else -> throw IllegalArgumentException("Not a corner")
    }
}

private data class ViewState(
    // don't @ me with names
    val systemPrivacyEventIsActive: Boolean = false,
    val hideDotForQuickSettings: Boolean = false,
    val statusBarExpanded: Boolean = false,
    val rotation: Int = 0,
    val height: Int = 0,
    val marginLeft: Int = 0,
    val marginRight: Int = 0,
    val cornerIndex: Int = -1,
    val designatedCorner: View? = null
) {
    fun shouldShowDot(): Boolean {
        return systemPrivacyEventIsActive && !hideDotForQuickSettings
    }

    fun needsLayout(other: ViewState): Boolean {
        return rotation != other.rotation ||
                marginRight != other.marginRight ||
                height != other.height
    }
}
+5 −7
Original line number Diff line number Diff line
@@ -107,7 +107,7 @@ class SystemStatusAnimationScheduler @Inject constructor(
                scheduleEvent(event)
            } else if (event.forceVisible) {
                hasPersistentDot = true
                notifyTransitionToPersistentDot(showAnimation = false)
                notifyTransitionToPersistentDot()
            }
        } else {
            if (DEBUG) {
@@ -202,7 +202,7 @@ class SystemStatusAnimationScheduler @Inject constructor(

                aSet2.play(chipAnimator).before(systemAnimator)
                if (hasPersistentDot) {
                    val dotAnim = notifyTransitionToPersistentDot(showAnimation = true)
                    val dotAnim = notifyTransitionToPersistentDot()
                    if (dotAnim != null) aSet2.playTogether(systemAnimator, dotAnim)
                }

@@ -214,9 +214,9 @@ class SystemStatusAnimationScheduler @Inject constructor(
        }, DELAY)
    }

    private fun notifyTransitionToPersistentDot(showAnimation: Boolean): Animator? {
    private fun notifyTransitionToPersistentDot(): Animator? {
        val anims: List<Animator> = listeners.mapNotNull {
            it.onSystemStatusAnimationTransitionToPersistentDot(showAnimation)
            it.onSystemStatusAnimationTransitionToPersistentDot()
        }
        if (anims.isNotEmpty()) {
            val aSet = AnimatorSet()
@@ -326,9 +326,7 @@ interface SystemStatusAnimationCallback {
    @JvmDefault fun onSystemChromeAnimationEnd() {}

    // Best method name, change my mind
    @JvmDefault fun onSystemStatusAnimationTransitionToPersistentDot(
        showAnimation: Boolean
    ): Animator? { return null }
    @JvmDefault fun onSystemStatusAnimationTransitionToPersistentDot(): Animator? { return null }
    @JvmDefault fun onHidePersistentDot(): Animator? { return null }
}

+8 −1
Original line number Diff line number Diff line
@@ -16,13 +16,15 @@

package com.android.systemui.util.concurrency;

import android.os.Looper;

import java.util.concurrent.Executor;

/**
 * Factory for building Executors running on a unique named thread.
 *
 * Use this when our generally available @Main, @Background, @UiBackground, @LongRunning, or
 * similar global qualifiers don't quite cut it. Note that the methods here create entirely new
 * similar global qualifiers don't quite cut it. Note that the methods here can create entirely new
 * threads; there are no singletons here. Use responsibly.
 */
public interface ThreadFactory {
@@ -41,4 +43,9 @@ public interface ThreadFactory {
     * implementation. Assume this is the case and use responsibly.
     **/
    DelayableExecutor buildDelayableExecutorOnNewThread(String threadName);

    /**
     * Return an {@link DelayableExecutor} running the given Looper
     **/
    DelayableExecutor buildDelayableExecutorOnLooper(Looper looper);
}
+5 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.util.concurrency;

import android.os.HandlerThread;
import android.os.Looper;

import java.util.concurrent.Executor;

@@ -35,4 +36,8 @@ class ThreadFactoryImpl implements ThreadFactory {
        handlerThread.start();
        return new ExecutorImpl(handlerThread.getLooper());
    }

    public DelayableExecutor buildDelayableExecutorOnLooper(Looper looper) {
        return new ExecutorImpl(looper);
    }
}
Loading