Update DockObserver to consume extcon uevents
Bug: b/201683309, b/216506604 Test: ran on device, atest FrameworksServicesTests:DockObserverTest Change-Id: I91b8b82fa8044ee3014f629354c92e0370964521
This commit is contained in:
@@ -5634,4 +5634,13 @@
|
||||
<!-- The amount of time after becoming non-interactive (in ms) after which
|
||||
Low Power Standby can activate. -->
|
||||
<integer name="config_lowPowerStandbyNonInteractiveTimeout">5000</integer>
|
||||
|
||||
|
||||
<!-- Mapping to select an Intent.EXTRA_DOCK_STATE value from extcon state
|
||||
key-value pairs. Each entry is evaluated in order and is of the form:
|
||||
"[EXTRA_DOCK_STATE value],key1=value1,key2=value2[,...]"
|
||||
An entry with no key-value pairs is valid and can be used as a wildcard.
|
||||
-->
|
||||
<string-array name="config_dockExtconStateMapping">
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -4675,6 +4675,8 @@
|
||||
|
||||
<java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
|
||||
|
||||
<java-symbol type="array" name="config_dockExtconStateMapping" />
|
||||
|
||||
<java-symbol type="string" name="notification_channel_abusive_bg_apps"/>
|
||||
<java-symbol type="string" name="notification_title_abusive_bg_apps"/>
|
||||
<java-symbol type="string" name="notification_content_abusive_bg_apps"/>
|
||||
|
||||
@@ -19,7 +19,6 @@ package com.android.server;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioManager;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
@@ -32,15 +31,21 @@ import android.os.SystemClock;
|
||||
import android.os.UEventObserver;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.DumpUtils;
|
||||
import com.android.server.ExtconUEventObserver.ExtconInfo;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DockObserver monitors for a docking station.
|
||||
@@ -48,9 +53,6 @@ import java.io.PrintWriter;
|
||||
final class DockObserver extends SystemService {
|
||||
private static final String TAG = "DockObserver";
|
||||
|
||||
private static final String DOCK_UEVENT_MATCH = "DEVPATH=/devices/virtual/switch/dock";
|
||||
private static final String DOCK_STATE_PATH = "/sys/class/switch/dock/state";
|
||||
|
||||
private static final int MSG_DOCK_STATE_CHANGED = 0;
|
||||
|
||||
private final PowerManager mPowerManager;
|
||||
@@ -69,6 +71,92 @@ final class DockObserver extends SystemService {
|
||||
|
||||
private final boolean mAllowTheaterModeWakeFromDock;
|
||||
|
||||
private final List<ExtconStateConfig> mExtconStateConfigs;
|
||||
|
||||
static final class ExtconStateProvider {
|
||||
private final Map<String, String> mState;
|
||||
|
||||
ExtconStateProvider(Map<String, String> state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
String getValue(String key) {
|
||||
return mState.get(key);
|
||||
}
|
||||
|
||||
|
||||
static ExtconStateProvider fromString(String stateString) {
|
||||
Map<String, String> states = new HashMap<>();
|
||||
String[] lines = stateString.split("\n");
|
||||
for (String line : lines) {
|
||||
String[] fields = line.split("=");
|
||||
if (fields.length == 2) {
|
||||
states.put(fields[0], fields[1]);
|
||||
} else {
|
||||
Slog.e(TAG, "Invalid line: " + line);
|
||||
}
|
||||
}
|
||||
return new ExtconStateProvider(states);
|
||||
}
|
||||
|
||||
static ExtconStateProvider fromFile(String stateFilePath) {
|
||||
char[] buffer = new char[1024];
|
||||
try (FileReader file = new FileReader(stateFilePath)) {
|
||||
int len = file.read(buffer, 0, 1024);
|
||||
String stateString = (new String(buffer, 0, len)).trim();
|
||||
return ExtconStateProvider.fromString(stateString);
|
||||
} catch (FileNotFoundException e) {
|
||||
Slog.w(TAG, "No state file found at: " + stateFilePath);
|
||||
return new ExtconStateProvider(new HashMap<>());
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "" , e);
|
||||
return new ExtconStateProvider(new HashMap<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a mapping from extcon state to EXTRA_DOCK_STATE value. Each
|
||||
* instance corresponds to an entry in config_dockExtconStateMapping.
|
||||
*/
|
||||
private static final class ExtconStateConfig {
|
||||
|
||||
// The EXTRA_DOCK_STATE that will be used if the extcon key-value pairs match
|
||||
public final int extraStateValue;
|
||||
|
||||
// A list of key-value pairs that must be present in the extcon state for a match
|
||||
// to be considered. An empty list is considered a matching wildcard.
|
||||
public final List<Pair<String, String>> keyValuePairs = new ArrayList<>();
|
||||
|
||||
ExtconStateConfig(int extraStateValue) {
|
||||
this.extraStateValue = extraStateValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ExtconStateConfig> loadExtconStateConfigs(Context context) {
|
||||
String[] rows = context.getResources().getStringArray(
|
||||
com.android.internal.R.array.config_dockExtconStateMapping);
|
||||
try {
|
||||
ArrayList<ExtconStateConfig> configs = new ArrayList<>();
|
||||
for (String row : rows) {
|
||||
String[] rowFields = row.split(",");
|
||||
ExtconStateConfig config = new ExtconStateConfig(Integer.parseInt(rowFields[0]));
|
||||
for (int i = 1; i < rowFields.length; i++) {
|
||||
String[] keyValueFields = rowFields[i].split("=");
|
||||
if (keyValueFields.length != 2) {
|
||||
throw new IllegalArgumentException("Invalid key-value: " + rowFields[i]);
|
||||
}
|
||||
config.keyValuePairs.add(Pair.create(keyValueFields[0], keyValueFields[1]));
|
||||
}
|
||||
configs.add(config);
|
||||
}
|
||||
return configs;
|
||||
} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
|
||||
Slog.e(TAG, "Could not parse extcon state config", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public DockObserver(Context context) {
|
||||
super(context);
|
||||
|
||||
@@ -77,9 +165,25 @@ final class DockObserver extends SystemService {
|
||||
mAllowTheaterModeWakeFromDock = context.getResources().getBoolean(
|
||||
com.android.internal.R.bool.config_allowTheaterModeWakeFromDock);
|
||||
|
||||
init(); // set initial status
|
||||
mExtconStateConfigs = loadExtconStateConfigs(context);
|
||||
|
||||
mObserver.startObserving(DOCK_UEVENT_MATCH);
|
||||
List<ExtconInfo> infos = ExtconInfo.getExtconInfoForTypes(new String[] {
|
||||
ExtconInfo.EXTCON_DOCK
|
||||
});
|
||||
|
||||
if (!infos.isEmpty()) {
|
||||
ExtconInfo info = infos.get(0);
|
||||
Slog.i(TAG, "Found extcon info devPath: " + info.getDevicePath()
|
||||
+ ", statePath: " + info.getStatePath());
|
||||
|
||||
// set initial status
|
||||
setDockStateFromProviderLocked(ExtconStateProvider.fromFile(info.getStatePath()));
|
||||
mPreviousDockState = mActualDockState;
|
||||
|
||||
mExtconUEventObserver.startObserving(info);
|
||||
} else {
|
||||
Slog.i(TAG, "No extcon dock device found in this kernel.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,26 +205,6 @@ final class DockObserver extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
synchronized (mLock) {
|
||||
try {
|
||||
char[] buffer = new char[1024];
|
||||
FileReader file = new FileReader(DOCK_STATE_PATH);
|
||||
try {
|
||||
int len = file.read(buffer, 0, 1024);
|
||||
setActualDockStateLocked(Integer.parseInt((new String(buffer, 0, len)).trim()));
|
||||
mPreviousDockState = mActualDockState;
|
||||
} finally {
|
||||
file.close();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Slog.w(TAG, "This kernel does not have dock station support");
|
||||
} catch (Exception e) {
|
||||
Slog.e(TAG, "" , e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setActualDockStateLocked(int newState) {
|
||||
mActualDockState = newState;
|
||||
if (!mUpdatesStopped) {
|
||||
@@ -234,19 +318,50 @@ final class DockObserver extends SystemService {
|
||||
}
|
||||
};
|
||||
|
||||
private final UEventObserver mObserver = new UEventObserver() {
|
||||
@Override
|
||||
public void onUEvent(UEventObserver.UEvent event) {
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Slog.v(TAG, "Dock UEVENT: " + event.toString());
|
||||
private int getDockedStateExtraValue(ExtconStateProvider state) {
|
||||
for (ExtconStateConfig config : mExtconStateConfigs) {
|
||||
boolean match = true;
|
||||
for (Pair<String, String> keyValue : config.keyValuePairs) {
|
||||
String stateValue = state.getValue(keyValue.first);
|
||||
match = match && keyValue.second.equals(stateValue);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
synchronized (mLock) {
|
||||
setActualDockStateLocked(Integer.parseInt(event.get("SWITCH_STATE")));
|
||||
if (match) {
|
||||
return config.extraStateValue;
|
||||
}
|
||||
}
|
||||
|
||||
return Intent.EXTRA_DOCK_STATE_DESK;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setDockStateFromProviderForTesting(ExtconStateProvider provider) {
|
||||
synchronized (mLock) {
|
||||
setDockStateFromProviderLocked(provider);
|
||||
}
|
||||
}
|
||||
|
||||
private void setDockStateFromProviderLocked(ExtconStateProvider provider) {
|
||||
int state = Intent.EXTRA_DOCK_STATE_UNDOCKED;
|
||||
if ("1".equals(provider.getValue("DOCK"))) {
|
||||
state = getDockedStateExtraValue(provider);
|
||||
}
|
||||
setActualDockStateLocked(state);
|
||||
}
|
||||
|
||||
private final ExtconUEventObserver mExtconUEventObserver = new ExtconUEventObserver() {
|
||||
@Override
|
||||
public void onUEvent(ExtconInfo extconInfo, UEventObserver.UEvent event) {
|
||||
synchronized (mLock) {
|
||||
String stateString = event.get("STATE");
|
||||
if (stateString != null) {
|
||||
setDockStateFromProviderLocked(ExtconStateProvider.fromString(stateString));
|
||||
} else {
|
||||
Slog.e(TAG, "Extcon event missing STATE: " + event);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.e(TAG, "Could not parse switch state from event " + event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.server;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Looper;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableContext;
|
||||
import android.testing.TestableLooper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.util.test.BroadcastInterceptingContext;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class DockObserverTest {
|
||||
|
||||
@Rule
|
||||
public TestableContext mContext =
|
||||
new TestableContext(ApplicationProvider.getApplicationContext(), null);
|
||||
|
||||
private final BroadcastInterceptingContext mInterceptingContext =
|
||||
new BroadcastInterceptingContext(mContext);
|
||||
|
||||
BroadcastInterceptingContext.FutureIntent updateExtconDockState(DockObserver observer,
|
||||
String extconDockState) {
|
||||
BroadcastInterceptingContext.FutureIntent futureIntent =
|
||||
mInterceptingContext.nextBroadcastIntent(Intent.ACTION_DOCK_EVENT);
|
||||
observer.setDockStateFromProviderForTesting(
|
||||
DockObserver.ExtconStateProvider.fromString(extconDockState));
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
return futureIntent;
|
||||
}
|
||||
|
||||
DockObserver observerWithMappingConfig(String[] configEntries) {
|
||||
mContext.getOrCreateTestableResources().addOverride(
|
||||
R.array.config_dockExtconStateMapping,
|
||||
configEntries);
|
||||
return new DockObserver(mInterceptingContext);
|
||||
}
|
||||
|
||||
void assertDockEventIntentWithExtraThenUndock(DockObserver observer, String extconDockState,
|
||||
int expectedExtra) throws ExecutionException, InterruptedException {
|
||||
assertThat(updateExtconDockState(observer, extconDockState)
|
||||
.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
|
||||
.isEqualTo(expectedExtra);
|
||||
assertThat(updateExtconDockState(observer, "DOCK=0")
|
||||
.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
|
||||
.isEqualTo(Intent.EXTRA_DOCK_STATE_UNDOCKED);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
if (Looper.myLooper() == null) {
|
||||
Looper.prepare();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDockIntentBroadcast_onlyAfterBootReady()
|
||||
throws ExecutionException, InterruptedException {
|
||||
DockObserver observer = new DockObserver(mInterceptingContext);
|
||||
BroadcastInterceptingContext.FutureIntent futureIntent =
|
||||
updateExtconDockState(observer, "DOCK=1");
|
||||
updateExtconDockState(observer, "DOCK=1").assertNotReceived();
|
||||
// Last boot phase reached
|
||||
observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
|
||||
TestableLooper.get(this).processAllMessages();
|
||||
assertThat(futureIntent.get().getIntExtra(Intent.EXTRA_DOCK_STATE, -1))
|
||||
.isEqualTo(Intent.EXTRA_DOCK_STATE_DESK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDockIntentBroadcast_customConfigResource()
|
||||
throws ExecutionException, InterruptedException {
|
||||
DockObserver observer = observerWithMappingConfig(
|
||||
new String[] {"2,KEY1=1,KEY2=2", "3,KEY3=3"});
|
||||
observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
|
||||
|
||||
// Mapping should not match
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1",
|
||||
Intent.EXTRA_DOCK_STATE_DESK);
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY1=1",
|
||||
Intent.EXTRA_DOCK_STATE_DESK);
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY2=2",
|
||||
Intent.EXTRA_DOCK_STATE_DESK);
|
||||
|
||||
// 1st mapping now matches
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY2=2\nKEY1=1",
|
||||
Intent.EXTRA_DOCK_STATE_CAR);
|
||||
|
||||
// 2nd mapping now matches
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY3=3",
|
||||
Intent.EXTRA_DOCK_STATE_LE_DESK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDockIntentBroadcast_customConfigResourceWithWildcard()
|
||||
throws ExecutionException, InterruptedException {
|
||||
DockObserver observer = observerWithMappingConfig(new String[] {
|
||||
"2,KEY2=2",
|
||||
"3,KEY3=3",
|
||||
"4"
|
||||
});
|
||||
observer.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);
|
||||
assertDockEventIntentWithExtraThenUndock(observer, "DOCK=1\nKEY5=5",
|
||||
Intent.EXTRA_DOCK_STATE_HE_DESK);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user