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

Commit cfd20f72 authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Add status bar icon when a connected display is attached

Adds a "display" icon to the status bar when there is at least one
external display attached.

Bug: 286186256
Test: PhoneStatusBarPolicyTest
Change-Id: I0eaf74732a5d97a8a694e9cad55ac529154ef47b
parent 3214adb6
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@
        <item><xliff:g id="id">@string/status_bar_secure</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_managed_profile</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_connected_display</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_vpn</xliff:g></item>
        <item><xliff:g id="id">@string/status_bar_bluetooth</xliff:g></item>
@@ -72,6 +73,7 @@
    <string translatable="false" name="status_bar_sync_failing">sync_failing</string>
    <string translatable="false" name="status_bar_sync_active">sync_active</string>
    <string translatable="false" name="status_bar_cast">cast</string>
    <string translatable="false" name="status_bar_connected_display">connected_display</string>
    <string translatable="false" name="status_bar_hotspot">hotspot</string>
    <string translatable="false" name="status_bar_location">location</string>
    <string translatable="false" name="status_bar_bluetooth">bluetooth</string>
+1 −0
Original line number Diff line number Diff line
@@ -3099,6 +3099,7 @@
  <java-symbol type="string" name="status_bar_sync_failing" />
  <java-symbol type="string" name="status_bar_sync_active" />
  <java-symbol type="string" name="status_bar_cast" />
  <java-symbol type="string" name="status_bar_connected_display" />
  <java-symbol type="string" name="status_bar_hotspot" />
  <java-symbol type="string" name="status_bar_location" />
  <java-symbol type="string" name="status_bar_bluetooth" />
+25 −0
Original line number Diff line number Diff line
<!--
Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:fillColor="@android:color/white"
    android:viewportWidth="960"
    android:viewportHeight="960">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M320,840L320,760L400,760L400,680L160,680Q127,680 103.5,656.5Q80,633 80,600L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,600Q880,633 856.5,656.5Q833,680 800,680L560,680L560,760L640,760L640,840L320,840ZM160,600L800,600Q800,600 800,600Q800,600 800,600L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,600Q160,600 160,600Q160,600 160,600ZM160,600Q160,600 160,600Q160,600 160,600L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,600Q160,600 160,600Q160,600 160,600L160,600Z" />
</vector>
 No newline at end of file
+29 −1
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.PrivacyType;
@@ -74,6 +75,7 @@ import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.util.RingerModeTracker;
import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.DateFormatUtil;

import java.io.PrintWriter;
@@ -121,9 +123,12 @@ public class PhoneStatusBarPolicy
    private final String mSlotCamera;
    private final String mSlotSensorsOff;
    private final String mSlotScreenRecord;
    private final String mSlotConnectedDisplay;
    private final int mDisplayId;
    private final SharedPreferences mSharedPreferences;
    private final DateFormatUtil mDateFormatUtil;
    private final JavaAdapter mJavaAdapter;
    private final ConnectedDisplayInteractor mConnectedDisplayInteractor;
    private final TelecomManager mTelecomManager;

    private final Handler mHandler;
@@ -182,9 +187,13 @@ public class PhoneStatusBarPolicy
            @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
            RingerModeTracker ringerModeTracker,
            PrivacyItemController privacyItemController,
            PrivacyLogger privacyLogger) {
            PrivacyLogger privacyLogger,
            ConnectedDisplayInteractor connectedDisplayInteractor,
            JavaAdapter javaAdapter
    ) {
        mIconController = iconController;
        mCommandQueue = commandQueue;
        mConnectedDisplayInteractor = connectedDisplayInteractor;
        mBroadcastDispatcher = broadcastDispatcher;
        mHandler = new Handler(looper);
        mResources = resources;
@@ -211,8 +220,11 @@ public class PhoneStatusBarPolicy
        mTelecomManager = telecomManager;
        mRingerModeTracker = ringerModeTracker;
        mPrivacyLogger = privacyLogger;
        mJavaAdapter = javaAdapter;

        mSlotCast = resources.getString(com.android.internal.R.string.status_bar_cast);
        mSlotConnectedDisplay = resources.getString(
                com.android.internal.R.string.status_bar_connected_display);
        mSlotHotspot = resources.getString(com.android.internal.R.string.status_bar_hotspot);
        mSlotBluetooth = resources.getString(com.android.internal.R.string.status_bar_bluetooth);
        mSlotTty = resources.getString(com.android.internal.R.string.status_bar_tty);
@@ -285,6 +297,10 @@ public class PhoneStatusBarPolicy
        mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast, null);
        mIconController.setIconVisibility(mSlotCast, false);

        // connected display
        mIconController.setIcon(mSlotConnectedDisplay, R.drawable.stat_sys_connected_display, null);
        mIconController.setIconVisibility(mSlotConnectedDisplay, false);

        // hotspot
        mIconController.setIcon(mSlotHotspot, R.drawable.stat_sys_hotspot,
                mResources.getString(R.string.accessibility_status_bar_hotspot));
