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);
+ }
}