Skip to content

Commit 7ba7e31

Browse files
authored
Merge pull request #1860 from smartdevicelink/bugfix/issue_1859_foreground_service_type
Android 14 foreground service type required
2 parents 3a2e7d5 + 8333284 commit 7ba7e31

File tree

6 files changed

+209
-30
lines changed

6 files changed

+209
-30
lines changed

android/hello_sdl_android/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
tools:targetApi="31"/>
99
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"
1010
tools:targetApi="33"/>
11+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"
12+
tools:targetApi="34"/>
1113
<uses-permission android:name="android.permission.INTERNET" />
1214
<!-- Required to check if WiFi is enabled -->
1315
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlReceiver.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
package com.sdl.hellosdlandroid;
22

33
import android.app.PendingIntent;
4+
import android.content.BroadcastReceiver;
45
import android.content.Context;
56
import android.content.Intent;
7+
import android.content.IntentFilter;
8+
import android.hardware.usb.UsbAccessory;
9+
import android.hardware.usb.UsbManager;
610
import android.os.Build;
711

812
import com.smartdevicelink.transport.SdlBroadcastReceiver;
913
import com.smartdevicelink.transport.SdlRouterService;
1014
import com.smartdevicelink.transport.TransportConstants;
15+
import com.smartdevicelink.util.AndroidTools;
1116
import com.smartdevicelink.util.DebugTool;
1217