@@ -342,6 +358,8 @@ public class PhoneStatusBarPolicy
        mSensorPrivacyController.addCallback(mSensorPrivacyListener);
        mLocationController.addCallback(this);
        mRecordingController.addCallback(this);
        mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
                this::onConnectedDisplayAvailabilityChanged);

        mCommandQueue.addCallback(this);
    }
@@ -800,4 +818,14 @@ public class PhoneStatusBarPolicy
        if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
        mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
    }

    private void onConnectedDisplayAvailabilityChanged(ConnectedDisplayInteractor.State state) {
        boolean visible = state != ConnectedDisplayInteractor.State.DISCONNECTED;

        if (DEBUG) {
            Log.d(TAG, "connected_display: " + (visible ? "showing" : "hiding") + " icon");
        }

        mIconController.setIconVisibility(mSlotConnectedDisplay, visible);
    }
}
+78 −1
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.screenrecord.RecordingController
@@ -46,9 +48,17 @@ import com.android.systemui.statusbar.policy.UserInfoController
import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.RingerModeTracker
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.JavaAdapter
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.DateFormatUtil
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -57,6 +67,8 @@ import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -64,11 +76,13 @@ import org.mockito.MockitoAnnotations

@RunWith(AndroidTestingRunner::class)
@RunWithLooper
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class PhoneStatusBarPolicyTest : SysuiTestCase() {

    companion object {
        private const val ALARM_SLOT = "alarm"
        private const val CONNECTED_DISPLAY_SLOT = "connected_display"
    }

    @Mock private lateinit var iconController: StatusBarIconController
@@ -102,6 +116,9 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
    private lateinit var alarmCallbackCaptor:
        ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>

    private val testScope = TestScope(UnconfinedTestDispatcher())
    private val fakeConnectedDisplayStateProvider = FakeConnectedDisplayStateProvider()

    private lateinit var executor: FakeExecutor
    private lateinit var statusBarPolicy: PhoneStatusBarPolicy
    private lateinit var testableLooper: TestableLooper
@@ -164,6 +181,57 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
        verify(iconController).setIconVisibility(ALARM_SLOT, true)
    }

    @Test
    fun connectedDisplay_connected_iconShown() =
        testScope.runTest {
            statusBarPolicy.init()
            clearInvocations(iconController)

            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)
            runCurrent()

            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
        }

    @Test
    fun connectedDisplay_disconnected_iconHidden() =
        testScope.runTest {
            statusBarPolicy.init()
            clearInvocations(iconController)

            fakeConnectedDisplayStateProvider.emit(State.DISCONNECTED)

            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, false)
        }

    @Test
    fun connectedDisplay_disconnectedThenConnected_iconShown() =
        testScope.runTest {
            statusBarPolicy.init()
            clearInvocations(iconController)

            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)
            fakeConnectedDisplayStateProvider.emit(State.DISCONNECTED)
            fakeConnectedDisplayStateProvider.emit(State.CONNECTED)

            inOrder(iconController).apply {
                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, false)
                verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
            }
        }

    @Test
    fun connectedDisplay_connectSecureDisplay_iconShown() =
        testScope.runTest {
            statusBarPolicy.init()
            clearInvocations(iconController)

            fakeConnectedDisplayStateProvider.emit(State.CONNECTED_SECURE)

            verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
        }

    private fun createAlarmInfo(): AlarmManager.AlarmClockInfo {
        return AlarmManager.AlarmClockInfo(10L, null)
    }
@@ -200,7 +268,16 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
            dateFormatUtil,
            ringerModeTracker,
            privacyItemController,
            privacyLogger
            privacyLogger,
            fakeConnectedDisplayStateProvider,
            JavaAdapter(testScope.backgroundScope)
        )
    }

    private class FakeConnectedDisplayStateProvider : ConnectedDisplayInteractor {
        private val flow = MutableSharedFlow<State>()
        suspend fun emit(value: State) = flow.emit(value)
        override val connectedDisplayState: Flow<State>
            get() = flow
    }
}