Merge "Extend CarProjectionManager to support wireless"
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index c4c4385..55fc0c6 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -42,6 +42,8 @@
method public void onCarDisconnected();
method public void registerProjectionListener(android.car.CarProjectionManager.CarProjectionListener, int) throws android.car.CarNotConnectedException;
method public void registerProjectionRunner(android.content.Intent) throws android.car.CarNotConnectedException;
+ method public void startProjectionAccessPoint(android.car.CarProjectionManager.ProjectionAccessPointCallback);
+ method public void stopProjectionAccessPoint();
method public void unregisterProjectionListener();
method public void unregisterProjectionRunner(android.content.Intent);
field public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 2; // 0x2
@@ -52,6 +54,17 @@
method public abstract void onVoiceAssistantRequest(boolean);
}
+ public static abstract class CarProjectionManager.ProjectionAccessPointCallback {
+ ctor public CarProjectionManager.ProjectionAccessPointCallback();
+ method public void onFailed(int);
+ method public void onStarted(android.net.wifi.WifiConfiguration);
+ method public void onStopped();
+ field public static final int ERROR_GENERIC = 2; // 0x2
+ field public static final int ERROR_INCOMPATIBLE_MODE = 3; // 0x3
+ field public static final int ERROR_NO_CHANNEL = 1; // 0x1
+ field public static final int ERROR_TETHERING_DISALLOWED = 4; // 0x4
+ }
+
public final class VehicleAreaDoor {
field public static final int DOOR_HOOD = 268435456; // 0x10000000
field public static final int DOOR_REAR = 536870912; // 0x20000000
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index d1824f6..ba6f92a 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -18,19 +18,31 @@
import android.annotation.SystemApi;
import android.content.Intent;
+import android.net.wifi.WifiConfiguration;
+import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
import android.os.RemoteException;
+import android.util.Log;
import java.lang.ref.WeakReference;
/**
* CarProjectionManager allows applications implementing projection to register/unregister itself
* with projection manager, listen for voice notification.
+ *
+ * A client must have {@link Car#PERMISSION_CAR_PROJECTION} permission in order to access this
+ * manager.
+ *
* @hide
*/
@SystemApi
public final class CarProjectionManager implements CarManagerBase {
+ private static final String TAG = CarProjectionManager.class.getSimpleName();
+
/**
* Listener to get projected notifications.
*
@@ -52,6 +64,13 @@
*/
public static final int PROJECTION_LONG_PRESS_VOICE_SEARCH = 0x2;
+ /** @hide */
+ public static final int PROJECTION_AP_STARTED = 0;
+ /** @hide */
+ public static final int PROJECTION_AP_STOPPED = 1;
+ /** @hide */
+ public static final int PROJECTION_AP_FAILED = 2;
+
private final ICarProjection mService;
private final Handler mHandler;
private final ICarProjectionCallbackImpl mBinderListener;
@@ -59,6 +78,11 @@
private CarProjectionListener mListener;
private int mVoiceSearchFilter;
+ private ProjectionAccessPointCallbackProxy mProjectionAccessPointCallbackProxy;
+
+ // Only one access point proxy object per process.
+ private static final IBinder mAccessPointProxyToken = new Binder();
+
/**
* @hide
*/
@@ -171,6 +195,123 @@
// nothing to do
}
+ /**
+ * Request to start Wi-Fi access point if it hasn't been started yet for wireless projection
+ * receiver app.
+ *
+ * <p>A process can have only one request to start an access point, subsequent call of this
+ * method will invalidate previous calls.
+ */
+ public void startProjectionAccessPoint(ProjectionAccessPointCallback callback) {
+ synchronized (this) {
+ Looper looper = mHandler.getLooper();
+ ProjectionAccessPointCallbackProxy proxy =
+ new ProjectionAccessPointCallbackProxy(this, looper, callback);
+ try {
+ mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken);
+ mProjectionAccessPointCallbackProxy = proxy;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Stop Wi-Fi Access Point for wireless projection receiver app.
+ */
+ public void stopProjectionAccessPoint() {
+ ProjectionAccessPointCallbackProxy proxy;
+ synchronized (this) {
+ proxy = mProjectionAccessPointCallbackProxy;
+ mProjectionAccessPointCallbackProxy = null;
+ }
+ if (proxy == null) {
+ return;
+ }
+
+ try {
+ mService.stopProjectionAccessPoint(mAccessPointProxyToken);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Callback class for applications to receive updates about the LocalOnlyHotspot status.
+ */
+ public abstract static class ProjectionAccessPointCallback {
+ public static final int ERROR_NO_CHANNEL = 1;
+ public static final int ERROR_GENERIC = 2;
+ public static final int ERROR_INCOMPATIBLE_MODE = 3;
+ public static final int ERROR_TETHERING_DISALLOWED = 4;
+
+ /** Called when access point started successfully. */
+ public void onStarted(WifiConfiguration wifiConfiguration) {}
+ /** Called when access point is stopped. No events will be sent after that. */
+ public void onStopped() {}
+ /** Called when access point failed to start. No events will be sent after that. */
+ public void onFailed(int reason) {}
+ }
+
+ /**
+ * Callback proxy for LocalOnlyHotspotCallback objects.
+ */
+ private static class ProjectionAccessPointCallbackProxy {
+ private static final String LOG_PREFIX =
+ ProjectionAccessPointCallbackProxy.class.getSimpleName() + ": ";
+
+ private final Handler mHandler;
+ private final WeakReference<CarProjectionManager> mCarProjectionManagerRef;
+ private final Messenger mMessenger;
+
+ ProjectionAccessPointCallbackProxy(CarProjectionManager manager, Looper looper,
+ final ProjectionAccessPointCallback callback) {
+ mCarProjectionManagerRef = new WeakReference<>(manager);
+
+ mHandler = new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ Log.d(TAG, LOG_PREFIX + "handle message what: " + msg.what + " msg: " + msg);
+
+ CarProjectionManager manager = mCarProjectionManagerRef.get();
+ if (manager == null) {
+ Log.w(TAG, LOG_PREFIX + "handle message post GC");
+ return;
+ }
+
+ switch (msg.what) {
+ case PROJECTION_AP_STARTED:
+ WifiConfiguration config = (WifiConfiguration) msg.obj;
+ if (config == null) {
+ Log.e(TAG, LOG_PREFIX + "config cannot be null.");
+ callback.onFailed(ProjectionAccessPointCallback.ERROR_GENERIC);
+ return;
+ }
+ callback.onStarted(config);
+ break;
+ case PROJECTION_AP_STOPPED:
+ Log.i(TAG, LOG_PREFIX + "hotspot stopped");
+ callback.onStopped();
+ break;
+ case PROJECTION_AP_FAILED:
+ int reasonCode = msg.arg1;
+ Log.w(TAG, LOG_PREFIX + "failed to start. reason: "
+ + reasonCode);
+ callback.onFailed(reasonCode);
+ break;
+ default:
+ Log.e(TAG, LOG_PREFIX + "unhandled message. type: " + msg.what);
+ }
+ }
+ };
+ mMessenger = new Messenger(mHandler);
+ }
+
+ Messenger getMessenger() {
+ return mMessenger;
+ }
+ }
+
private void handleVoiceAssistantRequest(boolean fromLongPress) {
CarProjectionListener listener;
synchronized (this) {
@@ -196,12 +337,7 @@
if (manager == null) {
return;
}
- manager.mHandler.post(new Runnable() {
- @Override
- public void run() {
- manager.handleVoiceAssistantRequest(fromLongPress);
- }
- });
+ manager.mHandler.post(() -> manager.handleVoiceAssistantRequest(fromLongPress));
}
}
}
diff --git a/car-lib/src/android/car/ICarProjection.aidl b/car-lib/src/android/car/ICarProjection.aidl
index 15831cd..4f80e6c 100644
--- a/car-lib/src/android/car/ICarProjection.aidl
+++ b/car-lib/src/android/car/ICarProjection.aidl
@@ -18,6 +18,7 @@
import android.car.ICarProjectionCallback;
import android.content.Intent;
+import android.os.Messenger;
/**
* Binder interface for {@link android.car.CarProjectionManager}.
@@ -48,4 +49,15 @@
* Unregisters projection callback.
*/
void unregisterProjectionListener(ICarProjectionCallback callback) = 3;
+
+ /**
+ * Starts Wi-Fi access point if it hasn't been started yet for wireless projection and returns
+ * WiFiConfiguration object with access point details.
+ */
+ void startProjectionAccessPoint(in Messenger messenger, in IBinder binder) = 4;
+
+ /**
+ * Stops previously requested Wi-Fi access point.
+ */
+ void stopProjectionAccessPoint(in IBinder binder) = 5;
}
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 8d436e0..5a8fce6 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -135,4 +135,6 @@
<item>7d,3</item>
</string-array>
+ <string name="config_TetheredProjectionAccessPointSsid" translatable="false">CarAP</string>
+
</resources>
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 5eb453f..f47362c 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -15,21 +15,58 @@
*/
package com.android.car;
+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.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;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED;
+import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLING;
+
+import android.annotation.Nullable;
import android.car.CarProjectionManager;
+import android.car.CarProjectionManager.ProjectionAccessPointCallback;
import android.car.ICarProjection;
import android.car.ICarProjectionCallback;
+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.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.GroupCipher;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiConfiguration.PairwiseCipher;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
+import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
+import android.net.wifi.WifiManager.SoftApCallback;
import android.os.Binder;
+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.util.Log;
import android.view.KeyEvent;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
/**
* Car projection service allows to bound to projected app to boost it prioirity.
@@ -37,9 +74,41 @@
*/
class CarProjectionService extends ICarProjection.Stub implements CarServiceBase,
BinderInterfaceContainer.BinderEventHandler<ICarProjectionCallback> {
- private final ListenerHolder mAllListeners;
+ private static final String TAG = CarLog.TAG_PROJECTION;
+ private static final boolean DBG = true;
+
+ private final ProjectionCallbackHolder mProjectionCallbacks;
private final CarInputService mCarInputService;
private final Context mContext;
+ private final WifiManager mWifiManager;
+ private final Handler mHandler;
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final HashMap<IBinder, WirelessClient> mWirelessClients = new HashMap<>();
+
+ @Nullable
+ @GuardedBy("mLock")
+ private LocalOnlyHotspotReservation mLocalOnlyHotspotReservation;
+
+ @Nullable
+ @GuardedBy("mLock")
+ private SoftApCallback mSoftApCallback;
+
+ @Nullable
+ private String mApBssid;
+
+ private static final int WIFI_MODE_TETHERED = 1;
+ private static final int WIFI_MODE_LOCALONLY = 2;
+
+ private static final int RAND_SSID_INT_MIN = 1000;
+ private static final int RAND_SSID_INT_MAX = 9999;
+
+ // Could be one of the WIFI_MODE_* constants.
+ // TODO: read this from user settings, support runtime switch
+ private int mWifiMode = WIFI_MODE_LOCALONLY;
+
+ private final WifiConfiguration mProjectionWifiConfiguration;
private final CarInputService.KeyEventListener mVoiceAssistantKeyListener =
new CarInputService.KeyEventListener() {
@@ -62,7 +131,7 @@
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
- synchronized (CarProjectionService.this) {
+ synchronized (mLock) {
mBound = true;
}
}
@@ -83,14 +152,17 @@
CarProjectionService(Context context, CarInputService carInputService) {
mContext = context;
+ mHandler = new Handler();
mCarInputService = carInputService;
- mAllListeners = new ListenerHolder(this);
+ mProjectionCallbacks = new ProjectionCallbackHolder(this);
+ mWifiManager = context.getSystemService(WifiManager.class);
+ mProjectionWifiConfiguration = createWifiConfiguration(context);
}
@Override
public void registerProjectionRunner(Intent serviceIntent) {
// We assume one active projection app running in the system at one time.
- synchronized (this) {
+ synchronized (mLock) {
if (serviceIntent.filterEquals(mRegisteredService)) {
return;
}
@@ -105,7 +177,7 @@
@Override
public void unregisterProjectionRunner(Intent serviceIntent) {
- synchronized (this) {
+ synchronized (mLock) {
if (!serviceIntent.filterEquals(mRegisteredService)) {
Log.w(CarLog.TAG_PROJECTION, "Request to unbind unregistered service["
+ serviceIntent + "]. Registered service[" + mRegisteredService + "]");
@@ -117,7 +189,7 @@
}
private void bindToService(Intent serviceIntent) {
- synchronized (this) {
+ synchronized (mLock) {
mRegisteredService = serviceIntent;
}
UserHandle userHandle = UserHandle.getUserHandleForUid(Binder.getCallingUid());
@@ -126,7 +198,7 @@
}
private void unbindServiceIfBound() {
- synchronized (this) {
+ synchronized (mLock) {
if (!mBound) {
return;
}
@@ -135,26 +207,29 @@
mContext.unbindService(mConnection);
}
- private synchronized void handleVoiceAssitantRequest(boolean isTriggeredByLongPress) {
- for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener :
- mAllListeners.getInterfaces()) {
- ListenerInfo listenerInfo = (ListenerInfo) listener;
- if ((listenerInfo.hasFilter(CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH)
- && isTriggeredByLongPress)
- || (listenerInfo.hasFilter(CarProjectionManager.PROJECTION_VOICE_SEARCH)
- && !isTriggeredByLongPress)) {
- dispatchVoiceAssistantRequest(listenerInfo.binderInterface, isTriggeredByLongPress);
+ private void handleVoiceAssitantRequest(boolean isTriggeredByLongPress) {
+ synchronized (mLock) {
+ for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener :
+ mProjectionCallbacks.getInterfaces()) {
+ ProjectionCallback projectionCallback = (ProjectionCallback) listener;
+ if ((projectionCallback.hasFilter(PROJECTION_LONG_PRESS_VOICE_SEARCH)
+ && isTriggeredByLongPress)
+ || (projectionCallback.hasFilter(PROJECTION_VOICE_SEARCH)
+ && !isTriggeredByLongPress)) {
+ dispatchVoiceAssistantRequest(
+ projectionCallback.binderInterface, isTriggeredByLongPress);
+ }
}
}
}
@Override
- public void registerProjectionListener(ICarProjectionCallback listener, int filter) {
- synchronized (this) {
- ListenerInfo info = (ListenerInfo) mAllListeners.getBinderInterface(listener);
+ public void registerProjectionListener(ICarProjectionCallback callback, int filter) {
+ synchronized (mLock) {
+ ProjectionCallback info = mProjectionCallbacks.get(callback);
if (info == null) {
- info = new ListenerInfo(mAllListeners, listener, filter);
- mAllListeners.addBinderInterface(info);
+ info = new ProjectionCallback(mProjectionCallbacks, callback, filter);
+ mProjectionCallbacks.addBinderInterface(info);
} else {
info.setFilter(filter);
}
@@ -164,23 +239,221 @@
@Override
public void unregisterProjectionListener(ICarProjectionCallback listener) {
- synchronized (this) {
- mAllListeners.removeBinder(listener);
+ synchronized (mLock) {
+ mProjectionCallbacks.removeBinder(listener);
}
updateCarInputServiceListeners();
}
+ @Override
+ public void startProjectionAccessPoint(final Messenger messenger, IBinder binder)
+ throws RemoteException {
+ //TODO: check if access point already started with the desired configuration.
+ registerWirelessClient(WirelessClient.of(messenger, binder));
+ startAccessPoint();
+ }
+
+ @Override
+ public void stopProjectionAccessPoint(IBinder token) {
+ Log.i(TAG, "Received stop access point request from " + token);
+
+ boolean shouldReleaseAp;
+ synchronized (mLock) {
+ if (!unregisterWirelessClientLocked(token)) {
+ Log.w(TAG, "Client " + token + " was not registered");
+ return;
+ }
+ shouldReleaseAp = mWirelessClients.isEmpty();
+ }
+
+ if (shouldReleaseAp) {
+ stopAccessPoint();
+ }
+ }
+
+ private void startAccessPoint() {
+ synchronized (mLock) {
+ switch (mWifiMode) {
+ case WIFI_MODE_LOCALONLY: {
+ startLocalOnlyApLocked();
+ break;
+ }
+ case WIFI_MODE_TETHERED: {
+ startTetheredApLocked();
+ break;
+ }
+ default: {
+ Log.wtf(TAG, "Unexpected Access Point mode during starting: " + mWifiMode);
+ break;
+ }
+ }
+ }
+ }
+
+ private void stopAccessPoint() {
+ sendApStopped();
+
+ synchronized (mLock) {
+ switch (mWifiMode) {
+ case WIFI_MODE_LOCALONLY: {
+ stopLocalOnlyApLocked();
+ break;
+ }
+ case WIFI_MODE_TETHERED: {
+ stopTetheredApLocked();
+ break;
+ }
+ default: {
+ Log.wtf(TAG, "Unexpected Access Point mode during stopping : " + mWifiMode);
+ }
+ }
+ }
+ }
+
+ private void startTetheredApLocked() {
+ Log.d(TAG, "startTetheredApLocked");
+
+ final SoftApCallback callback = new ProjectionSoftApCallback();
+ mWifiManager.registerSoftApCallback(callback, mHandler);
+
+ if (!mWifiManager.startSoftAp(mProjectionWifiConfiguration)) {
+ Log.e(TAG, "Failed to start soft AP");
+ mWifiManager.unregisterSoftApCallback(callback);
+ sendApFailed(ERROR_GENERIC);
+ } else {
+ mSoftApCallback = callback;
+ }
+ }
+
+ private void stopTetheredApLocked() {
+ Log.d(TAG, "stopTetheredAp");
+
+ if (mSoftApCallback != null) {
+ mWifiManager.unregisterSoftApCallback(mSoftApCallback);
+ mSoftApCallback = null;
+ if (!mWifiManager.stopSoftAp()) {
+ Log.w(TAG, "Failed to request soft AP to stop.");
+ }
+ }
+ }
+
+ private void startLocalOnlyApLocked() {
+ if (mLocalOnlyHotspotReservation != null) {
+ Log.i(TAG, "Local-only hotspot is already registered.");
+ sendApStarted(mLocalOnlyHotspotReservation.getWifiConfiguration());
+ return;
+ }
+
+ Log.i(TAG, "Requesting to start local-only hotspot.");
+ mWifiManager.startLocalOnlyHotspot(new LocalOnlyHotspotCallback() {
+ @Override
+ public void onStarted(LocalOnlyHotspotReservation reservation) {
+ Log.d(TAG, "Local-only hotspot started");
+ synchronized (mLock) {
+ mLocalOnlyHotspotReservation = reservation;
+ }
+ sendApStarted(reservation.getWifiConfiguration());
+ }
+
+ @Override
+ public void onStopped() {
+ Log.i(TAG, "Local-only hotspot stopped.");
+ synchronized (mLock) {
+ mLocalOnlyHotspotReservation = null;
+ }
+ sendApStopped();
+ }
+
+ @Override
+ public void onFailed(int localonlyHostspotFailureReason) {
+ Log.w(TAG, "Local-only hotspot failed, reason: "
+ + localonlyHostspotFailureReason);
+ synchronized (mLock) {
+ mLocalOnlyHotspotReservation = null;
+ }
+ int reason;
+ switch (localonlyHostspotFailureReason) {
+ case LocalOnlyHotspotCallback.ERROR_NO_CHANNEL:
+ reason = ProjectionAccessPointCallback.ERROR_NO_CHANNEL;
+ break;
+ case LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED:
+ reason = ProjectionAccessPointCallback.ERROR_TETHERING_DISALLOWED;
+ break;
+ case LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE:
+ reason = ProjectionAccessPointCallback.ERROR_INCOMPATIBLE_MODE;
+ break;
+ default:
+ reason = ProjectionAccessPointCallback.ERROR_GENERIC;
+
+ }
+ sendApFailed(reason);
+ }
+ }, mHandler);
+ }
+
+ private void stopLocalOnlyApLocked() {
+ Log.i(TAG, "stopLocalOnlyApLocked");
+
+ if (mLocalOnlyHotspotReservation == null) {
+ Log.w(TAG, "Requested to stop local-only hotspot which was already stopped.");
+ return;
+ }
+
+ mLocalOnlyHotspotReservation.close();
+ mLocalOnlyHotspotReservation = null;
+ }
+
+ private void sendApStarted(WifiConfiguration wifiConfiguration) {
+ WifiConfiguration localWifiConfig = new WifiConfiguration(wifiConfiguration);
+ localWifiConfig.BSSID = mApBssid;
+
+ Message message = Message.obtain();
+ message.what = CarProjectionManager.PROJECTION_AP_STARTED;
+ message.obj = localWifiConfig;
+ Log.i(TAG, "Sending PROJECTION_AP_STARTED, ssid: "
+ + localWifiConfig.getPrintableSsid()
+ + ", apBand: " + localWifiConfig.apBand
+ + ", apChannel: " + localWifiConfig.apChannel
+ + ", bssid: " + localWifiConfig.BSSID);
+ sendApStatusMessage(message);
+ }
+
+ private void sendApStopped() {
+ Message message = Message.obtain();
+ message.what = CarProjectionManager.PROJECTION_AP_STOPPED;
+ sendApStatusMessage(message);
+ unregisterWirelessClients();
+ }
+
+ private void sendApFailed(int reason) {
+ Message message = Message.obtain();
+ message.what = CarProjectionManager.PROJECTION_AP_FAILED;
+ message.arg1 = reason;
+ sendApStatusMessage(message);
+ unregisterWirelessClients();
+ }
+
+ private void sendApStatusMessage(Message message) {
+ List<WirelessClient> clients;
+ synchronized (mLock) {
+ clients = new ArrayList<>(mWirelessClients.values());
+ }
+ for (WirelessClient client : clients) {
+ client.send(message);
+ }
+ }
+
private void updateCarInputServiceListeners() {
boolean listenShortPress = false;
boolean listenLongPress = false;
- synchronized (this) {
+ synchronized (mLock) {
for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener :
- mAllListeners.getInterfaces()) {
- ListenerInfo listenerInfo = (ListenerInfo) listener;
- listenShortPress |= listenerInfo.hasFilter(
- CarProjectionManager.PROJECTION_VOICE_SEARCH);
- listenLongPress |= listenerInfo.hasFilter(
- CarProjectionManager.PROJECTION_LONG_PRESS_VOICE_SEARCH);
+ mProjectionCallbacks.getInterfaces()) {
+ ProjectionCallback projectionCallback = (ProjectionCallback) listener;
+ listenShortPress |= projectionCallback.hasFilter(
+ PROJECTION_VOICE_SEARCH);
+ listenLongPress |= projectionCallback.hasFilter(
+ PROJECTION_LONG_PRESS_VOICE_SEARCH);
}
}
mCarInputService.setVoiceAssistantKeyListener(listenShortPress
@@ -191,13 +464,48 @@
@Override
public void init() {
- // nothing to do
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE,
+ WIFI_AP_STATE_DISABLED);
+ final int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
+ WIFI_AP_STATE_DISABLED);
+ final int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
+ final String ifaceName =
+ intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+ final int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
+ WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
+ }
+ },
+ new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
+ }
+
+ private void handleWifiApStateChange(int currState, int prevState, int errorCode,
+ String ifaceName, int mode) {
+ if (currState == WIFI_AP_STATE_ENABLING || currState == WIFI_AP_STATE_ENABLED) {
+ Log.d(TAG,
+ "handleWifiApStateChange, curState: " + currState + ", prevState: " + prevState
+ + ", errorCode: " + errorCode + ", ifaceName: " + ifaceName + ", mode: "
+ + mode);
+
+ try {
+ NetworkInterface iface = NetworkInterface.getByName(ifaceName);
+ byte[] bssid = iface.getHardwareAddress();
+ mApBssid = String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+ bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
+ } catch (SocketException e) {
+ Log.e(TAG, e.toString(), e);
+ }
+ }
}
@Override
public void release() {
- synchronized (this) {
- mAllListeners.clear();
+ synchronized (mLock) {
+ mProjectionCallbacks.clear();
}
}
@@ -210,12 +518,17 @@
@Override
public void dump(PrintWriter writer) {
writer.println("**CarProjectionService**");
- synchronized (this) {
+ synchronized (mLock) {
for (BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> listener :
- mAllListeners.getInterfaces()) {
- ListenerInfo listenerInfo = (ListenerInfo) listener;
- writer.println(listenerInfo.toString());
+ mProjectionCallbacks.getInterfaces()) {
+ ProjectionCallback projectionCallback = (ProjectionCallback) listener;
+ writer.println(projectionCallback.toString());
}
+
+ writer.println("Local-only hotspot reservation: " + mLocalOnlyHotspotReservation);
+ writer.println("Wireless clients: " + mWirelessClients.size());
+ writer.println("Current wifi mode: " + mWifiMode);
+ writer.println("SoftApCallback: " + mSoftApCallback);
}
}
@@ -227,17 +540,23 @@
}
}
- private static class ListenerHolder extends BinderInterfaceContainer<ICarProjectionCallback> {
- private ListenerHolder(CarProjectionService service) {
+ private static class ProjectionCallbackHolder
+ extends BinderInterfaceContainer<ICarProjectionCallback> {
+ ProjectionCallbackHolder(CarProjectionService service) {
super(service);
}
+
+ ProjectionCallback get(ICarProjectionCallback projectionCallback) {
+ return (ProjectionCallback) getBinderInterface(projectionCallback);
+ }
}
- private static class ListenerInfo extends
+ private static class ProjectionCallback extends
BinderInterfaceContainer.BinderInterface<ICarProjectionCallback> {
private int mFilter;
- private ListenerInfo(ListenerHolder holder, ICarProjectionCallback binder, int filter) {
+ private ProjectionCallback(ProjectionCallbackHolder holder, ICarProjectionCallback binder,
+ int filter) {
super(holder, binder);
this.mFilter = filter;
}
@@ -256,9 +575,166 @@
@Override
public String toString() {
- synchronized (this) {
- return "ListenerInfo{filter=" + Integer.toHexString(mFilter) + "}";
+ return "ListenerInfo{filter=" + Integer.toHexString(getFilter()) + "}";
+ }
+ }
+
+ private static WifiConfiguration createWifiConfiguration(Context context) {
+ //TODO: consider to read current AP configuration and modify only parts that matter for
+ //wireless projection (apBand, key management), do not modify password if it was set.
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_5GHZ;
+ config.SSID = context.getResources()
+ .getString(R.string.config_TetheredProjectionAccessPointSsid)
+ + "_" + getRandomIntForDefaultSsid();
+ config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
+ config.allowedPairwiseCiphers.set(PairwiseCipher.CCMP);
+ config.allowedGroupCiphers.set(GroupCipher.CCMP);
+ config.preSharedKey = RandomPassword.generate();
+ return config;
+ }
+
+ private void registerWirelessClient(WirelessClient client) throws RemoteException {
+ synchronized (mLock) {
+ if (unregisterWirelessClientLocked(client.token)) {
+ Log.i(TAG, "Client was already registered, override it.");
+ }
+ mWirelessClients.put(client.token, client);
+ }
+ client.token.linkToDeath(new WirelessClientDeathRecipient(this, client), 0);
+ }
+
+ private void unregisterWirelessClients() {
+ synchronized (mLock) {
+ for (WirelessClient client: mWirelessClients.values()) {
+ client.token.unlinkToDeath(client.deathRecipient, 0);
+ }
+ mWirelessClients.clear();
+ }
+ }
+
+ private boolean unregisterWirelessClientLocked(IBinder token) {
+ WirelessClient client = mWirelessClients.remove(token);
+ if (client != null) {
+ token.unlinkToDeath(client.deathRecipient, 0);
+ }
+
+ return client != null;
+ }
+
+ private class ProjectionSoftApCallback implements SoftApCallback {
+ @Override
+ public void onStateChanged(int state, int softApFailureReason) {
+ Log.i(TAG, "ProjectionSoftApCallback, onStateChanged, state: " + state
+ + ", failed reason: softApFailureReason");
+
+ switch (state) {
+ case WifiManager.WIFI_AP_STATE_ENABLED: {
+ sendApStarted(mProjectionWifiConfiguration);
+ break;
+ }
+ case WIFI_AP_STATE_DISABLED: {
+ sendApStopped();
+ break;
+ }
+ case WifiManager.WIFI_AP_STATE_FAILED: {
+ Log.w(TAG, "WIFI_AP_STATE_FAILED, reason: " + softApFailureReason);
+ int reason;
+ switch (softApFailureReason) {
+ case WifiManager.SAP_START_FAILURE_NO_CHANNEL:
+ reason = ProjectionAccessPointCallback.ERROR_NO_CHANNEL;
+ break;
+ default:
+ reason = ERROR_GENERIC;
+ }
+ sendApFailed(reason);
+ break;
+ }
}
}
+
+ @Override
+ public void onNumClientsChanged(int numClients) {
+ Log.i(TAG, "ProjectionSoftApCallback, onNumClientsChanged: " + numClients);
+ }
+ }
+
+ private static class WirelessClient {
+ public final Messenger messenger;
+ public final IBinder token;
+ public @Nullable DeathRecipient deathRecipient;
+
+ private WirelessClient(Messenger messenger, IBinder token) {
+ this.messenger = messenger;
+ this.token = token;
+ }
+
+ private static WirelessClient of(Messenger messenger, IBinder token) {
+ return new WirelessClient(messenger, token);
+ }
+
+ void send(Message message) {
+ try {
+ Log.d(TAG, "Sending message " + message.what + " to " + this);
+ messenger.send(message);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send message", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "{token= " + token
+ + ", deathRecipient=" + deathRecipient + "}";
+ }
+ }
+
+ private static class WirelessClientDeathRecipient implements DeathRecipient {
+ final WeakReference<CarProjectionService> mServiceRef;
+ final WirelessClient mClient;
+
+ WirelessClientDeathRecipient(CarProjectionService service, WirelessClient client) {
+ mServiceRef = new WeakReference<>(service);
+ mClient = client;
+ mClient.deathRecipient = this;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.w(TAG, "Wireless client " + mClient + " died.");
+ CarProjectionService service = mServiceRef.get();
+ if (service == null) return;
+
+ synchronized (service.mLock) {
+ service.unregisterWirelessClientLocked(mClient.token);
+ }
+ }
+ }
+
+ private static class RandomPassword {
+ private static final int PASSWORD_LENGTH = 12;
+ private static final String PW_NUMBER = "0123456789";
+ private static final String PW_LOWER_CASE = "abcdefghijklmnopqrstuvwxyz";
+ private static final String PW_UPPER_CASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private static final char[] SYMBOLS =
+ (PW_NUMBER + PW_LOWER_CASE + PW_UPPER_CASE).toCharArray();
+
+ static String generate() {
+ SecureRandom random = new SecureRandom();
+
+ StringBuilder password = new StringBuilder();
+ while (password.length() < PASSWORD_LENGTH) {
+ int randomIndex = random.nextInt(SYMBOLS.length);
+ password.append(SYMBOLS[randomIndex]);
+ }
+ return password.toString();
+ }
+ }
+
+ private static int getRandomIntForDefaultSsid() {
+ Random random = new Random();
+ return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
}
}
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 889a60a..5b3953f 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
@@ -18,12 +18,18 @@
import android.app.Service;
import android.car.Car;
import android.car.CarProjectionManager;
+import android.car.CarProjectionManager.ProjectionAccessPointCallback;
import android.content.Intent;
+import android.net.wifi.WifiConfiguration;
import android.os.Binder;
import android.os.IBinder;
-import android.test.suitebuilder.annotation.MediumTest;
+import android.support.test.filters.RequiresDevice;
+import android.test.suitebuilder.annotation.LargeTest;
-@MediumTest
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
public class CarProjectionManagerTest extends CarApiTestBase {
private static final String TAG = CarProjectionManagerTest.class.getSimpleName();
@@ -96,4 +102,20 @@
assertTrue(TestService.getBound());
mManager.unregisterProjectionRunner(intent);
}
+
+ @RequiresDevice
+ public void testAccessPoint() throws Exception {
+ CountDownLatch startedLatch = new CountDownLatch(1);
+
+ mManager.startProjectionAccessPoint(new ProjectionAccessPointCallback() {
+ @Override
+ public void onStarted(WifiConfiguration wifiConfiguration) {
+ startedLatch.countDown();
+ }
+ });
+
+ assertTrue(startedLatch.await(30, TimeUnit.SECONDS));
+
+ mManager.stopProjectionAccessPoint();
+ }
}