Snap for 9451646 from b638b557b8 to tm-qpr2-release

Change-Id: I7317b046b00a491745400be9aae4a12f3858da7d
This commit is contained in:
Android Build Coastguard Worker
2023-01-04 00:20:54 +00:00
12 changed files with 417 additions and 47 deletions

View File

@@ -752,6 +752,19 @@ public class WallpaperManager {
return sGlobals.mService;
}
/**
* Temporary method for project b/197814683
* Starting from U, this will return true if the new wallpaper logic is enabled,
* i.e. if the lockscreen wallpaper always uses a wallpaperService and not a static image.
* In T, this is just a stub method that always return false.
*
* @return false
* @hide
*/
public boolean isLockscreenLiveWallpaperEnabled() {
return false;
}
/**
* Indicate whether wcg (Wide Color Gamut) should be enabled.
* <p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,261 @@
# Status Bar Data Pipeline
## Background
The status bar is the UI shown at the top of the user's screen that gives them
information about the time, notifications, and system status like mobile
conectivity and battery level. This document is about the implementation of the
wifi and mobile system icons on the right side:
![image of status bar](status-bar.png)
In Android U, the data pipeline that determines what mobile and wifi icons to
show in the status bar has been re-written with a new architecture. This format
generally follows Android best practices to
[app architecture](https://developer.android.com/topic/architecture#recommended-app-arch).
This document serves as a guide for the new architecture, and as a guide for how
OEMs can add customizations to the new architecture.
## Architecture
In the new architecture, there is a separate pipeline for each type of icon. For
Android U, **only the wifi icon and mobile icons have been implemented in the
new architecture**.
As shown in the Android best practices guide, each new pipeline has a data
layer, a domain layer, and a UI layer:
![diagram of UI, domain, and data layers](modern-architecture.png)
The classes in the data layer are `repository` instances. The classes in the
domain layer are `interactor` instances. The classes in the UI layer are
`viewmodel` instances and `viewbinder` instances. In this document, "repository"
and "data layer" will be used interchangably (and the same goes for the other
layers).
The wifi logic is in `statusbar/pipeline/wifi` and the mobile logic is in
`statusbar/pipeline/mobile`.
#### Repository (data layer)
System callbacks, broadcast receivers, configuration values are all defined
here, and exposed through the appropriate interface. Where appropriate, we
define `Model` objects at this layer so that clients do not have to rely on
system-defined interfaces.
#### Interactor (domain layer)
Here is where we define the business logic and transform the data layer objects
into something consumable by the ViewModel classes. For example,
`MobileIconsInteractor` defines the CBRS filtering logic by exposing a
`filteredSubscriptions` list.
#### ViewModel (UI layer)
View models should define the final piece of business logic mapping to UI logic.
For example, the mobile view model checks the `IconInteractor.isRoaming` flow to
decide whether or not to show the roaming indicator.
#### ViewBinder
These have already been implemented and configured. ViewBinders replace the old
`applyMobileState` mechanism that existed in the `IconManager` classes of the
old pipeline. A view binder associates a ViewModel with a View, and keeps the
view up-to-date with the most recent information from the model.
Any new fields added to the ViewModel classes need to be equivalently bound to
the view here.
### Putting it all together
Putting that altogether, we have this overall architecture diagram for the
icons:
![diagram of wifi and mobile pipelines](status-bar-pipeline.png)
### Mobile icons architecture
Because there can be multiple mobile connections at the same time, the mobile
pipeline is split up hierarchically. At each level (data, domain, and UI), there
is a singleton parent class that manages information relevant to **all** mobile
connections, and multiple instances of child classes that manage information for
a **single** mobile connection.
For example, `MobileConnectionsRepository` is a singleton at the data layer that
stores information relevant to **all** mobile connections, and it also manages a
list of child `MobileConnectionRepository` classes. `MobileConnectionRepository`
is **not** a singleton, and each individual `MobileConnectionRepository`
instance fully qualifies the state of a **single** connection. This pattern is
repeated at the `Interactor` and `ViewModel` layers for mobile.
![diagram of mobile parent child relationship](status-bar-mobile-pipeline.png)
Note: Since there is at most one wifi connection, the wifi pipeline is not split
up in the same way.
## Customizations
The new pipeline completely replaces these classes:
* `WifiStatusTracker`
* `MobileStatusTracker`
* `NetworkSignalController` and `NetworkSignalControllerImpl`
* `MobileSignalController`
* `WifiSignalController`
* `StatusBarSignalPolicy` (including `SignalIconState`, `MobileIconState`, and
`WifiIconState`)
Any customizations in any of these classes will need to be migrated to the new
pipeline. As a general rule, any change that would have gone into
`NetworkControllerImpl` would be done in `MobileConnectionsRepository`, and any
change for `MobileSignalController` can be done in `MobileConnectionRepository`
(see above on the relationship between those repositories).
### Sample customization: New service
Some customizations require listening to additional services to get additional
data. This new architecture makes it easy to add additional services to the
status bar data pipeline to get icon customizations.
Below is a general guide to how a new service should be added. However, there
may be exceptions to this guide for specific use cases.
1. In the data layer (`repository` classes), add a new `StateFlow` that listens
to the service:
```kotlin
class MobileConnectionsRepositoryImpl {
...
val fooVal: StateFlow<Int> =
conflatedCallbackFlow {
val callback = object : FooServiceCallback(), FooListener {
override fun onFooChanged(foo: Int) {
trySend(foo)
}
}
fooService.registerCallback(callback)
awaitClose { fooService.unregisterCallback(callback) }
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), FOO_DEFAULT_VAL)
}
```
1. In the domain layer (`interactor` classes), either use this new flow to
process values, or just expose the flow as-is for the UI layer.
For example, if `bar` should only be true when `foo` is positive:
```kotlin
class MobileIconsInteractor {
...
val bar: StateFlow<Boolean> =
mobileConnectionsRepo
.mapLatest { foo -> foo > 0 }
.stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = false)
}
```
1. In the UI layer (`viewmodel` classes), update the existing flows to process
the new value from the interactor.
For example, if the icon should be hidden when `bar` is true:
```kotlin
class MobileIconViewModel {
...
iconId: Flow<Int> = combine(
iconInteractor.level,
iconInteractor.numberOfLevels,
iconInteractor.bar,
) { level, numberOfLevels, bar ->
if (bar) {
null
} else {
calcIcon(level, numberOfLevels)
}
}
```
## Demo mode
SystemUI demo mode is a first-class citizen in the new pipeline. It is
implemented as an entirely separate repository,
`DemoMobileConnectionsRepository`. When the system moves into demo mode, the
implementation of the data layer is switched to the demo repository via the
`MobileRepositorySwitcher` class.
Because the demo mode repositories implement the same interfaces as the
production classes, any changes made above will have to be implemented for demo
mode as well.
1. Following from above, if `fooVal` is added to the
`MobileConnectionsRepository` interface:
```kotlin
class DemoMobileConnectionsRepository {
private val _fooVal = MutableStateFlow(FOO_DEFAULT_VALUE)
override val fooVal: StateFlow<Int> = _fooVal.asStateFlow()
// Process the state. **See below on how to add the command to the CLI**
fun processEnabledMobileState(state: Mobile) {
...
_fooVal.value = state.fooVal
}
}
```
1. (Optional) If you want to enable the command line interface for setting and
testing this value in demo mode, you can add parsing logic to
`DemoModeMobileConnectionDataSource` and `FakeNetworkEventModel`:
```kotlin
sealed interface FakeNetworkEventModel {
data class Mobile(
...
// Add new fields here
val fooVal: Int?
)
}
```
```kotlin
class DemoModeMobileConnectionDataSource {
// Currently, the demo commands are implemented as an extension function on Bundle
private fun Bundle.activeMobileEvent(): Mobile {
...
val fooVal = getString("fooVal")?.toInt()
return Mobile(
...
fooVal = fooVal,
)
}
}
```
If step 2 is implemented, then you will be able to pass demo commands via the
command line:
```
adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e fooVal <test value>
```
## Migration plan
For Android U, the new pipeline will be enabled and default. However, the old
pipeline code will still be around just in case the new pipeline doesn’t do well
in the testing phase.
For Android V, the old pipeline will be completely removed and the new pipeline
will be the one source of truth.
Our ask for OEMs is to default to using the new pipeline in Android U. If there
are customizations that seem difficult to migrate over to the new pipeline,
please file a bug with us and we’d be more than happy to consult on the best
solution. The new pipeline was designed with customizability in mind, so our
hope is that working the new pipeline will be easier and faster.
Note: The new pipeline currently only supports the wifi and mobile icons. The
other system status bar icons may be migrated to a similar architecture in the
future.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -636,6 +636,7 @@ public class FrameworkServicesModule {
@Provides
@Singleton
@Nullable
static BluetoothAdapter provideBluetoothAdapter(BluetoothManager bluetoothManager) {
return bluetoothManager.getAdapter();
}

View File

@@ -38,7 +38,7 @@ class StylusManager
@Inject
constructor(
private val inputManager: InputManager,
private val bluetoothAdapter: BluetoothAdapter,
private val bluetoothAdapter: BluetoothAdapter?,
@Background private val handler: Handler,
@Background private val executor: Executor,
) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener {
@@ -141,7 +141,7 @@ constructor(
}
private fun onStylusBluetoothConnected(btAddress: String) {
val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
try {
bluetoothAdapter.addOnMetadataChangedListener(device, executor, this)
} catch (e: IllegalArgumentException) {
@@ -150,7 +150,7 @@ constructor(
}
private fun onStylusBluetoothDisconnected(btAddress: String) {
val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return
val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return
try {
bluetoothAdapter.removeOnMetadataChangedListener(device, this)
} catch (e: IllegalArgumentException) {

View File

@@ -2644,6 +2644,9 @@ public class DisplayDeviceConfig {
}
}
/**
* Uniquely identifies a Sensor, with the combination of Type and Name.
*/
static class SensorData {
public String type;
public String name;

View File

@@ -50,7 +50,6 @@ import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigInterface;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
@@ -68,6 +67,7 @@ import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.display.utils.AmbientFilter;
import com.android.server.display.utils.AmbientFilterFactory;
import com.android.server.display.utils.SensorUtils;
import com.android.server.sensors.SensorManagerInternal;
import com.android.server.sensors.SensorManagerInternal.ProximityActiveListener;
import com.android.server.statusbar.StatusBarManagerInternal;
@@ -520,14 +520,20 @@ public class DisplayModeDirector {
}
/**
* A utility to make this class aware of the new display configs whenever the default display is
* changed
* Called when the underlying display device of the default display is changed.
* Some data in this class relates to the physical display of the device, and so we need to
* reload the configurations based on this.
* E.g. the brightness sensors and refresh rate capabilities depend on the physical display
* device that is being used, so will be reloaded.
*
* @param displayDeviceConfig configurations relating to the underlying display device.
*/
public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) {
mSettingsObserver.setRefreshRates(displayDeviceConfig,
/* attemptLoadingFromDeviceConfig= */ true);
mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
/* attemptLoadingFromDeviceConfig= */ true);
mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
}
/**
@@ -1541,6 +1547,9 @@ public class DisplayModeDirector {
private SensorManager mSensorManager;
private Sensor mLightSensor;
private Sensor mRegisteredLightSensor;
private String mLightSensorType;
private String mLightSensorName;
private final LightSensorEventListener mLightSensorListener =
new LightSensorEventListener();
// Take it as low brightness before valid sensor data comes
@@ -1701,17 +1710,8 @@ public class DisplayModeDirector {
return mLowAmbientBrightnessThresholds;
}
public void registerLightSensor(SensorManager sensorManager, Sensor lightSensor) {
mSensorManager = sensorManager;
mLightSensor = lightSensor;
mSensorManager.registerListener(mLightSensorListener,
mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
}
public void observe(SensorManager sensorManager) {
mSensorManager = sensorManager;
final ContentResolver cr = mContext.getContentResolver();
mBrightness = getBrightness(Display.DEFAULT_DISPLAY);
// DeviceConfig is accessible after system ready.
@@ -1855,6 +1855,10 @@ public class DisplayModeDirector {
pw.println(" mAmbientHighBrightnessThresholds: " + d);
}
pw.println(" mLightSensor: " + mLightSensor);
pw.println(" mRegisteredLightSensor: " + mRegisteredLightSensor);
pw.println(" mLightSensorName: " + mLightSensorName);
pw.println(" mLightSensorType: " + mLightSensorType);
mLightSensorListener.dumpLocked(pw);
if (mAmbientFilter != null) {
@@ -1908,27 +1912,9 @@ public class DisplayModeDirector {
}
if (mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange) {
Resources resources = mContext.getResources();
String lightSensorType = resources.getString(
com.android.internal.R.string.config_displayLightSensorType);
Sensor lightSensor = getLightSensor();
Sensor lightSensor = null;
if (!TextUtils.isEmpty(lightSensorType)) {
List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
for (int i = 0; i < sensors.size(); i++) {
Sensor sensor = sensors.get(i);
if (lightSensorType.equals(sensor.getStringType())) {
lightSensor = sensor;
break;
}
}
}
if (lightSensor == null) {
lightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
}
if (lightSensor != null) {
if (lightSensor != null && lightSensor != mLightSensor) {
final Resources res = mContext.getResources();
mAmbientFilter = AmbientFilterFactory.createBrightnessFilter(TAG, res);
@@ -1938,15 +1924,40 @@ public class DisplayModeDirector {
mAmbientFilter = null;
mLightSensor = null;
}
updateSensorStatus();
if (mRefreshRateChangeable) {
updateSensorStatus();
synchronized (mLock) {
onBrightnessChangedLocked();
}
}
}
private void reloadLightSensor(DisplayDeviceConfig displayDeviceConfig) {
reloadLightSensorData(displayDeviceConfig);
restartObserver();
}
private void reloadLightSensorData(DisplayDeviceConfig displayDeviceConfig) {
// The displayDeviceConfig (ddc) contains display specific preferences. When loaded,
// it naturally falls back to the global config.xml.
if (displayDeviceConfig != null
&& displayDeviceConfig.getAmbientLightSensor() != null) {
// This covers both the ddc and the config.xml fallback
mLightSensorType = displayDeviceConfig.getAmbientLightSensor().type;
mLightSensorName = displayDeviceConfig.getAmbientLightSensor().name;
} else if (mLightSensorName == null && mLightSensorType == null) {
Resources resources = mContext.getResources();
mLightSensorType = resources.getString(
com.android.internal.R.string.config_displayLightSensorType);
mLightSensorName = "";
}
}
private Sensor getLightSensor() {
return SensorUtils.findSensor(mSensorManager, mLightSensorType,
mLightSensorName, Sensor.TYPE_LIGHT);
}
/**
* Checks to see if at least one value is positive, in which case it is necessary to listen
* to value changes.
@@ -2088,17 +2099,36 @@ public class DisplayModeDirector {
if ((mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange)
&& isDeviceActive() && !mLowPowerModeEnabled && mRefreshRateChangeable) {
mSensorManager.registerListener(mLightSensorListener,
mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
if (mLoggingEnabled) {
Slog.d(TAG, "updateSensorStatus: registerListener");
}
registerLightSensor();
} else {
mLightSensorListener.removeCallbacks();
mSensorManager.unregisterListener(mLightSensorListener);
if (mLoggingEnabled) {
Slog.d(TAG, "updateSensorStatus: unregisterListener");
}
unregisterSensorListener();
}
}
private void registerLightSensor() {
if (mRegisteredLightSensor == mLightSensor) {
return;
}
if (mRegisteredLightSensor != null) {
unregisterSensorListener();
}
mSensorManager.registerListener(mLightSensorListener,
mLightSensor, LIGHT_SENSOR_RATE_MS * 1000, mHandler);
mRegisteredLightSensor = mLightSensor;
if (mLoggingEnabled) {
Slog.d(TAG, "updateSensorStatus: registerListener");
}
}
private void unregisterSensorListener() {
mLightSensorListener.removeCallbacks();
mSensorManager.unregisterListener(mLightSensorListener);
mRegisteredLightSensor = null;
if (mLoggingEnabled) {
Slog.d(TAG, "updateSensorStatus: unregisterListener");
}
}

View File

@@ -33,6 +33,10 @@ public class SensorUtils {
*/
public static Sensor findSensor(SensorManager sensorManager, String sensorType,
String sensorName, int fallbackType) {
if (sensorManager == null) {
return null;
}
if ("".equals(sensorName) && "".equals(sensorType)) {
return null;
}

View File

@@ -40,6 +40,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -74,6 +75,7 @@ import android.provider.Settings;
import android.test.mock.MockContentResolver;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Display;
import androidx.test.core.app.ApplicationProvider;
@@ -102,6 +104,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -1939,6 +1942,61 @@ public class DisplayModeDirectorTest {
new int[]{20});
}
@Test
public void testSensorReloadOnDeviceSwitch() throws Exception {
// First, configure brightness zones or DMD won't register for sensor data.
final FakeDeviceConfig config = mInjector.getDeviceConfig();
config.setRefreshRateInHighZone(60);
config.setHighDisplayBrightnessThresholds(new int[] { 255 });
config.setHighAmbientBrightnessThresholds(new int[] { 8000 });
DisplayModeDirector director =
createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0);
setPeakRefreshRate(90 /*fps*/);
director.getSettingsObserver().setDefaultRefreshRate(90);
director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
Sensor lightSensorOne = TestUtils.createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
Sensor lightSensorTwo = TestUtils.createSensor(Sensor.TYPE_LIGHT, Sensor.STRING_TYPE_LIGHT);
SensorManager sensorManager = createMockSensorManager(lightSensorOne, lightSensorTwo);
when(sensorManager.getDefaultSensor(5)).thenReturn(lightSensorOne, lightSensorTwo);
director.start(sensorManager);
ArgumentCaptor<SensorEventListener> listenerCaptor =
ArgumentCaptor.forClass(SensorEventListener.class);
verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1)))
.registerListener(
listenerCaptor.capture(),
eq(lightSensorOne),
anyInt(),
any(Handler.class));
DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class);
when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50);
when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55);
when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25});
when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
when(ddcMock.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100});
Resources resMock = mock(Resources.class);
when(resMock.getInteger(
com.android.internal.R.integer.config_displayWhiteBalanceBrightnessFilterHorizon))
.thenReturn(3);
ArgumentCaptor<TypedValue> valueArgumentCaptor = ArgumentCaptor.forClass(TypedValue.class);
doAnswer((Answer<Void>) invocation -> {
valueArgumentCaptor.getValue().type = 4;
valueArgumentCaptor.getValue().data = 13;
return null;
}).when(resMock).getValue(anyInt(), valueArgumentCaptor.capture(), eq(true));
when(mContext.getResources()).thenReturn(resMock);
director.defaultDisplayDeviceUpdated(ddcMock);
verify(sensorManager).unregisterListener(any(SensorEventListener.class));
verify(sensorManager).registerListener(any(SensorEventListener.class),
eq(lightSensorTwo), anyInt(), any(Handler.class));
}
private Temperature getSkinTemp(@Temperature.ThrottlingStatus int status) {
return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
}