diff options
Diffstat (limited to 'android/sdl_android/src/main/java/com/smartdevicelink')
13 files changed, 960 insertions, 264 deletions
diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/SdlManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/SdlManager.java index 2a34dc62e..b993724ba 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/managers/SdlManager.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/SdlManager.java @@ -49,7 +49,6 @@ import com.smartdevicelink.managers.permission.PermissionManager; import com.smartdevicelink.managers.screen.ScreenManager; import com.smartdevicelink.managers.video.VideoStreamManager; import com.smartdevicelink.proxy.rpc.enums.AppHMIType; -import com.smartdevicelink.proxy.rpc.enums.SdlDisconnectedReason; import com.smartdevicelink.transport.BaseTransportConfig; import com.smartdevicelink.transport.MultiplexTransportConfig; import com.smartdevicelink.transport.enums.TransportType; @@ -211,25 +210,13 @@ public class SdlManager extends BaseSdlManager { } } - @Override - void onProxyClosed(SdlDisconnectedReason reason) { - Log.i(TAG, "Proxy is closed."); - if (managerListener != null) { - managerListener.onDestroy(); - } - - if (reason == null || !reason.equals(SdlDisconnectedReason.LANGUAGE_CHANGE)) { - dispose(); - } - } - /** * Dispose SdlManager and clean its resources * <strong>Note: new instance of SdlManager should be created on every connection. SdlManager cannot be reused after getting disposed.</strong> */ @SuppressLint("NewApi") @Override - public void dispose() { + public synchronized void dispose() { if (this.permissionManager != null) { this.permissionManager.dispose(); } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/lifecycle/LifecycleManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lifecycle/LifecycleManager.java index 777e29f1b..38c8e4c26 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/managers/lifecycle/LifecycleManager.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/lifecycle/LifecycleManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Livio, Inc. + * Copyright (c) 2019-2020 Livio, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -54,38 +54,29 @@ import com.smartdevicelink.transport.USBTransportConfig; import com.smartdevicelink.transport.enums.TransportType; import com.smartdevicelink.util.DebugTool; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ScheduledExecutorService; /** * The lifecycle manager creates a central point for all SDL session logic to converge. It should only be used by * the library itself. Usage outside the library is not permitted and will not be protected for in the future. * - * @author Bilal Alsharifi. */ @RestrictTo(RestrictTo.Scope.LIBRARY) public class LifecycleManager extends BaseLifecycleManager { - private static final int RESPONSE_WAIT_TIME = 2000; - private ISdlServiceListener navServiceListener; - private boolean navServiceStartResponseReceived = false; - private boolean navServiceStartResponse = false; - private boolean navServiceEndResponseReceived = false; - private boolean navServiceEndResponse = false; - private boolean pcmServiceEndResponseReceived = false; - private boolean pcmServiceEndResponse = false; - private Context context; + private ISdlServiceListener videoServiceListener; + private boolean videoServiceStartResponseReceived = false; + private boolean videoServiceStartResponse = false; + private WeakReference<Context> contextWeakReference; public LifecycleManager(AppConfig appConfig, BaseTransportConfig config, LifecycleListener listener) { super(appConfig, config, listener); } @Override - void initializeProxy() { - super.initializeProxy(); + void initialize() { + super.initialize(); //Handle legacy USB connections if (_transportConfig != null && TransportType.USB.equals(_transportConfig.getTransportType())) { @@ -112,30 +103,38 @@ public class LifecycleManager extends BaseLifecycleManager { } } - private void cycleProxy(SdlDisconnectedReason disconnectedReason) { - cleanProxy(); - initializeProxy(); + @Override + void cycle(SdlDisconnectedReason disconnectedReason) { + clean(); + initialize(); if (!SdlDisconnectedReason.LEGACY_BLUETOOTH_MODE_ENABLED.equals(disconnectedReason) && !SdlDisconnectedReason.PRIMARY_TRANSPORT_CYCLE_REQUEST.equals(disconnectedReason)) { //We don't want to alert higher if we are just cycling for legacy bluetooth - onClose("Sdl Proxy Cycled", new SdlException("Sdl Proxy Cycled", SdlExceptionCause.SDL_PROXY_CYCLED)); + onClose("Sdl Proxy Cycled", new SdlException("Sdl Proxy Cycled", SdlExceptionCause.SDL_PROXY_CYCLED), disconnectedReason); } - try { - session.startSession(); - } catch (SdlException e) { - e.printStackTrace(); + if (session != null) { + try { + session.startSession(); + } catch (SdlException e) { + e.printStackTrace(); + } } } @RestrictTo(RestrictTo.Scope.LIBRARY) public void setContext(Context context) { - this.context = context; + this.contextWeakReference = new WeakReference<>(context); } @Override void setSdlSecurityStaticVars() { super.setSdlSecurityStaticVars(); + Context context = null; Service service = null; + + if(this.contextWeakReference != null){ + context = contextWeakReference.get(); + } if (context != null && context instanceof Service) { service = (Service) context; } @@ -144,11 +143,11 @@ public class LifecycleManager extends BaseLifecycleManager { } @Override - void onProtocolSessionStarted(SessionType sessionType) { - super.onProtocolSessionStarted(sessionType); + void onServiceStarted(SessionType sessionType) { + super.onServiceStarted(sessionType); if (sessionType.eq(SessionType.NAV)) { - navServiceStartResponseReceived = true; - navServiceStartResponse = true; + videoServiceStartResponseReceived = true; + videoServiceStartResponse = true; } } @@ -158,42 +157,18 @@ public class LifecycleManager extends BaseLifecycleManager { if (availablePrimary) { _transportConfig = transportConfig; Log.d(TAG, "notifying RPC session ended, but potential primary transport available"); - cycleProxy(SdlDisconnectedReason.PRIMARY_TRANSPORT_CYCLE_REQUEST); + cycle(SdlDisconnectedReason.PRIMARY_TRANSPORT_CYCLE_REQUEST); } else { - onClose(info, null); - } - } - - @Override - void onProtocolSessionStartedNACKed(SessionType sessionType) { - super.onProtocolSessionStartedNACKed(sessionType); - if (sessionType.eq(SessionType.NAV)) { - navServiceStartResponseReceived = true; - navServiceStartResponse = false; + onClose(info, null, null); } } @Override - void onProtocolSessionEnded(SessionType sessionType) { - super.onProtocolSessionEnded(sessionType); + void onStartServiceNACKed(SessionType sessionType) { + super.onStartServiceNACKed(sessionType); if (sessionType.eq(SessionType.NAV)) { - navServiceEndResponseReceived = true; - navServiceEndResponse = true; - } else if (sessionType.eq(SessionType.PCM)) { - pcmServiceEndResponseReceived = true; - pcmServiceEndResponse = true; - } - } - - @Override - void onProtocolSessionEndedNACKed(SessionType sessionType) { - super.onProtocolSessionEndedNACKed(sessionType); - if (sessionType.eq(SessionType.NAV)) { - navServiceEndResponseReceived = true; - navServiceEndResponse = false; - } else if (sessionType.eq(SessionType.PCM)) { - pcmServiceEndResponseReceived = true; - pcmServiceEndResponse = false; + videoServiceStartResponseReceived = true; + videoServiceStartResponse = false; } } @@ -230,72 +205,60 @@ public class LifecycleManager extends BaseLifecycleManager { * mode (i.e. without any negotiation) then an instance of VideoStreamingParams is * returned. If the service was not opened then null is returned. */ - private VideoStreamingParameters tryStartVideoStream(boolean isEncrypted, VideoStreamingParameters parameters) { + private void tryStartVideoStream(boolean isEncrypted, VideoStreamingParameters parameters) { if (session == null) { DebugTool.logWarning("SdlSession is not created yet."); - return null; + return; } if (getProtocolVersion() != null && getProtocolVersion().getMajor() >= 5 && !systemCapabilityManager.isCapabilitySupported(SystemCapabilityType.VIDEO_STREAMING)) { DebugTool.logWarning("Module doesn't support video streaming."); - return null; + return; } if (parameters == null) { DebugTool.logWarning("Video parameters were not supplied."); - return null; + return; } - if (!navServiceStartResponseReceived || !navServiceStartResponse //If we haven't started the service before - || (navServiceStartResponse && isEncrypted && !session.isServiceProtected(SessionType.NAV))) { //Or the service has been started but we'd like to start an encrypted one + + if (!videoServiceStartResponseReceived || !videoServiceStartResponse //If we haven't started the service before + || (videoServiceStartResponse && isEncrypted && !session.isServiceProtected(SessionType.NAV))) { //Or the service has been started but we'd like to start an encrypted one session.setDesiredVideoParams(parameters); - navServiceStartResponseReceived = false; - navServiceStartResponse = false; + videoServiceStartResponseReceived = false; + videoServiceStartResponse = false; + addVideoServiceListener(); session.startService(SessionType.NAV, session.getSessionId(), isEncrypted); - addNavListener(); - FutureTask<Void> fTask = new FutureTask<>(new CallableMethod(RESPONSE_WAIT_TIME)); - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.execute(fTask); - - //noinspection StatementWithEmptyBody - while (!navServiceStartResponseReceived && !fTask.isDone()) ; - scheduler.shutdown(); - } - if (navServiceStartResponse) { - if (getProtocolVersion() != null && getProtocolVersion().getMajor() < 5) { //Versions 1-4 do not support streaming parameter negotiations - session.setAcceptedVideoParams(parameters); - } - return session.getAcceptedVideoParams(); } - - return null; } - private void addNavListener() { + private void addVideoServiceListener() { // videos may be started and stopped. Only add this once - if (navServiceListener == null) { + if (videoServiceListener == null) { - navServiceListener = new ISdlServiceListener() { + videoServiceListener = new ISdlServiceListener() { @Override public void onServiceStarted(SdlSession session, SessionType type, boolean isEncrypted) { + videoServiceStartResponseReceived = true; + videoServiceStartResponse = true; } @Override public void onServiceEnded(SdlSession session, SessionType type) { // reset nav flags so nav can start upon the next transport connection - navServiceStartResponseReceived = false; - navServiceStartResponse = false; + videoServiceStartResponseReceived = false; + videoServiceStartResponse = false; } @Override public void onServiceError(SdlSession session, SessionType type, String reason) { // if there is an error reset the flags so that there is a chance to restart streaming - navServiceStartResponseReceived = false; - navServiceStartResponse = false; + videoServiceStartResponseReceived = false; + videoServiceStartResponse = false; } }; - session.addServiceListener(SessionType.NAV, navServiceListener); + session.addServiceListener(SessionType.NAV, videoServiceListener); } } @@ -305,29 +268,17 @@ public class LifecycleManager extends BaseLifecycleManager { * @return true if the video service is closed successfully, return false otherwise */ @Override - boolean endVideoStream() { + void endVideoStream() { if (session == null) { DebugTool.logWarning("SdlSession is not created yet."); - return false; + return; } if (!session.getIsConnected()) { DebugTool.logWarning("Connection is not available."); - return false; + return; } - navServiceEndResponseReceived = false; - navServiceEndResponse = false; session.stopVideoStream(); - - FutureTask<Void> fTask = new FutureTask<>(new CallableMethod(RESPONSE_WAIT_TIME)); - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.execute(fTask); - - //noinspection StatementWithEmptyBody - while (!navServiceEndResponseReceived && !fTask.isDone()) ; - scheduler.shutdown(); - - return navServiceEndResponse; } @Override @@ -349,46 +300,16 @@ public class LifecycleManager extends BaseLifecycleManager { * @return true if the audio service is closed successfully, return false otherwise */ @Override - boolean endAudioStream() { + void endAudioStream() { if (session == null) { DebugTool.logWarning("SdlSession is not created yet."); - return false; + return; } if (!session.getIsConnected()) { DebugTool.logWarning("Connection is not available."); - return false; + return; } - pcmServiceEndResponseReceived = false; - pcmServiceEndResponse = false; session.stopAudioStream(); - - FutureTask<Void> fTask = new FutureTask<>(new CallableMethod(RESPONSE_WAIT_TIME)); - ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - scheduler.execute(fTask); - - //noinspection StatementWithEmptyBody - while (!pcmServiceEndResponseReceived && !fTask.isDone()) ; - scheduler.shutdown(); - - return pcmServiceEndResponse; - } - - private class CallableMethod implements Callable<Void> { - private final long waitTime; - - public CallableMethod(int timeInMillis) { - this.waitTime = timeInMillis; - } - - @Override - public Void call() { - try { - Thread.sleep(waitTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return null; - } } } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/managers/screen/SubscribeButtonManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/managers/screen/SubscribeButtonManager.java new file mode 100644 index 000000000..f38cf79d0 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/managers/screen/SubscribeButtonManager.java @@ -0,0 +1,17 @@ +package com.smartdevicelink.managers.screen; + +import android.support.annotation.NonNull; +import com.smartdevicelink.proxy.interfaces.ISdl; + +/** + * <strong>SubscribeButtonManager</strong> <br> + * + * Note: This class must be accessed through the SdlManager. Do not instantiate it by itself. <br> + * + */ +class SubscribeButtonManager extends BaseSubscribeButtonManager { + + SubscribeButtonManager(@NonNull ISdl internalInterface) { + super(internalInterface); + } +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/protocol/SdlPacket.java b/android/sdl_android/src/main/java/com/smartdevicelink/protocol/SdlPacket.java new file mode 100644 index 000000000..7a6292505 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/protocol/SdlPacket.java @@ -0,0 +1,115 @@ +package com.smartdevicelink.protocol; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.smartdevicelink.transport.utl.TransportRecord; +import com.smartdevicelink.util.DebugTool; + +public class SdlPacket extends BaseSdlPacket implements Parcelable { + private static final int EXTRA_PARCEL_DATA_LENGTH = 24; + + public SdlPacket(int version, boolean encryption, int frameType, + int serviceType, int frameInfo, int sessionId, + int dataSize, int messageId, byte[] payload) { + super(version, encryption, frameType, serviceType, frameInfo, sessionId, dataSize, messageId, payload); + } + + public SdlPacket(int version, boolean encryption, int frameType, + int serviceType, int frameInfo, int sessionId, + int dataSize, int messageId, byte[] payload, int offset, int bytesToWrite) { + super(version, encryption, frameType, serviceType, frameInfo, sessionId, dataSize, messageId, payload, offset, bytesToWrite); + } + + protected SdlPacket() { + super(); + } + + protected SdlPacket(BaseSdlPacket packet) { + super(packet); + } + + /* *************************************************************************************************************************************************** + * *********************************************************** Parceable Overrides ***************************************************************** + *****************************************************************************************************************************************************/ + + + + //I think this is FIFO...right? + public SdlPacket(Parcel p) { + this.version = p.readInt(); + this.encryption = (p.readInt() == 0) ? false : true; + this.frameType = p.readInt(); + this.serviceType = p.readInt(); + this.frameInfo = p.readInt(); + this.sessionId = p.readInt(); + this.dataSize = p.readInt(); + this.messageId = p.readInt(); + if(p.readInt() == 1){ //We should have a payload attached + payload = new byte[dataSize]; + p.readByteArray(payload); + } + + this.priorityCoefficient = p.readInt(); + + if(p.dataAvail() > EXTRA_PARCEL_DATA_LENGTH) { //See note on constant for why not 0 + try { + messagingVersion = p.readInt(); + if (messagingVersion >= 2) { + if (p.readInt() == 1) { //We should have a transport type attached + this.transportRecord = (TransportRecord) p.readParcelable(TransportRecord.class.getClassLoader()); + } + } + }catch (RuntimeException e){ + DebugTool.logError("Error creating packet from parcel", e); + } + } + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + + dest.writeInt(version); + dest.writeInt(encryption? 1 : 0); + dest.writeInt(frameType); + dest.writeInt(serviceType); + dest.writeInt(frameInfo); + dest.writeInt(sessionId); + dest.writeInt(dataSize); + dest.writeInt(messageId); + dest.writeInt(payload!=null? 1 : 0); + if(payload!=null){ + dest.writeByteArray(payload); + } + dest.writeInt(priorityCoefficient); + + ///Additions after initial creation + if(messagingVersion > 1){ + dest.writeInt(messagingVersion); + + dest.writeInt(transportRecord!=null? 1 : 0); + if(transportRecord != null){ + dest.writeParcelable(transportRecord,0); + } + } + + } + + public static final Parcelable.Creator<SdlPacket> CREATOR = new Parcelable.Creator<SdlPacket>() { + public SdlPacket createFromParcel(Parcel in) { + return new SdlPacket(in); + } + + @Override + public SdlPacket[] newArray(int size) { + return new SdlPacket[size]; + } + + }; +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/proxy/SdlProxyBase.java b/android/sdl_android/src/main/java/com/smartdevicelink/proxy/SdlProxyBase.java index b0bda2d26..b42360019 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/proxy/SdlProxyBase.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/proxy/SdlProxyBase.java @@ -50,6 +50,7 @@ import android.view.InputDevice; import android.view.MotionEvent;
import android.view.Surface;
+import com.livio.taskmaster.Taskmaster;
import com.smartdevicelink.BuildConfig;
import com.smartdevicelink.Dispatcher.IDispatchingStrategy;
import com.smartdevicelink.Dispatcher.ProxyMessageDispatcher;
@@ -313,6 +314,7 @@ public abstract class SdlProxyBase<proxyListenerType extends IProxyListenerBase> private Set<String> encryptionRequiredRPCs = new HashSet<>();
private boolean rpcSecuredServiceStarted;
private ServiceEncryptionListener serviceEncryptionListener;
+ private Taskmaster taskmaster;
// Interface broker
private SdlInterfaceBroker _interfaceBroker = null;
@@ -537,7 +539,23 @@ public abstract class SdlProxyBase<proxyListenerType extends IProxyListenerBase> public void startRPCEncryption() {
SdlProxyBase.this.startProtectedRPCService();
}
+
+ @Override
+ public Taskmaster getTaskmaster() {
+ return SdlProxyBase.this.getTaskmaster();
+ }
};
+
+ Taskmaster getTaskmaster() {
+ if (taskmaster == null) {
+ Taskmaster.Builder builder = new Taskmaster.Builder();
+ builder.setThreadCount(2);
+ builder.shouldBeDaemon(false);
+ taskmaster = builder.build();
+ taskmaster.start();
+ }
+ return taskmaster;
+ }
private void notifyPutFileStreamError(Exception e, String info)
{
@@ -1832,6 +1850,9 @@ public abstract class SdlProxyBase<proxyListenerType extends IProxyListenerBase> public void dispose() throws SdlException {
SdlTrace.logProxyEvent("Application called dispose() method.", SDL_LIB_TRACE_KEY);
disposeInternal(SdlDisconnectedReason.APPLICATION_REQUESTED_DISCONNECT);
+ if (taskmaster != null) {
+ taskmaster.shutdown();
+ }
}
/**
* Terminates the App's Interface Registration, closes the transport connection, ends the protocol session, and frees any resources used by the proxy.
diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexBluetoothTransport.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexBluetoothTransport.java index cc7b030c7..ba34a3f58 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexBluetoothTransport.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/MultiplexBluetoothTransport.java @@ -35,6 +35,7 @@ import android.util.Log; import com.smartdevicelink.protocol.SdlPacket; import com.smartdevicelink.transport.enums.TransportType; import com.smartdevicelink.transport.utl.TransportRecord; +import com.smartdevicelink.util.DebugTool; import java.io.IOException; import java.io.InputStream; @@ -815,7 +816,9 @@ public class MultiplexBluetoothTransport extends MultiplexBaseTransport{ } } catch (IOException|NullPointerException e) { // NPE is ONLY to catch error on mmInStream Log.e(TAG, "Lost connection in the Connected Thread"); - e.printStackTrace(); + if(DebugTool.isDebugEnabled()){ + e.printStackTrace(); + } connectionLost(); break; } 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 ba1cc80a0..3baabc74a 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 @@ -53,6 +53,7 @@ import android.util.Log; import com.smartdevicelink.R; import com.smartdevicelink.transport.RouterServiceValidator.TrustedListCallback; import com.smartdevicelink.transport.enums.TransportType; +import com.smartdevicelink.transport.utl.SdlDeviceListener; import com.smartdevicelink.util.AndroidTools; import com.smartdevicelink.util.DebugTool; import com.smartdevicelink.util.SdlAppInfo; @@ -85,6 +86,8 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ private static final Object QUEUED_SERVICE_LOCK = new Object(); private static ComponentName queuedService = null; private static Thread.UncaughtExceptionHandler foregroundExceptionHandler = null; + private static final Object DEVICE_LISTENER_LOCK = new Object(); + private static SdlDeviceListener sdlDeviceListener; public int getRouterServiceVersion(){ return SdlRouterService.ROUTER_SERVICE_VERSION_NUMBER; @@ -190,9 +193,6 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ }); } - }else{ - //This was previously not hooked up, so let's leave it commented out - //onSdlDisabled(context); } return; }else if(intent.getBooleanExtra(TransportConstants.PING_ROUTER_SERVICE_EXTRA, false)){ @@ -223,8 +223,57 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ } } + /** + * This method will attempt to start the router service. + * @param context to be used to start the service and send broadcasts + * @param componentName the router service that should be started + * @param altTransportWake if the alt transport flag should be set. Only used in debug + * @param device the connected bluetooth device + */ + private static void startRouterService(Context context, ComponentName componentName, boolean altTransportWake, BluetoothDevice device, boolean confirmedDevice) { + if (componentName == null) { + return; + } + + Intent serviceIntent = new Intent(); + serviceIntent.setComponent(componentName); + + if (altTransportWake) { + serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT); + } + + if (device != null) { + serviceIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + } + + if (confirmedDevice) { + serviceIntent.putExtra(TransportConstants.CONFIRMED_SDL_DEVICE, confirmedDevice); + } + + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + context.startService(serviceIntent); + } else { + serviceIntent.putExtra(FOREGROUND_EXTRA, true); + DebugTool.logInfo("Attempting to startForegroundService - " + System.currentTimeMillis()); + setForegroundExceptionHandler(); //Prevent ANR in case the OS takes too long to start the service + context.startForegroundService(serviceIntent); + + } + //Make sure to send this out for old apps to close down + SdlRouterService.LocalRouterService self = SdlRouterService.getLocalRouterService(serviceIntent, serviceIntent.getComponent()); + Intent restart = new Intent(SdlRouterService.REGISTER_NEWER_SERVER_INSTANCE_ACTION); + restart.putExtra(LOCAL_ROUTER_SERVICE_EXTRA, self); + restart.putExtra(LOCAL_ROUTER_SERVICE_DID_START_OWN, true); + context.sendBroadcast(restart); + + } catch (SecurityException e) { + Log.e(TAG, "Security exception, process is bad"); + } + } + private boolean wakeUpRouterService(final Context context, final boolean ping, final boolean altTransportWake, final BluetoothDevice device){ - new ServiceFinder(context, context.getPackageName(), new ServiceFinder.ServiceFinderCallback() { + new ServiceFinder(context, context.getPackageName(), new ServiceFinder.ServiceFinderCallback() { @Override public void onComplete(Vector<ComponentName> routerServices) { runningBluetoothServicePackage = new Vector<ComponentName>(); @@ -232,43 +281,43 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ if (runningBluetoothServicePackage.isEmpty()) { //If there isn't a service running we should try to start one //We will try to sort the SDL enabled apps and find the one that's been installed the longest - Intent serviceIntent; - List<SdlAppInfo> sdlAppInfoList = AndroidTools.querySdlAppInfo(context, new SdlAppInfo.BestRouterComparator()); + final List<SdlAppInfo> sdlAppInfoList = AndroidTools.querySdlAppInfo(context, new SdlAppInfo.BestRouterComparator()); + synchronized (DEVICE_LISTENER_LOCK) { + final boolean sdlDeviceListenerEnabled = SdlDeviceListener.isFeatureSupported(sdlAppInfoList); + if (sdlDeviceListenerEnabled) { + String myPackage = context.getPackageName(); + String routerServicePackage = null; + if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty() && sdlAppInfoList.get(0).getRouterServiceComponentName() != null) { + routerServicePackage = sdlAppInfoList.get(0).getRouterServiceComponentName().getPackageName(); + } + DebugTool.logInfo(TAG + ": This app's package: " + myPackage); + DebugTool.logInfo(TAG + ": Router service app's package: " + routerServicePackage); + if (myPackage != null && myPackage.equalsIgnoreCase(routerServicePackage)) { + SdlDeviceListener sdlDeviceListener = getSdlDeviceListener(context, device); + if (!sdlDeviceListener.isRunning()) { + sdlDeviceListener.start(); + } + } else { + DebugTool.logInfo(TAG + ": Not the app to start the router service nor device listener"); + } + return; + } + } + if (sdlAppInfoList != null && !sdlAppInfoList.isEmpty()) { - serviceIntent = new Intent(); - serviceIntent.setComponent(sdlAppInfoList.get(0).getRouterServiceComponentName()); + startRouterService(context, sdlAppInfoList.get(0).getRouterServiceComponentName(), altTransportWake, device, false); } else{ Log.d(TAG, "No SDL Router Services found"); Log.d(TAG, "WARNING: This application has not specified its SdlRouterService correctly in the manifest. THIS WILL THROW AN EXCEPTION IN FUTURE RELEASES!!"); return; } - if (altTransportWake) { - serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT); - } - if(device != null){ - serviceIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); - } - try { - if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - context.startService(serviceIntent); - }else { - serviceIntent.putExtra(FOREGROUND_EXTRA, true); - DebugTool.logInfo("Attempting to startForegroundService - " + System.currentTimeMillis()); - setForegroundExceptionHandler(); //Prevent ANR in case the OS takes too long to start the service - context.startForegroundService(serviceIntent); + } else { //There are currently running services + if(DebugTool.isDebugEnabled()){ + for(ComponentName service : runningBluetoothServicePackage){ + DebugTool.logInfo("Currently running router service: " + service.getPackageName()); } - //Make sure to send this out for old apps to close down - SdlRouterService.LocalRouterService self = SdlRouterService.getLocalRouterService(serviceIntent, serviceIntent.getComponent()); - Intent restart = new Intent(SdlRouterService.REGISTER_NEWER_SERVER_INSTANCE_ACTION); - restart.putExtra(LOCAL_ROUTER_SERVICE_EXTRA, self); - restart.putExtra(LOCAL_ROUTER_SERVICE_DID_START_OWN, true); - context.sendBroadcast(restart); - - } catch (SecurityException e) { - Log.e(TAG, "Security exception, process is bad"); } - } else { if (altTransportWake) { wakeRouterServiceAltTransport(context); return; @@ -281,7 +330,7 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ } } }); - return true; + return true; } private void wakeRouterServiceAltTransport(Context context){ @@ -289,7 +338,11 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ serviceIntent.setAction(TransportConstants.BIND_REQUEST_TYPE_ALT_TRANSPORT); for (ComponentName compName : runningBluetoothServicePackage) { serviceIntent.setComponent(compName); - context.startService(serviceIntent); + try{ + context.startService(serviceIntent); + } catch (Exception e){ + DebugTool.logError("Can't start router service for alt transport"); + } } } @@ -329,10 +382,9 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ * Determines if an instance of the Router Service is currently running on the device.<p> * <b>Note:</b> This method no longer works on Android Oreo or newer * @param context A context to access Android system services through. - * @param pingService Set this to true if you want to make sure the service is up and listening to bluetooth * @return True if a SDL Router Service is currently running, false otherwise. */ - private static boolean isRouterServiceRunning(Context context, boolean pingService){ + private static boolean isRouterServiceRunning(Context context){ if(context == null){ Log.e(TAG, "Can't look for router service, context supplied was null"); return false; @@ -356,9 +408,6 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ //Log.d(TAG, "Found Service: "+ service.service.getClassName()); if ((service.service.getClassName()).toLowerCase(Locale.US).contains(SDL_ROUTER_SERVICE_CLASS_NAME) && AndroidTools.isServiceExported(context, service.service)) { runningBluetoothServicePackage.add(service.service); //Store which instance is running - if (pingService) { - pingRouterService(context, service.service.getPackageName(), service.service.getClassName()); - } } } return runningBluetoothServicePackage.size() > 0; @@ -366,7 +415,8 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ } /** - * Attempts to ping a running router service + * Attempts to ping a running router service. It does call startForegroundService so it is + * important to only call this as a ping if the service is already started. * @param context A context to access Android system services through. * @param packageName Package name for service to ping * @param className Class name for service to ping @@ -431,7 +481,7 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ } return; } - if((!lookForServices || isRouterServiceRunning(context,false)) && !runningBluetoothServicePackage.isEmpty()){ //So there is a service up, let's see if it's connected + if((!lookForServices || isRouterServiceRunning(context)) && !runningBluetoothServicePackage.isEmpty()){ //So there is a service up, let's see if it's connected final ConcurrentLinkedQueue<ComponentName> list = new ConcurrentLinkedQueue<ComponentName>(runningBluetoothServicePackage); final SdlRouterStatusProvider.ConnectedStatusCallback sdlBrCallback = new SdlRouterStatusProvider.ConnectedStatusCallback() { @@ -471,12 +521,13 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ }else{ Log.w(TAG, "Router service isn't running, returning false."); if(isBluetoothConnected()){ - Log.d(TAG, "Bluetooth is connected. Attempting to start Router Service"); + Log.d(TAG, "Bluetooth is connected. Attempting to ping Router Service"); Intent serviceIntent = new Intent(); serviceIntent.setAction(TransportConstants.START_ROUTER_SERVICE_ACTION); serviceIntent.putExtra(TransportConstants.PING_ROUTER_SERVICE_EXTRA, true); - AndroidTools.sendExplicitBroadcast(context,serviceIntent,null); + AndroidTools.sendExplicitBroadcast(context, serviceIntent,null); } + if(callback!=null){ callback.onConnectionStatusUpdate(false, null,context); } @@ -500,6 +551,49 @@ public abstract class SdlBroadcastReceiver extends BroadcastReceiver{ return false; } + + private static SdlDeviceListener getSdlDeviceListener(Context context, BluetoothDevice bluetoothDevice){ + + synchronized (DEVICE_LISTENER_LOCK){ + if (sdlDeviceListener == null){ + sdlDeviceListener = new SdlDeviceListener(context, bluetoothDevice, new SdlDeviceListener.Callback() { + @Override + public boolean onTransportConnected(Context context, BluetoothDevice bluetoothDevice) { + + synchronized (DEVICE_LISTENER_LOCK){ + sdlDeviceListener = null; + if(context != null) { + final List<SdlAppInfo> sdlAppInfoList = AndroidTools.querySdlAppInfo(context, new SdlAppInfo.BestRouterComparator()); + if(sdlAppInfoList != null && !sdlAppInfoList.isEmpty()) { + ComponentName routerService = sdlAppInfoList.get(0).getRouterServiceComponentName(); + startRouterService(context, routerService, false, bluetoothDevice, true); + } + } + } + + return false; + } + + @Override + public void onTransportDisconnected(BluetoothDevice bluetoothDevice) { + synchronized (DEVICE_LISTENER_LOCK){ + sdlDeviceListener = null; + } + } + + @Override + public void onTransportError(BluetoothDevice bluetoothDevice) { + synchronized (DEVICE_LISTENER_LOCK){ + sdlDeviceListener = null; + } + } + }); + } + } + + return sdlDeviceListener; + } + public static ComponentName consumeQueuedRouterService(){ synchronized(QUEUED_SERVICE_LOCK){ ComponentName retVal = queuedService; 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 4edc639e8..2e3317661 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 @@ -140,7 +140,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 = 12; + protected static final int ROUTER_SERVICE_VERSION_NUMBER = 13; private static final String ROUTER_SERVICE_PROCESS = "com.smartdevicelink.router"; @@ -1259,7 +1259,9 @@ public class SdlRouterService extends Service{ address = device.getAddress(); } } - int timeout = getNotificationTimeout(address); + boolean confirmedDevice = intent.getBooleanExtra(TransportConstants.CONFIRMED_SDL_DEVICE, false); + int timeout = getNotificationTimeout(address, confirmedDevice); + enterForeground("Waiting for connection...", timeout, false); resetForegroundTimeOut(timeout); } else { @@ -1383,9 +1385,9 @@ public class SdlRouterService extends Service{ * @return the amount of time for a timeout handler to remove the notification. */ @SuppressLint("MissingPermission") - private int getNotificationTimeout(String address){ + private int getNotificationTimeout(String address, boolean confirmedDevice){ if(address != null){ - if(hasSDLConnected(address)){ + if(confirmedDevice || hasSDLConnected(address)){ return FOREGROUND_TIMEOUT * 2; }else if(this.isFirstStatusCheck(address)) { // If this is the first time the service has ever connected to this device we want diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportBroker.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportBroker.java index 710e9d610..003556109 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportBroker.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportBroker.java @@ -86,8 +86,7 @@ public class TransportBroker { private Context currentContext = null; private final Object INIT_LOCK = new Object(); - - private TransportType queuedOnTransportConnect = null; + private final Object MESSAGE_SEND_LOCK = new Object(); Messenger routerServiceMessenger = null; final Messenger clientMessenger; @@ -126,59 +125,55 @@ public class TransportBroker { }; } - protected synchronized boolean sendMessageToRouterService(Message message) { + protected boolean sendMessageToRouterService(Message message) { return sendMessageToRouterService(message, 0); } - protected synchronized boolean sendMessageToRouterService(Message message, int retryCount) { - if (message == null) { - Log.w(TAG, "Attempted to send null message"); - return false; - } - //Log.i(TAG, "Attempting to send message type - " + message.what); - if (isBound && routerServiceMessenger != null) { - if (registeredWithRouterService - || message.what == TransportConstants.ROUTER_REGISTER_CLIENT) { //We can send a message if we are registered or are attempting to register - try { - routerServiceMessenger.send(message); - return true; - } catch (RemoteException e) { - e.printStackTrace(); - //Let's check to see if we should retry - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && e instanceof TransactionTooLargeException ) - || (retryCount < 5 && routerServiceMessenger.getBinder().isBinderAlive() && routerServiceMessenger.getBinder().pingBinder())) { //We probably just failed on a small transaction =\ - try { - Thread.sleep(100); - } catch (InterruptedException e1) { - e1.printStackTrace(); + protected boolean sendMessageToRouterService(Message message, int retryCount) { + synchronized (MESSAGE_SEND_LOCK) { + if (message == null) { + Log.w(TAG, "Attempted to send null message"); + return false; + } + //Log.i(TAG, "Attempting to send message type - " + message.what); + if (isBound && routerServiceMessenger != null && routerServiceMessenger.getBinder() != null && routerServiceMessenger.getBinder().isBinderAlive()) { + if (registeredWithRouterService + || message.what == TransportConstants.ROUTER_REGISTER_CLIENT) { //We can send a message if we are registered or are attempting to register + try { + routerServiceMessenger.send(message); + return true; + } catch (RemoteException e) { + e.printStackTrace(); + //Let's check to see if we should retry + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 && e instanceof TransactionTooLargeException) + || (retryCount < 5 && routerServiceMessenger.getBinder().isBinderAlive() && routerServiceMessenger.getBinder().pingBinder())) { //We probably just failed on a small transaction =\ + try { + Thread.sleep(100); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + return sendMessageToRouterService(message, retryCount++); + } else { + //DeadObject, time to kill our connection + Log.d(TAG, "Dead object while attempting to send packet"); + stop(); + onHardwareDisconnected(null, null); + return false; } - return sendMessageToRouterService(message, retryCount++); - } else { - //DeadObject, time to kill our connection - Log.d(TAG, "Dead object while attempting to send packet"); - routerServiceMessenger = null; - registeredWithRouterService = false; - unBindFromRouterService(); - isBound = false; + } catch (NullPointerException e) { + Log.d(TAG, "Null messenger while attempting to send packet"); // NPE, routerServiceMessenger is null + stop(); onHardwareDisconnected(null, null); return false; } - } catch (NullPointerException e) { - Log.d(TAG, "Null messenger while attempting to send packet"); // NPE, routerServiceMessenger is null - routerServiceMessenger = null; - registeredWithRouterService = false; - unBindFromRouterService(); - isBound = false; - onHardwareDisconnected(null, null); + } else { + Log.e(TAG, "Unable to send message to router service. Not registered."); return false; } } else { - Log.e(TAG, "Unable to send message to router service. Not registered."); + Log.e(TAG, "Unable to send message to router service. Not bound."); return false; } - } else { - Log.e(TAG, "Unable to send message to router service. Not bound."); - return false; } } @@ -416,7 +411,6 @@ public class TransportBroker { } //this.appId = appId.concat(timeStamp); this.appId = appId; - queuedOnTransportConnect = null; currentContext = context; //Log.d(TAG, "Registering our reply receiver: " + whereToReply); this.routerService = service; @@ -448,7 +442,6 @@ public class TransportBroker { synchronized (INIT_LOCK) { unregisterWithRouterService(); routerServiceMessenger = null; - queuedOnTransportConnect = null; unBindFromRouterService(); isBound = false; } @@ -463,13 +456,12 @@ public class TransportBroker { unregisterWithRouterService(); unBindFromRouterService(); routerServiceMessenger = null; - queuedOnTransportConnect = null; currentContext = null; } } - private synchronized void unBindFromRouterService() { + private void unBindFromRouterService() { try { getContext().unbindService(routerConnection); @@ -487,27 +479,17 @@ public class TransportBroker { public void onServiceUnregsiteredFromRouterService(int unregisterCode) { - queuedOnTransportConnect = null; } @Deprecated public void onHardwareDisconnected(TransportType type) { - routerServiceDisconnect(); + stop(); } public void onHardwareDisconnected(TransportRecord record, List<TransportRecord> connectedTransports) { } - private void routerServiceDisconnect() { - synchronized (INIT_LOCK) { - unBindFromRouterService(); - routerServiceMessenger = null; - routerConnection = null; - queuedOnTransportConnect = null; - } - } - /** * WILL NO LONGER BE CALLED * @@ -518,7 +500,6 @@ public class TransportBroker { public boolean onHardwareConnected(TransportType type) { synchronized (INIT_LOCK) { if (routerServiceMessenger == null) { - queuedOnTransportConnect = type; return false; } return true; @@ -528,7 +509,6 @@ public class TransportBroker { public boolean onHardwareConnected(List<TransportRecord> transports) { synchronized (INIT_LOCK) { if (routerServiceMessenger == null && transports != null && transports.size() > 0) { - queuedOnTransportConnect = transports.get(transports.size() - 1).getType(); return false; } return true; @@ -616,7 +596,7 @@ public class TransportBroker { ByteArrayMessageSpliter splitter = new ByteArrayMessageSpliter(appId, TransportConstants.ROUTER_SEND_PACKET, bytes, packet.getPrioirtyCoefficient()); splitter.setRouterServiceVersion(routerServiceVersion); splitter.setTransportRecord(packet.getTransportRecord()); - while (splitter.isActive()) { + while (splitter.isActive() && routerServiceMessenger != null) { sendMessageToRouterService(splitter.nextMessage()); } return splitter.close(); diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportManager.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportManager.java index 1a0951503..459a221f9 100644 --- a/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportManager.java +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/TransportManager.java @@ -49,6 +49,7 @@ import android.util.Log; import com.smartdevicelink.protocol.SdlPacket; import com.smartdevicelink.protocol.enums.ControlFrameTags; import com.smartdevicelink.transport.enums.TransportType; +import com.smartdevicelink.transport.utl.SdlDeviceListener; import com.smartdevicelink.transport.utl.TransportRecord; import com.smartdevicelink.util.DebugTool; @@ -100,7 +101,7 @@ public class TransportManager extends TransportManagerBase{ if (valid) { mConfig.service = name; transport = new TransportBrokerImpl(contextWeakReference.get(), mConfig.appId, mConfig.service); - DebugTool.logInfo("TransportManager start got called; transport=" + transport); + DebugTool.logInfo("TransportManager start was called; transport = " + transport); if(transport != null){ transport.start(); } @@ -302,6 +303,16 @@ public class TransportManager extends TransportManagerBase{ transportStatus.clear(); transportStatus.addAll(transports); } + //If a bluetooth device has connected, make sure to save the mac address in the case + //this app is asked to host the router service, the app knows to do so immediately on connection. + if(transports != null && transports.size() > 0) { + for (TransportRecord record : transports) { + if(record != null && TransportType.BLUETOOTH.equals(record.getType())) { + SdlDeviceListener.setSDLConnectedStatus(contextWeakReference.get(), record.getAddress(),true); + } + } + } + transportListener.onTransportConnected(transports); return true; } diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/SdlDeviceListener.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/SdlDeviceListener.java new file mode 100644 index 000000000..a396d186e --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/SdlDeviceListener.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2020 Livio, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following + * disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of the Livio Inc. nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +package com.smartdevicelink.transport.utl; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.NonNull; + +import com.smartdevicelink.transport.MultiplexBaseTransport; +import com.smartdevicelink.transport.MultiplexBluetoothTransport; +import com.smartdevicelink.transport.SdlRouterService; +import com.smartdevicelink.util.DebugTool; +import com.smartdevicelink.util.SdlAppInfo; + +import java.lang.ref.WeakReference; +import java.util.List; + + +public class SdlDeviceListener { + + private static final String TAG = "SdlListener"; + private static final int MIN_VERSION_REQUIRED = 13; + private static final String SDL_DEVICE_STATUS_SHARED_PREFS = "sdl.device.status"; + private static final Object LOCK = new Object(), RUNNING_LOCK = new Object(); + + private final WeakReference<Context> contextWeakReference; + private final Callback callback; + private final BluetoothDevice connectedDevice; + private MultiplexBluetoothTransport bluetoothTransport; + private TransportHandler bluetoothHandler; + private Handler timeoutHandler; + private Runnable timeoutRunner; + private boolean isRunning = false; + + + public SdlDeviceListener(Context context, BluetoothDevice device, Callback callback) { + this.contextWeakReference = new WeakReference<>(context); + this.connectedDevice = device; + this.callback = callback; + } + + /** + * This will start the SDL Device Listener with two paths. The first path will be a check + * against the supplied bluetooth device to see if it has already successfully connected as an + * SDL device. If it has, the supplied callback will be called immediately. If the device hasn't + * connected as an SDL device before, the SDL Device Listener will then open up an RFCOMM channel + * using the SDL UUID and await a potential connection. A timeout is used to ensure this only + * listens for a finite amount of time. If this is the first time the device has been seen, this + * will listen for 30 seconds, if it is not, this will listen for 15 seconds instead. + */ + public void start() { + if(connectedDevice == null) { + DebugTool.logInfo(TAG + ": No supplied bluetooth device"); + if(callback != null){ + callback.onTransportError(null); + } + return; + } + + if (hasSDLConnected(contextWeakReference.get(), connectedDevice.getAddress())) { + DebugTool.logInfo(TAG + ": Confirmed SDL device, should start router service"); + //This device has connected to SDL previously, it is ok to start the RS right now + callback.onTransportConnected(contextWeakReference.get(), connectedDevice); + return; + } + synchronized (RUNNING_LOCK) { + isRunning = true; + // set timeout = if first time seeing BT device, 30s, if not 15s + int timeout = isFirstStatusCheck(connectedDevice.getAddress()) ? 30000 : 15000; + //Set our preference as false for this device for now + setSDLConnectedStatus(contextWeakReference.get(), connectedDevice.getAddress(), false); + bluetoothHandler = new TransportHandler(this); + bluetoothTransport = new MultiplexBluetoothTransport(bluetoothHandler); + bluetoothTransport.start(); + timeoutRunner = new Runnable() { + @Override + public void run() { + if (bluetoothTransport != null) { + int state = bluetoothTransport.getState(); + if (state != MultiplexBluetoothTransport.STATE_CONNECTED) { + DebugTool.logInfo(TAG + ": No bluetooth connection made"); + bluetoothTransport.stop(); + } //else BT is connected; it will close itself through callbacks + } + } + }; + timeoutHandler = new Handler(Looper.getMainLooper()); + timeoutHandler.postDelayed(timeoutRunner, timeout); + } + } + + /** + * Check to see if this instance is in the middle of running or not + * + * @return if this is already in the process of running + */ + public boolean isRunning() { + synchronized (RUNNING_LOCK) { + return isRunning; + } + } + + private static class TransportHandler extends Handler { + + final WeakReference<SdlDeviceListener> provider; + + TransportHandler(SdlDeviceListener provider) { + this.provider = new WeakReference<>(provider); + } + + @Override + public void handleMessage(@NonNull Message msg) { + if (this.provider.get() == null) { + return; + } + SdlDeviceListener sdlListener = this.provider.get(); + switch (msg.what) { + + case SdlRouterService.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case MultiplexBaseTransport.STATE_CONNECTED: + sdlListener.setSDLConnectedStatus(sdlListener.contextWeakReference.get(), sdlListener.connectedDevice.getAddress(), true); + boolean keepConnectionOpen = sdlListener.callback.onTransportConnected(sdlListener.contextWeakReference.get(), sdlListener.connectedDevice); + if (!keepConnectionOpen) { + sdlListener.bluetoothTransport.stop(); + sdlListener.bluetoothTransport = null; + sdlListener.timeoutHandler.removeCallbacks(sdlListener.timeoutRunner); + } + break; + case MultiplexBaseTransport.STATE_NONE: + // We've just lost the connection + sdlListener.callback.onTransportDisconnected(sdlListener.connectedDevice); + break; + case MultiplexBaseTransport.STATE_ERROR: + sdlListener.callback.onTransportError(sdlListener.connectedDevice); + break; + } + break; + + case com.smartdevicelink.transport.SdlRouterService.MESSAGE_READ: + break; + } + } + } + + + /** + * Set the connection establishment status of the particular device + * + * @param address address of the device in question + * @param hasSDLConnected true if a connection has been established, false if not + */ + public static void setSDLConnectedStatus(Context context, String address, boolean hasSDLConnected) { + synchronized (LOCK) { + if (context != null) { + DebugTool.logInfo(TAG + ": Saving connected status - " + address + " : " + hasSDLConnected); + SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + if (preferences.contains(address) && hasSDLConnected == preferences.getBoolean(address, false)) { + //The same key/value exists in our shared preferences. No reason to write again. + return; + } + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(address, hasSDLConnected); + editor.commit(); + } + } + } + + /** + * Checks to see if a device address has connected to SDL before. + * + * @param address the mac address of the device in question + * @return if this is the first status check of this device + */ + private boolean isFirstStatusCheck(String address) { + synchronized (LOCK) { + Context context = contextWeakReference.get(); + if (context != null) { + SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + return !preferences.contains(address); + } + return false; + } + } + + /** + * Checks to see if a device address has connected to SDL before. + * + * @param address the mac address of the device in question + * @return if an SDL connection has ever been established with this device + */ + public static boolean hasSDLConnected(Context context, String address) { + synchronized (LOCK) { + if (context != null) { + SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE); + return preferences.contains(address) && preferences.getBoolean(address, false); + } + return false; + } + } + + /** + * This method will check the current device and list of SDL enabled apps to derive if the + * feature can be supported. Due to older libraries sending their intents to start the router + * service right at the bluetooth A2DP/HFS connections, this feature can't be used until all + * applications are updated to the point they include the feature. + * + * @param sdlAppInfoList current list of SDL enabled applications on the device + * @return if this feature is supported or not. If it is not, the caller should follow the + * previously used flow, ie start the router service. + */ + public static boolean isFeatureSupported(List<SdlAppInfo> sdlAppInfoList) { + + SdlAppInfo appInfo; + for (int i = sdlAppInfoList.size() - 1; i >= 0; i--) { + appInfo = sdlAppInfoList.get(i); + if (appInfo != null + && !appInfo.isCustomRouterService() + && appInfo.getRouterServiceVersion() < MIN_VERSION_REQUIRED) { + return false; + } + } + + return true; + } + + /** + * Callback for the SdlDeviceListener. It will return if the supplied device makes a bluetooth + * connection on the SDL UUID RFCOMM chanel or not. Most of the time the only callback that + * matters will be the onTransportConnected. + */ + public interface Callback { + /** + * @param bluetoothDevice the BT device that successfully connected to SDL's UUID + * @return if the RFCOMM connection should stay open. In most cases this should be false + */ + boolean onTransportConnected(Context context, BluetoothDevice bluetoothDevice); + + void onTransportDisconnected(BluetoothDevice bluetoothDevice); + + void onTransportError(BluetoothDevice bluetoothDevice); + } +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/TransportRecord.java b/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/TransportRecord.java new file mode 100644 index 000000000..bd31e746e --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/transport/utl/TransportRecord.java @@ -0,0 +1,56 @@ +package com.smartdevicelink.transport.utl; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.smartdevicelink.transport.enums.TransportType; + +public class TransportRecord extends BaseTransportRecord implements Parcelable { + + public TransportRecord(TransportType transportType, String address) { + super(transportType, address); + } + + public TransportRecord(Parcel p) { + if (p.readInt() == 1) { //We should have a transport type attached + String transportName = p.readString(); + if(transportName != null){ + this.type = TransportType.valueOf(transportName); + } + } + + if (p.readInt() == 1) { //We should have a transport address attached + address = p.readString(); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(type!=null? 1 : 0); + if(type != null){ + dest.writeString(type.name()); + } + + dest.writeInt(address !=null? 1 : 0); + if(address != null){ + dest.writeString(address); + } + } + + public static final Parcelable.Creator<TransportRecord> CREATOR = new Parcelable.Creator<TransportRecord>() { + public TransportRecord createFromParcel(Parcel in) { + return new TransportRecord(in); + } + + @Override + public TransportRecord[] newArray(int size) { + return new TransportRecord[size]; + } + + }; +} diff --git a/android/sdl_android/src/main/java/com/smartdevicelink/util/HttpRequestTask.java b/android/sdl_android/src/main/java/com/smartdevicelink/util/HttpRequestTask.java new file mode 100644 index 000000000..3eee85a05 --- /dev/null +++ b/android/sdl_android/src/main/java/com/smartdevicelink/util/HttpRequestTask.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2017 - 2019, SmartDeviceLink Consortium, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following + * disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of the SmartDeviceLink Consortium, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.smartdevicelink.util; + +import android.os.AsyncTask; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URL; + +public class HttpRequestTask extends AsyncTask<String, String, String> { + private static final String TAG = "Http Request Task"; + + public static final String REQUEST_TYPE_POST = "POST"; + public static final String REQUEST_TYPE_GET = "GET"; + public static final String REQUEST_TYPE_DELETE = "DELETE"; + + HttpRequestTaskCallback cb; + + /** + * @param hcb callback for when this task finishes + * <br><br><b> - When calling execute, params as followed: </b><br> + * 1. Url String<br> + * 2. Request type (Defined in this class) REQUEST_TYPE_POST, REQUEST_TYPE_GET, REQUEST_TYPE_DELETE<br> + * 3. (Optional) Data to be sent. <br> + * 4. (Optional) Content Type Default will be application/json<br> + * 5. (Optional) Accept Type default will be application/json + * + */ + public HttpRequestTask( HttpRequestTaskCallback hcb){ + this.cb = hcb; + } + + @Override + protected String doInBackground(String... params) { + int length = params.length; + String urlString = params[0]; + String request_type = params[1]; + + //Grab and set data to be written if included + String data; + if(length>2){ + data = params[2]; + }else{ + data = null; + } + + //Grab and set content type for the header if included + String contentType; + if(length>3){ + contentType = params[3]; + }else{ + contentType = "application/json"; + } + //Grab and set accept type for the header if included + String acceptType; + if(length>4){ + acceptType = params[4]; + }else{ + acceptType = "application/json"; + } + + if(urlString == null || request_type == null){ + Log.e(TAG, "Can't process request, param error"); + if(cb!=null){ + cb.httpFailure(-1); + cb = null; + } + return "Error"; + } + + HttpURLConnection urlConnection = null; + BufferedReader reader = null; + try { + URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setDoOutput(true); + urlConnection.setRequestMethod(request_type); + urlConnection.setRequestProperty("Content-Type", contentType); + urlConnection.setRequestProperty("Accept", acceptType); + //If we have data, we should write it out + if(data !=null){ + Writer writer = new BufferedWriter(new OutputStreamWriter(urlConnection.getOutputStream(), "UTF-8")); + writer.write(data); + writer.close(); + } + InputStream inputStream = urlConnection.getInputStream(); + + int responseCode = urlConnection.getResponseCode(); + if (responseCode == 200) { //Success + //input stream + StringBuffer buffer = new StringBuffer(); + if (inputStream == null) { + // Nothing to do. + if(cb!=null){ + cb.httpCallComplete(null); + cb = null; + } + return null; + } + reader = new BufferedReader(new InputStreamReader(inputStream)); + + String inputLine; + while ((inputLine = reader.readLine()) != null) + buffer.append(inputLine).append("\n"); + if (buffer.length() == 0) { + // Stream was empty. No point in parsing. + if(cb!=null){ + cb.httpCallComplete(null); + cb = null; + } + return null; + } + String response = null; + + response = buffer.toString(); + //send to post execute + if(cb!=null){ + cb.httpCallComplete(response); + cb = null; + } + return response; + }else{ + if(cb!=null){ + cb.httpFailure(responseCode); + cb = null; + } + Log.e(TAG, "Failed to download file - " + responseCode); + return null; + } + + + } catch (IOException e) { + e.printStackTrace(); + } catch (NullPointerException e){ // Only to catch error in urlConnection.getOutputStream() - when servers are down + e.printStackTrace(); + urlConnection = null; + } + finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + if (reader != null) { + try { + reader.close(); + } catch (final IOException e) { + Log.e(TAG, "Error closing stream", e); + } + } + if(cb!=null){ + cb.httpFailure(-1); + } + } + return null; + } + + /** + * Callback interface for HTTP requests. + * @author Joey Grover + * + */ + public interface HttpRequestTaskCallback{ + /** + * Called when HTTP request is successfully completed. + * @param response The response to the HTTP request. + */ + public abstract void httpCallComplete(String response); + /** + * Called when HTTP request failed. + * @param statusCode The HTTP failure code. + */ + public abstract void httpFailure(int statusCode); + } + +} |