Merge "Update DockObserver to consume extcon uevents"

This commit is contained in:
Felipe Salazar
2022-02-01 16:02:01 +00:00
committed by Android (Google) Code Review
4 changed files with 297 additions and 37 deletions

View File

@@ -5648,4 +5648,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>

View File

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

View File

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

View File

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