diff --git a/android/hello_sdl_android/src/main/AndroidManifest.xml b/android/hello_sdl_android/src/main/AndroidManifest.xml index 6bc47c2158..cdb26819db 100755 --- a/android/hello_sdl_android/src/main/AndroidManifest.xml +++ b/android/hello_sdl_android/src/main/AndroidManifest.xml @@ -8,6 +8,8 @@ tools:targetApi="31"/> + diff --git a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlReceiver.java b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlReceiver.java index e130d3d78a..1facc6868c 100755 --- a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlReceiver.java +++ b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlReceiver.java @@ -1,18 +1,27 @@ package com.sdl.hellosdlandroid; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; import android.os.Build; import com.smartdevicelink.transport.SdlBroadcastReceiver; import com.smartdevicelink.transport.SdlRouterService; import com.smartdevicelink.transport.TransportConstants; +import com.smartdevicelink.util.AndroidTools; import com.smartdevicelink.util.DebugTool; public class SdlReceiver extends SdlBroadcastReceiver { private static final String TAG = "SdlBroadcastReceiver"; + private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; + private PendingIntent pendingIntentToStartService; + private Intent startSdlServiceIntent; + @Override public void onSdlEnabled(Context context, Intent intent) { DebugTool.logInfo(TAG, "SDL Enabled"); @@ -24,6 +33,15 @@ public void onSdlEnabled(Context context, Intent intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent pendingIntent = (PendingIntent) intent.getParcelableExtra(TransportConstants.PENDING_INTENT_EXTRA); if (pendingIntent != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (!AndroidTools.hasForegroundServiceTypePermission(context)) { + requestUsbAccessory(context); + startSdlServiceIntent = intent; + this.pendingIntentToStartService = pendingIntent; + DebugTool.logInfo(TAG, "Permission missing for ForegroundServiceType connected device." + context); + return; + } + } try { pendingIntent.send(context, 0, intent); } catch (PendingIntent.CanceledException e) { @@ -56,4 +74,47 @@ public void onReceive(Context context, Intent intent) { public String getSdlServiceName() { return SdlService.class.getSimpleName(); } + + private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ACTION_USB_PERMISSION.equals(action) && context != null && startSdlServiceIntent != null && pendingIntentToStartService != null) { + if (AndroidTools.hasForegroundServiceTypePermission(context)) { + try { + pendingIntentToStartService.send(context, 0, startSdlServiceIntent); + context.unregisterReceiver(this); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + }; + + /** + * Request permission from USB Accessory if USB accessory is not null. + * If the user has not granted the BLUETOOTH_CONNECT permission, + * we can request the USB Accessory permission to satisfy the requirements for + * FOREGROUND_SERVICE_CONNECTED_DEVICE and can start our service and allow + * it to enter the foreground. FOREGROUND_SERVICE_CONNECTED_DEVICE is a requirement + * in Android 14 + */ + private void requestUsbAccessory(Context context) { + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + UsbAccessory[] accessoryList = manager.getAccessoryList(); + if (accessoryList == null || accessoryList.length == 0) { + startSdlServiceIntent = null; + pendingIntentToStartService = null; + return; + } + PendingIntent mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE); + IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); + + AndroidTools.registerReceiver(context, usbPermissionReceiver, filter, + Context.RECEIVER_EXPORTED); + + for (final UsbAccessory usbAccessory : accessoryList) { + manager.requestPermission(usbAccessory, mPermissionIntent); + } + } } \ No newline at end of file diff --git a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlService.java b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlService.java index 1b3e718edd..1b42e0179d 100755 --- a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlService.java +++ b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlService.java @@ -102,18 +102,25 @@ public void onCreate() { @SuppressLint("NewApi") public void enterForeground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(BuildConfig.SDL_APP_ID, "SdlService", NotificationManager.IMPORTANCE_DEFAULT); - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - if (notificationManager != null) { - notificationManager.createNotificationChannel(channel); - Notification.Builder builder = new Notification.Builder(this, channel.getId()) - .setContentTitle("Connected through SDL") - .setSmallIcon(R.drawable.ic_sdl); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE); + try { + NotificationChannel channel = new NotificationChannel(BuildConfig.SDL_APP_ID, "SdlService", NotificationManager.IMPORTANCE_DEFAULT); + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + Notification.Builder builder = new Notification.Builder(this, channel.getId()) + .setContentTitle("Connected through SDL") + .setSmallIcon(R.drawable.ic_sdl); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE); + } + Notification serviceNotification = builder.build(); + startForeground(FOREGROUND_SERVICE_ID, serviceNotification); } - Notification serviceNotification = builder.build(); - startForeground(FOREGROUND_SERVICE_ID, serviceNotification); + } catch (Exception e) { + // This should only catch for TCP connections on Android 14+ due to needing + // permissions for ForegroundServiceType ConnectedDevice that don't make sense for + // a TCP connection + DebugTool.logError(TAG, "Unable to start service in foreground", e); } } } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java index 0c45cf7118..4a73b2c7ea 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java @@ -862,6 +862,14 @@ public void handleMessage(Message msg) { ParcelFileDescriptor parcelFileDescriptor = (ParcelFileDescriptor) msg.obj; if (parcelFileDescriptor != null) { + // Added requirements with Android 14, Checking if we have proper permission to enter the foreground for Foreground service type connectedDevice. + // If we do not have permission to enter the Foreground, we pass off hosting the RouterService to another app. + if (!AndroidTools.hasForegroundServiceTypePermission(service.getApplicationContext())) { + service.deployNextRouterService(parcelFileDescriptor); + acknowledgeUSBAccessoryReceived(msg); + return; + } + //New USB constructor with PFD service.usbTransport = new MultiplexUsbTransport(parcelFileDescriptor, service.usbHandler, msg.getData()); @@ -900,16 +908,7 @@ public void onReceive(Context context, Intent intent) { } - - if (msg.replyTo != null) { - Message message = Message.obtain(); - message.what = TransportConstants.ROUTER_USB_ACC_RECEIVED; - try { - msg.replyTo.send(message); - } catch (RemoteException e) { - e.printStackTrace(); - } - } + acknowledgeUSBAccessoryReceived(msg); break; case TransportConstants.ALT_TRANSPORT_CONNECTED: @@ -919,6 +918,18 @@ public void onReceive(Context context, Intent intent) { break; } } + + private void acknowledgeUSBAccessoryReceived(Message msg) { + if (msg.replyTo != null) { + Message message = Message.obtain(); + message.what = TransportConstants.ROUTER_USB_ACC_RECEIVED; + try { + msg.replyTo.send(message); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + } } /* ************************************************************************************************************************************** @@ -1164,9 +1175,16 @@ public void onCreate() { } /** - * The method will attempt to start up the next router service in line based on the sorting criteria of best router service. + * The method will attempt to start up the next router service in line based on the sorting + * criteria of best router service. + * If a ParcelFileDescriptor is not null, we pass it along to the next RouterService to give + * it a chane to connected via AOA. This only happens on Android 14 and above when the app + * selected to host the RouterService does not satisfy the requirements for permission + * FOREGROUND_SERVICE_CONNECTED_DEVICE. By passing along the usbPfd, it will give the next + * RouterService selected a chance to connect. + * @param usbPfd a ParcelFileDescriptor used for AOA connections. */ - protected void deployNextRouterService() { + protected void deployNextRouterService(ParcelFileDescriptor usbPfd) { List sdlAppInfoList = AndroidTools.querySdlAppInfo(getApplicationContext(), new SdlAppInfo.BestRouterComparator(), null); if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty()) { ComponentName name = new ComponentName(this, this.getClass()); @@ -1178,11 +1196,25 @@ protected void deployNextRouterService() { SdlAppInfo nextUp = sdlAppInfoList.get(i + 1); Intent serviceIntent = new Intent(); serviceIntent.setComponent(nextUp.getRouterServiceComponentName()); + if (usbPfd != null) { + serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT); + serviceIntent.putExtra(TransportConstants.CONNECTION_TYPE_EXTRA, TransportConstants.AOA_USB); + serviceIntent.putExtra(FOREGROUND_EXTRA, true); + } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { startService(serviceIntent); } else { try { startForegroundService(serviceIntent); + if (usbPfd != null) { + new UsbTransferProvider(getApplicationContext(), nextUp.getRouterServiceComponentName(), usbPfd, new UsbTransferProvider.UsbTransferCallback() { + @Override + public void onUsbTransferUpdate(boolean success) { + closeSelf(); + } + }); + } + } catch (Exception e) { DebugTool.logError(TAG, "Unable to start next SDL router service. " + e.getMessage()); } @@ -1282,7 +1314,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (firstStart) { firstStart = false; if (!initCheck(isConnectedOverUSB)) { // Run checks on process and permissions - deployNextRouterService(); + deployNextRouterService(null); closeSelf(); return START_REDELIVER_INTENT; } @@ -2632,15 +2664,11 @@ protected static LocalRouterService getLocalRouterService(Intent launchIntent, C * This method is used to check for the newest version of this class to make sure the latest and greatest is up and running. */ private void startAltTransportTimer() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - if (altTransportTimerHandler != null && altTransportTimerRunnable != null) { altTransportTimerHandler.removeCallbacks(altTransportTimerRunnable); } - altTransportTimerHandler = new Handler(Looper.myLooper()); + altTransportTimerHandler = new Handler(Looper.getMainLooper()); altTransportTimerRunnable = new Runnable() { public void run() { altTransportTimerHandler = null; diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/UsbTransferProvider.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/UsbTransferProvider.java index b588f15123..875f070c6e 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/UsbTransferProvider.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/UsbTransferProvider.java @@ -40,6 +40,7 @@ import android.content.ServiceConnection; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -130,6 +131,26 @@ public UsbTransferProvider(Context context, ComponentName service, UsbAccessory } + protected UsbTransferProvider(Context context, ComponentName service, ParcelFileDescriptor usbPfd, UsbTransferCallback callback) { + if (context == null || service == null || usbPfd == null) { + throw new IllegalStateException("Supplied params are not correct. Context == null? " + (context == null) + " ComponentName == null? " + (service == null) + " Usb PFD == null? " + usbPfd); + } + if (usbPfd.getFileDescriptor() != null && usbPfd.getFileDescriptor().valid()) { + this.context = context; + this.routerService = service; + this.callback = callback; + this.clientMessenger = new Messenger(new ClientHandler(this)); + this.usbPfd = usbPfd; + checkIsConnected(); + } else { + DebugTool.logError(TAG, "Unable to open accessory"); + clientMessenger = null; + if (callback != null) { + callback.onUsbTransferUpdate(false); + } + } + } + @SuppressLint("NewApi") private ParcelFileDescriptor getFileDescriptor(UsbAccessory accessory, Context context) { if (AndroidTools.isUSBCableConnected(context)) { @@ -161,6 +182,7 @@ public void cancel() { if (isBound) { unBindFromService(); } + context = null; } private boolean bindToService() { @@ -173,7 +195,12 @@ private boolean bindToService() { Intent bindingIntent = new Intent(); bindingIntent.setClassName(this.routerService.getPackageName(), this.routerService.getClassName());//This sets an explicit intent //Quickly make sure it's just up and running - context.startService(bindingIntent); + bindingIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + context.startService(bindingIntent); + } else { + context.startForegroundService(bindingIntent); + } bindingIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_USB_PROVIDER); return context.bindService(bindingIntent, routerConnection, Context.BIND_AUTO_CREATE); } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/util/AndroidTools.java b/android/sdl_android/src/main/java/com/smartdevicelink/util/AndroidTools.java index a4d8ca901e..3f361d1c26 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/util/AndroidTools.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/util/AndroidTools.java @@ -34,6 +34,7 @@ import android.annotation.SuppressLint; import android.content.BroadcastReceiver; +import android.Manifest; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -49,10 +50,13 @@ import android.content.res.XmlResourceParser; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; import android.os.BatteryManager; import android.os.Build; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import com.smartdevicelink.marshal.JsonRPCMarshaller; import com.smartdevicelink.proxy.rpc.VehicleType; @@ -417,4 +421,54 @@ public static void registerReceiver(Context context, BroadcastReceiver receiver, } } } + + /** + * A helper method is used to see if this app has permission for UsbAccessory. + * We need UsbAccessory permission if we are plugged in via AOA and do not have BLUETOOTH_CONNECT + * permission for our service to enter the foreground on Android UPSIDE_DOWN_CAKE and greater + * @param context a context that will be used to check the permission. + * @return true if connected via AOA and we have UsbAccessory permission. + */ + public static boolean hasUsbAccessoryPermission(Context context) { + if (context == null) { + return false; + } + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + if (manager == null || manager.getAccessoryList() == null) { + return false; + } + for (final UsbAccessory usbAccessory : manager.getAccessoryList()) { + if (manager.hasPermission(usbAccessory)) { + return true; + } + } + return false; + } + + /** + * Helper method used to check permission passed in. + * @param context Context used to check permission + * @param permission String representing permission that is being checked. + * @return true if app has permission. + */ + public static boolean checkPermission(Context context, String permission) { + if (context == null) { + return false; + } + return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission); + } + + /** + * A helper method used for Android 14 or greater to check if app has necessary permissions + * to have a service enter the foreground. + * @param context context used to check permissions. + * @return true if app has permission to have a service enter foreground or if Android version < 14 + */ + public static boolean hasForegroundServiceTypePermission(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return true; + } + return checkPermission(context, + Manifest.permission.BLUETOOTH_CONNECT) || hasUsbAccessoryPermission(context); + } }