Merge "Format sensor values and imperial unit conversions"
diff --git a/car-lib/Android.bp b/car-lib/Android.bp
index 1d887fa..4e03d8c 100644
--- a/car-lib/Android.bp
+++ b/car-lib/Android.bp
@@ -109,17 +109,6 @@
],
}
-genrule {
- name: "android-car-last-released-test-api",
- srcs: [
- "api/test-released/*.txt",
- ],
- cmd: "cp -f $$(echo $(in) | tr \" \" \"\\n\" | sort -n | tail -1) $(genDir)/last-released-test-api.txt",
- out: [
- "last-released-test-api.txt",
- ],
-}
-
droidstubs {
name: "android.car-stubs-docs",
defaults: ["android.car-docs-default"],
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index f520acc..c59a5e3 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -144,6 +144,7 @@
field public static final int EV_CHARGE_PORT_OPEN = 287310602; // 0x1120030a
field public static final int FOG_LIGHTS_STATE = 289410562; // 0x11400e02
field public static final int FOG_LIGHTS_SWITCH = 289410578; // 0x11400e12
+ field public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364; // 0x11200604
field public static final int FUEL_DOOR_OPEN = 287310600; // 0x11200308
field public static final int FUEL_LEVEL = 291504903; // 0x11600307
field public static final int FUEL_LEVEL_LOW = 287310853; // 0x11200405
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index ff4f8d1..4462f6c 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -17,6 +17,7 @@
field public static final String PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL = "android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
field public static final String PERMISSION_CAR_POWER = "android.car.permission.CAR_POWER";
field public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
+ field public static final String PERMISSION_CAR_PROJECTION_STATUS = "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
field public static final String PERMISSION_CAR_TEST_SERVICE = "android.car.permission.CAR_TEST_SERVICE";
field public static final String PERMISSION_CONTROL_APP_BLOCKING = "android.car.permission.CONTROL_APP_BLOCKING";
field public static final String PERMISSION_CONTROL_CAR_CLIMATE = "android.car.permission.CONTROL_CAR_CLIMATE";
@@ -43,15 +44,19 @@
}
public final class CarProjectionManager {
- method public void onCarDisconnected();
- method public void registerProjectionListener(android.car.CarProjectionManager.CarProjectionListener, int);
- method public void registerProjectionRunner(android.content.Intent);
- method public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
- method public boolean requestBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
- method public void startProjectionAccessPoint(android.car.CarProjectionManager.ProjectionAccessPointCallback);
- method public void stopProjectionAccessPoint();
- method public void unregisterProjectionListener();
- method public void unregisterProjectionRunner(android.content.Intent);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public java.util.List<java.lang.Integer> getAvailableWifiChannels(int);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) @NonNull public android.os.Bundle getProjectionOptions();
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionListener(@NonNull android.car.CarProjectionManager.CarProjectionListener, int);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void registerProjectionRunner(@NonNull android.content.Intent);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void registerProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean releaseBluetoothProfileInhibit(android.bluetooth.BluetoothDevice, int, android.os.IBinder);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public boolean requestBluetoothProfileInhibit(@NonNull android.bluetooth.BluetoothDevice, int, @NonNull android.os.IBinder);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void startProjectionAccessPoint(@NonNull android.car.CarProjectionManager.ProjectionAccessPointCallback);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void stopProjectionAccessPoint();
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionListener();
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void unregisterProjectionRunner(@NonNull android.content.Intent);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION_STATUS) public void unregisterProjectionStatusListener(@NonNull android.car.CarProjectionManager.ProjectionStatusListener);
+ method @RequiresPermission(android.car.Car.PERMISSION_CAR_PROJECTION) public void updateProjectionStatus(@NonNull android.car.projection.ProjectionStatus);
field public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 2; // 0x2
field public static final int PROJECTION_VOICE_SEARCH = 1; // 0x1
}
@@ -71,6 +76,10 @@
field public static final int ERROR_TETHERING_DISALLOWED = 4; // 0x4
}
+ public static interface CarProjectionManager.ProjectionStatusListener {
+ method public void onProjectionStatusChanged(int, @Nullable String, @NonNull java.util.List<android.car.projection.ProjectionStatus>);
+ }
+
public final class VehicleAreaDoor {
field public static final int DOOR_HOOD = 268435456; // 0x10000000
field public static final int DOOR_REAR = 536870912; // 0x20000000
@@ -791,6 +800,66 @@
}
+package android.car.projection {
+
+ public class ProjectionOptions {
+ ctor public ProjectionOptions(android.os.Bundle);
+ method @Nullable public android.app.ActivityOptions getActivityOptions();
+ method @Nullable public android.content.ComponentName getConsentActivity();
+ method public int getUiMode();
+ method @NonNull public android.os.Bundle toBundle();
+ field public static final int UI_MODE_BLENDED = 1; // 0x1
+ field public static final int UI_MODE_FULL_SCREEN = 0; // 0x0
+ }
+
+ public final class ProjectionStatus implements android.os.Parcelable {
+ method @NonNull public static android.car.projection.ProjectionStatus.Builder builder(String, int);
+ method public int describeContents();
+ method @NonNull public java.util.List<android.car.projection.ProjectionStatus.MobileDevice> getConnectedMobileDevices();
+ method @NonNull public android.os.Bundle getExtras();
+ method @NonNull public String getPackageName();
+ method public int getState();
+ method public int getTransport();
+ method public boolean isActive();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus> CREATOR;
+ field public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3; // 0x3
+ field public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2; // 0x2
+ field public static final int PROJECTION_STATE_INACTIVE = 0; // 0x0
+ field public static final int PROJECTION_STATE_READY_TO_PROJECT = 1; // 0x1
+ field public static final int PROJECTION_TRANSPORT_NONE = 0; // 0x0
+ field public static final int PROJECTION_TRANSPORT_USB = 1; // 0x1
+ field public static final int PROJECTION_TRANSPORT_WIFI = 2; // 0x2
+ }
+
+ public static final class ProjectionStatus.Builder {
+ method @NonNull public android.car.projection.ProjectionStatus.Builder addMobileDevice(android.car.projection.ProjectionStatus.MobileDevice);
+ method public android.car.projection.ProjectionStatus build();
+ method @NonNull public android.car.projection.ProjectionStatus.Builder setExtras(android.os.Bundle);
+ method @NonNull public android.car.projection.ProjectionStatus.Builder setProjectionTransport(int);
+ }
+
+ public static final class ProjectionStatus.MobileDevice implements android.os.Parcelable {
+ method @NonNull public static android.car.projection.ProjectionStatus.MobileDevice.Builder builder(int, String);
+ method public int describeContents();
+ method @NonNull public java.util.List<java.lang.Integer> getAvailableTransports();
+ method @NonNull public android.os.Bundle getExtras();
+ method public int getId();
+ method @NonNull public String getName();
+ method public boolean isProjecting();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.car.projection.ProjectionStatus.MobileDevice> CREATOR;
+ }
+
+ public static final class ProjectionStatus.MobileDevice.Builder {
+ method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder addTransport(int);
+ method @NonNull public android.car.projection.ProjectionStatus.MobileDevice build();
+ method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setExtras(android.os.Bundle);
+ method @NonNull public android.car.projection.ProjectionStatus.MobileDevice.Builder setProjecting(boolean);
+ }
+
+}
+
package android.car.storagemonitoring {
public final class CarStorageMonitoringManager {
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index ab6aa9f..0ab23b7 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -393,6 +393,14 @@
public static final String PERMISSION_CAR_PROJECTION = "android.car.permission.CAR_PROJECTION";
/**
+ * Permission necessary to access projection status.
+ * @hide
+ */
+ @SystemApi
+ public static final String PERMISSION_CAR_PROJECTION_STATUS =
+ "android.car.permission.ACCESS_CAR_PROJECTION_STATUS";
+
+ /**
* Permission necessary to mock vehicle hal for testing.
* @hide
* @deprecated mocking vehicle HAL in car service is no longer supported.
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 50606de..7706bf7 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -16,11 +16,18 @@
package android.car;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
import android.content.Intent;
import android.net.wifi.WifiConfiguration;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -29,7 +36,14 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
/**
* CarProjectionManager allows applications implementing projection to register/unregister itself
@@ -44,6 +58,9 @@
public final class CarProjectionManager implements CarManagerBase {
private static final String TAG = CarProjectionManager.class.getSimpleName();
+ private final Binder mToken = new Binder();
+ private final Object mLock = new Object();
+
/**
* Listener to get projected notifications.
*
@@ -81,10 +98,30 @@
private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy;
+ private final Set<ProjectionStatusListener> mProjectionStatusListeners = new LinkedHashSet<>();
+ private CarProjectionStatusListenerImpl mCarProjectionStatusListener;
+
// Only one access point proxy object per process.
private static final IBinder mAccessPointProxyToken = new Binder();
/**
+ * Interface to receive for projection status updates.
+ */
+ public interface ProjectionStatusListener {
+ /**
+ * This method gets invoked if projection status has been changed.
+ *
+ * @param state - current projection state
+ * @param packageName - if projection is currently running either in the foreground or
+ * in the background this argument will contain its package name
+ * @param details - contains detailed information about all currently registered projection
+ * receivers.
+ */
+ void onProjectionStatusChanged(@ProjectionState int state, @Nullable String packageName,
+ @NonNull List<ProjectionStatus> details);
+ }
+
+ /**
* @hide
*/
public CarProjectionManager(IBinder service, Handler handler) {
@@ -107,11 +144,11 @@
* @param listener
* @param voiceSearchFilter Flags of voice search requests to get notification.
*/
- public void registerProjectionListener(CarProjectionListener listener, int voiceSearchFilter) {
- if (listener == null) {
- throw new IllegalArgumentException("null listener");
- }
- synchronized (this) {
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public void registerProjectionListener(@NonNull CarProjectionListener listener,
+ int voiceSearchFilter) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ synchronized (mLock) {
if (mListener == null || mVoiceSearchFilter != voiceSearchFilter) {
try {
mService.registerProjectionListener(mBinderListener, voiceSearchFilter);
@@ -135,8 +172,9 @@
/**
* Unregister listener and stop listening projection events.
*/
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public void unregisterProjectionListener() {
- synchronized (this) {
+ synchronized (mLock) {
try {
mService.unregisterProjectionListener(mBinderListener);
} catch (RemoteException e) {
@@ -152,11 +190,10 @@
* to create reverse binding.
* @param serviceIntent
*/
- public void registerProjectionRunner(Intent serviceIntent) {
- if (serviceIntent == null) {
- throw new IllegalArgumentException("null serviceIntent");
- }
- synchronized (this) {
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public void registerProjectionRunner(@NonNull Intent serviceIntent) {
+ Preconditions.checkNotNull("serviceIntent cannot be null");
+ synchronized (mLock) {
try {
mService.registerProjectionRunner(serviceIntent);
} catch (RemoteException e) {
@@ -170,11 +207,10 @@
* reverse binding.
* @param serviceIntent
*/
- public void unregisterProjectionRunner(Intent serviceIntent) {
- if (serviceIntent == null) {
- throw new IllegalArgumentException("null serviceIntent");
- }
- synchronized (this) {
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public void unregisterProjectionRunner(@NonNull Intent serviceIntent) {
+ Preconditions.checkNotNull("serviceIntent cannot be null");
+ synchronized (mLock) {
try {
mService.unregisterProjectionRunner(serviceIntent);
} catch (RemoteException e) {
@@ -183,6 +219,7 @@
}
}
+ /** @hide */
@Override
public void onCarDisconnected() {
// nothing to do
@@ -194,9 +231,13 @@
*
* <p>A process can have only one request to start an access point, subsequent call of this
* method will invalidate previous calls.
+ *
+ * @param callback to receive notifications when access point status changed for the request
*/
- public void startProjectionAccessPoint(ProjectionAccessPointCallback callback) {
- synchronized (this) {
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
+ Preconditions.checkNotNull(callback, "callback cannot be null");
+ synchronized (mLock) {
Looper looper = mHandler.getLooper();
ProjectionAccessPointCallbackProxy proxy =
new ProjectionAccessPointCallbackProxy(this, looper, callback);
@@ -210,11 +251,32 @@
}
/**
+ * Returns a list of available Wi-Fi channels. A channel is specified as frequency in MHz,
+ * e.g. channel 1 will be represented as 2412 in the list.
+ *
+ * @param band one of the values from {@code android.net.wifi.WifiScanner#WIFI_BAND_*}
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public @NonNull List<Integer> getAvailableWifiChannels(int band) {
+ try {
+ int[] channels = mService.getAvailableWifiChannels(band);
+ List<Integer> channelList = new ArrayList<>(channels.length);
+ for (int v : channels) {
+ channelList.add(v);
+ }
+ return channelList;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Stop Wi-Fi Access Point for wireless projection receiver app.
*/
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public void stopProjectionAccessPoint() {
ProjectionAccessPointCallbackProxy proxy;
- synchronized (this) {
+ synchronized (mLock) {
proxy = mProjectionAccessPointCallbackProxy;
mProjectionAccessPointCallbackProxy = null;
}
@@ -239,8 +301,11 @@
* owning the token dies, the request will automatically be released.
* @return True if the profile was successfully inhibited, false if an error occurred.
*/
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public boolean requestBluetoothProfileInhibit(
- BluetoothDevice device, int profile, IBinder token) {
+ @NonNull BluetoothDevice device, int profile, @NonNull IBinder token) {
+ Preconditions.checkNotNull(device, "device cannot be null");
+ Preconditions.checkNotNull(token, "token cannot be null");
try {
return mService.requestBluetoothProfileInhibit(device, profile, token);
} catch (RemoteException e) {
@@ -258,8 +323,11 @@
* {@link #requestBluetoothProfileInhibit}.
* @return True if the request was released, false if an error occurred.
*/
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
public boolean releaseBluetoothProfileInhibit(
BluetoothDevice device, int profile, IBinder token) {
+ Preconditions.checkNotNull(device, "device cannot be null");
+ Preconditions.checkNotNull(token, "token cannot be null");
try {
return mService.releaseBluetoothProfileInhibit(device, profile, token);
} catch (RemoteException e) {
@@ -268,6 +336,108 @@
}
/**
+ * Call this method to report projection status of your app. The aggregated status (from other
+ * projection apps if available) will be broadcasted to interested parties.
+ *
+ * @param status the reported status that will be distributed to the interested listeners
+ *
+ * @see #registerProjectionListener(CarProjectionListener, int)
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public void updateProjectionStatus(@NonNull ProjectionStatus status) {
+ Preconditions.checkNotNull(status, "status cannot be null");
+ try {
+ mService.updateProjectionStatus(status, mToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Register projection status listener. See {@link ProjectionStatusListener} for details. It is
+ * allowed to register multiple listeners.
+ *
+ * <p>Note: provided listener will be called immediately with the most recent status.
+ *
+ * @param listener the listener to receive notification for any projection status changes
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+ public void registerProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ synchronized (mLock) {
+ mProjectionStatusListeners.add(listener);
+
+ if (mCarProjectionStatusListener == null) {
+ mCarProjectionStatusListener = new CarProjectionStatusListenerImpl(this);
+ try {
+ mService.registerProjectionStatusListener(mCarProjectionStatusListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ // Already subscribed to Car Service, immediately notify listener with the current
+ // projection status in the event handler thread.
+ mHandler.post(() ->
+ listener.onProjectionStatusChanged(
+ mCarProjectionStatusListener.mCurrentState,
+ mCarProjectionStatusListener.mCurrentPackageName,
+ mCarProjectionStatusListener.mDetails));
+ }
+ }
+ }
+
+ /**
+ * Unregister provided listener from projection status notifications
+ *
+ * @param listener the listener for projection status notifications that was previously
+ * registered with {@link #unregisterProjectionStatusListener(ProjectionStatusListener)}
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION_STATUS)
+ public void unregisterProjectionStatusListener(@NonNull ProjectionStatusListener listener) {
+ Preconditions.checkNotNull(listener, "listener cannot be null");
+ synchronized (mLock) {
+ if (!mProjectionStatusListeners.remove(listener)
+ || !mProjectionStatusListeners.isEmpty()) {
+ return;
+ }
+ unregisterProjectionStatusListenerFromCarServiceLocked();
+ }
+ }
+
+ private void unregisterProjectionStatusListenerFromCarServiceLocked() {
+ try {
+ mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
+ mCarProjectionStatusListener = null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private void handleProjectionStatusChanged(@ProjectionState int state,
+ String packageName, List<ProjectionStatus> details) {
+ List<ProjectionStatusListener> listeners;
+ synchronized (mLock) {
+ listeners = new ArrayList<>(mProjectionStatusListeners);
+ }
+ for (ProjectionStatusListener listener : listeners) {
+ listener.onProjectionStatusChanged(state, packageName, details);
+ }
+ }
+
+ /**
+ * Returns {@link Bundle} object that contains customization for projection app. This bundle
+ * can be parsed using {@link ProjectionOptions}.
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_PROJECTION)
+ public @NonNull Bundle getProjectionOptions() {
+ try {
+ return mService.getProjectionOptions();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Callback class for applications to receive updates about the LocalOnlyHotspot status.
*/
public abstract static class ProjectionAccessPointCallback {
@@ -345,7 +515,7 @@
private void handleVoiceAssistantRequest(boolean fromLongPress) {
CarProjectionListener listener;
- synchronized (this) {
+ synchronized (mLock) {
if (mListener == null) {
return;
}
@@ -371,4 +541,34 @@
manager.mHandler.post(() -> manager.handleVoiceAssistantRequest(fromLongPress));
}
}
+
+ private static class CarProjectionStatusListenerImpl
+ extends ICarProjectionStatusListener.Stub {
+
+ private @ProjectionState int mCurrentState;
+ private @Nullable String mCurrentPackageName;
+ private List<ProjectionStatus> mDetails = new ArrayList<>(0);
+
+ private final WeakReference<CarProjectionManager> mManagerRef;
+
+ private CarProjectionStatusListenerImpl(CarProjectionManager mgr) {
+ mManagerRef = new WeakReference<>(mgr);
+ }
+
+ @Override
+ public void onProjectionStatusChanged(int projectionState,
+ String packageName,
+ List<ProjectionStatus> details) {
+ CarProjectionManager mgr = mManagerRef.get();
+ if (mgr != null) {
+ mgr.mHandler.post(() -> {
+ mCurrentState = projectionState;
+ mCurrentPackageName = packageName;
+ mDetails = Collections.unmodifiableList(details);
+
+ mgr.handleProjectionStatusChanged(projectionState, packageName, mDetails);
+ });
+ }
+ }
+ }
}
diff --git a/car-lib/src/android/car/ICarProjection.aidl b/car-lib/src/android/car/ICarProjection.aidl
index efaac0d..6e85c45 100644
--- a/car-lib/src/android/car/ICarProjection.aidl
+++ b/car-lib/src/android/car/ICarProjection.aidl
@@ -17,8 +17,11 @@
package android.car;
import android.bluetooth.BluetoothDevice;
+import android.car.projection.ProjectionStatus;
import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
import android.content.Intent;
+import android.os.Bundle;
import android.os.Messenger;
/**
@@ -69,4 +72,22 @@
/** Undo the effects of requestBluetoothProfileInhibit. */
boolean releaseBluetoothProfileInhibit(
in BluetoothDevice device, in int profile, in IBinder token) = 7;
+
+ /** Reports projection status for a given projection receiver app. */
+ void updateProjectionStatus(in ProjectionStatus status, in IBinder token) = 8;
+
+ /** Registers projection status listener */
+ void registerProjectionStatusListener(in ICarProjectionStatusListener listener) = 9;
+
+ /** Unregister projection status listener */
+ void unregisterProjectionStatusListener(in ICarProjectionStatusListener listener) = 10;
+
+ /**
+ * Returns options for projection receiver app that can be un-packed using
+ * {@link android.car.projection.ProjectionOptions} class.
+ */
+ Bundle getProjectionOptions() = 11;
+
+ /** Returns a list of available Wi-Fi channels */
+ int[] getAvailableWifiChannels(int band) = 12;
}
diff --git a/car-lib/src/android/car/ICarProjectionStatusListener.aidl b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
new file mode 100644
index 0000000..cac48a0
--- /dev/null
+++ b/car-lib/src/android/car/ICarProjectionStatusListener.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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 android.car;
+
+import android.car.projection.ProjectionStatus;
+
+/**
+ * Listener interface to notify interested parties of projection status change.
+ *
+ * @hide
+ */
+oneway interface ICarProjectionStatusListener {
+ void onProjectionStatusChanged(int projectionState,
+ in String activeProjectionPackageName,
+ in List<ProjectionStatus> details) = 0;
+}
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index ce10d2a..cfc6e34 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -295,6 +295,10 @@
*/
public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
/**
+ * Fuel consumption units for display
+ */
+ public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
+ /**
* Outside temperature
*/
public static final int ENV_OUTSIDE_TEMPERATURE = 291505923;
@@ -776,6 +780,9 @@
if (o == EV_BATTERY_DISPLAY_UNITS) {
return "EV_BATTERY_DISPLAY_UNITS";
}
+ if (o == FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME) {
+ return "FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME";
+ }
if (o == ENV_OUTSIDE_TEMPERATURE) {
return "ENV_OUTSIDE_TEMPERATURE";
}
diff --git a/car-lib/src/android/car/VehicleUnit.java b/car-lib/src/android/car/VehicleUnit.java
index c9859dc..463d53b 100644
--- a/car-lib/src/android/car/VehicleUnit.java
+++ b/car-lib/src/android/car/VehicleUnit.java
@@ -43,7 +43,8 @@
public static final int KELVIN = 0x32;
public static final int MILLILITER = 0x40;
public static final int LITER = 0x41;
- public static final int GALLON = 0x42;
+ public static final int US_GALLON = 0x42;
+ public static final int IMPERIAL_GALLON = 0x43;
public static final int NANO_SECS = 0x50;
public static final int SECS = 0x53;
public static final int YEAR = 0x59;
@@ -74,7 +75,8 @@
KELVIN,
MILLILITER,
LITER,
- GALLON,
+ US_GALLON,
+ IMPERIAL_GALLON,
NANO_SECS,
SECS,
YEAR,
diff --git a/car-lib/src/android/car/projection/ProjectionOptions.java b/car-lib/src/android/car/projection/ProjectionOptions.java
new file mode 100644
index 0000000..26b8140
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionOptions.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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 android.car.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class holds OEM customization for projection receiver app. It is created by Car Service.
+ *
+ * @hide
+ */
+@SystemApi
+public class ProjectionOptions {
+ private static final String KEY_PREFIX = "android.car.projection.";
+
+ /** Immersive full screen mode (all system bars are hidden) */
+ public static final int UI_MODE_FULL_SCREEN = 0;
+
+ /** Show status and navigation bars. */
+ public static final int UI_MODE_BLENDED = 1;
+
+ private static final int UI_MODE_DEFAULT = UI_MODE_FULL_SCREEN;
+
+ /** @hide */
+ @IntDef({UI_MODE_FULL_SCREEN, UI_MODE_BLENDED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProjectionUiMode {}
+
+ private static final String KEY_ACTIVITY_OPTIONS = KEY_PREFIX + "activityOptions";
+ private static final String KEY_UI_MODE = KEY_PREFIX + "systemUiFlags";
+ private static final String KEY_CONSENT_ACTIVITY = KEY_PREFIX + "consentActivity";
+
+ private final ActivityOptions mActivityOptions;
+ private final int mUiMode;
+ private final ComponentName mConsentActivity;
+
+ /**
+ * Creates new instance for given {@code Bundle}
+ *
+ * @param bundle contains OEM specific information
+ */
+ public ProjectionOptions(Bundle bundle) {
+ Bundle activityOptionsBundle = bundle.getBundle(KEY_ACTIVITY_OPTIONS);
+ mActivityOptions = activityOptionsBundle != null
+ ? new ActivityOptions(activityOptionsBundle) : null;
+ mUiMode = bundle.getInt(KEY_UI_MODE, UI_MODE_DEFAULT);
+ mConsentActivity = bundle.getParcelable(KEY_CONSENT_ACTIVITY);
+ }
+
+ private ProjectionOptions(Builder builder) {
+ mActivityOptions = builder.mActivityOptions;
+ mUiMode = builder.mUiMode;
+ mConsentActivity = builder.mConsentActivity;
+ }
+
+ /**
+ * Returns combination of flags from View.SYSTEM_UI_FLAG_* which will be used by projection
+ * receiver app during rendering.
+ */
+ public @ProjectionUiMode int getUiMode() {
+ return mUiMode;
+ }
+
+ /**
+ * Returns {@link ActivityOptions} that needs to be applied when launching projection activity
+ */
+ public @Nullable ActivityOptions getActivityOptions() {
+ return mActivityOptions;
+ }
+
+ /**
+ * Returns package/activity name of the consent activity provided by OEM which needs to be shown
+ * for all mobile devices unless user accepted the consent.
+ *
+ * <p>If the method returns null then consent dialog should not be shown.
+ */
+ public @Nullable ComponentName getConsentActivity() {
+ return mConsentActivity;
+ }
+
+ /** Converts current object to {@link Bundle} */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ if (mActivityOptions != null) {
+ bundle.putBundle(KEY_ACTIVITY_OPTIONS, mActivityOptions.toBundle());
+ }
+ bundle.putParcelable(KEY_CONSENT_ACTIVITY, mConsentActivity);
+ if (mUiMode != UI_MODE_DEFAULT) {
+ bundle.putInt(KEY_UI_MODE, mUiMode);
+ }
+ return bundle;
+ }
+
+ /** @hide */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** @hide */
+ public static class Builder {
+ private ActivityOptions mActivityOptions;
+ private int mUiMode = UI_MODE_DEFAULT;
+ private ComponentName mConsentActivity;
+
+ /** Sets {@link ActivityOptions} to launch projection activity. */
+ public Builder setProjectionActivityOptions(ActivityOptions activityOptions) {
+ mActivityOptions = activityOptions;
+ return this;
+ }
+
+ /** Set UI for projection activity. It can be one of {@code UI_MODE_*} constants. */
+ public Builder setUiMode(@ProjectionUiMode int uiMode) {
+ mUiMode = uiMode;
+ return this;
+ }
+
+ /** Sets consent activity which will be shown before starting projection. */
+ public Builder setConsentActivity(ComponentName consentActivity) {
+ mConsentActivity = consentActivity;
+ return this;
+ }
+
+ /** Creates an instance of {@link android.car.projection.ProjectionOptions} */
+ public ProjectionOptions build() {
+ return new ProjectionOptions(this);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return toBundle().toString();
+ }
+}
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.aidl b/car-lib/src/android/car/projection/ProjectionStatus.aidl
new file mode 100644
index 0000000..f2ad77a
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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 android.car.projection;
+
+parcelable ProjectionStatus;
\ No newline at end of file
diff --git a/car-lib/src/android/car/projection/ProjectionStatus.java b/car-lib/src/android/car/projection/ProjectionStatus.java
new file mode 100644
index 0000000..37d0aee
--- /dev/null
+++ b/car-lib/src/android/car/projection/ProjectionStatus.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2019 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 android.car.projection;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.IntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class encapsulates information about projection status and connected mobile devices.
+ *
+ * <p>Since the number of connected devices expected to be small we include information about
+ * connected devices in every status update.
+ *
+ * @hide
+ */
+@SystemApi
+public final class ProjectionStatus implements Parcelable {
+ /** This state indicates that projection is not actively running and no compatible mobile
+ * devices available. */
+ public static final int PROJECTION_STATE_INACTIVE = 0;
+
+ /** At least one phone connected and ready to project. */
+ public static final int PROJECTION_STATE_READY_TO_PROJECT = 1;
+
+ /** Projecting in the foreground */
+ public static final int PROJECTION_STATE_ACTIVE_FOREGROUND = 2;
+
+ /** Projection is running in the background */
+ public static final int PROJECTION_STATE_ACTIVE_BACKGROUND = 3;
+
+ private static final int PROJECTION_STATE_MAX = PROJECTION_STATE_ACTIVE_BACKGROUND;
+
+ /** This status is used when projection is not actively running */
+ public static final int PROJECTION_TRANSPORT_NONE = 0;
+
+ /** This status is used when projection is not actively running */
+ public static final int PROJECTION_TRANSPORT_USB = 1;
+
+ /** This status is used when projection is not actively running */
+ public static final int PROJECTION_TRANSPORT_WIFI = 2;
+
+ private static final int PROJECTION_TRANSPORT_MAX = PROJECTION_TRANSPORT_WIFI;
+
+ /** @hide */
+ @IntDef(value = {
+ PROJECTION_TRANSPORT_NONE,
+ PROJECTION_TRANSPORT_USB,
+ PROJECTION_TRANSPORT_WIFI,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProjectionTransport {}
+
+ /** @hide */
+ @IntDef(value = {
+ PROJECTION_STATE_INACTIVE,
+ PROJECTION_STATE_READY_TO_PROJECT,
+ PROJECTION_STATE_ACTIVE_FOREGROUND,
+ PROJECTION_STATE_ACTIVE_BACKGROUND,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProjectionState {}
+
+ private final String mPackageName;
+ private final int mState;
+ private final int mTransport;
+ private final List<MobileDevice> mConnectedMobileDevices;
+ private final Bundle mExtras;
+
+ /** Creator for this class. Required to have in parcelable implementations. */
+ public static final Creator<ProjectionStatus> CREATOR = new Creator<ProjectionStatus>() {
+ @Override
+ public ProjectionStatus createFromParcel(Parcel source) {
+ return new ProjectionStatus(source);
+ }
+
+ @Override
+ public ProjectionStatus[] newArray(int size) {
+ return new ProjectionStatus[size];
+ }
+ };
+
+ private ProjectionStatus(Builder builder) {
+ mPackageName = builder.mPackageName;
+ mState = builder.mState;
+ mTransport = builder.mTransport;
+ mConnectedMobileDevices = new ArrayList<>(builder.mMobileDevices);
+ mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+ }
+
+ private ProjectionStatus(Parcel source) {
+ mPackageName = source.readString();
+ mState = source.readInt();
+ mTransport = source.readInt();
+ mExtras = source.readBundle(getClass().getClassLoader());
+ mConnectedMobileDevices = source.createTypedArrayList(MobileDevice.CREATOR);
+ }
+
+ /** Parcelable implementation */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mPackageName);
+ dest.writeInt(mState);
+ dest.writeInt(mTransport);
+ dest.writeBundle(mExtras);
+ dest.writeTypedList(mConnectedMobileDevices);
+ }
+
+ /** Returns projection state which could be one of the constants starting with
+ * {@code #PROJECTION_STATE_}.
+ */
+ public @ProjectionState int getState() {
+ return mState;
+ }
+
+ /** Returns package name of the projection receiver app. */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /** Returns extra information provided by projection receiver app */
+ public @NonNull Bundle getExtras() {
+ return mExtras == null ? new Bundle() : new Bundle(mExtras);
+ }
+
+ /** Returns true if currently projecting either in the foreground or in the background. */
+ public boolean isActive() {
+ return mState == PROJECTION_STATE_ACTIVE_BACKGROUND
+ || mState == PROJECTION_STATE_ACTIVE_FOREGROUND;
+ }
+
+ /** Returns transport which is used for active projection or
+ * {@link #PROJECTION_TRANSPORT_NONE} if projection is not running.
+ */
+ public @ProjectionTransport int getTransport() {
+ return mTransport;
+ }
+
+ /** Returns a list of currently connected mobile devices. */
+ public @NonNull List<MobileDevice> getConnectedMobileDevices() {
+ return new ArrayList<>(mConnectedMobileDevices);
+ }
+
+ /**
+ * Returns new {@link Builder} instance.
+ *
+ * @param packageName package name that will be associated with this status
+ * @param state current projection state, must be one of the {@code PROJECTION_STATE_*}
+ */
+ @NonNull
+ public static Builder builder(String packageName, @ProjectionState int state) {
+ return new Builder(packageName, state);
+ }
+
+ /** Builder class for {@link ProjectionStatus} */
+ public static final class Builder {
+ private final int mState;
+ private final String mPackageName;
+ private int mTransport = PROJECTION_TRANSPORT_NONE;
+ private List<MobileDevice> mMobileDevices = new ArrayList<>();
+ private Bundle mExtras;
+
+ private Builder(String packageName, @ProjectionState int state) {
+ if (packageName == null) {
+ throw new IllegalArgumentException("Package name can't be null");
+ }
+ if (state < 0 || state > PROJECTION_STATE_MAX) {
+ throw new IllegalArgumentException("Invalid projection state: " + state);
+ }
+ mPackageName = packageName;
+ mState = state;
+ }
+
+ /**
+ * Sets the transport which is used for currently projecting phone if any.
+ *
+ * @param transport transport of current projection, must be one of the
+ * {@code PROJECTION_TRANSPORT_*}
+ */
+ public @NonNull Builder setProjectionTransport(@ProjectionTransport int transport) {
+ checkProjectionTransport(transport);
+ mTransport = transport;
+ return this;
+ }
+
+ /**
+ * Add connected mobile device
+ *
+ * @param mobileDevice connected mobile device
+ * @return this builder
+ */
+ public @NonNull Builder addMobileDevice(MobileDevice mobileDevice) {
+ mMobileDevices.add(mobileDevice);
+ return this;
+ }
+
+ /**
+ * Add extra information.
+ *
+ * @param extras may contain an extra information that can be passed from the projection
+ * app to the projection status listeners
+ * @return this builder
+ */
+ public @NonNull Builder setExtras(Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /** Creates {@link ProjectionStatus} object. */
+ public ProjectionStatus build() {
+ return new ProjectionStatus(this);
+ }
+ }
+
+ private static void checkProjectionTransport(@ProjectionTransport int transport) {
+ if (transport < 0 || transport > PROJECTION_TRANSPORT_MAX) {
+ throw new IllegalArgumentException("Invalid projection transport: " + transport);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ProjectionStatus{"
+ + "mPackageName='" + mPackageName + '\''
+ + ", mState=" + mState
+ + ", mTransport=" + mTransport
+ + ", mConnectedMobileDevices=" + mConnectedMobileDevices
+ + (mExtras != null ? " (has extras)" : "")
+ + '}';
+ }
+
+ /** Class that represents information about connected mobile device. */
+ public static final class MobileDevice implements Parcelable {
+ private final int mId;
+ private final String mName;
+ private final int[] mAvailableTransports;
+ private final boolean mProjecting;
+ private final Bundle mExtras;
+
+ /** Creator for this class. Required to have in parcelable implementations. */
+ public static final Creator<MobileDevice> CREATOR = new Creator<MobileDevice>() {
+ @Override
+ public MobileDevice createFromParcel(Parcel source) {
+ return new MobileDevice(source);
+ }
+
+ @Override
+ public MobileDevice[] newArray(int size) {
+ return new MobileDevice[size];
+ }
+ };
+
+ private MobileDevice(Builder builder) {
+ mId = builder.mId;
+ mName = builder.mName;
+ mAvailableTransports = builder.mAvailableTransports.toArray();
+ mProjecting = builder.mProjecting;
+ mExtras = builder.mExtras == null ? null : new Bundle(builder.mExtras);
+ }
+
+ private MobileDevice(Parcel source) {
+ mId = source.readInt();
+ mName = source.readString();
+ mAvailableTransports = source.createIntArray();
+ mProjecting = source.readBoolean();
+ mExtras = source.readBundle(getClass().getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeString(mName);
+ dest.writeIntArray(mAvailableTransports);
+ dest.writeBoolean(mProjecting);
+ dest.writeBundle(mExtras);
+ }
+
+ /** Returns the device id which uniquely identifies the mobile device within projection */
+ public int getId() {
+ return mId;
+ }
+
+ /** Returns the name of the device */
+ public @NonNull String getName() {
+ return mName;
+ }
+
+ /** Returns a list of available projection transports. See {@code PROJECTION_TRANSPORT_*}
+ * for possible values. */
+ public @NonNull List<Integer> getAvailableTransports() {
+ List<Integer> transports = new ArrayList<>(mAvailableTransports.length);
+ for (int transport : mAvailableTransports) {
+ transports.add(transport);
+ }
+ return transports;
+ }
+
+ /** Indicates whether this mobile device is currently projecting */
+ public boolean isProjecting() {
+ return mProjecting;
+ }
+
+ /** Returns extra information for mobile device */
+ public @NonNull Bundle getExtras() {
+ return mExtras == null ? new Bundle() : new Bundle(mExtras);
+ }
+
+ /** Parcelable implementation */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Creates new instance of {@link Builder}
+ *
+ * @param id uniquely identifies the device
+ * @param name name of the connected device
+ * @return the instance of {@link Builder}
+ */
+ public static @NonNull Builder builder(int id, String name) {
+ return new Builder(id, name);
+ }
+
+ @Override
+ public String toString() {
+ return "MobileDevice{"
+ + "mId=" + mId
+ + ", mName='" + mName + '\''
+ + ", mAvailableTransports=" + Arrays.toString(mAvailableTransports)
+ + ", mProjecting=" + mProjecting
+ + (mExtras != null ? ", (has extras)" : "")
+ + '}';
+ }
+
+ /**
+ * Builder class for {@link MobileDevice}
+ */
+ public static final class Builder {
+ private int mId;
+ private String mName;
+ private IntArray mAvailableTransports = new IntArray();
+ private boolean mProjecting;
+ private Bundle mExtras;
+
+ private Builder(int id, String name) {
+ mId = id;
+ if (name == null) {
+ throw new IllegalArgumentException("Name of the device can't be null");
+ }
+ mName = name;
+ }
+
+ /**
+ * Add supported transport
+ *
+ * @param transport supported transport by given device, must be one of the
+ * {@code PROJECTION_TRANSPORT_*}
+ * @return this builder
+ */
+ public @NonNull Builder addTransport(@ProjectionTransport int transport) {
+ checkProjectionTransport(transport);
+ mAvailableTransports.add(transport);
+ return this;
+ }
+
+ /**
+ * Indicate whether the mobile device currently projecting or not.
+ *
+ * @param projecting {@code True} if this mobile device currently projecting
+ * @return this builder
+ */
+ public @NonNull Builder setProjecting(boolean projecting) {
+ mProjecting = projecting;
+ return this;
+ }
+
+ /**
+ * Add extra information for mobile device
+ *
+ * @param extras provides an arbitrary extra information about this mobile device
+ * @return this builder
+ */
+ public @NonNull Builder setExtras(Bundle extras) {
+ mExtras = extras;
+ return this;
+ }
+
+ /** Creates new instance of {@link MobileDevice} */
+ public @NonNull MobileDevice build() {
+ return new MobileDevice(this);
+ }
+ }
+ }
+}
diff --git a/car-test-lib/src/android/car/testapi/CarProjectionController.java b/car-test-lib/src/android/car/testapi/CarProjectionController.java
index 91de599..c3fada6 100644
--- a/car-test-lib/src/android/car/testapi/CarProjectionController.java
+++ b/car-test-lib/src/android/car/testapi/CarProjectionController.java
@@ -16,10 +16,16 @@
package android.car.testapi;
+import android.car.projection.ProjectionOptions;
import android.net.wifi.WifiConfiguration;
/** Controller to change behavior of {@link android.car.CarProjectionManager} */
public interface CarProjectionController {
/** Set WifiConfiguration for wireless projection or null to simulate failure to start AP */
void setWifiConfiguration(WifiConfiguration wifiConfiguration);
+ /**
+ * Sets {@link ProjectionOptions} object returns by
+ * {@link android.car.CarProjectionManager#getProjectionOptions()} call
+ */
+ void setProjectionOptions(ProjectionOptions projectionOptions);
}
diff --git a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
index 1bf15cd..bba5a04 100644
--- a/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
+++ b/car-test-lib/src/android/car/testapi/FakeCarProjectionService.java
@@ -21,16 +21,25 @@
import android.car.CarProjectionManager.ProjectionAccessPointCallback;
import android.car.ICarProjection;
import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.wifi.WifiConfiguration;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* Fake implementation of {@link ICarProjection} interface.
*
@@ -44,6 +53,11 @@
private WifiConfiguration mWifiConfiguration;
private Messenger mApMessenger;
private IBinder mApBinder;
+ private List<ICarProjectionStatusListener> mStatusListeners = new ArrayList<>();
+ private Map<IBinder, ProjectionStatus> mProjectionStatusMap = new HashMap<>();
+ private ProjectionStatus mCurrentProjectionStatus = ProjectionStatus.builder(
+ "", ProjectionStatus.PROJECTION_STATE_INACTIVE).build();
+ private ProjectionOptions mProjectionOptions;
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
@@ -55,6 +69,7 @@
FakeCarProjectionService(Context context) {
mContext = context;
+ mProjectionOptions = ProjectionOptions.builder().build();
}
@Override
@@ -118,7 +133,55 @@
}
@Override
+ public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+ throws RemoteException {
+ mCurrentProjectionStatus = status;
+ mProjectionStatusMap.put(token, status);
+ notifyStatusListeners(status,
+ mStatusListeners.toArray(new ICarProjectionStatusListener[0]));
+ }
+
+ private void notifyStatusListeners(ProjectionStatus status,
+ ICarProjectionStatusListener... listeners) throws RemoteException {
+ for (ICarProjectionStatusListener listener : listeners) {
+ listener.onProjectionStatusChanged(
+ status.getState(),
+ status.getPackageName(),
+ new ArrayList<>(mProjectionStatusMap.values()));
+ }
+ }
+
+ @Override
+ public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+ throws RemoteException {
+ mStatusListeners.add(listener);
+ notifyStatusListeners(mCurrentProjectionStatus, listener);
+ }
+
+ @Override
+ public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+ throws RemoteException {
+ mStatusListeners.remove(listener);
+ }
+
+ @Override
public void setWifiConfiguration(WifiConfiguration wifiConfiguration) {
mWifiConfiguration = wifiConfiguration;
}
+
+ @Override
+ public Bundle getProjectionOptions() throws RemoteException {
+ return mProjectionOptions.toBundle();
+ }
+
+ @Override
+ public int[] getAvailableWifiChannels(int band) throws RemoteException {
+ return new int[] {2412 /* Channel 1 */, 5180 /* Channel 36 */};
+ }
+
+
+ @Override
+ public void setProjectionOptions(ProjectionOptions projectionOptions) {
+ mProjectionOptions = projectionOptions;
+ }
}
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 34c4c31..76c4d37 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -119,6 +119,16 @@
android:label="@string/car_permission_label_projection"
android:description="@string/car_permission_desc_projection" />
<permission
+ android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"
+ android:protectionLevel="system|signature"
+ android:label="@string/car_permission_label_access_projection_status"
+ android:description="@string/car_permission_desc_access_projection_status" />
+ <permission
+ android:name="android.car.permission.BIND_PROJECTION_SERVICE"
+ android:protectionLevel="signature"
+ android:label="@string/car_permission_label_bind_projection_service"
+ android:description="@string/car_permission_desc_bind_projection_service" />
+ <permission
android:name="android.car.permission.CAR_MOCK_VEHICLE_HAL"
android:protectionLevel="system|signature"
android:label="@string/car_permission_label_mock_vehicle_hal"
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 6f5975a..48f346e 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -143,4 +143,19 @@
<string name="config_TetheredProjectionAccessPointSsid" translatable="false">CarAP</string>
+ <!-- The consent activity that must be shown for every unknown mobile device before projection
+ gets started. The format is: com.your.package/com.your.Activity -->
+ <string name="config_projectionConsentActivity" translatable="false"/>
+
+ <!-- Display Id where projection rendering activity needs to be shown, Specify -1 to use system
+ defaults -->
+ <integer name="config_projectionActivityDisplayId" translatable="false">-1</integer>
+
+ <!-- Bounds of the projection activity on the screen. It should be in the pixels and screen
+ coordinates in the following order: left, top, right, bottom. -->
+ <integer-array name="config_projectionActivityLaunchBounds" translatable="false"/>
+
+ <!-- UI mode for projection activity. See ProjectionOptions class for possible values. -->
+ <integer name="config_projectionUiMode" translatable="false">0</integer>
+
</resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 0d9e698..f74710c 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -60,12 +60,20 @@
<string name="car_permission_desc_radio">Access your car\u2019s radio.</string>
<!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
<string name="car_permission_label_projection">Car Projection</string>
+ <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_projection">Allows an app to project an interface from a phone on the car\u2019s display.</string>
+ <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+ <string name="car_permission_label_access_projection_status">Access projection status</string>
+ <!-- Permission text: apps can listen car-projection status[CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_access_projection_status">Allows an app to get the status of other apps projecting to the car\u2019s display.</string>
+ <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+ <string name="car_permission_label_bind_projection_service">Bind to a projection service</string>
+ <!-- Permission text: allows framework to bind to the services in projection apps[CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_bind_projection_service">Allows the holder to bind to the top-level interface of a projection service. Should never be needed for normal apps."</string>
<!-- Permission text: apps can control car-audio-volume [CHAR LIMIT=NONE] -->
<string name="car_permission_label_audio_volume">Car Audio Volume</string>
<!-- Permission text: apps can control car-audio-settings [CHAR LIMIT=NONE] -->
<string name="car_permission_label_audio_settings">Car Audio Settings</string>
- <!-- Permission text: apps can control car-projection [CHAR LIMIT=NONE] -->
- <string name="car_permission_desc_projection">Project phone interface on car display.</string>
<string name="car_permission_label_mock_vehicle_hal">Emulate vehicle HAL</string>
<!-- Permission text: can emulate information from your car [CHAR LIMIT=NONE] -->
<string name="car_permission_desc_mock_vehicle_hal">Emulate your car\u2019s vehicle HAL for internal
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index 0bec14c..8cf7c46 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -19,6 +19,7 @@
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.car.hardware.power.ICarPower;
import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
import android.content.Context;
import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
import android.os.Handler;
@@ -29,6 +30,7 @@
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.os.UserHandle;
import android.util.Log;
import com.android.car.hal.PowerHalService;
@@ -75,6 +77,9 @@
private int mNextWakeupSec = 0;
private int mTokenValue = 1;
private boolean mShutdownOnFinish = false;
+ private boolean mIsBooting = true;
+
+ private final CarUserManagerHelper mCarUserManagerHelper;
// TODO: Make this OEM configurable.
private static final int SHUTDOWN_POLLING_INTERVAL_MS = 2000;
@@ -96,10 +101,12 @@
}
public CarPowerManagementService(
- Context context, PowerHalService powerHal, SystemInterface systemInterface) {
+ Context context, PowerHalService powerHal, SystemInterface systemInterface,
+ CarUserManagerHelper carUserManagerHelper) {
mContext = context;
mHal = powerHal;
mSystemInterface = systemInterface;
+ mCarUserManagerHelper = carUserManagerHelper;
}
/**
@@ -113,6 +120,7 @@
mSystemInterface = null;
mHandlerThread = null;
mHandler = new PowerHandler(Looper.getMainLooper());
+ mCarUserManagerHelper = null;
}
@VisibleForTesting
@@ -186,6 +194,11 @@
handler.handlePowerStateChange();
}
+ @VisibleForTesting
+ protected void clearIsBooting() {
+ mIsBooting = false;
+ }
+
/**
* Initiate state change from CPMS directly.
*/
@@ -262,6 +275,17 @@
}
private void handleOn() {
+ // Do not switch user if it is booting as there can be a race with CarServiceHelperService
+ if (mIsBooting) {
+ mIsBooting = false;
+ } else {
+ int targetUserId = mCarUserManagerHelper.getInitialUser();
+ if (targetUserId != UserHandle.USER_SYSTEM
+ && targetUserId != mCarUserManagerHelper.getCurrentForegroundUserId()) {
+ Log.i(CarLog.TAG_POWER, "Desired user changed, switching to user:" + targetUserId);
+ mCarUserManagerHelper.switchToUserId(targetUserId);
+ }
+ }
mSystemInterface.setDisplayState(true);
sendPowerManagerEvent(CarPowerStateListener.ON);
mHal.sendOn();
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 582d535..9570b2a 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -18,6 +18,7 @@
import static android.car.CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH;
import static android.car.CarProjectionManager.PROJECTION_VOICE_SEARCH;
import static android.car.CarProjectionManager.ProjectionAccessPointCallback.ERROR_GENERIC;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
import static android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_FAILURE_REASON;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
@@ -28,17 +29,25 @@
import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLING;
import android.annotation.Nullable;
+import android.app.ActivityOptions;
import android.bluetooth.BluetoothDevice;
import android.car.CarProjectionManager;
import android.car.CarProjectionManager.ProjectionAccessPointCallback;
import android.car.ICarProjection;
import android.car.ICarProjectionCallback;
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.ProjectionState;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Rect;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.GroupCipher;
import android.net.wifi.WifiConfiguration.KeyMgmt;
@@ -47,13 +56,16 @@
import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
import android.net.wifi.WifiManager.SoftApCallback;
+import android.net.wifi.WifiScanner;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
@@ -67,6 +79,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* Car projection service allows to bound to projected app to boost it prioirity.
@@ -88,17 +101,36 @@
@GuardedBy("mLock")
private final HashMap<IBinder, WirelessClient> mWirelessClients = new HashMap<>();
- @Nullable
@GuardedBy("mLock")
- private LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
+ private @Nullable LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
- @Nullable
+
@GuardedBy("mLock")
- private SoftApCallback mSoftApCallback;
+ private @Nullable SoftApCallback mSoftApCallback;
+
+ @GuardedBy("mLock")
+ private final HashMap<IBinder, ProjectionReceiverClient> mProjectionReceiverClients =
+ new HashMap<>();
@Nullable
private String mApBssid;
+ @GuardedBy("mLock")
+ private @Nullable WifiScanner mWifiScanner;
+
+ @GuardedBy("mLock")
+ private @ProjectionState int mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+
+ @GuardedBy("mLock")
+ private ProjectionOptions mProjectionOptions;
+
+ @GuardedBy("mLock")
+ private @Nullable String mCurrentProjectionPackage;
+
+ private final List<ICarProjectionStatusListener> mProjectionStatusListeners =
+ new CopyOnWriteArrayList<>();
+
+
private static final int WIFI_MODE_TETHERED = 1;
private static final int WIFI_MODE_LOCALONLY = 2;
@@ -139,10 +171,10 @@
private boolean mBound;
private Intent mRegisteredService;
- CarProjectionService(Context context, CarInputService carInputService,
- CarBluetoothService carBluetoothService) {
+ CarProjectionService(Context context, @Nullable Handler handler,
+ CarInputService carInputService, CarBluetoothService carBluetoothService) {
mContext = context;
- mHandler = new Handler();
+ mHandler = handler == null ? new Handler() : handler;
mCarInputService = carInputService;
mCarBluetoothService = carBluetoothService;
mProjectionCallbacks = new ProjectionCallbackHolder(this);
@@ -152,6 +184,7 @@
@Override
public void registerProjectionRunner(Intent serviceIntent) {
+ ICarImpl.assertProjectionPermission(mContext);
// We assume one active projection app running in the system at one time.
synchronized (mLock) {
if (serviceIntent.filterEquals(mRegisteredService) && mBound) {
@@ -168,6 +201,7 @@
@Override
public void unregisterProjectionRunner(Intent serviceIntent) {
+ ICarImpl.assertProjectionPermission(mContext);
synchronized (mLock) {
if (!serviceIntent.filterEquals(mRegisteredService)) {
Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
@@ -218,6 +252,7 @@
@Override
public void registerProjectionListener(ICarProjectionCallback callback, int filter) {
+ ICarImpl.assertProjectionPermission(mContext);
synchronized (mLock) {
ProjectionCallback info = mProjectionCallbacks.get(callback);
if (info == null) {
@@ -232,6 +267,7 @@
@Override
public void unregisterProjectionListener(ICarProjectionCallback listener) {
+ ICarImpl.assertProjectionPermission(mContext);
synchronized (mLock) {
mProjectionCallbacks.removeBinder(listener);
}
@@ -241,6 +277,7 @@
@Override
public void startProjectionAccessPoint(final Messenger messenger, IBinder binder)
throws RemoteException {
+ ICarImpl.assertProjectionPermission(mContext);
//TODO: check if access point already started with the desired configuration.
registerWirelessClient(WirelessClient.of(messenger, binder));
startAccessPoint();
@@ -248,6 +285,7 @@
@Override
public void stopProjectionAccessPoint(IBinder token) {
+ ICarImpl.assertProjectionPermission(mContext);
Log.i(TAG, "Received stop access point request from " + token);
boolean shouldReleaseAp;
@@ -264,6 +302,35 @@
}
}
+ @Override
+ public int[] getAvailableWifiChannels(int band) {
+ ICarImpl.assertProjectionPermission(mContext);
+ WifiScanner scanner;
+ synchronized (mLock) {
+ // Lazy initialization
+ if (mWifiScanner == null) {
+ mWifiScanner = mContext.getSystemService(WifiScanner.class);
+ }
+ scanner = mWifiScanner;
+ }
+ if (scanner == null) {
+ Log.w(TAG, "Unable to get WifiScanner");
+ return new int[0];
+ }
+
+ List<Integer> channels = scanner.getAvailableChannels(band);
+ if (channels == null || channels.isEmpty()) {
+ Log.w(TAG, "WifiScanner reported no available channels");
+ return new int[0];
+ }
+
+ int[] array = new int[channels.size()];
+ for (int i = 0; i < channels.size(); i++) {
+ array[i] = channels.get(i);
+ }
+ return array;
+ }
+
/**
* Request to disconnect the given profile on the given device, and prevent it from reconnecting
* until either the request is released, or the process owning the given token dies.
@@ -281,6 +348,7 @@
Log.d(TAG, "requestBluetoothProfileInhibit device=" + device + " profile=" + profile
+ " from uid " + Binder.getCallingUid());
}
+ ICarImpl.assertProjectionPermission(mContext);
try {
if (device == null) {
// Will be caught by AIDL and thrown to caller.
@@ -313,6 +381,7 @@
Log.d(TAG, "releaseBluetoothProfileInhibit device=" + device + " profile=" + profile
+ " from uid " + Binder.getCallingUid());
}
+ ICarImpl.assertProjectionPermission(mContext);
try {
if (device == null) {
// Will be caught by AIDL and thrown to caller.
@@ -328,6 +397,155 @@
}
}
+ @Override
+ public void updateProjectionStatus(ProjectionStatus status, IBinder token)
+ throws RemoteException {
+ if (DBG) {
+ Log.d(TAG, "updateProjectionStatus, status: " + status + ", token: " + token);
+ }
+ ICarImpl.assertProjectionPermission(mContext);
+ final String packageName = status.getPackageName();
+ final int uid = Binder.getCallingUid();
+ try {
+ if (uid != mContext.getPackageManager().getPackageUid(packageName, 0)) {
+ throw new SecurityException(
+ "UID " + uid + " cannot update status for package " + packageName);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new SecurityException("Package " + packageName + " does not exist", e);
+ }
+
+ synchronized (mLock) {
+ ProjectionReceiverClient client = getOrCreateProjectionReceiverClientLocked(token);
+ client.mProjectionStatus = status;
+
+ if (status.isActive() || TextUtils.equals(packageName, mCurrentProjectionPackage)) {
+ mCurrentProjectionState = status.getState();
+ mCurrentProjectionPackage = packageName;
+ }
+ }
+ notifyProjectionStatusChanged(null /* notify all listeners */);
+ }
+
+ @Override
+ public void registerProjectionStatusListener(ICarProjectionStatusListener listener)
+ throws RemoteException {
+ ICarImpl.assertProjectionStatusPermission(mContext);
+ mProjectionStatusListeners.add(listener);
+
+ // Immediately notify listener with the current status.
+ notifyProjectionStatusChanged(listener);
+ }
+
+ @Override
+ public void unregisterProjectionStatusListener(ICarProjectionStatusListener listener)
+ throws RemoteException {
+ ICarImpl.assertProjectionStatusPermission(mContext);
+ mProjectionStatusListeners.remove(listener);
+ }
+
+ private ProjectionReceiverClient getOrCreateProjectionReceiverClientLocked(
+ IBinder token) throws RemoteException {
+ ProjectionReceiverClient client;
+ client = mProjectionReceiverClients.get(token);
+ if (client == null) {
+ client = new ProjectionReceiverClient(() -> unregisterProjectionReceiverClient(token));
+ token.linkToDeath(client.mDeathRecipient, 0 /* flags */);
+ mProjectionReceiverClients.put(token, client);
+ }
+ return client;
+ }
+
+ private void unregisterProjectionReceiverClient(IBinder token) {
+ synchronized (mLock) {
+ ProjectionReceiverClient client = mProjectionReceiverClients.remove(token);
+ if (client != null && TextUtils.equals(
+ client.mProjectionStatus.getPackageName(), mCurrentProjectionPackage)) {
+ mCurrentProjectionPackage = null;
+ mCurrentProjectionState = PROJECTION_STATE_INACTIVE;
+ }
+ }
+ }
+
+ private void notifyProjectionStatusChanged(
+ @Nullable ICarProjectionStatusListener singleListenerToNotify)
+ throws RemoteException {
+ int currentState;
+ String currentPackage;
+ List<ProjectionStatus> statuses = new ArrayList<>();
+ synchronized (mLock) {
+ for (ProjectionReceiverClient client : mProjectionReceiverClients.values()) {
+ statuses.add(client.mProjectionStatus);
+ }
+ currentState = mCurrentProjectionState;
+ currentPackage = mCurrentProjectionPackage;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "Notify projection status change, state: " + currentState + ", pkg: "
+ + currentPackage + ", listeners: " + mProjectionStatusListeners.size()
+ + ", listenerToNotify: " + singleListenerToNotify);
+ }
+
+ if (singleListenerToNotify == null) {
+ for (ICarProjectionStatusListener listener : mProjectionStatusListeners) {
+ listener.onProjectionStatusChanged(currentState, currentPackage, statuses);
+ }
+ } else {
+ singleListenerToNotify.onProjectionStatusChanged(
+ currentState, currentPackage, statuses);
+ }
+ }
+
+ @Override
+ public Bundle getProjectionOptions() {
+ ICarImpl.assertProjectionPermission(mContext);
+ synchronized (mLock) {
+ if (mProjectionOptions == null) {
+ mProjectionOptions = createProjectionOptionsBuilder()
+ .build();
+ }
+ }
+ return mProjectionOptions.toBundle();
+ }
+
+ private ProjectionOptions.Builder createProjectionOptionsBuilder() {
+ Resources res = mContext.getResources();
+
+ ProjectionOptions.Builder builder = ProjectionOptions.builder();
+
+ ActivityOptions activityOptions = createActivityOptions(res);
+ if (activityOptions != null) {
+ builder.setProjectionActivityOptions(activityOptions);
+ }
+
+ String consentActivity = res.getString(R.string.config_projectionConsentActivity);
+ if (!TextUtils.isEmpty(consentActivity)) {
+ builder.setConsentActivity(ComponentName.unflattenFromString(consentActivity));
+ }
+
+ builder.setUiMode(res.getInteger(R.integer.config_projectionUiMode));
+ return builder;
+ }
+
+ @Nullable
+ private static ActivityOptions createActivityOptions(Resources res) {
+ ActivityOptions activityOptions = ActivityOptions.makeBasic();
+ boolean changed = false;
+ int displayId = res.getInteger(R.integer.config_projectionActivityDisplayId);
+ if (displayId != -1) {
+ activityOptions.setLaunchDisplayId(displayId);
+ changed = true;
+ }
+ int[] rawBounds = res.getIntArray(R.array.config_projectionActivityLaunchBounds);
+ if (rawBounds != null && rawBounds.length == 4) {
+ Rect bounds = new Rect(rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3]);
+ activityOptions.setLaunchBounds(bounds);
+ changed = true;
+ }
+ return changed ? activityOptions : null;
+ }
+
private void startAccessPoint() {
synchronized (mLock) {
switch (mWifiMode) {
@@ -588,6 +806,10 @@
writer.println("SoftApCallback: " + mSoftApCallback);
writer.println("Bound to projection app: " + mBound);
writer.println("Registered Service: " + mRegisteredService);
+ writer.println("Current projection state: " + mCurrentProjectionState);
+ writer.println("Current projection package: " + mCurrentProjectionPackage);
+ writer.println("Projection status: " + mProjectionReceiverClients);
+ writer.println("WifiScanner: " + mWifiScanner);
}
}
@@ -599,6 +821,14 @@
}
}
+ void setUiMode(Integer uiMode) {
+ synchronized (mLock) {
+ mProjectionOptions = createProjectionOptionsBuilder()
+ .setUiMode(uiMode)
+ .build();
+ }
+ }
+
private static class ProjectionCallbackHolder
extends BinderInterfaceContainer<ICarProjectionCallback> {
ProjectionCallbackHolder(CarProjectionService service) {
@@ -796,4 +1026,21 @@
Random random = new Random();
return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
}
+
+ private static class ProjectionReceiverClient {
+ private final DeathRecipient mDeathRecipient;
+ private ProjectionStatus mProjectionStatus;
+
+ ProjectionReceiverClient(DeathRecipient deathRecipient) {
+ mDeathRecipient = deathRecipient;
+ }
+
+ @Override
+ public String toString() {
+ return "ProjectionReceiverClient{"
+ + "mDeathRecipient=" + mDeathRecipient
+ + ", mProjectionStatus=" + mProjectionStatus
+ + '}';
+ }
+ }
}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 6d51640..2df562e 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -121,7 +121,7 @@
mCarUserService = new CarUserService(serviceContext, mUserManagerHelper);
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
- systemInterface);
+ systemInterface, mUserManagerHelper);
mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
@@ -134,7 +134,7 @@
mPerUserCarServiceHelper, mCarUXRestrictionsService);
mCarInputService = new CarInputService(serviceContext, mHal.getInputHal());
mCarProjectionService = new CarProjectionService(
- serviceContext, mCarInputService, mCarBluetoothService);
+ serviceContext, null /* handler */, mCarInputService, mCarBluetoothService);
mGarageModeService = new GarageModeService(mContext);
mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
mCarAudioService = new CarAudioService(serviceContext);
@@ -274,7 +274,6 @@
assertClusterManagerPermission(mContext);
return mInstrumentClusterService.getManagerService();
case Car.PROJECTION_SERVICE:
- assertProjectionPermission(mContext);
return mCarProjectionService;
case Car.VMS_SUBSCRIBER_SERVICE:
assertVmsSubscriberPermission(mContext);
@@ -349,6 +348,11 @@
assertPermission(context, Car.PERMISSION_CAR_PROJECTION);
}
+ /** Verify the calling context has the {@link Car#PERMISSION_CAR_PROJECTION_STATUS} */
+ public static void assertProjectionStatusPermission(Context context) {
+ assertPermission(context, Car.PERMISSION_CAR_PROJECTION_STATUS);
+ }
+
public static void assertAnyDiagnosticPermission(Context context) {
assertAnyPermission(context,
Car.PERMISSION_CAR_DIAGNOSTIC_READ_ALL,
@@ -471,6 +475,7 @@
private static final String COMMAND_GARAGE_MODE = "garage-mode";
private static final String COMMAND_GET_DO_ACTIVITIES = "get-do-activities";
private static final String COMMAND_GET_CARPROPERTYCONFIG = "get-carpropertyconfig";
+ private static final String COMMAND_PROJECTION_UI_MODE = "projection-ui-mode";
private static final String PARAM_DAY_MODE = "day";
private static final String PARAM_NIGHT_MODE = "night";
@@ -568,6 +573,13 @@
String propertyId = args.length < 2 ? "" : args[1];
mHal.dumpPropertyConfigs(writer, propertyId);
break;
+ case COMMAND_PROJECTION_UI_MODE:
+ if (args.length != 2) {
+ writer.println("Incorrect number of arguments");
+ dumpHelp(writer);
+ break;
+ }
+ mCarProjectionService.setUiMode(Integer.valueOf(args[1]));
default:
writer.println("Unknown command: \"" + arg + "\"");
dumpHelp(writer);
diff --git a/service/src/com/android/car/SystemActivityMonitoringService.java b/service/src/com/android/car/SystemActivityMonitoringService.java
index c2abdc6..11b6887 100644
--- a/service/src/com/android/car/SystemActivityMonitoringService.java
+++ b/service/src/com/android/car/SystemActivityMonitoringService.java
@@ -425,6 +425,10 @@
}
@Override
+ public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+ }
+
+ @Override
public void onProcessDied(int pid, int uid) {
mHandler.requestProcessDied(pid, uid);
}
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index dd38c33..ca90f78 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -397,6 +397,9 @@
mProps.put(VehicleProperty.EV_BATTERY_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
+ Car.PERMISSION_READ_DISPLAY_UNITS,
+ Car.PERMISSION_CONTROL_DISPLAY_UNITS));
}
/**
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
index d102a49..6cabd69 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ActivityMonitor.java
@@ -60,6 +60,10 @@
}
@Override
+ public void onForegroundServicesChanged(int pid, int uid, int fgServicetypes) {
+ }
+
+ @Override
public void onProcessDied(int pid, int uid) {
notifyTopActivities();
}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
index abfeb11..db04c24 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
@@ -85,7 +85,7 @@
try {
mManager.registerProjectionListener(null, CarProjectionManager.PROJECTION_VOICE_SEARCH);
fail();
- } catch (IllegalArgumentException e) {
+ } catch (NullPointerException e) {
// expected.
}
}
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
index 557816a..e1d9c90 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -16,8 +16,15 @@
package com.android.car;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.car.hardware.power.ICarPowerStateListener;
+import android.car.userlib.CarUserManagerHelper;
import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateShutdownParam;
import android.os.RemoteException;
@@ -52,6 +59,7 @@
private final MockWakeLockInterface mWakeLockInterface = new MockWakeLockInterface();
private final MockIOInterface mIOInterface = new MockIOInterface();
private final PowerSignalListener mPowerSignalListener = new PowerSignalListener();
+ private CarUserManagerHelper mCarUserManagerHelper;
private MockedPowerHalService mPowerHal;
private SystemInterface mSystemInterface;
@@ -68,6 +76,10 @@
.withSystemStateInterface(mSystemStateInterface)
.withWakeLockInterface(mWakeLockInterface)
.withIOInterface(mIOInterface).build();
+ mCarUserManagerHelper = mock(CarUserManagerHelper.class);
+ doReturn(true).when(mCarUserManagerHelper).switchToUserId(anyInt());
+ doReturn(10).when(mCarUserManagerHelper).getInitialUser();
+ doReturn(10).when(mCarUserManagerHelper).getCurrentForegroundUserId();
}
@Override
@@ -83,7 +95,8 @@
* Helper method to create mService and initialize a test case
*/
private void initTest(int wakeupTime) throws Exception {
- mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface);
+ mService = new CarPowerManagementService(getContext(), mPowerHal, mSystemInterface,
+ mCarUserManagerHelper);
mService.init();
CarPowerManagementService.setShutdownPrepareTimeout(0);
mPowerHal.setSignalListener(mPowerSignalListener);
@@ -103,7 +116,6 @@
mSystemInterface.setDisplayState(false);
mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS);
initTest(0);
-
// Transition to ON state
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
@@ -160,6 +172,13 @@
public void testSleepEntryAndWakeUpForProcessing() throws Exception {
final int wakeupTime = 100;
initTest(wakeupTime);
+
+ // set up for user switching after display on
+ final int currentUserId = 10;
+ final int newUserId = 11;
+ doReturn(newUserId).when(mCarUserManagerHelper).getInitialUser();
+ doReturn(currentUserId).when(mCarUserManagerHelper).getCurrentForegroundUserId();
+
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
@@ -177,8 +196,12 @@
mService.scheduleNextWakeupTime(wakeupTime);
// second processing after wakeup
assertFalse(mDisplayInterface.getDisplayState());
+ // do not skip user switching part.
+ mService.clearIsBooting();
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+ // user switching should have been requested.
+ verify(mCarUserManagerHelper, times(1)).switchToUserId(newUserId);
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.SHUTDOWN_PREPARE,
VehicleApPowerStateShutdownParam.CAN_SLEEP));
assertStateReceivedForShutdownOrSleepWithPostpone(
diff --git a/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
new file mode 100644
index 0000000..476c152
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/CarProjectionServiceTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2019 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.car;
+
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND;
+import static android.car.projection.ProjectionStatus.PROJECTION_STATE_INACTIVE;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_USB;
+import static android.car.projection.ProjectionStatus.PROJECTION_TRANSPORT_WIFI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.car.ICarProjectionStatusListener;
+import android.car.projection.ProjectionOptions;
+import android.car.projection.ProjectionStatus;
+import android.car.projection.ProjectionStatus.MobileDevice;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.net.wifi.WifiScanner;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class CarProjectionServiceTest {
+ private static final int MD_ID1 = 1;
+ private static final int MD_ID2 = 2;
+ private static final String MD_NAME1 = "Device1";
+ private static final String MD_NAME2 = "Device2";
+ private static final int DEFAULT_TIMEOUT_MS = 1000;
+ private static final String MD_EXTRA_KEY = "com.some.key.md";
+ private static final String MD_EXTRA_VALUE = "this is dummy value";
+ private static final String STATUS_EXTRA_KEY = "com.some.key.status";
+ private static final String STATUS_EXTRA_VALUE = "additional status value";
+
+ private final IBinder mToken = new Binder();
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ private CarProjectionService mService;
+
+ @Spy
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Mock
+ private Resources mResources;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ @Mock
+ private CarInputService mCarInputService;
+ @Mock
+ private CarBluetoothService mCarBluetoothService;
+
+ @Before
+ public void setUp() {
+ mService = new CarProjectionService(mContext, mHandler, mCarInputService,
+ mCarBluetoothService);
+ }
+
+ @Test
+ public void updateProjectionStatus_defaultState() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(2);
+
+ mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+ @Override
+ public void onProjectionStatusChanged(int projectionState,
+ String activeProjectionPackageName, List<ProjectionStatus> details) {
+ assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+ assertThat(activeProjectionPackageName).isNull();
+ assertThat(details).isEmpty();
+
+ latch.countDown();
+ }
+ });
+
+ latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void updateProjectionStatus_subscribeAfterUpdate() throws Exception {
+ final ProjectionStatus status = createProjectionStatus();
+ mService.updateProjectionStatus(status, mToken);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+ @Override
+ public void onProjectionStatusChanged(int projectionState,
+ String activeProjectionPackageName, List<ProjectionStatus> details) {
+ assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+ assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+ assertThat(details).hasSize(1);
+ assertThat(details.get(0)).isEqualTo(status);
+ ProjectionStatus status = details.get(0);
+ assertThat(status.getTransport()).isEqualTo(PROJECTION_TRANSPORT_WIFI);
+ assertThat(status.getExtras()).isNotNull();
+ assertThat(status.getExtras().getString(STATUS_EXTRA_KEY))
+ .isEqualTo(STATUS_EXTRA_VALUE);
+ assertThat(status.getConnectedMobileDevices()).hasSize(2);
+ MobileDevice md1 = status.getConnectedMobileDevices().get(0);
+ assertThat(md1.getId()).isEqualTo(MD_ID1);
+ assertThat(md1.getName()).isEqualTo(MD_NAME1);
+ assertThat(md1.getExtras()).isNotNull();
+ assertThat(md1.getExtras().getString(MD_EXTRA_KEY)).isEqualTo(MD_EXTRA_VALUE);
+ assertThat(md1.getAvailableTransports()).hasSize(1);
+ assertThat(md1.getAvailableTransports()).containsExactly(
+ PROJECTION_TRANSPORT_USB);
+
+ MobileDevice md2 = status.getConnectedMobileDevices().get(1);
+ assertThat(md2.getId()).isEqualTo(MD_ID2);
+ assertThat(md2.getName()).isEqualTo(MD_NAME2);
+ assertThat(md2.getExtras()).isNotNull();
+ assertThat(md2.getExtras().isEmpty()).isTrue();
+ assertThat(md2.getAvailableTransports()).containsExactly(
+ PROJECTION_TRANSPORT_USB, PROJECTION_TRANSPORT_WIFI);
+
+ latch.countDown();
+ }
+ });
+
+ latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void updateProjectionStatus_subscribeBeforeUpdate() throws Exception {
+
+ // We will receive notification twice - with default value and with updated one.
+ final CountDownLatch latch = new CountDownLatch(2);
+
+ mService.registerProjectionStatusListener(new ICarProjectionStatusListener.Stub() {
+ @Override
+ public void onProjectionStatusChanged(int projectionState,
+ String activeProjectionPackageName, List<ProjectionStatus> details) {
+ if (latch.getCount() == 2) {
+ assertThat(projectionState).isEqualTo(PROJECTION_STATE_INACTIVE);
+ assertThat(activeProjectionPackageName).isNull();
+ } else {
+ assertThat(projectionState).isEqualTo(PROJECTION_STATE_ACTIVE_FOREGROUND);
+ assertThat(activeProjectionPackageName).isEqualTo(mContext.getPackageName());
+ }
+
+ latch.countDown();
+ }
+ });
+ mService.updateProjectionStatus(createProjectionStatus(), mToken);
+
+ latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void getProjectionOptions_defaults() {
+ when(mContext.getResources()).thenReturn(mResources);
+ final int uiMode = ProjectionOptions.UI_MODE_FULL_SCREEN;
+
+ when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+ .thenReturn(uiMode);
+ when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+ .thenReturn("");
+ when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+ .thenReturn(-1);
+ when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+ .thenReturn(new int[0]);
+
+ Bundle bundle = mService.getProjectionOptions();
+
+ ProjectionOptions options = new ProjectionOptions(bundle);
+ assertThat(options.getActivityOptions()).isNull();
+ assertThat(options.getConsentActivity()).isNull();
+ assertThat(options.getUiMode()).isEqualTo(uiMode);
+ }
+
+ @Test
+ public void getProjectionOptions_nonDefaults() {
+ when(mContext.getResources()).thenReturn(mResources);
+ final int uiMode = ProjectionOptions.UI_MODE_BLENDED;
+ final String consentActivity = "com.my.app/.MyActivity";
+ final int[] bounds = new int[] {1, 2, 3, 4};
+ final int displayId = 1;
+
+ when(mResources.getInteger(com.android.car.R.integer.config_projectionUiMode))
+ .thenReturn(uiMode);
+ when(mResources.getString(com.android.car.R.string.config_projectionConsentActivity))
+ .thenReturn(consentActivity);
+ when(mResources.getInteger(com.android.car.R.integer.config_projectionActivityDisplayId))
+ .thenReturn(displayId);
+ when(mResources.getIntArray(com.android.car.R.array.config_projectionActivityLaunchBounds))
+ .thenReturn(bounds);
+
+ Bundle bundle = mService.getProjectionOptions();
+
+ ProjectionOptions options = new ProjectionOptions(bundle);
+ assertThat(options.getActivityOptions().getLaunchDisplayId()).isEqualTo(displayId);
+ assertThat(options.getActivityOptions().getLaunchBounds())
+ .isEqualTo(new Rect(bounds[0], bounds[1], bounds[2], bounds[3]));
+ assertThat(options.getConsentActivity()).isEqualTo(
+ ComponentName.unflattenFromString(consentActivity));
+ assertThat(options.getUiMode()).isEqualTo(uiMode);
+ }
+
+ @Test
+ public void getWifiChannels() {
+ int[] wifiChannels = mService.getAvailableWifiChannels(WifiScanner.WIFI_BAND_BOTH_WITH_DFS);
+ assertThat(wifiChannels).isNotNull();
+ assertThat(wifiChannels).isNotEmpty();
+ }
+
+ private ProjectionStatus createProjectionStatus() {
+ Bundle statusExtra = new Bundle();
+ statusExtra.putString(STATUS_EXTRA_KEY, STATUS_EXTRA_VALUE);
+ Bundle mdExtra = new Bundle();
+ mdExtra.putString(MD_EXTRA_KEY, MD_EXTRA_VALUE);
+
+ return ProjectionStatus
+ .builder(mContext.getPackageName(), PROJECTION_STATE_ACTIVE_FOREGROUND)
+ .setExtras(statusExtra)
+ .setProjectionTransport(PROJECTION_TRANSPORT_WIFI)
+ .addMobileDevice(MobileDevice
+ .builder(MD_ID1, MD_NAME1)
+ .addTransport(PROJECTION_TRANSPORT_USB)
+ .setExtras(mdExtra)
+ .build())
+ .addMobileDevice(MobileDevice
+ .builder(MD_ID2, MD_NAME2)
+ .addTransport(PROJECTION_TRANSPORT_USB)
+ .addTransport(PROJECTION_TRANSPORT_WIFI)
+ .setProjecting(true)
+ .build())
+ .build();
+ }
+}