Update Telecom to use new Projection State APIs.
Bug: 134997071
Bug: 169702986
Test: Code builds, works on device, unit tests written and pass
Change-Id: Ia5032f87ea218c754687fba39a90f983cfd8fb5d
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index e3eb2f3..45e3151 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -47,6 +47,7 @@
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.READ_PROJECTION_STATE"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
<uses-permission android:name="android.permission.VIBRATE"/>
diff --git a/src/com/android/server/telecom/CarModeTracker.java b/src/com/android/server/telecom/CarModeTracker.java
index e64ef5d..737ce5a 100644
--- a/src/com/android/server/telecom/CarModeTracker.java
+++ b/src/com/android/server/telecom/CarModeTracker.java
@@ -30,6 +30,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.PriorityQueue;
+import java.util.function.Function;
import java.util.stream.Collectors;
/**
@@ -40,30 +41,40 @@
* Data class holding information about apps which have requested to enter car mode.
*/
private class CarModeApp {
- private @IntRange(from = 0) int mPriority;
+ private final boolean mAutomotiveProjection;
+ private final @IntRange(from = 0) int mPriority;
private @NonNull String mPackageName;
+ public CarModeApp(@NonNull String packageName) {
+ this(true, 0, packageName);
+ }
+
public CarModeApp(int priority, @NonNull String packageName) {
+ this(false, priority, packageName);
+ }
+
+ private CarModeApp(boolean automotiveProjection, int priority, @NonNull String packageName) {
+ mAutomotiveProjection = automotiveProjection;
mPriority = priority;
mPackageName = Objects.requireNonNull(packageName);
}
+ public boolean hasSetAutomotiveProjection() {
+ return mAutomotiveProjection;
+ }
+
/**
* The priority at which the app requested to enter car mode.
* Will be the same as the one specified when {@link UiModeManager#enableCarMode(int, int)}
- * was called, or {@link UiModeManager#DEFAULT_PRIORITY} if no priority was specifeid.
+ * was called, or {@link UiModeManager#DEFAULT_PRIORITY} if no priority was specified.
* @return The priority.
*/
public int getPriority() {
return mPriority;
}
- public void setPriority(int priority) {
- mPriority = priority;
- }
-
/**
- * @return The package name of the app which requested to enter car mode.
+ * @return The package name of the app which requested to enter car mode/set projection.
*/
public String getPackageName() {
return mPackageName;
@@ -72,26 +83,24 @@
public void setPackageName(String packageName) {
mPackageName = packageName;
}
- }
- /**
- * Comparator used to maintain the car mode priority queue ordering.
- */
- private class CarModeAppComparator implements Comparator<CarModeApp> {
- @Override
- public int compare(CarModeApp o1, CarModeApp o2) {
- // highest priority takes precedence.
- return Integer.compare(o2.getPriority(), o1.getPriority());
+ public String toString() {
+ return String.format("[%s, %s]",
+ mAutomotiveProjection ? "PROJECTION SET" : mPriority,
+ mPackageName);
}
}
/**
- * Priority list of apps which have entered or exited car mode, ordered with the highest
- * priority app at the top of the queue. Where items have the same priority, they are ordered
- * by insertion time.
+ * Priority list of apps which have entered or exited car mode, ordered first by whether the app
+ * has set automotive projection, and then by highest priority. Where items have the same
+ * priority, order is arbitrary, but we only allow one item in the queue per priority.
*/
private PriorityQueue<CarModeApp> mCarModeApps = new PriorityQueue<>(2,
- new CarModeAppComparator());
+ // Natural ordering of booleans is False, True. Natural ordering of ints is increasing.
+ Comparator.comparing(CarModeApp::hasSetAutomotiveProjection)
+ .thenComparing(CarModeApp::getPriority)
+ .reversed());
private final LocalLog mCarModeChangeLog = new LocalLog(20);
@@ -144,6 +153,47 @@
mCarModeApps.removeIf(c -> c.getPriority() == priority);
}
+ public void handleSetAutomotiveProjection(@NonNull String packageName) {
+ Optional<CarModeApp> projectingApp = mCarModeApps.stream()
+ .filter(CarModeApp::hasSetAutomotiveProjection)
+ .findAny();
+ // No app with automotive projection? Easy peasy, just add it.
+ if (!projectingApp.isPresent()) {
+ Log.i(this, "handleSetAutomotiveProjection: %s", packageName);
+ mCarModeChangeLog.log("setAutomotiveProjection: packageName=" + packageName);
+ mCarModeApps.add(new CarModeApp(packageName));
+ return;
+ }
+ // Otherwise an app already has automotive projection set. Is it the same app?
+ if (packageName.equals(projectingApp.get().getPackageName())) {
+ Log.w(this, "handleSetAutomotiveProjection: %s already the automotive projection app",
+ packageName);
+ return;
+ }
+ // We have a new app for automotive projection. As a shortcut just reuse the same object by
+ // overwriting the package name.
+ Log.i(this, "handleSetAutomotiveProjection: %s replacing %s as automotive projection app",
+ packageName, projectingApp.get().getPackageName());
+ mCarModeChangeLog.log("setAutomotiveProjection: " + packageName + " replaces "
+ + projectingApp.get().getPackageName());
+ projectingApp.get().setPackageName(packageName);
+ }
+
+ public void handleReleaseAutomotiveProjection() {
+ Optional<String> projectingPackage = mCarModeApps.stream()
+ .filter(CarModeApp::hasSetAutomotiveProjection)
+ .map(CarModeApp::getPackageName)
+ .findAny();
+ if (!projectingPackage.isPresent()) {
+ Log.w(this, "handleReleaseAutomotiveProjection: no current automotive projection app");
+ return;
+ }
+ Log.i(this, "handleReleaseAutomotiveProjection: %s", projectingPackage.get());
+ mCarModeChangeLog.log("releaseAutomotiveProjection: packageName="
+ + projectingPackage.get());
+ mCarModeApps.removeIf(CarModeApp::hasSetAutomotiveProjection);
+ }
+
/**
* Force-removes a package from the car mode tracking list, no matter at which priority.
*
@@ -151,19 +201,21 @@
* from the tracking list so they don't cause a leak.
* @param packageName Package name of the app to force-remove
*/
- public void forceExitCarMode(@NonNull String packageName) {
- Optional<CarModeApp> forcedApp = mCarModeApps.stream()
+ public void forceRemove(@NonNull String packageName) {
+ // We must account for the possibility that the app has set both car mode AND projection.
+ List<CarModeApp> forcedApp = mCarModeApps.stream()
.filter(c -> c.getPackageName().equals(packageName))
- .findAny();
- if (forcedApp.isPresent()) {
- String logString = String.format("forceExitCarMode: packageName=%s, was at priority=%s",
- packageName, forcedApp.get().getPriority());
+ .collect(Collectors.toList());
+ if (forcedApp.isEmpty()) {
+ Log.i(this, "Package %s is not tracked.", packageName);
+ return;
+ }
+ for (CarModeApp app : forcedApp) {
+ String logString = "forceRemove: " + app;
Log.i(this, logString);
mCarModeChangeLog.log(logString);
- mCarModeApps.removeIf(c -> c.getPackageName().equals(packageName));
- } else {
- Log.i(this, "Package %s is not tracked as requesting car mode", packageName);
}
+ mCarModeApps.removeIf(c -> c.getPackageName().equals(packageName));
}
/**
@@ -175,7 +227,7 @@
return mCarModeApps
.stream()
.sorted(mCarModeApps.comparator())
- .map(cma -> cma.getPackageName())
+ .map(CarModeApp::getPackageName)
.collect(Collectors.toList());
}
@@ -183,7 +235,7 @@
return mCarModeApps
.stream()
.sorted(mCarModeApps.comparator())
- .map(cma -> "[" + cma.getPriority() + ", " + cma.getPackageName() + "]")
+ .map(CarModeApp::toString)
.collect(Collectors.joining(", "));
}
@@ -216,7 +268,7 @@
pw.increaseIndent();
for (CarModeApp app : mCarModeApps) {
pw.print("[");
- pw.print(app.getPriority());
+ pw.print(app.hasSetAutomotiveProjection() ? "PROJECTION SET" : app.getPriority());
pw.print("] ");
pw.println(app.getPackageName());
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 4beff9f..9f5923a 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -913,8 +913,18 @@
}
@Override
+ public void onAutomotiveProjectionStateSet(String automotiveProjectionPackage) {
+ InCallController.this.handleSetAutomotiveProjection(automotiveProjectionPackage);
+ }
+
+ @Override
+ public void onAutomotiveProjectionStateReleased() {
+ InCallController.this.handleReleaseAutomotiveProjection();
+ }
+
+ @Override
public void onPackageUninstalled(String packageName) {
- mCarModeTracker.forceExitCarMode(packageName);
+ mCarModeTracker.forceRemove(packageName);
updateCarModeForSwitchingConnection();
}
};
@@ -1962,6 +1972,25 @@
updateCarModeForSwitchingConnection();
}
+ public void handleSetAutomotiveProjection(@NonNull String packageName) {
+ Log.i(this, "handleSetAutomotiveProjection: packageName=%s", packageName);
+ if (!isCarModeInCallService(packageName)) {
+ Log.i(this, "handleSetAutomotiveProjection: not a valid InCallService: packageName=%s",
+ packageName);
+ return;
+ }
+ mCarModeTracker.handleSetAutomotiveProjection(packageName);
+
+ updateCarModeForSwitchingConnection();
+ }
+
+ public void handleReleaseAutomotiveProjection() {
+ Log.i(this, "handleReleaseAutomotiveProjection");
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+
+ updateCarModeForSwitchingConnection();
+ }
+
public void updateCarModeForSwitchingConnection() {
if (mInCallServiceConnection != null) {
Log.i(this, "updateCarModeForSwitchingConnection: car mode apps: %s",
diff --git a/src/com/android/server/telecom/SystemStateHelper.java b/src/com/android/server/telecom/SystemStateHelper.java
index 3be3d5e..8fb6bc5 100644
--- a/src/com/android/server/telecom/SystemStateHelper.java
+++ b/src/com/android/server/telecom/SystemStateHelper.java
@@ -16,6 +16,7 @@
package com.android.server.telecom;
+import android.annotation.NonNull;
import android.app.UiModeManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -38,7 +39,7 @@
/**
* Provides various system states to the rest of the telecom codebase.
*/
-public class SystemStateHelper {
+public class SystemStateHelper implements UiModeManager.OnProjectionStateChangeListener {
public interface SystemStateListener {
/**
* Listener method to inform interested parties when a package name requests to enter or
@@ -51,6 +52,19 @@
void onCarModeChanged(int priority, String packageName, boolean isCarMode);
/**
+ * Listener method to inform interested parties when a package has set automotive projection
+ * state.
+ * @param automotiveProjectionPackage the package that set automotive projection.
+ */
+ void onAutomotiveProjectionStateSet(String automotiveProjectionPackage);
+
+ /**
+ * Listener method to inform interested parties when automotive projection state has been
+ * cleared.
+ */
+ void onAutomotiveProjectionStateReleased();
+
+ /**
* Notifies when a package has been uninstalled.
* @param packageName the package name of the uninstalled package
*/
@@ -99,8 +113,18 @@
}
};
+ @Override
+ public void onProjectionStateChanged(int activeProjectionTypes,
+ @NonNull Set<String> projectingPackages) {
+ if (projectingPackages.isEmpty()) {
+ onReleaseAutomotiveProjection();
+ } else {
+ onSetAutomotiveProjection(projectingPackages.iterator().next());
+ }
+ }
+
private Set<SystemStateListener> mListeners = new CopyOnWriteArraySet<>();
- private boolean mIsCarMode;
+ private boolean mIsCarModeOrProjectionActive;
public SystemStateHelper(Context context) {
mContext = context;
@@ -116,7 +140,9 @@
Log.i(this, "Registering broadcast receiver: %s", intentFilter1);
Log.i(this, "Registering broadcast receiver: %s", intentFilter2);
- mIsCarMode = getSystemCarMode();
+ mContext.getSystemService(UiModeManager.class).addOnProjectionStateChangeListener(
+ UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, mContext.getMainExecutor(), this);
+ mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState();
}
public void addListener(SystemStateListener listener) {
@@ -129,8 +155,8 @@
return mListeners.remove(listener);
}
- public boolean isCarMode() {
- return mIsCarMode;
+ public boolean isCarModeOrProjectionActive() {
+ return mIsCarModeOrProjectionActive;
}
public boolean isDeviceAtEar() {
@@ -215,7 +241,7 @@
private void onEnterCarMode(int priority, String packageName) {
Log.i(this, "Entering carmode");
- mIsCarMode = getSystemCarMode();
+ mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState();
for (SystemStateListener listener : mListeners) {
listener.onCarModeChanged(priority, packageName, true /* isCarMode */);
}
@@ -223,25 +249,44 @@
private void onExitCarMode(int priority, String packageName) {
Log.i(this, "Exiting carmode");
- mIsCarMode = getSystemCarMode();
+ mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState();
for (SystemStateListener listener : mListeners) {
listener.onCarModeChanged(priority, packageName, false /* isCarMode */);
}
}
- /**
- * Checks the system for the current car mode.
- *
- * @return True if in car mode, false otherwise.
- */
- private boolean getSystemCarMode() {
- UiModeManager uiModeManager =
- (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
-
- if (uiModeManager != null) {
- return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
+ private void onSetAutomotiveProjection(String packageName) {
+ Log.i(this, "Automotive projection set.");
+ mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState();
+ for (SystemStateListener listener : mListeners) {
+ listener.onAutomotiveProjectionStateSet(packageName);
}
+ }
+
+ private void onReleaseAutomotiveProjection() {
+ Log.i(this, "Automotive projection released.");
+ mIsCarModeOrProjectionActive = getSystemCarModeOrProjectionState();
+ for (SystemStateListener listener : mListeners) {
+ listener.onAutomotiveProjectionStateReleased();
+ }
+ }
+
+ /**
+ * Checks the system for the current car projection state.
+ *
+ * @return True if projection is active, false otherwise.
+ */
+ private boolean getSystemCarModeOrProjectionState() {
+ UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class);
+
+ if (uiModeManager != null) {
+ return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+ || (uiModeManager.getActiveProjectionTypes()
+ & UiModeManager.PROJECTION_TYPE_AUTOMOTIVE) != 0;
+ }
+
+ Log.w(this, "Got null UiModeManager, returning false.");
return false;
}
}
diff --git a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
index e93ef22..a1f357b 100644
--- a/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
+++ b/src/com/android/server/telecom/callredirection/CallRedirectionProcessor.java
@@ -294,7 +294,7 @@
* The current rule to decide whether the implemented {@link CallRedirectionService} should
* allow interactive responses with users is only based on whether it is in car mode.
*/
- mAllowInteractiveResponse = !callsManager.getSystemStateHelper().isCarMode();
+ mAllowInteractiveResponse = !callsManager.getSystemStateHelper().isCarModeOrProjectionActive();
mCallRedirectionProcessorHelper = new CallRedirectionProcessorHelper(
context, callsManager, phoneAccountRegistrar);
mProcessedDestinationUri = mCallRedirectionProcessorHelper.formatNumberForRedirection(
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 22f5348..caaf4d6 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -36,6 +36,9 @@
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <!-- Used to access Projection State APIs -->
+ <uses-permission android:name="android.permission.READ_PROJECTION_STATE"/>
+
<application android:label="@string/app_name"
android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
index ff16880..0a896a8 100644
--- a/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRedirectionProcessorTest.java
@@ -65,8 +65,6 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.util.ArrayList;
-
@RunWith(JUnit4.class)
public class CallRedirectionProcessorTest extends TelecomTestCase {
@Mock private Context mContext;
@@ -151,7 +149,7 @@
}
private void setIsInCarMode(boolean isInCarMode) {
- when(mSystemStateHelper.isCarMode()).thenReturn(isInCarMode);
+ when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(isInCarMode);
}
private void enableUserDefinedCallRedirectionService() {
diff --git a/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java b/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
index dbfcdb1..4ad46ae 100644
--- a/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CarModeTrackerTest.java
@@ -21,9 +21,6 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.assertNull;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.when;
-
import android.app.UiModeManager;
import com.android.server.telecom.CarModeTracker;
@@ -33,7 +30,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import org.mockito.Mock;
@RunWith(JUnit4.class)
public class CarModeTrackerTest extends TelecomTestCase {
@@ -110,7 +106,7 @@
@Test
public void testForceExitCarMode() {
testEnterCarModeBasic();
- mCarModeTracker.forceExitCarMode(CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.forceRemove(CAR_MODE_APP1_PACKAGE_NAME);
assertFalse(mCarModeTracker.isInCarMode());
assertNull(mCarModeTracker.getCurrentCarModePackage());
}
@@ -226,4 +222,109 @@
CAR_MODE_APP3_PACKAGE_NAME);
assertNull(mCarModeTracker.getCurrentCarModePackage());
}
+
+ /**
+ * Verifies that setting automotive projection by itself works.
+ */
+ @Test
+ public void testSetAutomotiveProjectionBasic() {
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP1_PACKAGE_NAME);
+ assertEquals(CAR_MODE_APP1_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ // We should be tracking our car mode app.
+ assertEquals(1, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ }
+
+ /**
+ * Verifies that if we set automotive projection more than once with the same package, nothing
+ * changes.
+ */
+ @Test
+ public void testSetAutomotiveProjectionMultipleTimes() {
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP1_PACKAGE_NAME);
+ // Should still only have one app.
+ assertEquals(1, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ // It should be the same one.
+ assertEquals(CAR_MODE_APP1_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ }
+
+ /**
+ * Verifies that if we set automotive projection more than once, the new package overrides.
+ */
+ @Test
+ public void testSetAutomotiveProjectionMultipleTimesDifferentPackages() {
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP2_PACKAGE_NAME);
+ // Should still only have one app.
+ assertEquals(1, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ // It should be the newer one.
+ assertEquals(CAR_MODE_APP2_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ }
+
+ /**
+ * Verifies that releasing automotive projection works as expected.
+ */
+ @Test
+ public void testReleaseAutomotiveProjectionBasic() {
+ // Releasing before something's set shouldn't break anything.
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+ assertEquals(0, mCarModeTracker.getCarModeApps().size());
+ assertFalse(mCarModeTracker.isInCarMode());
+
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+ // Should be gone now.
+ assertEquals(0, mCarModeTracker.getCarModeApps().size());
+ assertFalse(mCarModeTracker.isInCarMode());
+ }
+
+ /**
+ * Verifies that setting automotive projection overrides but doesn't overwrite car mode apps.
+ */
+ @Test
+ public void testAutomotiveProjectionOverridesCarMode() {
+ mCarModeTracker.handleEnterCarMode(50, CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.handleSetAutomotiveProjection(CAR_MODE_APP4_PACKAGE_NAME);
+
+ // Should have two apps now, the car mode and the automotive projection one.
+ assertEquals(2, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+
+ // Automotive projection takes priority.
+ assertEquals(CAR_MODE_APP4_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+
+ // If we add another car mode app, automotive projection still has priority.
+ mCarModeTracker.handleEnterCarMode(Integer.MAX_VALUE, CAR_MODE_APP2_PACKAGE_NAME);
+ assertEquals(3, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ assertEquals(CAR_MODE_APP4_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+
+ // If we release automotive projection, we go back to the prioritized list of plain car
+ // mode apps.
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+ assertEquals(2, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ assertEquals(CAR_MODE_APP2_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+
+ // Make sure we didn't mess with the first app that was added.
+ mCarModeTracker.handleExitCarMode(Integer.MAX_VALUE, CAR_MODE_APP2_PACKAGE_NAME);
+ assertEquals(1, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ assertEquals(CAR_MODE_APP1_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ }
+
+ /**
+ * Verifies that releasing automotive projection doesn't interfere with plain car mode apps.
+ */
+ @Test
+ public void testReleaseAutomotiveProjectionNoopForCarModeApps() {
+ mCarModeTracker.handleEnterCarMode(50, CAR_MODE_APP1_PACKAGE_NAME);
+ mCarModeTracker.handleReleaseAutomotiveProjection();
+ assertEquals(1, mCarModeTracker.getCarModeApps().size());
+ assertTrue(mCarModeTracker.isInCarMode());
+ assertEquals(CAR_MODE_APP1_PACKAGE_NAME, mCarModeTracker.getCurrentCarModePackage());
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index a4302b6..a44d90b 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -30,6 +30,7 @@
import android.app.AppOpsManager;
import android.app.NotificationManager;
import android.app.StatusBarManager;
+import android.app.UiModeManager;
import android.app.role.RoleManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -206,6 +207,8 @@
return mRoleManager;
case Context.TELEPHONY_REGISTRY_SERVICE:
return mTelephonyRegistryManager;
+ case Context.UI_MODE_SERVICE:
+ return mUiModeManager;
default:
return null;
}
@@ -227,6 +230,8 @@
return Context.TELEPHONY_SUBSCRIPTION_SERVICE;
} else if (svcClass == TelephonyRegistryManager.class) {
return Context.TELEPHONY_REGISTRY_SERVICE;
+ } else if (svcClass == UiModeManager.class) {
+ return Context.UI_MODE_SERVICE;
}
throw new UnsupportedOperationException();
}
@@ -486,6 +491,7 @@
private final RoleManager mRoleManager = mock(RoleManager.class);
private final TelephonyRegistryManager mTelephonyRegistryManager =
mock(TelephonyRegistryManager.class);
+ private final UiModeManager mUiModeManager = mock(UiModeManager.class);
private final PermissionInfo mPermissionInfo = mock(PermissionInfo.class);
private TelecomManager mTelecomManager = mock(TelecomManager.class);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 59f75fc..3307a93 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -243,14 +243,32 @@
when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
mSystemStateListener.onCarModeChanged(666, CAR_PKG, true);
verify(mCarModeTracker).handleEnterCarMode(666, CAR_PKG);
assertTrue(mCarModeTracker.isInCarMode());
mSystemStateListener.onPackageUninstalled(CAR_PKG);
- verify(mCarModeTracker).forceExitCarMode(CAR_PKG);
+ verify(mCarModeTracker).forceRemove(CAR_PKG);
+ assertFalse(mCarModeTracker.isInCarMode());
+ }
+
+ @SmallTest
+ @Test
+ public void testAutomotiveProjectionAppRemoval() {
+ setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
+ when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
+
+ mSystemStateListener.onAutomotiveProjectionStateSet(CAR_PKG);
+ verify(mCarModeTracker).handleSetAutomotiveProjection(CAR_PKG);
+ assertTrue(mCarModeTracker.isInCarMode());
+
+ mSystemStateListener.onPackageUninstalled(CAR_PKG);
+ verify(mCarModeTracker).forceRemove(CAR_PKG);
assertFalse(mCarModeTracker.isInCarMode());
}
@@ -786,7 +804,7 @@
setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
// Enable car mode
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// Now bind; we should only bind to one app.
@@ -816,7 +834,7 @@
matches(Manifest.permission.CONTROL_INCALL_EXPERIENCE),
matches(CAR_PKG))).thenReturn(PackageManager.PERMISSION_DENIED);
// Enable car mode
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
// Register the fact that the invalid app entered car mode.
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
@@ -928,7 +946,7 @@
mInCallController.bindToServices(mMockCall);
// Enable car mode and enter car mode at default priority.
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// And change to the second car mode app.
@@ -1075,7 +1093,7 @@
// Now switch to car mode.
// Enable car mode and enter car mode at default priority.
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1115,7 +1133,7 @@
// Now switch to car mode.
// Enable car mode and enter car mode at default priority.
- when(mMockSystemStateHelper.isCarMode()).thenReturn(true);
+ when(mMockSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(true);
mInCallController.handleCarModeChange(UiModeManager.DEFAULT_PRIORITY, CAR_PKG, true);
// We currently will bind to the car-mode InCallService even if there are no calls available
diff --git a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
index a5b78b7..e6c6bac 100644
--- a/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
+++ b/tests/src/com/android/server/telecom/tests/NewOutgoingCallIntentBroadcasterTest.java
@@ -110,7 +110,7 @@
when(mPhoneAccountRegistrar.getPhoneAccountUnchecked(
any(PhoneAccountHandle.class))).thenReturn(mPhoneAccount);
when(mPhoneAccount.isSelfManaged()).thenReturn(true);
- when(mSystemStateHelper.isCarMode()).thenReturn(false);
+ when(mSystemStateHelper.isCarModeOrProjectionActive()).thenReturn(false);
}
@Override
diff --git a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
index 893ae3d..ad52625 100644
--- a/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/SystemStateHelperTest.java
@@ -60,6 +60,7 @@
import org.mockito.internal.util.reflection.FieldSetter;
import java.util.List;
+import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -91,6 +92,8 @@
when(mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)).thenReturn(mGravitySensor);
when(mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)).thenReturn(mProxSensor);
+ doReturn(mUiModeManager).when(mContext).getSystemService(UiModeManager.class);
+
mComponentContextFixture.putFloatResource(
R.dimen.device_on_ear_xy_gravity_threshold, 5.5f);
mComponentContextFixture.putFloatResource(
@@ -117,17 +120,53 @@
@SmallTest
@Test
public void testQuerySystemForCarMode_True() {
- when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
- assertTrue(new SystemStateHelper(mContext).isCarMode());
+ assertTrue(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
}
@SmallTest
@Test
public void testQuerySystemForCarMode_False() {
- when(mContext.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(mUiModeManager);
when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
- assertFalse(new SystemStateHelper(mContext).isCarMode());
+ assertFalse(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testQuerySystemForAutomotiveProjection_True() {
+ when(mUiModeManager.getActiveProjectionTypes())
+ .thenReturn(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+ assertTrue(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
+
+ when(mUiModeManager.getActiveProjectionTypes())
+ .thenReturn(UiModeManager.PROJECTION_TYPE_ALL);
+ assertTrue(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testQuerySystemForAutomotiveProjection_False() {
+ when(mUiModeManager.getActiveProjectionTypes())
+ .thenReturn(UiModeManager.PROJECTION_TYPE_NONE);
+ assertFalse(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testQuerySystemForAutomotiveProjectionAndCarMode_True() {
+ when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+ when(mUiModeManager.getActiveProjectionTypes())
+ .thenReturn(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE);
+ assertTrue(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
+ }
+
+ @SmallTest
+ @Test
+ public void testQuerySystemForAutomotiveProjectionOrCarMode_nullService() {
+ when(mContext.getSystemService(UiModeManager.class))
+ .thenReturn(mUiModeManager) // Without this, class construction will throw NPE.
+ .thenReturn(null);
+ assertFalse(new SystemStateHelper(mContext).isCarModeOrProjectionActive());
}
@SmallTest
@@ -204,6 +243,40 @@
@SmallTest
@Test
+ public void testOnSetReleaseAutomotiveProjection() {
+ SystemStateHelper systemStateHelper = new SystemStateHelper(mContext);
+ // We don't care what listener is registered, that's an implementation detail, but we need
+ // to call methods on whatever it is.
+ ArgumentCaptor<UiModeManager.OnProjectionStateChangeListener> listenerCaptor =
+ ArgumentCaptor.forClass(UiModeManager.OnProjectionStateChangeListener.class);
+ verify(mUiModeManager).addOnProjectionStateChangeListener(
+ eq(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE), any(), listenerCaptor.capture());
+ systemStateHelper.addListener(mSystemStateListener);
+
+ String packageName1 = "Sufjan Stevens";
+ String packageName2 = "The Ascension";
+
+ // Should pay attention to automotive projection, though.
+ listenerCaptor.getValue().onProjectionStateChanged(
+ UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, Set.of(packageName2));
+ verify(mSystemStateListener).onAutomotiveProjectionStateSet(packageName2);
+
+ // Without any automotive projection, it should see it as released.
+ listenerCaptor.getValue().onProjectionStateChanged(
+ UiModeManager.PROJECTION_TYPE_NONE, Set.of());
+ verify(mSystemStateListener).onAutomotiveProjectionStateReleased();
+
+ // Try the whole thing again, with different values.
+ listenerCaptor.getValue().onProjectionStateChanged(
+ UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, Set.of(packageName1));
+ verify(mSystemStateListener).onAutomotiveProjectionStateSet(packageName1);
+ listenerCaptor.getValue().onProjectionStateChanged(
+ UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, Set.of());
+ verify(mSystemStateListener, times(2)).onAutomotiveProjectionStateReleased();
+ }
+
+ @SmallTest
+ @Test
public void testDeviceOnEarCorrectlyDetected() {
doAnswer(invocation -> {
SensorEventListener listener = invocation.getArgument(0);