diff options
| -rw-r--r-- | core/res/res/values/config.xml | 9 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 2 | ||||
| -rw-r--r-- | services/core/java/com/android/server/DockObserver.java | 189 | ||||
| -rw-r--r-- | services/tests/servicestests/src/com/android/server/DockObserverTest.java | 134 |
4 files changed, 297 insertions, 37 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7e14dc9b3336..73568c1deaab 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -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> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f86add6e4e3b..94043ed6d8ed 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -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"/> diff --git a/services/core/java/com/android/server/DockObserver.java b/services/core/java/com/android/server/DockObserver.java index e5a7b4e4ee23..8a6b54fd9769 100644 --- a/services/core/java/com/android/server/DockObserver.java +++ b/services/core/java/com/android/server/DockObserver.java @@ -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); + + List<ExtconInfo> infos = ExtconInfo.getExtconInfoForTypes(new String[] { + ExtconInfo.EXTCON_DOCK + }); - mObserver.startObserving(DOCK_UEVENT_MATCH); + 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); } } }; diff --git a/services/tests/servicestests/src/com/android/server/DockObserverTest.java b/services/tests/servicestests/src/com/android/server/DockObserverTest.java new file mode 100644 index 000000000000..c325778a5683 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/DockObserverTest.java @@ -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); + } +} |