|
63 | 63 | import android.os.Handler; |
64 | 64 | import android.os.IBinder; |
65 | 65 | import android.os.IBinder.DeathRecipient; |
| 66 | +import android.os.Looper; |
66 | 67 | import android.os.Message; |
67 | 68 | import android.os.Messenger; |
68 | 69 | import android.os.Parcel; |
69 | 70 | import android.os.ParcelFileDescriptor; |
70 | 71 | import android.os.Parcelable; |
71 | 72 | import android.os.RemoteException; |
72 | | -import android.service.notification.StatusBarNotification; |
73 | 73 | import android.support.annotation.NonNull; |
74 | 74 | import android.support.v4.app.NotificationCompat; |
| 75 | +import android.util.AndroidRuntimeException; |
75 | 76 | import android.util.Log; |
76 | 77 | import android.util.SparseArray; |
77 | 78 | import android.util.SparseIntArray; |
@@ -139,7 +140,7 @@ public class SdlRouterService extends Service{ |
139 | 140 | /** |
140 | 141 | * <b> NOTE: DO NOT MODIFY THIS UNLESS YOU KNOW WHAT YOU'RE DOING.</b> |
141 | 142 | */ |
142 | | - protected static final int ROUTER_SERVICE_VERSION_NUMBER = 11; |
| 143 | + protected static final int ROUTER_SERVICE_VERSION_NUMBER = 12; |
143 | 144 |
|
144 | 145 | private static final String ROUTER_SERVICE_PROCESS = "com.smartdevicelink.router"; |
145 | 146 |
|
@@ -185,11 +186,18 @@ public class SdlRouterService extends Service{ |
185 | 186 | * Preference location where the service stores known SDL status based on device address |
186 | 187 | */ |
187 | 188 | protected static final String SDL_DEVICE_STATUS_SHARED_PREFS = "sdl.device.status"; |
| 189 | + /** |
| 190 | + * Preference location where generic key/values can be stored |
| 191 | + */ |
| 192 | + protected static final String SDL_ROUTER_SERVICE_PREFS = "sdl.router.service.prefs"; |
| 193 | + protected static final String KEY_AVOID_NOTIFICATION_CHANNEL_DELETE = "avoidNotificationChannelDelete"; |
| 194 | + |
188 | 195 |
|
189 | 196 |
|
190 | 197 |
|
191 | 198 | private static boolean connectAsClient = false; |
192 | 199 | private static boolean closing = false; |
| 200 | + private static Thread.UncaughtExceptionHandler routerServiceExceptionHandler = null; |
193 | 201 |
|
194 | 202 | private Handler altTransportTimerHandler, foregroundTimeoutHandler; |
195 | 203 | private Runnable altTransportTimerRunnable, foregroundTimeoutRunnable; |
@@ -1099,6 +1107,8 @@ private boolean initCheck(){ |
1099 | 1107 | @Override |
1100 | 1108 | public void onCreate() { |
1101 | 1109 | super.onCreate(); |
| 1110 | + //Add this first to avoid the runtime exceptions for the entire lifecycle of the service |
| 1111 | + setRouterServiceExceptionHandler(); |
1102 | 1112 | //This must be done regardless of if this service shuts down or not |
1103 | 1113 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
1104 | 1114 | hasCalledStartForeground = false; |
@@ -1196,6 +1206,43 @@ public void startUpSequence(){ |
1196 | 1206 | startSequenceComplete= true; |
1197 | 1207 | } |
1198 | 1208 |
|
| 1209 | + /** |
| 1210 | + * This method will set a new UncaughtExceptionHandler for the current thread. The only |
| 1211 | + * purpose of the custom UncaughtExceptionHandler is to catch the rare occurrence that the |
| 1212 | + * a specific mobile device/OS can't properly handle the deletion and creation of the foreground |
| 1213 | + * notification channel that is necessary for foreground services after Android Oreo. |
| 1214 | + * The new UncaughtExceptionHandler will catch that specific exception and tell the |
| 1215 | + * main looper to continue forward. This still leaves the SdlRouterService killed, but prevents |
| 1216 | + * an ANR to the app that makes the startForegroundService call. It will set a flag that will |
| 1217 | + * prevent the channel from being deleted in the future and therefore avoiding this exception. |
| 1218 | + */ |
| 1219 | + protected void setRouterServiceExceptionHandler() { |
| 1220 | + final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); |
| 1221 | + if (defaultUncaughtExceptionHandler != routerServiceExceptionHandler) { |
| 1222 | + routerServiceExceptionHandler = new Thread.UncaughtExceptionHandler() { |
| 1223 | + @Override |
| 1224 | + public void uncaughtException(Thread t, Throwable e) { |
| 1225 | + if (e != null |
| 1226 | + && e instanceof AndroidRuntimeException |
| 1227 | + && "android.app.RemoteServiceException".equals(e.getClass().getName()) //android.app.RemoteServiceException is a private class |
| 1228 | + && e.getMessage().contains("invalid channel for service notification")) { //This is the message received in the exception for notification channel issues |
| 1229 | + |
| 1230 | + // Set the flag to not delete the notification channel to avoid this exception in the future |
| 1231 | + try{ |
| 1232 | + SdlRouterService.this.setSdlRouterServicePrefs(KEY_AVOID_NOTIFICATION_CHANNEL_DELETE, true); |
| 1233 | + }catch (Exception exception){ |
| 1234 | + //Unable to save flag for KEY_AVOID_NOTIFICATION_CHANNEL_DELETE |
| 1235 | + } |
| 1236 | + Looper.loop(); |
| 1237 | + } else if (defaultUncaughtExceptionHandler != null) { //No other exception should be handled |
| 1238 | + defaultUncaughtExceptionHandler.uncaughtException(t, e); |
| 1239 | + } |
| 1240 | + } |
| 1241 | + }; |
| 1242 | + Thread.setDefaultUncaughtExceptionHandler(routerServiceExceptionHandler); |
| 1243 | + } |
| 1244 | + } |
| 1245 | + |
1199 | 1246 |
|
1200 | 1247 | @SuppressLint({"NewApi", "MissingPermission"}) |
1201 | 1248 | @Override |
@@ -1528,7 +1575,7 @@ private void exitForeground(){ |
1528 | 1575 | if (notificationManager!= null){ |
1529 | 1576 | try { |
1530 | 1577 | notificationManager.cancelAll(); |
1531 | | - if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| 1578 | + if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !getBooleanPref(KEY_AVOID_NOTIFICATION_CHANNEL_DELETE,false)) { |
1532 | 1579 | notificationManager.deleteNotificationChannel(SDL_NOTIFICATION_CHANNEL_ID); |
1533 | 1580 | } |
1534 | 1581 | } catch (Exception e) { |
@@ -2445,6 +2492,33 @@ protected boolean hasSDLConnected(String address){ |
2445 | 2492 | return preferences.contains(address) && preferences.getBoolean(address,false); |
2446 | 2493 | } |
2447 | 2494 |
|
| 2495 | + /** |
| 2496 | + * Set specific settings through key/value to the SDL_ROUTER_SERVICE_PREFS |
| 2497 | + * @param key the key of the pair to set in the preferences |
| 2498 | + * @param value boolean to attach to key in the preferences |
| 2499 | + */ |
| 2500 | + protected void setSdlRouterServicePrefs(String key, boolean value){ |
| 2501 | + SharedPreferences preferences = this.getSharedPreferences(SDL_ROUTER_SERVICE_PREFS, Context.MODE_PRIVATE); |
| 2502 | + SharedPreferences.Editor editor = preferences.edit(); |
| 2503 | + editor.putBoolean(key,value); |
| 2504 | + editor.commit(); |
| 2505 | + Log.d(TAG, "Preference set: " + key + " : " + value); |
| 2506 | + } |
| 2507 | + |
| 2508 | + /** |
| 2509 | + * Retrieves a boolean value for the given key in the SDL_ROUTER_SERVICE_PREFS |
| 2510 | + * @param key the string key that will be used to retrieve the boolean value |
| 2511 | + * @param defaultValue if they key does not exist or there is no value to be found, this is the |
| 2512 | + * value that will be returned |
| 2513 | + * @return the value associated with the supplied key or defaultValue if one does not exist |
| 2514 | + */ |
| 2515 | + protected boolean getBooleanPref(String key, boolean defaultValue){ |
| 2516 | + SharedPreferences preferences = this.getSharedPreferences(SDL_ROUTER_SERVICE_PREFS, Context.MODE_PRIVATE); |
| 2517 | + if(preferences != null){ |
| 2518 | + return preferences.getBoolean(key, defaultValue); |
| 2519 | + } |
| 2520 | + return false; |
| 2521 | + } |
2448 | 2522 |
|
2449 | 2523 |
|
2450 | 2524 | /* *********************************************************************************************************************************************************************** |
|
0 commit comments