Skip to content

Commit 1aa070b

Browse files
committed
Add SdlDeviceListener
1 parent dfcdd32 commit 1aa070b

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* Copyright (c) 2020 Livio, Inc.
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* Redistributions of source code must retain the above copyright notice, this
9+
* list of conditions and the following disclaimer.
10+
*
11+
* Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following
13+
* disclaimer in the documentation and/or other materials provided with the
14+
* distribution.
15+
*
16+
* Neither the name of the Livio Inc. nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30+
* POSSIBILITY OF SUCH DAMAGE.
31+
*/
32+
33+
34+
package com.smartdevicelink.transport.utl;
35+
36+
import android.bluetooth.BluetoothDevice;
37+
import android.content.Context;
38+
import android.content.SharedPreferences;
39+
import android.os.Handler;
40+
import android.os.Looper;
41+
import android.os.Message;
42+
import android.support.annotation.NonNull;
43+
44+
import com.smartdevicelink.transport.MultiplexBaseTransport;
45+
import com.smartdevicelink.transport.MultiplexBluetoothTransport;
46+
import com.smartdevicelink.transport.SdlRouterService;
47+
import com.smartdevicelink.util.DebugTool;
48+
import com.smartdevicelink.util.SdlAppInfo;
49+
50+
import java.lang.ref.WeakReference;
51+
import java.util.List;
52+
53+
54+
public class SdlDeviceListener {
55+
56+
private static final String TAG = "SdlListener";
57+
private static final int MIN_VERSION_REQUIRED = 13;
58+
private static final String SDL_DEVICE_STATUS_SHARED_PREFS = "sdl.device.status";
59+
private static final Object LOCK = new Object(), RUNNING_LOCK = new Object();
60+
61+
private final WeakReference<Context> contextWeakReference;
62+
private final Callback callback;
63+
private final BluetoothDevice connectedDevice;
64+
private MultiplexBluetoothTransport bluetoothTransport;
65+
private TransportHandler bluetoothHandler;
66+
private Handler timeoutHandler;
67+
private Runnable timeoutRunner;
68+
private boolean isRunning = false;
69+
70+
71+
public SdlDeviceListener(Context context, BluetoothDevice device, Callback callback){
72+
this.contextWeakReference = new WeakReference<>(context);
73+
this.connectedDevice = device;
74+
this.callback = callback;
75+
}
76+
77+
/**
78+
* This will start the SDL Device Listener with two paths. The first path will be a check
79+
* against the supplied bluetooth device to see if it has already successfully connected as an
80+
* SDL device. If it has, the supplied callback will be called immediately. If the device hasn't
81+
* connected as an SDL device before, the SDL Device Listener will then open up an RFCOMM channel
82+
* using the SDL UUID and await a potential connection. A timeout is used to ensure this only
83+
* listens for a finite amount of time. If this is the first time the device has been seen, this
84+
* will listen for 30 seconds, if it is not, this will listen for 15 seconds instead.
85+
*/
86+
public void start(){
87+
if(hasSDLConnected(contextWeakReference.get(), connectedDevice.getAddress())){
88+
DebugTool.logInfo(TAG + ": Confirmed SDL device, should start router service");
89+
//This device has connected to SDL previously, it is ok to start the RS right now
90+
callback.onTransportConnected(contextWeakReference.get(), connectedDevice);
91+
return;
92+
}
93+
synchronized (RUNNING_LOCK) {
94+
isRunning = true;
95+
// set timeout = if first time seeing BT device, 30s, if not 15s
96+
int timeout = isFirstStatusCheck(connectedDevice.getAddress()) ? 30000 : 15000;
97+
//Set our preference as false for this device for now
98+
setSDLConnectedStatus(contextWeakReference.get(), connectedDevice.getAddress(), false);
99+
bluetoothHandler = new TransportHandler(this);
100+
bluetoothTransport = new MultiplexBluetoothTransport(bluetoothHandler);
101+
bluetoothTransport.start();
102+
timeoutRunner = new Runnable() {
103+
@Override
104+
public void run() {
105+
if (bluetoothTransport != null) {
106+
int state = bluetoothTransport.getState();
107+
if (state != MultiplexBluetoothTransport.STATE_CONNECTED) {
108+
DebugTool.logInfo(TAG + ": No bluetooth connection made");
109+
bluetoothTransport.stop();
110+
} //else BT is connected; it will close itself through callbacks
111+
}
112+
}
113+
};
114+
timeoutHandler = new Handler(Looper.getMainLooper());
115+
timeoutHandler.postDelayed(timeoutRunner, timeout);
116+
}
117+
}
118+
119+
/**
120+
* Check to see if this instance is in the middle of running or not
121+
* @return if this is already in the process of running
122+
*/
123+
public boolean isRunning(){
124+
synchronized (RUNNING_LOCK){
125+
return isRunning;
126+
}
127+
}
128+
129+
private static class TransportHandler extends Handler {
130+
131+
final WeakReference<SdlDeviceListener> provider;
132+
133+
TransportHandler(SdlDeviceListener provider){
134+
this.provider = new WeakReference<>(provider);
135+
}
136+
137+
@Override
138+
public void handleMessage(@NonNull Message msg) {
139+
if(this.provider.get() == null){
140+
return;
141+
}
142+
SdlDeviceListener sdlListener = this.provider.get();
143+
switch (msg.what) {
144+
145+
case SdlRouterService.MESSAGE_STATE_CHANGE:
146+
switch (msg.arg1) {
147+
case MultiplexBaseTransport.STATE_CONNECTED:
148+
sdlListener.setSDLConnectedStatus(sdlListener.contextWeakReference.get(), sdlListener.connectedDevice.getAddress(),true);
149+
boolean keepConnectionOpen = sdlListener.callback.onTransportConnected(sdlListener.contextWeakReference.get(), sdlListener.connectedDevice);
150+
if( !keepConnectionOpen ) {
151+
sdlListener.bluetoothTransport.stop();
152+
sdlListener.bluetoothTransport = null;
153+
sdlListener.timeoutHandler.removeCallbacks(sdlListener.timeoutRunner);
154+
}
155+
break;
156+
case MultiplexBaseTransport.STATE_NONE:
157+
// We've just lost the connection
158+
sdlListener.callback.onTransportDisconnected(sdlListener.connectedDevice);
159+
break;
160+
case MultiplexBaseTransport.STATE_ERROR:
161+
sdlListener.callback.onTransportError(sdlListener.connectedDevice);
162+
break;
163+
}
164+
break;
165+
166+
case com.smartdevicelink.transport.SdlRouterService.MESSAGE_READ:
167+
break;
168+
}
169+
}
170+
}
171+
172+
173+
/**
174+
* Set the connection establishment status of the particular device
175+
* @param address address of the device in quesiton
176+
* @param hasSDLConnected true if a connection has been established, false if not
177+
*/
178+
public static void setSDLConnectedStatus(Context context, String address, boolean hasSDLConnected){
179+
synchronized (LOCK) {
180+
if (context != null) {
181+
DebugTool.logInfo( TAG + ": Saving connected status - " + address + " : " + hasSDLConnected);
182+
SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
183+
if(preferences.contains(address) && hasSDLConnected == preferences.getBoolean(address, false)) {
184+
//The same key/value exists in our shared preferences. No reason to write again.
185+
return;
186+
}
187+
SharedPreferences.Editor editor = preferences.edit();
188+
editor.putBoolean(address, hasSDLConnected);
189+
editor.commit();
190+
}
191+
}
192+
}
193+
194+
/**
195+
* Checks to see if a device address has connected to SDL before.
196+
* @param address the mac address of the device in quesiton
197+
* @return if this is the first status check of this device
198+
*/
199+
private boolean isFirstStatusCheck(String address){
200+
synchronized (LOCK) {
201+
Context context = contextWeakReference.get();
202+
if (context != null) {
203+
SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
204+
return !preferences.contains(address);
205+
}
206+
return false;
207+
}
208+
}
209+
/**
210+
* Checks to see if a device address has connected to SDL before.
211+
* @param address the mac address of the device in quesiton
212+
* @return if an SDL connection has ever been established with this device
213+
*/
214+
public static boolean hasSDLConnected(Context context, String address){
215+
synchronized (LOCK) {
216+
if (context != null) {
217+
SharedPreferences preferences = context.getSharedPreferences(SDL_DEVICE_STATUS_SHARED_PREFS, Context.MODE_PRIVATE);
218+
return preferences.contains(address) && preferences.getBoolean(address, false);
219+
}
220+
return false;
221+
}
222+
}
223+
224+
/**
225+
* This method will check the current device and list of SDL enabled apps to derive if the
226+
* feature can be supported. Due to older libraries sending their intents to start the router
227+
* service right at the bluetooth A2DP/HFS connections, this feature can't be used until all
228+
* applications are updated to the point they include the feature.
229+
* @param sdlAppInfoList current list of SDL enabled applications on the device
230+
* @return if this feature is supported or not. If it is not, the caller should follow the
231+
* previously used flow, ie start the router service.
232+
*/
233+
public static boolean isFeatureSupported(List<SdlAppInfo> sdlAppInfoList){
234+
235+
SdlAppInfo appInfo;
236+
for(int i = sdlAppInfoList.size() - 1; i >=0; i--){
237+
appInfo = sdlAppInfoList.get(i);
238+
if(appInfo != null
239+
&& !appInfo.isCustomRouterService()
240+
&& appInfo.getRouterServiceVersion() < MIN_VERSION_REQUIRED ){
241+
return false;
242+
}
243+
}
244+
245+
return true;
246+
}
247+
248+
/**
249+
* Callback for the SdlDeviceListener. It will return if the supplied device makes a bluetooth
250+
* connection on the SDL UUID RFCOMM chanel or not. Most of the time the only callback that
251+
* matters will be the onTransportConnected.
252+
*/
253+
public interface Callback{
254+
/**
255+
*
256+
* @param bluetoothDevice the BT device that successfully connected to SDL's UUID
257+
* @return if the RFCOMM connection should stay open. In most cases this should be false
258+
*/
259+
boolean onTransportConnected(Context context, BluetoothDevice bluetoothDevice);
260+
void onTransportDisconnected(BluetoothDevice bluetoothDevice);
261+
void onTransportError(BluetoothDevice bluetoothDevice);
262+
}
263+
}

0 commit comments

Comments
 (0)