/* * 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.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.SystemApi; import android.app.Service; import android.content.Intent; import android.hardware.usb.UsbDevice; import android.os.Bundle; 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.io.FileDescriptor; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.Objects; /** * The service that must be implemented by USB AOAP handler system apps. The app must hold the * following permission: {@code android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE}. * *

This service gets bound by the framework and the service needs to be protected by * {@code android.permission.MANAGE_USB} permission to ensure nobody else can * bind to the service. At most only one client should be bound at a time. * * @hide */ @SystemApi public abstract class AoapService extends Service { private static final String TAG = AoapService.class.getSimpleName(); /** Indicates success or confirmation. */ public static final int RESULT_OK = 0; /** * Indicates that the device is not supported by this service and system shouldn't associate * given device with this service. */ public static final int RESULT_DEVICE_NOT_SUPPORTED = 1; /** * Indicates that device shouldn't be switch to AOAP mode at this time. */ public static final int RESULT_DO_NOT_SWITCH_TO_AOAP = 2; /** @hide */ @IntDef(value = { RESULT_OK, RESULT_DEVICE_NOT_SUPPORTED, RESULT_DO_NOT_SWITCH_TO_AOAP }) @Retention(RetentionPolicy.SOURCE) public @interface Result {} /** * A message sent from the system USB handler service to AOAP handler service to check if the * device is supported. The message must have {@link #KEY_DEVICE} with {@link UsbDevice} object. * * @hide */ public static final int MSG_NEW_DEVICE_ATTACHED = 1; /** * A response message for {@link #MSG_NEW_DEVICE_ATTACHED}. Must contain {@link #KEY_RESULT} * with one of the {@code RESULT_*} constant. * * @hide */ public static final int MSG_NEW_DEVICE_ATTACHED_RESPONSE = 2; /** * A message sent from the system USB handler service to AOAP handler service to check if the * device can be switched to AOAP mode. The message must have {@link #KEY_DEVICE} with * {@link UsbDevice} object. * * @hide */ public static final int MSG_CAN_SWITCH_TO_AOAP = 3; /** * A response message for {@link #MSG_CAN_SWITCH_TO_AOAP}. Must contain {@link #KEY_RESULT} * with one of the {@code RESULT_*} constant. * * @hide */ public static final int MSG_CAN_SWITCH_TO_AOAP_RESPONSE = 4; /** @hide */ public static final String KEY_DEVICE = "usb-device"; /** @hide */ public static final String KEY_RESULT = "result"; /** * Returns {@code true} if the given USB device is supported by this service. * *

The device is not expected to be in AOAP mode when this method is called. The purpose of * this method is just to give the service a chance to tell whether based on the information * provided in {@link UsbDevice} class (e.g. PID/VID) this service supports or doesn't support * given device. * *

The method must return one of the following status: {@link #RESULT_OK} or * {@link #RESULT_DEVICE_NOT_SUPPORTED} */ @MainThread public abstract @Result int isDeviceSupported(@NonNull UsbDevice device); /** * This method will be called at least once per connection session before switching device into * AOAP mode. * *

The device is connected, but not in AOAP mode yet. Implementors of this method may ask * the framework to ignore this device for now and do not switch to AOAP. This may make sense if * a connection to the device has been established through other means, and switching the device * to AOAP would break that connection. * *

Note: the method may be called only if this device was claimed to be supported in * {@link #isDeviceSupported(UsbDevice)} method, and this app has been chosen to handle the * device. * *

The method must return one of the following status: {@link #RESULT_OK}, * {@link #RESULT_DEVICE_NOT_SUPPORTED} or {@link #RESULT_DO_NOT_SWITCH_TO_AOAP} */ @MainThread public @Result int canSwitchToAoap(@NonNull UsbDevice device) { return RESULT_OK; } private Messenger mMessenger; private boolean mBound; @Override public void onCreate() { super.onCreate(); mMessenger = new Messenger(new IncomingHandler(this)); } @Override public IBinder onBind(Intent intent) { if (mBound) { Log.w(TAG, "Received onBind event when the service was already bound"); } mBound = true; return mMessenger.getBinder(); } @Override public boolean onUnbind(Intent intent) { mBound = false; return super.onUnbind(intent); } @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { writer.write("Bound: " + mBound); } private static class IncomingHandler extends Handler { private final WeakReference mServiceRef; IncomingHandler(AoapService service) { super(Looper.getMainLooper()); mServiceRef = new WeakReference<>(service); } @Override public void handleMessage(Message msg) { AoapService service = mServiceRef.get(); if (service == null) { return; } Bundle data = msg.getData(); if (data == null) { Log.e(TAG, "Ignoring message " + msg.what + " without data"); return; } Log.i(TAG, "Message received: " + msg.what); switch (msg.what) { case MSG_NEW_DEVICE_ATTACHED: { int res = service.isDeviceSupported( Objects.requireNonNull(data.getParcelable(KEY_DEVICE))); if (res != RESULT_OK && res != RESULT_DEVICE_NOT_SUPPORTED) { throw new IllegalArgumentException("Result can not be " + res); } sendResponse(msg.replyTo, MSG_NEW_DEVICE_ATTACHED_RESPONSE, res); break; } case MSG_CAN_SWITCH_TO_AOAP: { int res = service.canSwitchToAoap( Objects.requireNonNull(data.getParcelable(KEY_DEVICE))); if (res != RESULT_OK && res != RESULT_DEVICE_NOT_SUPPORTED && res != RESULT_DO_NOT_SWITCH_TO_AOAP) { throw new IllegalArgumentException("Result can not be " + res); } sendResponse(msg.replyTo, MSG_CAN_SWITCH_TO_AOAP_RESPONSE, res); break; } default: Log.e(TAG, "Unknown message received: " + msg.what); break; } } private void sendResponse(Messenger messenger, int msg, int result) { try { messenger.send(createResponseMessage(msg, result)); } catch (RemoteException e) { Log.e(TAG, "Failed to send message", e); } } private Message createResponseMessage(int msg, int result) { Message response = Message.obtain(null, msg); Bundle data = new Bundle(); data.putInt(KEY_RESULT, result); response.setData(data); return response; } } }