1318
public class SdlReceiver extends SdlBroadcastReceiver {
1419
private static final String TAG = "SdlBroadcastReceiver";
1520

21+
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
22+
private PendingIntent pendingIntentToStartService;
23+
private Intent startSdlServiceIntent;
24+
1625
@Override
1726
public void onSdlEnabled(Context context, Intent intent) {
1827
DebugTool.logInfo(TAG, "SDL Enabled");
@@ -24,6 +33,15 @@ public void onSdlEnabled(Context context, Intent intent) {
2433
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
2534
PendingIntent pendingIntent = (PendingIntent) intent.getParcelableExtra(TransportConstants.PENDING_INTENT_EXTRA);
2635
if (pendingIntent != null) {
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
37+
if (!AndroidTools.hasForegroundServiceTypePermission(context)) {
38+
requestUsbAccessory(context);
39+
startSdlServiceIntent = intent;
40+
this.pendingIntentToStartService = pendingIntent;
41+
DebugTool.logInfo(TAG, "Permission missing for ForegroundServiceType connected device." + context);
42+
return;
43+
}
44+
}
2745
try {
2846
pendingIntent.send(context, 0, intent);
2947
} catch (PendingIntent.CanceledException e) {
@@ -56,4 +74,47 @@ public void onReceive(Context context, Intent intent) {
5674
public String getSdlServiceName() {
5775
return SdlService.class.getSimpleName();
5876
}
77+
78+
private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() {
79+
public void onReceive(Context context, Intent intent) {
80+
String action = intent.getAction();
81+
if (ACTION_USB_PERMISSION.equals(action) && context != null && startSdlServiceIntent != null && pendingIntentToStartService != null) {
82+
if (AndroidTools.hasForegroundServiceTypePermission(context)) {
83+
try {
84+
pendingIntentToStartService.send(context, 0, startSdlServiceIntent);
85+
context.unregisterReceiver(this);
86+
} catch (Exception e) {
87+
e.printStackTrace();
88+
}
89+
}
90+
}
91+
}
92+
};
93+
94+
/**
95+
* Request permission from USB Accessory if USB accessory is not null.
96+
* If the user has not granted the BLUETOOTH_CONNECT permission,
97+
* we can request the USB Accessory permission to satisfy the requirements for
98+
* FOREGROUND_SERVICE_CONNECTED_DEVICE and can start our service and allow
99+
* it to enter the foreground. FOREGROUND_SERVICE_CONNECTED_DEVICE is a requirement
100+
* in Android 14
101+
*/
102+
private void requestUsbAccessory(Context context) {
103+
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
104+
UsbAccessory[] accessoryList = manager.getAccessoryList();
105+
if (accessoryList == null || accessoryList.length == 0) {
106+
startSdlServiceIntent = null;
107+
pendingIntentToStartService = null;
108+
return;
109+
}
110+
PendingIntent mPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
111+
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
112+
113+
AndroidTools.registerReceiver(context, usbPermissionReceiver, filter,
114+
Context.RECEIVER_EXPORTED);
115+
116+
for (final UsbAccessory usbAccessory : accessoryList) {
117+
manager.requestPermission(usbAccessory, mPermissionIntent);
118+
}
119+
}
59120
}

android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/SdlService.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,25 @@ public void onCreate() {
102102
@SuppressLint("NewApi")
103103
public void enterForeground() {
104104
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
105-
NotificationChannel channel = new NotificationChannel(BuildConfig.SDL_APP_ID, "SdlService", NotificationManager.IMPORTANCE_DEFAULT);
106-
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
107-
if (notificationManager != null) {
108-
notificationManager.createNotificationChannel(channel);
109-
Notification.Builder builder = new Notification.Builder(this, channel.getId())
110-
.setContentTitle("Connected through SDL")
111-
.setSmallIcon(R.drawable.ic_sdl);
112-
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
113-
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE);
105+
try {
106+
NotificationChannel channel = new NotificationChannel(BuildConfig.SDL_APP_ID, "SdlService", NotificationManager.IMPORTANCE_DEFAULT);
107+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
108+
if (notificationManager != null) {
109+
notificationManager.createNotificationChannel(channel);
110+
Notification.Builder builder = new Notification.Builder(this, channel.getId())
111+
.setContentTitle("Connected through SDL")
112+
.setSmallIcon(R.drawable.ic_sdl);
113+
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
114+
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE);
115+
}
116+
Notification serviceNotification = builder.build();
117+
startForeground(FOREGROUND_SERVICE_ID, serviceNotification);
114118
}
115-
Notification serviceNotification = builder.build();
116-
startForeground(FOREGROUND_SERVICE_ID, serviceNotification);
119+
} catch (Exception e) {
120+
// This should only catch for TCP connections on Android 14+ due to needing
121+
// permissions for ForegroundServiceType ConnectedDevice that don't make sense for
122+
// a TCP connection
123+
DebugTool.logError(TAG, "Unable to start service in foreground", e);
117124
}
118125
}
119126
}

android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlRouterService.java

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,14 @@ public void handleMessage(Message msg) {
862862
ParcelFileDescriptor parcelFileDescriptor = (ParcelFileDescriptor) msg.obj;
863863

864864
if (parcelFileDescriptor != null) {
865+
// Added requirements with Android 14, Checking if we have proper permission to enter the foreground for Foreground service type connectedDevice.
866+
// If we do not have permission to enter the Foreground, we pass off hosting the RouterService to another app.
867+
if (!AndroidTools.hasForegroundServiceTypePermission(service.getApplicationContext())) {
868+
service.deployNextRouterService(parcelFileDescriptor);
869+
acknowledgeUSBAccessoryReceived(msg);
870+
return;
871+
}
872+
865873
//New USB constructor with PFD
866874
service.usbTransport = new MultiplexUsbTransport(parcelFileDescriptor, service.usbHandler, msg.getData());
867875

@@ -900,16 +908,7 @@ public void onReceive(Context context, Intent intent) {
900908

901909

902910
}
903-
904-
if (msg.replyTo != null) {
905-
Message message = Message.obtain();
906-
message.what = TransportConstants.ROUTER_USB_ACC_RECEIVED;
907-
try {
908-
msg.replyTo.send(message);
909-
} catch (RemoteException e) {
910-
e.printStackTrace();
911-
}
912-
}
911+
acknowledgeUSBAccessoryReceived(msg);
913912

914913
break;
915914
case TransportConstants.ALT_TRANSPORT_CONNECTED:
@@ -919,6 +918,18 @@ public void onReceive(Context context, Intent intent) {
919918
break;
920919
}
921920
}
921+
922+
private void acknowledgeUSBAccessoryReceived(Message msg) {
923+
if (msg.replyTo != null) {
924+
Message message = Message.obtain();
925+
message.what = TransportConstants.ROUTER_USB_ACC_RECEIVED;
926+
try {
927+
msg.replyTo.send(message);
928+
} catch (RemoteException e) {
929+
e.printStackTrace();
930+
}
931+
}
932+
}
922933
}
923934

924935
/* **************************************************************************************************************************************
@@ -1164,9 +1175,16 @@ public void onCreate() {
11641175
}
11651176

11661177
/**
1167-
* The method will attempt to start up the next router service in line based on the sorting criteria of best router service.
1178+
* The method will attempt to start up the next router service in line based on the sorting
1179+
* criteria of best router service.
1180+
* If a ParcelFileDescriptor is not null, we pass it along to the next RouterService to give
1181+
* it a chane to connected via AOA. This only happens on Android 14 and above when the app
1182+
* selected to host the RouterService does not satisfy the requirements for permission
1183+
* FOREGROUND_SERVICE_CONNECTED_DEVICE. By passing along the usbPfd, it will give the next
1184+
* RouterService selected a chance to connect.
1185+
* @param usbPfd a ParcelFileDescriptor used for AOA connections.
11681186
*/
1169-
protected void deployNextRouterService() {
1187+
protected void deployNextRouterService(ParcelFileDescriptor usbPfd) {
11701188
List<SdlAppInfo> sdlAppInfoList = AndroidTools.querySdlAppInfo(getApplicationContext(), new SdlAppInfo.BestRouterComparator(), null);
11711189
if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty()) {
11721190
ComponentName name = new ComponentName(this, this.getClass());
@@ -1178,11 +1196,25 @@ protected void deployNextRouterService() {
11781196
SdlAppInfo nextUp = sdlAppInfoList.get(i + 1);
11791197
Intent serviceIntent = new Intent();
11801198
serviceIntent.setComponent(nextUp.getRouterServiceComponentName());
1199+
if (usbPfd != null) {
1200+
serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT);
1201+
serviceIntent.putExtra(TransportConstants.CONNECTION_TYPE_EXTRA, TransportConstants.AOA_USB);
1202+
serviceIntent.putExtra(FOREGROUND_EXTRA, true);
1203+
}
11811204
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
11821205
startService(serviceIntent);
11831206
} else {
11841207
try {
11851208
startForegroundService(serviceIntent);
1209+
if (usbPfd != null) {
1210+
new UsbTransferProvider(getApplicationContext(), nextUp.getRouterServiceComponentName(), usbPfd, new UsbTransferProvider.UsbTransferCallback() {
1211+
@Override
1212+
public void onUsbTransferUpdate(boolean success) {
1213+
closeSelf();
1214+
}
1215+
});
1216+
}
1217+
11861218
} catch (Exception e) {
11871219
DebugTool.logError(TAG, "Unable to start next SDL router service. " + e.getMessage());
11881220
}
@@ -1282,7 +1314,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
12821314
if (firstStart) {
12831315
firstStart = false;
12841316
if (!initCheck(isConnectedOverUSB)) { // Run checks on process and permissions
1285-
deployNextRouterService();
1317+
deployNextRouterService(null);
12861318
closeSelf();
12871319
return START_REDELIVER_INTENT;
12881320
}
@@ -2632,15 +2664,11 @@ protected static LocalRouterService getLocalRouterService(Intent launchIntent, C
26322664
* This method is used to check for the newest version of this class to make sure the latest and greatest is up and running.
26332665
*/
26342666
private void startAltTransportTimer() {
2635-
if (Looper.myLooper() == null) {
2636-
Looper.prepare();
2637-
}
2638-
26392667
if (altTransportTimerHandler != null && altTransportTimerRunnable != null) {
26402668
altTransportTimerHandler.removeCallbacks(altTransportTimerRunnable);
26412669
}
26422670

2643-
altTransportTimerHandler = new Handler(Looper.myLooper());
2671+
altTransportTimerHandler = new Handler(Looper.getMainLooper());
26442672
altTransportTimerRunnable = new Runnable() {
26452673
public void run() {
26462674
altTransportTimerHandler = null;

android/sdl_android/src/main/java/com/smartdevicelink/transport/UsbTransferProvider.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import android.content.ServiceConnection;
4141
import android.hardware.usb.UsbAccessory;
4242
import android.hardware.usb.UsbManager;
43+
import android.os.Build;
4344
import android.os.Bundle;
4445
import android.os.Handler;
4546
import android.os.IBinder;
@@ -130,6 +131,26 @@ public UsbTransferProvider(Context context, ComponentName service, UsbAccessory
130131

131132
}
132133

134+
protected UsbTransferProvider(Context context, ComponentName service, ParcelFileDescriptor usbPfd, UsbTransferCallback callback) {
135+
if (context == null || service == null || usbPfd == null) {
136+
throw new IllegalStateException("Supplied params are not correct. Context == null? " + (context == null) + " ComponentName == null? " + (service == null) + " Usb PFD == null? " + usbPfd);
137+
}
138+
if (usbPfd.getFileDescriptor() != null && usbPfd.getFileDescriptor().valid()) {
139+
this.context = context;
140+
this.routerService = service;
141+
this.callback = callback;
142+
this.clientMessenger = new Messenger(new ClientHandler(this));
143+
this.usbPfd = usbPfd;
144+
checkIsConnected();
145+
} else {
146+
DebugTool.logError(TAG, "Unable to open accessory");
147+
clientMessenger = null;
148+
if (callback != null) {
149+
callback.onUsbTransferUpdate(false);
150+
}
151+
}
152+
}
153+
133154
@SuppressLint("NewApi")
134155
private ParcelFileDescriptor getFileDescriptor(UsbAccessory accessory, Context context) {
135156
if (AndroidTools.isUSBCableConnected(context)) {
@@ -161,6 +182,7 @@ public void cancel() {
161182
if (isBound) {
162183
unBindFromService();
163184
}
185+
context = null;
164186
}
165187

166188
private boolean bindToService() {
@@ -173,7 +195,12 @@ private boolean bindToService() {
173195
Intent bindingIntent = new Intent();
174196
bindingIntent.setClassName(this.routerService.getPackageName(), this.routerService.getClassName());//This sets an explicit intent
175197
//Quickly make sure it's just up and running
176-
context.startService(bindingIntent);
198+
bindingIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT);
199+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
200+
context.startService(bindingIntent);
201+
} else {
202+
context.startForegroundService(bindingIntent);
203+
}
177204
bindingIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_USB_PROVIDER);
178205
return context.bindService(bindingIntent, routerConnection, Context.BIND_AUTO_CREATE);
179206
}

android/sdl_android/src/main/java/com/smartdevicelink/util/AndroidTools.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import android.annotation.SuppressLint;
3636
import android.content.BroadcastReceiver;
37+
import android.Manifest;
3738
import android.content.ComponentName;
3839
import android.content.Context;
3940
import android.content.Intent;
@@ -49,10 +50,13 @@
4950
import android.content.res.XmlResourceParser;
5051
import android.graphics.Bitmap;
5152
import android.graphics.BitmapFactory;
53+
import android.hardware.usb.UsbAccessory;
54+
import android.hardware.usb.UsbManager;
5255
import android.os.BatteryManager;
5356
import android.os.Build;
5457
import android.os.Bundle;
5558
import androidx.annotation.Nullable;
59+
import androidx.core.content.ContextCompat;
5660

5761
import com.smartdevicelink.marshal.JsonRPCMarshaller;
5862
import com.smartdevicelink.proxy.rpc.VehicleType;
@@ -417,4 +421,54 @@ public static void registerReceiver(Context context, BroadcastReceiver receiver,
417421
}
418422
}
419423
}
424+
425+
/**
426+
* A helper method is used to see if this app has permission for UsbAccessory.
427+
* We need UsbAccessory permission if we are plugged in via AOA and do not have BLUETOOTH_CONNECT
428+
* permission for our service to enter the foreground on Android UPSIDE_DOWN_CAKE and greater
429+
* @param context a context that will be used to check the permission.
430+
* @return true if connected via AOA and we have UsbAccessory permission.
431+
*/
432+
public static boolean hasUsbAccessoryPermission(Context context) {
433+
if (context == null) {
434+
return false;
435+
}
436+
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
437+
if (manager == null || manager.getAccessoryList() == null) {
438+
return false;
439+
}
440+
for (final UsbAccessory usbAccessory : manager.getAccessoryList()) {
441+
if (manager.hasPermission(usbAccessory)) {
442+
return true;
443+
}
444+
}
445+
return false;
446+
}
447+
448+
/**
449+
* Helper method used to check permission passed in.
450+
* @param context Context used to check permission
451+
* @param permission String representing permission that is being checked.
452+
* @return true if app has permission.
453+
*/
454+
public static boolean checkPermission(Context context, String permission) {
455+
if (context == null) {
456+
return false;
457+
}
458+
return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission);
459+
}
460+
461+
/**
462+
* A helper method used for Android 14 or greater to check if app has necessary permissions
463+
* to have a service enter the foreground.
464+
* @param context context used to check permissions.
465+
* @return true if app has permission to have a service enter foreground or if Android version < 14
466+
*/
467+
public static boolean hasForegroundServiceTypePermission(Context context) {
468+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
469+
return true;
470+
}
471+
return checkPermission(context,
472+
Manifest.permission.BLUETOOTH_CONNECT) || hasUsbAccessoryPermission(context);
473+
}
420474
}

0 commit comments

Comments
 (0)