Loading core/res/res/values/config.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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> core/res/res/values/symbols.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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"/> Loading services/core/java/com/android/server/DockObserver.java +152 −37 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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; Loading @@ -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); Loading @@ -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 Loading @@ -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) { Loading Loading @@ -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 { if (match) { return config.extraStateValue; } } return Intent.EXTRA_DOCK_STATE_DESK; } @VisibleForTesting void setDockStateFromProviderForTesting(ExtconStateProvider provider) { synchronized (mLock) { setActualDockStateLocked(Integer.parseInt(event.get("SWITCH_STATE"))); 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); } } }; Loading services/tests/servicestests/src/com/android/server/DockObserverTest.java 0 → 100644 +134 −0 Original line number Diff line number Diff line /* * 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); } } Loading
core/res/res/values/config.xml +9 −0 Original line number Diff line number Diff line Loading @@ -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>
core/res/res/values/symbols.xml +2 −0 Original line number Diff line number Diff line Loading @@ -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"/> Loading
services/core/java/com/android/server/DockObserver.java +152 −37 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading @@ -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; Loading @@ -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); Loading @@ -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 Loading @@ -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) { Loading Loading @@ -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 { if (match) { return config.extraStateValue; } } return Intent.EXTRA_DOCK_STATE_DESK; } @VisibleForTesting void setDockStateFromProviderForTesting(ExtconStateProvider provider) { synchronized (mLock) { setActualDockStateLocked(Integer.parseInt(event.get("SWITCH_STATE"))); 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); } } }; Loading
services/tests/servicestests/src/com/android/server/DockObserverTest.java 0 → 100644 +134 −0 Original line number Diff line number Diff line /* * 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); } }