summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Android Build Coastguard Worker <android-build-coastguard-worker@google.com> 2023-01-04 00:20:54 +0000
committer Android Build Coastguard Worker <android-build-coastguard-worker@google.com> 2023-01-04 00:20:54 +0000
commitf4c70d67ca52f714956dc3c167d383d1db2b16d8 (patch)
tree640f7c58d227aad51825fc24f26911fef206e23e
parent060e7b6f9656a03b79d44c6f2bda03b3b9ff4b0a (diff)
parentb638b557b80bc71ef6d1d9272d3fdc675795eca9 (diff)
Snap for 9451646 from b638b557b80bc71ef6d1d9272d3fdc675795eca9 to tm-qpr2-release
Change-Id: I7317b046b00a491745400be9aae4a12f3858da7d
-rw-r--r--core/java/android/app/WallpaperManager.java13
-rw-r--r--packages/SystemUI/docs/modern-architecture.pngbin0 -> 23573 bytes
-rw-r--r--packages/SystemUI/docs/status-bar-data-pipeline.md261
-rw-r--r--packages/SystemUI/docs/status-bar-mobile-pipeline.pngbin0 -> 50983 bytes
-rw-r--r--packages/SystemUI/docs/status-bar-pipeline.pngbin0 -> 89119 bytes
-rw-r--r--packages/SystemUI/docs/status-bar.pngbin0 -> 19725 bytes
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt6
-rw-r--r--services/core/java/com/android/server/display/DisplayDeviceConfig.java3
-rw-r--r--services/core/java/com/android/server/display/DisplayModeDirector.java118
-rw-r--r--services/core/java/com/android/server/display/utils/SensorUtils.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java58
12 files changed, 417 insertions, 47 deletions
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 7eacc3c3bdf1..dea1a050a27a 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -753,6 +753,19 @@ public class WallpaperManager {
}
/**
+ * 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>
* Some devices lack of capability of mixed color spaces composition,
diff --git a/packages/SystemUI/docs/modern-architecture.png b/packages/SystemUI/docs/modern-architecture.png
new file mode 100644
index 000000000000..2636362dd2ec
--- /dev/null
+++ b/packages/SystemUI/docs/modern-architecture.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar-data-pipeline.md b/packages/SystemUI/docs/status-bar-data-pipeline.md
new file mode 100644
index 000000000000..9fcdce1ef717
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-data-pipeline.md
@@ -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.
diff --git a/packages/SystemUI/docs/status-bar-mobile-pipeline.png b/packages/SystemUI/docs/status-bar-mobile-pipeline.png
new file mode 100644
index 000000000000..620563de3daa
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-mobile-pipeline.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar-pipeline.png b/packages/SystemUI/docs/status-bar-pipeline.png
new file mode 100644
index 000000000000..1c568c9bcda9
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar-pipeline.png
Binary files differ
diff --git a/packages/SystemUI/docs/status-bar.png b/packages/SystemUI/docs/status-bar.png
new file mode 100644
index 000000000000..3a5af0e2c3e0
--- /dev/null
+++ b/packages/SystemUI/docs/status-bar.png
Binary files differ
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 2260e35fd6bc..4bb5d04e7fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -636,6 +636,7 @@ public class FrameworkServicesModule {
@Provides
@Singleton
+ @Nullable
static BluetoothAdapter provideBluetoothAdapter(BluetoothManager bluetoothManager) {
return bluetoothManager.getAdapter();
}
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
index 3e111e6de785..302d6a9ca1b7 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt
@@ -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) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 5bdfa70096c0..927874373df8 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -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;
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 6331a5dd07b4..aafba5a2a1b4 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -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 = 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);
- }
+ Sensor lightSensor = getLightSensor();
- 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");
}
}
diff --git a/services/core/java/com/android/server/display/utils/SensorUtils.java b/services/core/java/com/android/server/display/utils/SensorUtils.java
index 4924ad525fcc..48bc46c88df1 100644
--- a/services/core/java/com/android/server/display/utils/SensorUtils.java
+++ b/services/core/java/com/android/server/display/utils/SensorUtils.java
@@ -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;
}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index cfea63babac3..b133a2a44fcf 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -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);
}