Merge changes I0eaf7473,Ib31a9bcb into udc-qpr-dev

* changes:
  Add status bar icon when a connected display is attached
  Introduce ConnectedDisplayInteractor
This commit is contained in:
Treehugger Robot
2023-06-26 23:24:17 +00:00
committed by Android (Google) Code Review
9 changed files with 393 additions and 2 deletions

View File

@@ -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>

View File

@@ -3100,6 +3100,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" />

View File

@@ -0,0 +1,25 @@
<!--
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>

View File

@@ -47,6 +47,7 @@ import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.SystemUser;
import com.android.systemui.demomode.dagger.DemoModeModule;
import com.android.systemui.display.DisplayModule;
import com.android.systemui.doze.dagger.DozeComponent;
import com.android.systemui.dreams.dagger.DreamModule;
import com.android.systemui.dump.DumpManager;
@@ -163,6 +164,7 @@ import javax.inject.Named;
ClipboardOverlayModule.class,
ClockRegistryModule.class,
CommonRepositoryModule.class,
DisplayModule.class,
ConnectivityModule.class,
CoroutinesModule.class,
DreamModule.class,

View File

@@ -0,0 +1,35 @@
/*
* 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.
*/
package com.android.systemui.display
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl
import dagger.Binds
import dagger.Module
/** Module binding display related classes. */
@Module
interface DisplayModule {
@Binds
fun bindConnectedDisplayInteractor(
provider: ConnectedDisplayInteractorImpl
): ConnectedDisplayInteractor
@Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
package com.android.systemui.display.domain.interactor
import android.view.Display
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** Provides information about an external connected display. */
interface ConnectedDisplayInteractor {
/**
* Provides the current external display state.
*
* The state is:
* - [State.CONNECTED] when there is at least one display with [TYPE_EXTERNAL].
* - [State.CONNECTED_SECURE] when is at least one display with both [TYPE_EXTERNAL] AND
* [Display.FLAG_SECURE] set
*/
val connectedDisplayState: Flow<State>
/** Possible connected display state. */
enum class State {
DISCONNECTED,
CONNECTED,
CONNECTED_SECURE,
}
}
@SysUISingleton
class ConnectedDisplayInteractorImpl
@Inject
constructor(
displayRepository: DisplayRepository,
) : ConnectedDisplayInteractor {
override val connectedDisplayState: Flow<State> =
displayRepository.displays
.map { displays ->
val externalDisplays =
displays.filter { display -> display.type == Display.TYPE_EXTERNAL }
val secureExternalDisplays =
externalDisplays.filter { it.flags and Display.FLAG_SECURE != 0 }
if (externalDisplays.isEmpty()) {
State.DISCONNECTED
} else if (!secureExternalDisplays.isEmpty()) {
State.CONNECTED_SECURE
} else {
State.CONNECTED
}
}
.distinctUntilChanged()
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.
*/
package com.android.systemui.display.domain.interactor
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.Display.TYPE_INTERNAL
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
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.runTest
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class ConnectedDisplayInteractorTest : SysuiTestCase() {
private val fakeDisplayRepository = FakeDisplayRepository()
private val connectedDisplayStateProvider: ConnectedDisplayInteractor =
ConnectedDisplayInteractorImpl(fakeDisplayRepository)
private val testScope = TestScope(UnconfinedTestDispatcher())
@Test
fun displayState_nullDisplays_disconnected() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(emptySet())
assertThat(value).isEqualTo(State.DISCONNECTED)
}
@Test
fun displayState_emptyDisplays_disconnected() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(emptySet())
assertThat(value).isEqualTo(State.DISCONNECTED)
}
@Test
fun displayState_internalDisplay_disconnected() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(setOf(display(type = TYPE_INTERNAL)))
assertThat(value).isEqualTo(State.DISCONNECTED)
}
@Test
fun displayState_externalDisplay_connected() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(setOf(display(type = TYPE_EXTERNAL)))
assertThat(value).isEqualTo(State.CONNECTED)
}
@Test
fun displayState_multipleExternalDisplays_connected() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(
setOf(display(type = TYPE_EXTERNAL), display(type = TYPE_EXTERNAL))
)
assertThat(value).isEqualTo(State.CONNECTED)
}
@Test
fun displayState_externalSecure_connectedSecure() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(
setOf(display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE))
)
assertThat(value).isEqualTo(State.CONNECTED_SECURE)
}
@Test
fun displayState_multipleExternal_onlyOneSecure_connectedSecure() =
testScope.runTest {
val value by lastValue()
fakeDisplayRepository.emit(
setOf(
display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE),
display(type = TYPE_EXTERNAL, flags = 0)
)
)
assertThat(value).isEqualTo(State.CONNECTED_SECURE)
}
private fun TestScope.lastValue(): FlowValue<State?> =
collectLastValue(connectedDisplayStateProvider.connectedDisplayState)
private fun display(type: Int, flags: Int = 0): Display {
return mock<Display>().also { mockDisplay ->
whenever(mockDisplay.type).thenReturn(type)
whenever(mockDisplay.flags).thenReturn(flags)
}
}
private class FakeDisplayRepository : DisplayRepository {
private val flow = MutableSharedFlow<Set<Display>>()
suspend fun emit(value: Set<Display>) = flow.emit(value)
override val displays: Flow<Set<Display>>
get() = flow
}
}

View File

@@ -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
}
}