diff options
author | Joey Grover <joeygrover@gmail.com> | 2022-02-24 17:08:56 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-24 17:08:56 -0500 |
commit | dab6bc68e84f4caa54865f6d2379625ebece00a5 (patch) | |
tree | 2a366f0f27785dd245e7e117e2cb7b84d17b463e /android | |
parent | 52192be1ca1b09bbf732584672cca175dd4d33b0 (diff) | |
parent | 03a0682f24d970806e6b627519668a0599762edf (diff) | |
download | sdl_android-dab6bc68e84f4caa54865f6d2379625ebece00a5.tar.gz |
Merge pull request #1771 from smartdevicelink/feature/android_12_fixes
Feature/android 12 fixes
Diffstat (limited to 'android')
12 files changed, 330 insertions, 35 deletions
diff --git a/android/hello_sdl_android/build.gradle b/android/hello_sdl_android/build.gradle index 7640e89c5..5feeba93f 100755 --- a/android/hello_sdl_android/build.gradle +++ b/android/hello_sdl_android/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "com.sdl.hellosdlandroid" minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 1 versionName "1.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' diff --git a/android/hello_sdl_android/src/main/AndroidManifest.xml b/android/hello_sdl_android/src/main/AndroidManifest.xml index 1183e7b2c..415aa66c2 100755 --- a/android/hello_sdl_android/src/main/AndroidManifest.xml +++ b/android/hello_sdl_android/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ package="com.sdl.hellosdlandroid"> <uses-permission android:name="android.permission.BLUETOOTH" /> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" + tools:targetApi="31"/> <uses-permission android:name="android.permission.INTERNET" /> <!-- Required to check if WiFi is enabled --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> @@ -30,6 +32,7 @@ tools:ignore="DeepLinks"> <activity android:name=".MainActivity" + android:exported="true" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -40,6 +43,7 @@ <activity android:name="com.smartdevicelink.transport.USBAccessoryAttachmentActivity" + android:exported="true" android:launchMode="singleTop"> <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> @@ -52,6 +56,7 @@ <service android:name="com.sdl.hellosdlandroid.SdlService" + android:exported="true" android:foregroundServiceType="connectedDevice"> </service> <service diff --git a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/MainActivity.java b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/MainActivity.java index 43ad79ac7..8497e3b73 100755 --- a/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/MainActivity.java +++ b/android/hello_sdl_android/src/main/java/com/sdl/hellosdlandroid/MainActivity.java @@ -1,27 +1,66 @@ package com.sdl.hellosdlandroid; import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import static android.Manifest.permission.BLUETOOTH_CONNECT; public class MainActivity extends AppCompatActivity { + private static final int REQUEST_CODE = 200; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - //If we are connected to a module we want to start our SdlService + + if (BuildConfig.TRANSPORT.equals("MULTI") || BuildConfig.TRANSPORT.equals("MULTI_HB")) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !checkPermission()) { + requestPermission(); + return; + } + //If we are connected to a module we want to start our SdlService SdlReceiver.queryForConnectedService(this); - } else if (BuildConfig.TRANSPORT.equals("TCP")) { + } else if (BuildConfig.TRANSPORT.equals("TCP")){ Intent proxyIntent = new Intent(this, SdlService.class); startService(proxyIntent); } } + private boolean checkPermission() { + return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(getApplicationContext(), BLUETOOTH_CONNECT); + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, new String[]{BLUETOOTH_CONNECT}, REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_CODE: + if (grantResults.length > 0) { + + boolean btConnectGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; + + if (btConnectGranted) { + SdlReceiver.queryForConnectedService(this); + } + } + break; + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. 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 32c0ab1d5..e130d3d78 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,11 +1,13 @@ package com.sdl.hellosdlandroid; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; import com.smartdevicelink.transport.SdlBroadcastReceiver; import com.smartdevicelink.transport.SdlRouterService; +import com.smartdevicelink.transport.TransportConstants; import com.smartdevicelink.util.DebugTool; public class SdlReceiver extends SdlBroadcastReceiver { @@ -16,13 +18,27 @@ public class SdlReceiver extends SdlBroadcastReceiver { DebugTool.logInfo(TAG, "SDL Enabled"); intent.setClass(context, SdlService.class); - // SdlService needs to be foregrounded in Android O and above - // This will prevent apps in the background from crashing when they try to start SdlService - // Because Android O doesn't allow background apps to start background services - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(intent); + // Starting with Android S SdlService needs to be started from a foreground context. + // We will check the intent for a pendingIntent parcelable extra + // This pendingIntent allows us to start the SdlService from the context of the active router service which is in the foreground + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent pendingIntent = (PendingIntent) intent.getParcelableExtra(TransportConstants.PENDING_INTENT_EXTRA); + if (pendingIntent != null) { + try { + pendingIntent.send(context, 0, intent); + } catch (PendingIntent.CanceledException e) { + e.printStackTrace(); + } + } } else { - context.startService(intent); + // SdlService needs to be foregrounded in Android O and above + // This will prevent apps in the background from crashing when they try to start SdlService + // Because Android O doesn't allow background apps to start background services + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } } } @@ -35,4 +51,9 @@ public class SdlReceiver extends SdlBroadcastReceiver { public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); // Required if overriding this method } + + @Override + public String getSdlServiceName() { + return SdlService.class.getSimpleName(); + } }
\ 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 c6e9ce3ef..59daf2050 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 @@ -108,10 +108,13 @@ public class SdlService extends Service { NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager != null) { notificationManager.createNotificationChannel(channel); - Notification serviceNotification = new Notification.Builder(this, channel.getId()) + Notification.Builder builder = new Notification.Builder(this, channel.getId()) .setContentTitle("Connected through SDL") - .setSmallIcon(R.drawable.ic_sdl) - .build(); + .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); } } diff --git a/android/sdl_android/build.gradle b/android/sdl_android/build.gradle index f3dfdfda1..575ec5f93 100644 --- a/android/sdl_android/build.gradle +++ b/android/sdl_android/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 21 versionName new File(projectDir.path, ('/../../VERSION')).text.trim() buildConfigField "String", "VERSION_NAME", '\"' + versionName + '\"' diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlBroadcastReceiver.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlBroadcastReceiver.java index 93f6cc850..7bed1b482 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlBroadcastReceiver.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/SdlBroadcastReceiver.java @@ -66,6 +66,7 @@ import java.util.Locale; import java.util.Vector; import java.util.concurrent.ConcurrentLinkedQueue; +import static android.Manifest.permission.BLUETOOTH_CONNECT; import static com.smartdevicelink.transport.TransportConstants.FOREGROUND_EXTRA; public abstract class SdlBroadcastReceiver extends BroadcastReceiver { @@ -73,6 +74,7 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "Sdl Broadcast Receiver"; protected static final String SDL_ROUTER_SERVICE_CLASS_NAME = "sdlrouterservice"; + protected static final int ANDROID_12_ROUTER_SERVICE_VERSION = 16; public static final String LOCAL_ROUTER_SERVICE_EXTRA = "router_service"; public static final String LOCAL_ROUTER_SERVICE_DID_START_OWN = "did_start"; @@ -90,6 +92,7 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { private static Thread.UncaughtExceptionHandler foregroundExceptionHandler = null; private static final Object DEVICE_LISTENER_LOCK = new Object(); private static SdlDeviceListener sdlDeviceListener; + private static String serviceName = null; public int getRouterServiceVersion() { return SdlRouterService.ROUTER_SERVICE_VERSION_NUMBER; @@ -98,6 +101,7 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { @Override @CallSuper public void onReceive(Context context, Intent intent) { + //Log.i(TAG, "Sdl Receiver Activated"); final String action = intent.getAction(); if (action == null) { @@ -132,6 +136,10 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); } + if (serviceName == null) { + serviceName = getSdlServiceName(); + } + boolean didStart = false; if (localRouterClass == null) { localRouterClass = defineLocalSdlRouterClass(); @@ -296,6 +304,18 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { String routerServicePackage = null; if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty() && sdlAppInfoList.get(0).getRouterServiceComponentName() != null) { routerServicePackage = sdlAppInfoList.get(0).getRouterServiceComponentName().getPackageName(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // If all apps have a RS newer than the Android 12 update, chosen app does not have BT Connect permissions, and more than 1 sdl app is installed + if (!isPreAndroid12RSOnDevice(sdlAppInfoList) && !AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, context, routerServicePackage) && sdlAppInfoList.size() > 1) { + for (SdlAppInfo appInfo : sdlAppInfoList) { + if (AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, context, appInfo.getRouterServiceComponentName().getPackageName())) { + //If this app in the list has BT Connect permissions, we want to use that apps RS + routerServicePackage = appInfo.getRouterServiceComponentName().getPackageName(); + break; + } + } + } + } } DebugTool.logInfo(TAG, ": This app's package: " + myPackage); DebugTool.logInfo(TAG, ": Router service app's package: " + routerServicePackage); @@ -362,13 +382,16 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { } /** - * This method will set a new UncaughtExceptionHandler for the current thread. The only - * purpose of the custom UncaughtExceptionHandler is to catch the rare occurrence that the - * SdlRouterService can't be started fast enough by the system after calling - * startForegroundService so the onCreate method doesn't get called before the foreground promise - * timer expires. The new UncaughtExceptionHandler will catch that specific exception and tell the - * main looper to continue forward. This still leaves the SdlRouterService killed, but prevents - * an ANR to the app that makes the startForegroundService call. + * This method will set a new UncaughtExceptionHandler for the current thread. + * There are two exceptions we want to catch here. The first exception is the rare + * occurrence that the SdlRouterService can't be started fast enough by the system after calling + * startForegroundService so the onCreate method doesn't get called before the foreground + * promise timer expires. The second is for the instance where the developers "SdlService" class + * can't be started fast enough by the system after calling startForegroundService OR the app + * is unable to start the "SdlService" class because the developer did not export the service + * in the manifest. The new UncaughtExceptionHandler will catch these specific exception and + * tell the main looper to continue forward. This still leaves the respective Service killed, + * but prevents an ANR to the app that makes the startForegroundService call. */ static protected void setForegroundExceptionHandler() { final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); @@ -378,10 +401,9 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { public void uncaughtException(Thread t, Throwable e) { if (e != null && e instanceof AndroidRuntimeException - && "android.app.RemoteServiceException".equals(e.getClass().getName()) //android.app.RemoteServiceException is a private class + && ("android.app.RemoteServiceException".equals(e.getClass().getName()) || "android.app.ForegroundServiceDidNotStartInTimeException".equals(e.getClass().getName())) //android.app.RemoteServiceException is a private class && e.getMessage() != null - && e.getMessage().contains("SdlRouterService")) { - + && (e.getMessage().contains("SdlRouterService")) || e.getMessage().contains(serviceName)) { DebugTool.logInfo(TAG, "Handling failed startForegroundService call"); Looper.loop(); } else if (defaultUncaughtExceptionHandler != null) { //No other exception should be handled @@ -598,6 +620,18 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { final List<SdlAppInfo> sdlAppInfoList = AndroidTools.querySdlAppInfo(context, new SdlAppInfo.BestRouterComparator(), vehicleType); if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty()) { ComponentName routerService = sdlAppInfoList.get(0).getRouterServiceComponentName(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // If all apps have a RS newer than the Android 12 update, chosen app does not have BT Connect permissions, and more than 1 sdl app is installed + if (!isPreAndroid12RSOnDevice(sdlAppInfoList) && !AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, context, routerService.getPackageName()) && sdlAppInfoList.size() > 1) { + for (SdlAppInfo appInfo : sdlAppInfoList) { + if (AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, context, appInfo.getRouterServiceComponentName().getPackageName())) { + routerService = appInfo.getRouterServiceComponentName(); + //If this app in the list has BT Connect permissions, we want to use that apps RS + break; + } + } + } + } startRouterService(context, routerService, false, bluetoothDevice, true, vehicleType); } } @@ -634,6 +668,16 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { } } + private static boolean isPreAndroid12RSOnDevice(List<SdlAppInfo> sdlAppInfoList) { + for (SdlAppInfo appInfo : sdlAppInfoList) { + //If an installed app RS version is older than Android 12 update version (16) + if (appInfo.getRouterServiceVersion() < ANDROID_12_ROUTER_SERVICE_VERSION) { + return true; + } + } + return false; + } + /** * We need to define this for local copy of the Sdl Router Service class. * It will be the main point of connection for Sdl enabled apps @@ -656,6 +700,16 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver { */ public abstract void onSdlEnabled(Context context, Intent intent); + + /** + * The developer can override this method to return the name of the class that manages their + * SdlService. This method is used to ensure the SdlBroadcastReceivers exception catcher catches + * the correct exception that may be thrown by the app trying to start their SdlService. If this + * exception is not caught the user may experience an ANR for that app. + */ + public String getSdlServiceName() { + return "SdlService"; + } //public abstract void onSdlDisabled(Context context); //Removing for now until we're able to abstract from developer 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 db281a351..a4926372d 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 @@ -70,6 +70,7 @@ import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.RemoteException; +import android.provider.Settings; import android.util.AndroidRuntimeException; import android.util.SparseArray; import android.util.SparseIntArray; @@ -119,6 +120,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; import static com.smartdevicelink.transport.TransportConstants.CONNECTED_DEVICE_STRING_EXTRA_NAME; import static com.smartdevicelink.transport.TransportConstants.FOREGROUND_EXTRA; import static com.smartdevicelink.transport.TransportConstants.FORMED_PACKET_EXTRA_NAME; @@ -142,7 +145,7 @@ public class SdlRouterService extends Service { /** * <b> NOTE: DO NOT MODIFY THIS UNLESS YOU KNOW WHAT YOU'RE DOING.</b> */ - protected static final int ROUTER_SERVICE_VERSION_NUMBER = 15; + protected static final int ROUTER_SERVICE_VERSION_NUMBER = 16; private static final String ROUTER_SERVICE_PROCESS = "com.smartdevicelink.router"; @@ -202,6 +205,7 @@ public class SdlRouterService extends Service { private boolean wrongProcess = false; private boolean initPassed = false; private boolean hasCalledStartForeground = false; + private boolean hasConnectedBefore = false; boolean firstStart = true; public static HashMap<String, RegisteredApp> registeredApps; @@ -215,6 +219,10 @@ public class SdlRouterService extends Service { private boolean startSequenceComplete = false; private VehicleType receivedVehicleType; + private boolean waitingForBTRuntimePermissions = false; + private Handler btPermissionsHandler; + private Runnable btPermissionsRunnable; + private final static int BT_PERMISSIONS_CHECK_FREQUENCY = 1000; private ExecutorService packetExecutor = null; ConcurrentHashMap<TransportType, PacketWriteTaskMaster> packetWriteTaskMasterMap = null; @@ -365,6 +373,11 @@ public class SdlRouterService extends Service { switch (msg.what) { case TransportConstants.ROUTER_REQUEST_BT_CLIENT_CONNECT: + //Starting with Android 12 this use case will require the BLUETOOTH_SCAN PERMISSION + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !AndroidTools.isPermissionGranted(BLUETOOTH_SCAN, service.getApplicationContext(), service.getPackageName())) { + DebugTool.logError(TAG, "BLUETOOTH_SCAN Permissions not granted for this app"); + break; + } if (receivedBundle.getBoolean(TransportConstants.CONNECT_AS_CLIENT_BOOLEAN_EXTRA, false) && !connectAsClient) { //We check this flag to make sure we don't try to connect over and over again. On D/C we should set to false //Log.d(TAG,"Attempting to connect as bt client"); @@ -1085,7 +1098,7 @@ public class SdlRouterService extends Service { * * @return true if this service is set up correctly */ - private boolean initCheck() { + private boolean initCheck(boolean isConnectedOverUSB) { if (!processCheck()) { DebugTool.logError(TAG, "Not using correct process. Shutting down"); wrongProcess = true; @@ -1095,6 +1108,14 @@ public class SdlRouterService extends Service { DebugTool.logError(TAG, "Bluetooth Permission is not granted. Shutting down"); return false; } + + // If Android 12 or newer make sure we have BLUETOOTH_CONNECT Runtime permission + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, this, this.getPackageName())) { + if (!isConnectedOverUSB) { //If BLUETOOTH_CONNECT permission is not granted We want to make sure we are connected over USB + return false; + } + } + if (!AndroidTools.isServiceExported(this, new ComponentName(this, this.getClass()))) { //We want to check to see if our service is actually exported DebugTool.logError(TAG, "Service isn't exported. Shutting down"); return false; @@ -1256,10 +1277,14 @@ public class SdlRouterService extends Service { (HashMap<String, Object>) intent.getSerializableExtra(TransportConstants.VEHICLE_INFO_EXTRA) ); } + boolean isConnectedOverUSB = false; + if (intent != null && intent.hasExtra(TransportConstants.CONNECTION_TYPE_EXTRA)) { + isConnectedOverUSB = TransportConstants.AOA_USB.equalsIgnoreCase(intent.getStringExtra(TransportConstants.CONNECTION_TYPE_EXTRA)); + } // Only trusting the first intent received to start the RouterService and run initial checks to avoid a case where an app could send incorrect data after the spp connection has started. if (firstStart) { firstStart = false; - if (!initCheck()) { // Run checks on process and permissions + if (!initCheck(isConnectedOverUSB)) { // Run checks on process and permissions deployNextRouterService(); closeSelf(); return START_REDELIVER_INTENT; @@ -1296,6 +1321,7 @@ public class SdlRouterService extends Service { } boolean confirmedDevice = intent.getBooleanExtra(TransportConstants.CONFIRMED_SDL_DEVICE, false); int timeout = getNotificationTimeout(address, confirmedDevice); + hasConnectedBefore = hasSDLConnected(address); enterForeground("Waiting for connection...", timeout, false); resetForegroundTimeOut(timeout); @@ -1337,6 +1363,10 @@ public class SdlRouterService extends Service { altTransportTimerHandler = null; } + if (btPermissionsHandler != null && btPermissionsRunnable != null) { + btPermissionsHandler.removeCallbacks(btPermissionsRunnable); + } + DebugTool.logWarning(TAG, "Sdl Router Service Destroyed"); closing = true; //No need for this Broadcast Receiver anymore @@ -1499,6 +1529,10 @@ public class SdlRouterService extends Service { builder = new Notification.Builder(this, SDL_NOTIFICATION_CHANNEL_ID); } + if (hasConnectedBefore && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE); + } + if (0 != (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) { //If we are in debug mode, include what app has the router service open ComponentName name = new ComponentName(this, this.getClass()); builder.setContentTitle("SDL: " + name.getPackageName()); @@ -1521,7 +1555,8 @@ public class SdlRouterService extends Service { // Create an intent that will be fired when the user clicks the notification. Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(SDL_NOTIFICATION_FAQS_PAGE)); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); + int flag = android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0; + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flag); builder.setContentIntent(pendingIntent); if (chronometerLength > (FOREGROUND_TIMEOUT / 1000) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -1569,12 +1604,22 @@ public class SdlRouterService extends Service { private void safeStartForeground(int id, Notification notification) { try { if (notification == null) { - //Try the NotificationCompat this time in case there was a previous error - NotificationCompat.Builder builder = - new NotificationCompat.Builder(this, SDL_NOTIFICATION_CHANNEL_ID) + if (hasConnectedBefore && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + Notification.Builder builder = + new Notification.Builder(this, SDL_NOTIFICATION_CHANNEL_ID) .setContentTitle("SmartDeviceLink") - .setContentText("Service Running"); - notification = builder.build(); + .setContentText("Service Running") + .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE); + notification = builder.build(); + } else { + //Try the NotificationCompat this time in case there was a previous error + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this, SDL_NOTIFICATION_CHANNEL_ID) + .setContentTitle("SmartDeviceLink") + .setContentText("Service Running"); + + notification = builder.build(); + } } startForeground(id, notification); DebugTool.logInfo(TAG, "Entered the foreground - " + System.currentTimeMillis()); @@ -1684,6 +1729,9 @@ public class SdlRouterService extends Service { */ @SuppressWarnings("MissingPermission") private boolean bluetoothAvailable() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, SdlRouterService.this, SdlRouterService.this.getPackageName())) { + return false; + } try { return (!(BluetoothAdapter.getDefaultAdapter() == null) && BluetoothAdapter.getDefaultAdapter().isEnabled()); } catch (NullPointerException e) { // only for BluetoothAdapter.getDefaultAdapter().isEnabled() call @@ -1745,6 +1793,11 @@ public class SdlRouterService extends Service { } private synchronized void initBluetoothSerialService() { + if (waitingForBTRuntimePermissions) { + DebugTool.logWarning(TAG, "This app has not been granted the BLUETOOTH_CONNECT runtime permission"); + return; + } + if (legacyModeEnabled) { DebugTool.logInfo(TAG, "Not starting own bluetooth during legacy mode"); return; @@ -1801,6 +1854,17 @@ public class SdlRouterService extends Service { startService.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + //Starting in Android 12 we need to start services from a foreground context + //To enable developers to be able to start their SdlService from the "background" + //we will attach a pendingIntent as an extra to the intent + //the developer can use this pendingIntent to start their SdlService from the context of + //the active RouterService + Intent pending = new Intent(); + PendingIntent pendingIntent = PendingIntent.getForegroundService(this, (int) System.currentTimeMillis(), pending, PendingIntent.FLAG_MUTABLE | Intent.FILL_IN_COMPONENT); + startService.putExtra(TransportConstants.PENDING_INTENT_EXTRA, pendingIntent); + } + AndroidTools.sendExplicitBroadcast(getApplicationContext(), startService, null); //HARDWARE_CONNECTED @@ -1808,6 +1872,31 @@ public class SdlRouterService extends Service { //If we have clients notifyClients(createHardwareConnectedMessage(record)); } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && TransportType.USB.equals(record.getType()) && !AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, SdlRouterService.this, SdlRouterService.this.getPackageName())) { + //Delay starting bluetoothTransport when we are connected over USB and the app does not have the BLUETOOTH_CONNECT permissions + waitingForBTRuntimePermissions = true; + btPermissionsHandler = new Handler(Looper.myLooper()); + //Continuously Check for the BLUETOOTH_CONNECT Permission + btPermissionsRunnable = new Runnable() { + @Override + public void run() { + if (!AndroidTools.isPermissionGranted(BLUETOOTH_CONNECT, SdlRouterService.this, SdlRouterService.this.getPackageName())) { + btPermissionsHandler.postDelayed(btPermissionsRunnable, BT_PERMISSIONS_CHECK_FREQUENCY); + } else { + waitingForBTRuntimePermissions = false; + initBluetoothSerialService(); + final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if(notificationManager != null) { + notificationManager.cancel("SDL", TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID_INT); + } + } + } + }; + btPermissionsHandler.postDelayed(btPermissionsRunnable, BT_PERMISSIONS_CHECK_FREQUENCY); + //Present Notification to take user to permissions page for the app + showBTPermissionsNotification(); + } } private Message createHardwareConnectedMessage(final TransportRecord record) { @@ -3805,6 +3894,10 @@ public class SdlRouterService extends Service { } else { builder = new Notification.Builder(getApplicationContext(), TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID); } + + if (hasConnectedBefore && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE); + } ComponentName name = new ComponentName(this, this.getClass()); if (0 != (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) { //If we are in debug mode, include what app has the router service open builder.setContentTitle("SDL: " + name.getPackageName()); @@ -3843,4 +3936,59 @@ public class SdlRouterService extends Service { DebugTool.logError(TAG, "notifySppError: Unable to retrieve notification Manager service"); } } + + private void showBTPermissionsNotification() { + Notification.Builder builder; + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + builder = new Notification.Builder(getApplicationContext()); + } else { + builder = new Notification.Builder(getApplicationContext(), TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID); + } + + ComponentName name = new ComponentName(this, this.getClass()); + if (0 != (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) { //If we are in debug mode, include what app has the router service open + builder.setContentTitle("SDL: " + name.getPackageName()); + } else { + builder.setContentTitle(getString(R.string.notification_title)); + } + builder.setTicker(getString(R.string.sdl_error_notification_channel_name)); + builder.setContentText(getString(R.string.allow_bluetooth_permissions)); + + //We should use icon from library resources if available + int trayId = getResources().getIdentifier("sdl_tray_icon", "drawable", getPackageName()); + + builder.setSmallIcon(trayId); + Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.spp_error); + builder.setLargeIcon(icon); + + builder.setOngoing(false); + builder.setAutoCancel(true); + + // Create an intent that will be fired when the user clicks the notification. + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", getPackageName(), null); + intent.setData(uri); + int flag = android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0; + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flag); + builder.setContentIntent(pendingIntent); + + final String tag = "SDL"; + //Now we need to add a notification channel + final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.cancel(tag, TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID_INT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID, getString(R.string.sdl_error_notification_channel_name), NotificationManager.IMPORTANCE_HIGH); + notificationChannel.enableLights(true); + notificationChannel.enableVibration(true); + notificationChannel.setShowBadge(false); + notificationManager.createNotificationChannel(notificationChannel); + builder.setChannelId(notificationChannel.getId()); + } + Notification notification = builder.build(); + notificationManager.notify(tag, TransportConstants.SDL_ERROR_NOTIFICATION_CHANNEL_ID_INT, notification); + } else { + DebugTool.logError(TAG, "Unable to retrieve notification Manager service"); + } + } } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/USBAccessoryAttachmentActivity.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/USBAccessoryAttachmentActivity.java index 8e54de9a1..22ee5d1db 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/USBAccessoryAttachmentActivity.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/USBAccessoryAttachmentActivity.java @@ -167,6 +167,7 @@ public class USBAccessoryAttachmentActivity extends Activity { return;
}
serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT);
+ serviceIntent.putExtra(TransportConstants.CONNECTION_TYPE_EXTRA, TransportConstants.AOA_USB);
ComponentName startedService;
try {
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 4f554f0d7..2700d35d9 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 @@ -48,6 +48,7 @@ import android.content.res.XmlResourceParser; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.BatteryManager; +import android.os.Build; import android.os.Bundle; import androidx.annotation.Nullable; @@ -69,6 +70,9 @@ import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import static android.Manifest.permission.BLUETOOTH_CONNECT; +import static android.Manifest.permission.BLUETOOTH_SCAN; + public class AndroidTools { @@ -199,6 +203,25 @@ public class AndroidTools { return sdlAppInfoList; } + public static boolean isPermissionGranted(String permissionName, Context context, String servicePackageName) { + PackageManager packageManager = context.getPackageManager(); + if (packageManager == null) { + return false; + } + PackageInfo packageInfo; + try { + packageInfo = packageManager.getPackageInfo(servicePackageName, PackageManager.GET_PERMISSIONS); + if (packageInfo == null) { + return false; + } + int permissionResult = packageManager.checkPermission(permissionName, packageInfo.packageName); + return permissionResult == PackageManager.PERMISSION_GRANTED; + } catch (NameNotFoundException e) { + e.printStackTrace(); + DebugTool.logError(TAG, "servicePackageName not found while checking " + permissionName + " permission", e); + return false; + } + } /** * Sends the provided intent to the specified destinations making it an explicit intent, rather diff --git a/android/sdl_android/src/main/res/values/sdl.xml b/android/sdl_android/src/main/res/values/sdl.xml index 7bb919f0b..bd96c54ab 100644 --- a/android/sdl_android/src/main/res/values/sdl.xml +++ b/android/sdl_android/src/main/res/values/sdl.xml @@ -2,7 +2,7 @@ <resources> <string name="sdl_router_service_version_name" translatable="false">sdl_router_version</string> - <integer name="sdl_router_service_version_value">15</integer> + <integer name="sdl_router_service_version_value">16</integer> <string name="sdl_router_service_is_custom_name" translatable="false">sdl_custom_router</string> <string name="sdl_oem_vehicle_type_filter_name" translatable="false">sdl_oem_vehicle_type</string> diff --git a/android/sdl_android/src/main/res/values/strings.xml b/android/sdl_android/src/main/res/values/strings.xml index 89322299f..36a089366 100644 --- a/android/sdl_android/src/main/res/values/strings.xml +++ b/android/sdl_android/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ <string name="lockscreen_device_image_description">Device Icon</string> <string name="default_lockscreen_warning_message">Swipe down to dismiss, acknowledging that you are not the driver.</string> <string name="spp_out_of_resource">Too many apps are using Bluetooth</string> + <string name="allow_bluetooth_permissions">Please grant this app the Nearby Devices Permission to use bluetooth</string> <string name="notification_title">SmartDeviceLink</string> <string name="sdl_error_notification_channel_name">SDL Error</string> </resources>
\ No newline at end of file |