├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── vcs.xml ├── modules.xml ├── runConfigurations.xml ├── gradle.xml ├── compiler.xml └── misc.xml ├── discovery ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── joshblour │ │ │ └── discovery │ │ │ ├── EasedValue.java │ │ │ ├── BLEUser.java │ │ │ ├── MultiScanner.java │ │ │ ├── GattManager.java │ │ │ ├── AdvertiserService.java │ │ │ └── Discovery.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── joshblour │ │ └── discovery │ │ └── ApplicationTest.java ├── proguard-rules.pro ├── build.gradle └── discovery.iml ├── settings.gradle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── Discovery.iml ├── gradlew.bat ├── README.md └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | Discovery -------------------------------------------------------------------------------- /discovery/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':discovery' 2 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /discovery/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Discovery 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yonahforst/discovery-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 18 12:52:29 CET 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /discovery/src/androidTest/java/com/joshblour/discovery/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /discovery/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Yonah/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /discovery/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 8 | 10 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /Discovery.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/EasedValue.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | /** 4 | * Created by Yonah on 16/10/15. 5 | */ 6 | public class EasedValue { 7 | private Float mValue; 8 | private Float mVelocity; 9 | private Float mTargetValue; 10 | private Float mCurrentValue; 11 | 12 | public EasedValue() { 13 | this.mVelocity = 0.0f; 14 | this.mTargetValue = 0.0f; 15 | this.mCurrentValue = 0.0f; 16 | } 17 | 18 | public void setValue(Float value) { 19 | this.mTargetValue = value; 20 | } 21 | 22 | public Float getValue() { 23 | return mCurrentValue; 24 | } 25 | 26 | public void update() { 27 | // determine speed at which the ease will happen 28 | // this is based on difference between target and current value 29 | mVelocity += (mTargetValue - mCurrentValue) * 0.01f; 30 | mVelocity *= 0.7f; 31 | 32 | // ease the current value 33 | mCurrentValue += mVelocity; 34 | 35 | // limit how small the ease can get 36 | if(Math.abs(mTargetValue - mCurrentValue) < 0.001f){ 37 | mCurrentValue = mTargetValue; 38 | mVelocity = 0.0f; 39 | } 40 | 41 | // keep above zero 42 | mCurrentValue = Math.max(0.0f, mCurrentValue); 43 | 44 | } 45 | 46 | public void reset() { 47 | mCurrentValue = mTargetValue; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /discovery/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | ext { 4 | bintrayRepo = 'maven' 5 | bintrayName = 'discovery' 6 | 7 | publishedGroupId = 'com.joshblour.discovery' 8 | libraryName = 'discovery' 9 | artifact = 'discovery' 10 | 11 | libraryDescription = 'Android port of https://github.com/omergul123/Discovery' 12 | 13 | siteUrl = 'https://github.com/joshblour/discovery-android' 14 | gitUrl = 'https://github.com/joshblour/discovery-android.git' 15 | 16 | libraryVersion = '0.0.6' 17 | 18 | developerId = 'joshblour' 19 | developerName = 'Yonah Forst' 20 | developerEmail = 'yonaforst@hotmail.com' 21 | 22 | licenseName = 'The Apache Software License, Version 2.0' 23 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | allLicenses = ["Apache-2.0"] 25 | } 26 | 27 | android { 28 | compileSdkVersion 23 29 | buildToolsVersion "23.0.1" 30 | 31 | defaultConfig { 32 | minSdkVersion 18 33 | targetSdkVersion 23 34 | versionCode 1 35 | versionName "1.0" 36 | } 37 | buildTypes { 38 | release { 39 | minifyEnabled false 40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | compile fileTree(dir: 'libs', include: ['*.jar']) 47 | } 48 | 49 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' 50 | apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' 51 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/BLEUser.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.bluetooth.BluetoothDevice; 4 | 5 | /** 6 | * Created by Yonah on 15/10/15. 7 | */ 8 | public class BLEUser { 9 | private BluetoothDevice mDevice; 10 | private String mDeviceAddress; 11 | private String mUsername; 12 | private Boolean mIdentified; 13 | private Boolean mIsMyService; 14 | private Integer mRssi; 15 | private Integer mProximity; 16 | private long mUpdateTime; 17 | private EasedValue mEasedProximity; 18 | 19 | public BLEUser(final BluetoothDevice device) { 20 | this.mDevice = device; 21 | this.mDeviceAddress = device.getAddress(); 22 | this.mRssi = 0; 23 | this.mEasedProximity = new EasedValue(); 24 | } 25 | 26 | public Integer convertRSSItoProximity(Integer rssi) { 27 | // eased value doesn't support negative values 28 | this.mEasedProximity.setValue(Math.abs(rssi) * 1.0f);//convert to float 29 | this.mEasedProximity.update(); 30 | Integer proximity = Math.round(this.mEasedProximity.getValue() * -1.0f); 31 | return proximity; 32 | } 33 | public String getDeviceAddress() { 34 | return mDeviceAddress; 35 | } 36 | 37 | public void setDeviceAddress(String deviceAddress) { 38 | this.mDeviceAddress = deviceAddress; 39 | } 40 | 41 | public String getUsername() { 42 | return mUsername; 43 | } 44 | 45 | public void setUsername(String mUsername) { 46 | this.mUsername = mUsername; 47 | } 48 | 49 | public Boolean isIdentified() { 50 | return mIdentified; 51 | } 52 | 53 | public void setIdentified(Boolean mIdentified) { 54 | this.mIdentified = mIdentified; 55 | } 56 | 57 | public Integer getRssi() { 58 | return mRssi; 59 | } 60 | 61 | public void setRssi(int mRssi) { 62 | this.mRssi = mRssi; 63 | this.setProximity(convertRSSItoProximity(mRssi)); 64 | } 65 | 66 | public Integer getProximity() { 67 | return mProximity; 68 | } 69 | 70 | public void setProximity(Integer mProximity) { 71 | this.mProximity = mProximity; 72 | } 73 | 74 | public long getUpdateTime() { 75 | return mUpdateTime; 76 | } 77 | 78 | public void setUpdateTime(long mUpdateTime) { 79 | this.mUpdateTime = mUpdateTime; 80 | } 81 | 82 | // we need this because we are not filtering by serviceUUID. 83 | // with this flag, we can store them as identifed but not our service, so that we don't need to always reconnect. 84 | public void setIsMyService(Boolean isMyService) { 85 | this.mIsMyService = isMyService; 86 | } 87 | 88 | public Boolean isMyService() { 89 | return mIsMyService; 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discovery 2 | Android port of https://github.com/omergul123/Discovery 3 | Discover nearby devices using BLE. 4 | 5 | ##What 6 | Discovery is a very simple but useful library for discovering nearby devices with BLE(Bluetooth Low Energy) and for exchanging a value (kind of ID or username determined by you on the running app on peer device) regardless of whether the app on peer device works at foreground or background state. 7 | 8 | ##Why 9 | I ported [Discovery](https://github.com/omergul123/Discovery) from iOS to Android because I needed a beacon technology that worked cross-platform. I chose Discovery for its straightforward implementation and reliability in the background. You can read more about why Discovery was necessary for iOS here: https://github.com/omergul123/Discovery#the-concept-the-problem-and-why-we-need-discovery 10 | 11 | ##Install 12 | 13 | Add to your bundle.gradle (module) dependencies 14 | 15 | ````java 16 | dependencies { 17 | //... 18 | compile "com.joshblour.discovery:discovery:0.0.3" 19 | //... 20 | } 21 | ```` 22 | ##Example usage 23 | 24 | ````java 25 | public class MainActivity extends Activity implements Discovery.DiscoveryCallback { 26 | public static final ParcelUuid uuidStr = ParcelUuid.fromString("B9407F30-F5F8-466E-AFF9-25556B57FE99"); 27 | public static final String username = "myUsername"; 28 | 29 | private Discovery mDiscovery; 30 | 31 | //...boilerplate onCreate code... 32 | 33 | public void startDiscovery() { 34 | mDiscovery = new Discovery(getApplicationContext(), uuidStr, username, this); 35 | } 36 | 37 | @Override 38 | public void didUpdateUsers(ArrayList users, Boolean usersChanged) { 39 | //reload your table from users 40 | } 41 | } 42 | ```` 43 | 44 | ##API 45 | `public Discovery(Context context, ParcelUuid uuid, String username, DIStartOptions startOptions, DiscoveryCallback discoveryCallback )` 46 | - `uuid`: A UUID that identifies your application. If you want to discover iOS devices, both libraries need to be using the same UUID 47 | - `username`: The username to include in the broadcast. Note: in this implementation, we can only broadcast a max of 7 characters. You can use longer usernames but nearby devices will need to connect in order to read it. (the username is then cached so this only happens once per discovery) 48 | - `startOptions`: 49 | - DIStartAdvertisingAndDetecting 50 | - DIStartAdvertisingOnly 51 | - DIStartDetectingOnly 52 | - DIStartNone 53 | - `discoveryCallback`: implements `didUpdateUsers(ArrayList users, Boolean usersChanged)` 54 | 55 | `public Discovery(Context context, ParcelUuid uuid, String username, DiscoveryCallback discoveryCallback)` - same as above but starts using `DIStartAdvertisingAndDetecting` 56 | 57 | `public void setPaused(Boolean paused)` - pauses advertising and detection 58 | 59 | `public void setShouldDiscover(Boolean shouldDiscover)` - starts and stops discovery only 60 | 61 | `public void setShouldAdvertise(Boolean shouldAdvertise)` - starts and stops advertising only 62 | 63 | `public void setUserTimeoutInterval(Integer mUserTimeoutInterval)` - in seconds, default is 5. After not seeing a user for x seconds, we remove him from the users list in our callback. 64 | 65 | 66 | *The following two methods are specific to the Android version, since the Android docs advise against continuous scanning. Instead, we cycle scanning on and off. This also allows us to modify the scan behaviour when the app moves to the background.* 67 | 68 | `public void setScanForSeconds(Integer scanForSeconds)` - in seconds, default is 5. This parameter specifies the duration of the ON part of the scan cycle. 69 | 70 | `public void setWaitForSeconds(Integer waitForSeconds)` - in seconds default is 5. This parameter specifies the duration of the OFF part of the scan cycle. 71 | 72 | ##Problems 73 | 74 | ~~Can't detect iOS devices while they are in the background. This is because we are using a ScanFilter for the ServiceUUID to save battery. When an iOS app goes into the background, Apple moved all serviceUUIDs into a special 'overflow area' and our filter no longer picks them up~~ (disabled scan filters because I needed to detect iOS devices when they are in the background) 75 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /discovery/discovery.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/MultiScanner.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothDevice; 6 | import android.bluetooth.le.ScanCallback; 7 | import android.bluetooth.le.ScanFilter; 8 | import android.bluetooth.le.ScanResult; 9 | import android.bluetooth.le.ScanSettings; 10 | import android.os.Build; 11 | import android.os.ParcelUuid; 12 | import android.util.Log; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | /** 19 | * Created by Yonah on 18/03/16. 20 | * 21 | * The purpose of this class is to abstract the two types of scanning that we support 22 | * Pre-Lollipop and Lollipop+ 23 | * 24 | * You specify which type of scanning you want when you initiate the class. 25 | * We provide a consistent callback interface for both types of scanning 26 | * 27 | * You can optionally filter by a serviceUUID. 28 | * Note: specifying the service uuid will prevent discovery of ios apps in the background 29 | * since all serviceUUIDs get moved to an overflow area when the app goes to background, 30 | * and to our scanner, they disappear. To discovery ios backgrounded apps, you need to 31 | * start an unfiltered scan and then filter the results yourself. 32 | * 33 | */ 34 | public class MultiScanner { 35 | private final static String TAG = "discovery-MultiScanner"; 36 | 37 | public interface MultiScannerCallback { 38 | void onScanResult(BluetoothDevice device, int rssi, byte[] scanRecord); 39 | void onScanFailed(int errorCode); 40 | } 41 | 42 | BluetoothAdapter mAdapter; 43 | ParcelUuid mServiceUUID; 44 | MultiScannerCallback mScanCallback; 45 | boolean mUsePreLScanner; 46 | PostLScanCallback mPostLScanCallback; 47 | BluetoothAdapter.LeScanCallback mPreLScanCallback; 48 | 49 | public MultiScanner(BluetoothAdapter adapter, ParcelUuid uuid, MultiScannerCallback callback) { 50 | this(adapter, uuid, callback, false); 51 | } 52 | 53 | /* 54 | @param adapter - the system bluetooth adapter 55 | @param uuid - the uuid of the service we are searching for - leave this blank if you dont want to filter 56 | @param callback - a callback when the users are updated. 57 | @param usePreLScanner - an option to use the deprecated LEScanner, since it sometimes functions better 58 | */ 59 | 60 | public MultiScanner(BluetoothAdapter adapter, ParcelUuid uuid, MultiScannerCallback callback, boolean usePreLScanner) { 61 | mAdapter = adapter; 62 | mServiceUUID = uuid; 63 | mScanCallback = callback; 64 | mUsePreLScanner = usePreLScanner; 65 | 66 | } 67 | 68 | public void start() { 69 | if (!mAdapter.isEnabled()) 70 | return; 71 | 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !mUsePreLScanner) { 73 | // we only listen to the service that belongs to our uuid 74 | // this is important for performance and battery consumption 75 | ScanSettings settings = new ScanSettings.Builder() 76 | .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) 77 | .build(); 78 | 79 | List filters = new ArrayList<>(); 80 | 81 | if (mServiceUUID != null) { 82 | // filtering by the ServiceUUID prevents us from discovering iOS devices broadcasting in the background 83 | // since their serviceUUID gets moved into the 'overflow area'. 84 | // we need to find a way to filter also by the manufacturerData to find only devices with our UUID. 85 | // more here: https://forums.developer.apple.com/thread/11705 86 | ScanFilter serviceUUIDFilter = new ScanFilter.Builder().setServiceUuid(mServiceUUID).build(); 87 | filters.add(serviceUUIDFilter); 88 | 89 | 90 | // // An alternative is to filter by manufacturer data, 91 | // // but I still haven't figured out how to show only apple devices 92 | // 93 | // // Empty data 94 | // byte[] manData = new byte[]{1,0,0,0,0,0,0,0,0,0,0,0,0,8,64,0,0}; 95 | // // Data Mask 96 | // byte[] mask = new byte[]{1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1}; 97 | // // Add data array to filters 98 | // ScanFilter manDataFilter = new ScanFilter.Builder().setManufacturerData(76, manData, mask).build(); 99 | // filters.add(manDataFilter); 100 | } 101 | 102 | 103 | if (mPostLScanCallback == null) 104 | mPostLScanCallback = new PostLScanCallback(); 105 | mAdapter.getBluetoothLeScanner().startScan(filters, settings, mPostLScanCallback ); 106 | } else { 107 | if (mPreLScanCallback == null) 108 | mPreLScanCallback = new PreLScanCallback(); 109 | 110 | if (mServiceUUID != null) { 111 | UUID[] serviceUUIDs = {mServiceUUID.getUuid()}; 112 | mAdapter.startLeScan(serviceUUIDs, mPreLScanCallback); 113 | } else { 114 | mAdapter.startLeScan(mPreLScanCallback); 115 | } 116 | } 117 | } 118 | 119 | public void stop() { 120 | if (!mAdapter.isEnabled()) 121 | return; 122 | 123 | if (mPostLScanCallback != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 124 | mAdapter.getBluetoothLeScanner().stopScan(mPostLScanCallback); 125 | mAdapter.getBluetoothLeScanner().flushPendingScanResults(mPostLScanCallback); 126 | } 127 | 128 | if (mPreLScanCallback != null) { 129 | mAdapter.stopLeScan(mPreLScanCallback); 130 | } 131 | } 132 | 133 | 134 | private class PreLScanCallback implements BluetoothAdapter.LeScanCallback { 135 | @Override 136 | public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { 137 | mScanCallback.onScanResult(device, rssi, scanRecord); 138 | } 139 | } 140 | 141 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 142 | private class PostLScanCallback extends ScanCallback { 143 | @Override 144 | public void onScanResult(int callbackType, ScanResult result) { 145 | mScanCallback.onScanResult(result.getDevice(), result.getRssi(), result.getScanRecord().getBytes()); 146 | } 147 | 148 | @Override 149 | public void onBatchScanResults(List results) { 150 | Log.v(TAG, "ScanCallback batch results: " + results); 151 | for (ScanResult r : results) { 152 | onScanResult(-1, r); 153 | } 154 | } 155 | 156 | @Override 157 | public void onScanFailed(int errorCode) { 158 | mScanCallback.onScanFailed(errorCode); 159 | switch (errorCode) { 160 | case ScanCallback.SCAN_FAILED_ALREADY_STARTED: 161 | Log.e(TAG, "Scan failed: already started"); 162 | break; 163 | case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED: 164 | Log.e(TAG, "Scan failed: app registration failed"); 165 | break; 166 | case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED: 167 | Log.e(TAG, "Scan failed: feature unsupported"); 168 | break; 169 | case ScanCallback.SCAN_FAILED_INTERNAL_ERROR: 170 | Log.e(TAG, "Scan failed: internal error"); 171 | break; 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/GattManager.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.bluetooth.BluetoothDevice; 4 | import android.bluetooth.BluetoothGatt; 5 | import android.bluetooth.BluetoothGattCallback; 6 | import android.bluetooth.BluetoothGattCharacteristic; 7 | import android.bluetooth.BluetoothGattService; 8 | import android.bluetooth.BluetoothProfile; 9 | import android.content.Context; 10 | import android.os.Handler; 11 | import android.os.Looper; 12 | import android.os.ParcelUuid; 13 | import android.util.Log; 14 | 15 | import java.util.Date; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * Created by Yonah on 18/03/16. 22 | */ 23 | public class GattManager { 24 | private final static String TAG = "discovery-GattManager"; 25 | 26 | interface GattManagerCallback { 27 | void didIdentify(BluetoothDevice device, String username, ParcelUuid serviceUUID); 28 | void failedToMatchService(BluetoothDevice device); 29 | } 30 | 31 | private Integer mGattTimeoutInterval; 32 | private Map mGattConnectionStartTimes; 33 | private Map mGattConnections; 34 | 35 | private ParcelUuid mServiceUUID; 36 | private GattManagerCallback mCallback; 37 | private Context mContext; 38 | private final MyBluetoothGattCallback mMyBluetoothGattCallback = new MyBluetoothGattCallback(); 39 | 40 | public GattManager(Context context, ParcelUuid serviceUUID, GattManagerCallback callback) { 41 | mContext = context; 42 | mServiceUUID = serviceUUID; 43 | mCallback = callback; 44 | 45 | mGattConnections = new HashMap<>(); 46 | mGattConnectionStartTimes = new HashMap<>(); 47 | mGattTimeoutInterval = 30; 48 | 49 | } 50 | 51 | // call this method to try to identify a device. 52 | // this will attempt to connect to the device and read its services 53 | // if a service matching ours is found. the callback didMatchService is called and we try to read the characteristics 54 | // if we can read the characteristic matching our service, the callback didIdentify is called with the username 55 | // if no service matching ours is found, the callback failedToMatchService is called. 56 | public void identify(final BluetoothDevice device) { 57 | boolean shouldConnect = false; 58 | 59 | // first check if there are any existing connection attempts in progress. 60 | // If there are, check to see if they have timed out. 61 | // If they have, cancel them and try again. If not, wait.. 62 | // if no existing attempts, start a new one and store it (if successful). 63 | long currentTime = new Date().getTime(); 64 | BluetoothGatt existingGatt = mGattConnections.get(device.getAddress()); 65 | 66 | if (existingGatt == null) { 67 | Log.v(TAG, device.getAddress() + " - device not identified. will connect"); 68 | shouldConnect = true; 69 | } else { 70 | long startedAt = mGattConnectionStartTimes.get(device.getAddress()); 71 | if (currentTime - startedAt < mGattTimeoutInterval * 1000) { 72 | Log.v(TAG, device.getAddress() + " - device not identified. connection already in progress"); 73 | shouldConnect = false; 74 | } else { 75 | Log.w(TAG, device.getAddress() + " - connection did timeout. will retry"); 76 | existingGatt.disconnect(); 77 | existingGatt.close(); 78 | mGattConnections.remove(device.getAddress()); 79 | mGattConnectionStartTimes.remove(device.getAddress()); 80 | shouldConnect = true; 81 | } 82 | } 83 | 84 | if (shouldConnect) { 85 | Handler handler = new Handler(Looper.getMainLooper()); 86 | handler.post(new Runnable() { 87 | @Override 88 | public void run() { 89 | BluetoothGatt gatt = device.connectGatt(mContext, false, mMyBluetoothGattCallback); 90 | if (gatt != null) { 91 | Log.v(TAG, device.getAddress() + " - attempted connection"); 92 | mGattConnections.put(device.getAddress(), gatt); 93 | mGattConnectionStartTimes.put(device.getAddress(), new Date().getTime()); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | } 100 | 101 | private class MyBluetoothGattCallback extends BluetoothGattCallback { 102 | 103 | @Override 104 | public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) { 105 | // this will get called when a device connects or disconnects 106 | if (newState == BluetoothProfile.STATE_CONNECTED) { 107 | Log.v(TAG, gatt.getDevice().getAddress() + " - connected!"); 108 | 109 | boolean started = gatt.discoverServices(); 110 | 111 | if (!started) { 112 | gatt.disconnect(); 113 | } 114 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { 115 | Log.v(TAG, gatt.getDevice().getAddress() + " - disconnected..."); 116 | gatt.close(); 117 | mGattConnections.remove(gatt.getDevice().getAddress()); 118 | mGattConnectionStartTimes.remove(gatt.getDevice().getAddress()); 119 | 120 | } else { 121 | Log.v(TAG, gatt.getDevice().getAddress() + " status: " + status); 122 | } 123 | } 124 | 125 | @Override 126 | public void onServicesDiscovered(final BluetoothGatt gatt, final int status) { 127 | // this will get called after the client initiates a BluetoothGatt.discoverServices() call 128 | BluetoothGattService service = gatt.getService(mServiceUUID.getUuid()); 129 | Log.v(TAG, gatt.getDevice().getAddress() + " - services discovered"); 130 | Boolean isMyService = false; 131 | 132 | if (service != null) { 133 | List characteristics = service.getCharacteristics(); 134 | 135 | for (BluetoothGattCharacteristic characteristic : characteristics) { 136 | if (characteristic.getUuid().equals(mServiceUUID.getUuid())) { 137 | isMyService = true; 138 | Log.v(TAG, gatt.getDevice().getAddress() + " - found MY service!"); 139 | 140 | gatt.setCharacteristicNotification(characteristic, true); 141 | gatt.readCharacteristic(characteristic); 142 | } 143 | } 144 | } 145 | 146 | 147 | if (!isMyService) { 148 | mCallback.failedToMatchService(gatt.getDevice()); 149 | gatt.disconnect(); 150 | } 151 | 152 | } 153 | 154 | @Override 155 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 156 | onCharacteristicChanged(gatt, characteristic); 157 | } 158 | 159 | @Override 160 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { 161 | if (characteristic.getUuid().equals(mServiceUUID.getUuid())) { 162 | if (characteristic.getValue() != null) { 163 | String value = characteristic.getStringValue(0); 164 | ParcelUuid uuid = new ParcelUuid(characteristic.getUuid()); 165 | 166 | // if the value is not nil, we found our username! 167 | if (value != null && value.length() > 0) { 168 | Log.v(TAG, gatt.getDevice().getAddress() + " - got username!!"); 169 | 170 | mCallback.didIdentify(gatt.getDevice(), value, uuid); 171 | 172 | // cancel the subscription to our characteristic 173 | gatt.setCharacteristicNotification(characteristic, false); 174 | // and disconnect from the peripehral 175 | gatt.disconnect(); 176 | 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/AdvertiserService.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Service; 5 | import android.bluetooth.BluetoothAdapter; 6 | import android.bluetooth.BluetoothDevice; 7 | import android.bluetooth.BluetoothGatt; 8 | import android.bluetooth.BluetoothGattCharacteristic; 9 | import android.bluetooth.BluetoothGattServer; 10 | import android.bluetooth.BluetoothGattServerCallback; 11 | import android.bluetooth.BluetoothGattService; 12 | import android.bluetooth.BluetoothManager; 13 | import android.bluetooth.le.AdvertiseCallback; 14 | import android.bluetooth.le.AdvertiseData; 15 | import android.bluetooth.le.AdvertiseSettings; 16 | import android.bluetooth.le.BluetoothLeAdvertiser; 17 | import android.content.BroadcastReceiver; 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.content.IntentFilter; 21 | import android.os.Build; 22 | import android.os.Bundle; 23 | import android.os.IBinder; 24 | import android.os.ParcelUuid; 25 | import android.util.Log; 26 | 27 | /** 28 | * Created by Yonah on 21/01/16. 29 | * base on: http://developer.android.com/samples/BluetoothAdvertisements/project.html 30 | */ 31 | 32 | 33 | /** 34 | * Manages BLE Advertising independent of the main app. 35 | * If the app goes off screen (or gets killed completely) advertising can continue because this 36 | * Service is maintaining the necessary Callback in memory. 37 | */ 38 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 39 | public class AdvertiserService extends Service { 40 | private final static String TAG = "discovery-AdvertiserSvc"; 41 | 42 | /** 43 | * A global variable to let AdvertiserFragment check if the Service is running without needing 44 | * to start or bind to it. 45 | * This is the best practice method as defined here: 46 | * https://groups.google.com/forum/#!topic/android-developers/jEvXMWgbgzE 47 | */ 48 | public static boolean running = false; 49 | 50 | /** 51 | * Setting autorestart to true will cause the service to automatically relaunch if it's 52 | * killed by the system. Note: this will not auto relaunch the service if it's killed by 53 | * the user. 54 | * 55 | * This will also restart advertising if the bluetooth state is toggled off then on. 56 | */ 57 | public static boolean shouldAutoRestart = false; 58 | 59 | /** 60 | * The number of times the service will try to restart itself after a failure. 61 | * Failure, not being killed. 62 | */ 63 | public static int maxRetriesAfterFailure = 3; 64 | 65 | 66 | public static final String ADVERTISING_FAILED = 67 | "com.example.android.bluetoothadvertisements.advertising_failed"; 68 | 69 | public static final String ADVERTISING_FAILED_EXTRA_CODE = "failureCode"; 70 | 71 | private BluetoothLeAdvertiser mBluetoothLeAdvertiser; 72 | private BluetoothAdapter mBluetoothAdapter; 73 | private BluetoothManager mBluetoothManager; 74 | private BluetoothGattServer mGattServer; 75 | 76 | private AdvertiseCallback mAdvertiseCallback; 77 | private BluetoothGattServerCallback mGattServerCallback; 78 | private ParcelUuid mUUID; 79 | private String mUsername; 80 | 81 | /** 82 | * how many times in a row we failed to start advertising 83 | */ 84 | private int mRetriesAfterFailure = 0; 85 | 86 | /** 87 | * Monitor the bluetooth state. If autoRestart is true, start advertising whenever bluetooth 88 | * is turned back on. 89 | */ 90 | private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 91 | @Override 92 | public void onReceive(Context context, Intent intent) { 93 | final String action = intent.getAction(); 94 | 95 | if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 96 | final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 97 | BluetoothAdapter.ERROR); 98 | 99 | switch (state) { 100 | case BluetoothAdapter.STATE_ON: 101 | if (shouldAutoRestart) 102 | startAdvertising(); 103 | break; 104 | } 105 | } 106 | } 107 | }; 108 | 109 | @Override 110 | public void onCreate() { 111 | super.onCreate(); 112 | 113 | // Register for broadcasts on BluetoothAdapter state change 114 | IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 115 | registerReceiver(mReceiver, filter); 116 | } 117 | 118 | @Override 119 | public void onDestroy() { 120 | // Log.v(TAG, "onDestroy"); 121 | 122 | /** 123 | * Note that onDestroy is not guaranteed to be called quickly or at all. Services exist at 124 | * the whim of the system, and onDestroy can be delayed or skipped entirely if memory need 125 | * is critical. 126 | */ 127 | running = false; 128 | stopAdvertising(); 129 | 130 | // Unregister broadcast listeners 131 | unregisterReceiver(mReceiver); 132 | 133 | 134 | /** 135 | * If autorestart is true, launch a new service right before this one is killed. this 136 | * ensures that the system does turn off advertising by killing the service. 137 | */ 138 | if (shouldAutoRestart) { 139 | Intent intent = new Intent(this, AdvertiserService.class); 140 | intent.putExtra("uuid", mUUID.toString()); 141 | intent.putExtra("username", mUsername); 142 | startService(intent); 143 | } 144 | 145 | super.onDestroy(); 146 | } 147 | 148 | /** 149 | * Required for extending service, but this will be a Started Service only, so no need for 150 | * binding. 151 | */ 152 | @Override 153 | public IBinder onBind(Intent intent) { 154 | return null; 155 | } 156 | 157 | /** 158 | * Get references to system Bluetooth objects if we don't have them already. 159 | */ 160 | 161 | @Override 162 | public int onStartCommand(Intent intent, int flags, int startId) { 163 | super.onStartCommand(intent, flags, startId); 164 | Bundle extras = intent.getExtras(); 165 | 166 | this.mUUID = ParcelUuid.fromString(extras.getString("uuid")); 167 | this.mUsername = extras.getString("username"); 168 | 169 | if (mBluetoothLeAdvertiser == null) { 170 | mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); 171 | } 172 | 173 | if (mBluetoothManager != null) { 174 | mBluetoothAdapter = mBluetoothManager.getAdapter(); 175 | } 176 | 177 | if (mBluetoothAdapter != null) { 178 | mBluetoothAdapter.setName(mUsername); 179 | mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); 180 | } 181 | 182 | 183 | running = true; 184 | startAdvertising(); 185 | 186 | return START_REDELIVER_INTENT; 187 | } 188 | 189 | /** 190 | * Starts BLE Advertising. 191 | */ 192 | private void startAdvertising() { 193 | // Log.d(TAG, "Service: Starting Advertising"); 194 | 195 | if (mAdvertiseCallback == null) { 196 | mAdvertiseCallback = new MyAdvertiseCallback(); 197 | } 198 | 199 | if (mGattServerCallback == null) { 200 | mGattServerCallback = new MyGattServerCallback(); 201 | } 202 | 203 | if (mGattServer == null) { 204 | mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback); 205 | mGattServer.addService(buildGattService()); 206 | } 207 | 208 | if (mBluetoothLeAdvertiser != null) { 209 | AdvertiseSettings settings = buildAdvertiseSettings(); 210 | AdvertiseData data = buildAdvertiseData(); 211 | mBluetoothLeAdvertiser.startAdvertising(settings, data, 212 | mAdvertiseCallback); 213 | } 214 | } 215 | 216 | /** 217 | * Stops BLE Advertising. 218 | */ 219 | private void stopAdvertising() { 220 | // Log.d(TAG, "Service: Stopping Advertising"); 221 | if (mBluetoothLeAdvertiser != null) { 222 | mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback); 223 | mAdvertiseCallback = null; 224 | } 225 | 226 | if (mGattServer != null) { 227 | mGattServer.clearServices(); 228 | mGattServer.close(); 229 | mGattServer = null; 230 | } 231 | } 232 | 233 | /** 234 | * Returns an AdvertiseData object which includes the Service UUID and Device Name. 235 | */ 236 | private AdvertiseData buildAdvertiseData() { 237 | 238 | /** 239 | * Note: There is a strict limit of 31 Bytes on packets sent over BLE Advertisements. 240 | * This includes everything put into AdvertiseData including UUIDs, device info, & 241 | * arbitrary service or manufacturer data. 242 | * Attempting to send packets over this limit will result in a failure with error code 243 | * AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE. Catch this error in the 244 | * onStartFailure() method of an AdvertiseCallback implementation. 245 | */ 246 | 247 | AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder(); 248 | dataBuilder.addServiceUuid(mUUID); 249 | dataBuilder.setIncludeDeviceName(mUsername.length() < 8); 250 | dataBuilder.setIncludeTxPowerLevel(false); 251 | 252 | /* For example - this will cause advertising to fail (exceeds size limit) */ 253 | //String failureData = "asdghkajsghalkxcjhfa;sghtalksjcfhalskfjhasldkjfhdskf"; 254 | //dataBuilder.addServiceData(Constants.Service_UUID, failureData.getBytes()); 255 | 256 | return dataBuilder.build(); 257 | } 258 | 259 | /** 260 | * Returns an AdvertiseSettings object set to use low power (to help preserve battery life) 261 | * and disable the built-in timeout since this code uses its own timeout runnable. 262 | */ 263 | private AdvertiseSettings buildAdvertiseSettings() { 264 | AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder(); 265 | settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER); 266 | settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH); 267 | settingsBuilder.setConnectable(true); 268 | settingsBuilder.setTimeout(0); 269 | return settingsBuilder.build(); 270 | } 271 | 272 | /** 273 | * Returns a gatt service for the service uuid containing 274 | * a characteristic with the device name 275 | */ 276 | private BluetoothGattService buildGattService() { 277 | BluetoothGattService gattService = new BluetoothGattService( 278 | mUUID.getUuid(), 279 | BluetoothGattService.SERVICE_TYPE_PRIMARY 280 | ); 281 | BluetoothGattCharacteristic gattCharacteristic = new BluetoothGattCharacteristic( 282 | mUUID.getUuid(), 283 | BluetoothGattCharacteristic.PROPERTY_READ, 284 | BluetoothGattCharacteristic.PERMISSION_READ 285 | ); 286 | 287 | gattCharacteristic.setValue(mUsername); 288 | gattService.addCharacteristic(gattCharacteristic); 289 | return gattService; 290 | } 291 | 292 | /* 293 | * Callback handles all incoming requests from GATT clients. 294 | * From connections to read/write requests. 295 | */ 296 | private class MyGattServerCallback extends BluetoothGattServerCallback { 297 | @Override 298 | public void onCharacteristicReadRequest(BluetoothDevice device, 299 | int requestId, 300 | int offset, 301 | BluetoothGattCharacteristic characteristic) { 302 | super.onCharacteristicReadRequest(device, requestId, offset, characteristic); 303 | // Log.i(TAG, "onCharacteristicReadRequest " + characteristic.getUuid().toString()); 304 | 305 | if (characteristic.getUuid().equals(mUUID.getUuid())) { 306 | mGattServer.sendResponse(device, 307 | requestId, 308 | BluetoothGatt.GATT_SUCCESS, 309 | 0, 310 | mUsername.getBytes()); 311 | 312 | } 313 | } 314 | } 315 | 316 | /** 317 | * Custom callback after Advertising succeeds or fails to start. Broadcasts the error code 318 | * in an Intent. Will rety advertising x times before finally failing and stopping 319 | * 320 | * 321 | */ 322 | private class MyAdvertiseCallback extends AdvertiseCallback { 323 | 324 | @Override 325 | public void onStartFailure(int errorCode) { 326 | super.onStartFailure(errorCode); 327 | 328 | switch (errorCode) { 329 | case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED: 330 | Log.e(TAG, "Advertise failed: already started"); 331 | break; 332 | case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE: 333 | Log.e(TAG, "Advertise failed: data too large"); 334 | break; 335 | case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED: 336 | Log.e(TAG, "Advertise failed: feature unsupported"); 337 | break; 338 | case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR: 339 | Log.e(TAG, "Advertise failed: internal error"); 340 | break; 341 | case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS: 342 | Log.e(TAG, "Advertise failed: too many advertisers"); 343 | break; 344 | } 345 | mRetriesAfterFailure++; 346 | 347 | if (mRetriesAfterFailure < maxRetriesAfterFailure) { 348 | //RETRY CODE HERE!! 349 | 350 | new android.os.Handler().postDelayed( 351 | new Runnable() { 352 | public void run() { 353 | stopAdvertising(); 354 | startAdvertising(); 355 | } 356 | }, (int)(Math.pow(2, mRetriesAfterFailure) * 1000)); 357 | 358 | } else { 359 | sendFailureIntent(errorCode); 360 | shouldAutoRestart = false; 361 | stopSelf(); 362 | } 363 | } 364 | 365 | @Override 366 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 367 | super.onStartSuccess(settingsInEffect); 368 | mRetriesAfterFailure = 0; 369 | // Log.d(TAG, "Advertising successfully started"); 370 | } 371 | } 372 | 373 | /** 374 | * Builds and sends a broadcast intent indicating Advertising has failed. Includes the error 375 | * code as an extra. This is intended to be picked up by the {@code AdvertiserFragment}. 376 | */ 377 | private void sendFailureIntent(int errorCode){ 378 | 379 | Intent failureIntent = new Intent(); 380 | failureIntent.setAction(ADVERTISING_FAILED); 381 | failureIntent.putExtra(ADVERTISING_FAILED_EXTRA_CODE, errorCode); 382 | sendBroadcast(failureIntent); 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /discovery/src/main/java/com/joshblour/discovery/Discovery.java: -------------------------------------------------------------------------------- 1 | package com.joshblour.discovery; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothDevice; 6 | import android.bluetooth.BluetoothGatt; 7 | import android.bluetooth.BluetoothGattCallback; 8 | import android.bluetooth.BluetoothGattCharacteristic; 9 | import android.bluetooth.BluetoothGattService; 10 | import android.bluetooth.BluetoothManager; 11 | import android.bluetooth.BluetoothProfile; 12 | import android.bluetooth.le.AdvertiseCallback; 13 | import android.bluetooth.le.BluetoothLeScanner; 14 | import android.bluetooth.le.ScanCallback; 15 | import android.bluetooth.le.ScanFilter; 16 | import android.bluetooth.le.ScanResult; 17 | import android.bluetooth.le.ScanSettings; 18 | import android.content.BroadcastReceiver; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Build; 22 | import android.os.Handler; 23 | import android.os.ParcelUuid; 24 | import android.util.Log; 25 | 26 | 27 | import java.util.ArrayList; 28 | import java.util.Collections; 29 | import java.util.Comparator; 30 | import java.util.Date; 31 | import java.util.HashMap; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.UUID; 35 | 36 | /** 37 | * Created by Yonah on 15/10/15. 38 | */ 39 | @TargetApi(Build.VERSION_CODES.KITKAT) 40 | public class Discovery implements MultiScanner.MultiScannerCallback, GattManager.GattManagerCallback{ 41 | private final static String TAG = "discovery-Discovery"; 42 | 43 | public interface DiscoveryCallback { 44 | void didUpdateUsers(ArrayList users, Boolean usersChanged); 45 | } 46 | 47 | 48 | 49 | public enum DIStartOptions{ 50 | DIStartAdvertisingAndDetecting, 51 | DIStartAdvertisingOnly, 52 | DIStartDetectingOnly, 53 | DIStartNone 54 | } 55 | 56 | private Context mContext; 57 | private String mUsername; 58 | private ParcelUuid mUUID; 59 | private Boolean mPaused; 60 | private Integer mUserTimeoutInterval; 61 | private Integer mScanForSeconds; 62 | private Integer mWaitForSeconds; 63 | private Boolean mShouldAdvertise; 64 | private Boolean mShouldDiscover; 65 | private Boolean mDisableAndroidLScanner; 66 | private Map mUsersMap; 67 | 68 | 69 | private Handler mHandler; 70 | private Runnable mRunnable; 71 | private DiscoveryCallback mDiscoveryCallback; 72 | private BluetoothAdapter mBluetoothAdapter; 73 | 74 | private GattManager mGattManager; 75 | private GattManager.GattManagerCallback mGattManagerCallback; 76 | private MultiScanner mScanner; 77 | 78 | public Discovery(Context context, ParcelUuid uuid, String username, DiscoveryCallback discoveryCallback) { 79 | this(context, uuid, username, DIStartOptions.DIStartAdvertisingAndDetecting, discoveryCallback); 80 | } 81 | 82 | public Discovery(Context context, ParcelUuid uuid, String username, DIStartOptions startOptions, DiscoveryCallback discoveryCallback ) { 83 | // initialize defaults 84 | mShouldAdvertise = false; 85 | mShouldDiscover = false; 86 | mDisableAndroidLScanner = false; 87 | mPaused = false; 88 | mUserTimeoutInterval = 5; 89 | mScanForSeconds = 5; 90 | mWaitForSeconds = 5; 91 | mContext = context; 92 | mUUID = uuid; 93 | mUsername = username; 94 | mDiscoveryCallback = discoveryCallback; 95 | mUsersMap = new HashMap<>(); 96 | mHandler = new Handler(); 97 | 98 | switch (startOptions) { 99 | case DIStartAdvertisingAndDetecting: 100 | this.setShouldAdvertise(true); 101 | this.setShouldDiscover(true); 102 | break; 103 | case DIStartAdvertisingOnly: 104 | this.setShouldAdvertise(true); 105 | break; 106 | case DIStartDetectingOnly: 107 | this.setShouldDiscover(true); 108 | break; 109 | case DIStartNone: 110 | default: 111 | break; 112 | } 113 | } 114 | 115 | public void setPaused(Boolean paused) { 116 | if (getBluetoothAdapter() == null) 117 | return; 118 | 119 | if (this.mPaused == paused) 120 | return; 121 | this.mPaused = paused; 122 | 123 | if (paused) { 124 | stopDetecting(); 125 | stopAdvertising(); 126 | } else { 127 | startDetectionCycling(); 128 | startAdvertising(); 129 | } 130 | } 131 | 132 | //***BEGIN DETECTION METHODS*** 133 | public void setShouldDiscover(Boolean shouldDiscover) { 134 | if (getBluetoothAdapter() == null) 135 | return; 136 | 137 | if (this.mShouldDiscover == shouldDiscover) 138 | return; 139 | 140 | this.mShouldDiscover = shouldDiscover; 141 | 142 | if (shouldDiscover) { 143 | startDetectionCycling(); 144 | } else { 145 | stopDetecting(); 146 | checkList(); 147 | } 148 | } 149 | 150 | // A more energy efficient way to detect. 151 | // It detects for mScanForSeconds(default: 5) then stops for mWaitForSeconds(default: 5) then starts again. 152 | // mShouldDiscover starts THIS method when set to true and stops it when set to false. 153 | private void startDetectionCycling() { 154 | if (!mShouldDiscover || mPaused) 155 | return; 156 | 157 | if (getBluetoothAdapter() == null) 158 | return; 159 | 160 | startDetecting(); 161 | Log.v(TAG, "detection cycle started"); 162 | 163 | if (mRunnable != null) 164 | mHandler.removeCallbacks(mRunnable); 165 | 166 | mRunnable = new Runnable() { 167 | @Override 168 | public void run() { 169 | stopDetecting(); 170 | Log.v(TAG, "detection cycle stopped"); 171 | 172 | Runnable runable = new Runnable() { 173 | @Override 174 | public void run() { 175 | startDetectionCycling(); 176 | } 177 | }; 178 | mHandler.postDelayed(runable, mWaitForSeconds * 1000); 179 | checkList(); 180 | } 181 | }; 182 | mHandler.postDelayed(mRunnable, mScanForSeconds * 1000); 183 | } 184 | 185 | public void startDetecting() { 186 | if (mScanner == null) 187 | mScanner = new MultiScanner(getBluetoothAdapter(), null, this, true); 188 | 189 | mScanner.start(); 190 | } 191 | 192 | public void stopDetecting(){ 193 | if (mScanner != null) 194 | mScanner.stop(); 195 | }//***END DETECTION METHODS*** 196 | 197 | 198 | 199 | 200 | 201 | //***BEGIN ADVERTISING METHODS*** 202 | public void setShouldAdvertise(Boolean shouldAdvertise) { 203 | if (getBluetoothAdapter() == null) 204 | return; 205 | 206 | if (this.mShouldAdvertise == shouldAdvertise) 207 | return; 208 | 209 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 210 | this.mShouldAdvertise = false; 211 | return; 212 | } 213 | 214 | this.mShouldAdvertise = shouldAdvertise; 215 | 216 | if (shouldAdvertise) { 217 | startAdvertising(); 218 | } else { 219 | stopAdvertising(); 220 | } 221 | } 222 | 223 | private void startAdvertising() { 224 | if (getBluetoothAdapter().isEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 225 | AdvertiserService.shouldAutoRestart = true; 226 | if (!AdvertiserService.running) { 227 | mContext.startService(getAdvertiserServiceIntent(mContext)); 228 | Log.v(TAG, "started advertising"); 229 | } 230 | } 231 | } 232 | 233 | private void stopAdvertising() { 234 | if (getBluetoothAdapter().isEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 235 | AdvertiserService.shouldAutoRestart = false; 236 | mContext.stopService(getAdvertiserServiceIntent(mContext)); 237 | Log.v(TAG, "stopped advertising"); 238 | } 239 | } 240 | 241 | /** 242 | * Returns Intent addressed to the {@code AdvertiserService} class. 243 | */ 244 | private Intent getAdvertiserServiceIntent(Context c) { 245 | Intent intent = new Intent(c, AdvertiserService.class); 246 | intent.putExtra("uuid", getUUID().toString()); 247 | intent.putExtra("username", getUsername()); 248 | return intent; 249 | } // ***END ADVERTISING METHODS*** 250 | 251 | 252 | //***BEGIN METHODS TO PROCESS SCAN RESULTS*** 253 | 254 | public void updateList() { 255 | updateList(true); 256 | } 257 | 258 | // sends an update to the delegate with an array of identified users 259 | public void updateList(Boolean usersChanged) { 260 | ArrayList users = new ArrayList<>(getUsersMap().values()); 261 | 262 | // remove unidentified users and users who dont belong to our service 263 | ArrayList discardedItems = new ArrayList<>(); 264 | for (BLEUser user : users) { 265 | if (!user.isIdentified()) { 266 | discardedItems.add(user); 267 | } 268 | } 269 | users.removeAll(discardedItems); 270 | 271 | // we sort the list according to "proximity". 272 | // so the client will receive ordered users according to the proximity. 273 | Collections.sort(users, new Comparator() { 274 | public int compare(BLEUser s1, BLEUser s2) { 275 | if (s1.getProximity() == null || s2.getProximity() == null) 276 | return 0; 277 | return s1.getProximity().compareTo(s2.getProximity()); 278 | } 279 | }); 280 | 281 | 282 | if (mDiscoveryCallback != null) { 283 | mDiscoveryCallback.didUpdateUsers(users, usersChanged); 284 | } 285 | } 286 | 287 | // removes users who haven't been seen in mUserTimeoutInterval seconds and triggers 288 | // an update to the delegate 289 | private void checkList() { 290 | 291 | if (getUsersMap() == null) 292 | return; 293 | 294 | long currentTime = new Date().getTime(); 295 | ArrayList discardedKeys = new ArrayList<>(); 296 | 297 | for (String key : getUsersMap().keySet()) { 298 | BLEUser bleUser = getUsersMap().get(key); 299 | long diff = currentTime - bleUser.getUpdateTime(); 300 | 301 | // We remove the user if we haven't seen him for the userTimeInterval amount of seconds. 302 | // You can simply set the userTimeInterval variable anything you want. 303 | if (diff > getUserTimeoutInterval() * 1000) { 304 | discardedKeys.add(key); 305 | } 306 | } 307 | 308 | 309 | // update the list if we removed a user. 310 | if (discardedKeys.size() > 0) { 311 | for (String key : discardedKeys) { 312 | getUsersMap().remove(key); 313 | } 314 | updateList(); 315 | } else { 316 | // simply update the list, because the order of the users may have changed. 317 | updateList(false); 318 | } 319 | 320 | } 321 | 322 | private BLEUser userForDevice(BluetoothDevice device) { 323 | BLEUser bleUser = getUsersMap().get(device.getAddress()); 324 | 325 | if (bleUser == null) { 326 | bleUser = new BLEUser(device); 327 | bleUser.setUsername(null); 328 | bleUser.setIdentified(false); 329 | getUsersMap().put(bleUser.getDeviceAddress(), bleUser); 330 | } 331 | 332 | return bleUser; 333 | } 334 | 335 | @Override 336 | public void onScanResult(BluetoothDevice device, int rssi, byte[] scanRecord) { 337 | 338 | BLEUser bleUser = userForDevice(device); 339 | 340 | // before we report this device to our delegate as a success, two things: 341 | // 1) Make sure it contains our service (it's another device advertising with our unique uuid) 342 | // 2) Make sure we can read its username 343 | 344 | // We check if we can get a cached copy of the devices service uuids 345 | if (bleUser.isMyService() == null) { 346 | ParcelUuid[] uuids = device.getUuids(); 347 | if (uuids != null && uuids.length > 0) { 348 | for (ParcelUuid uuid : uuids) { 349 | if (uuid.getUuid().equals(mUUID.getUuid())) { 350 | bleUser.setIsMyService(true); 351 | updateList(true); 352 | } 353 | } 354 | } 355 | } 356 | 357 | // We check if we can get the username from the advertisement data, 358 | // in case the advertising peer application is working at foreground 359 | if (bleUser.getUsername() == null) { 360 | String username = device.getName(); 361 | 362 | if (username != null && username.length() > 0) { 363 | bleUser.setUsername(username); 364 | updateList(true); 365 | } 366 | } 367 | 368 | //if you have the username and a boolean value for isMyService, you have enough to identify the user 369 | if (bleUser.isMyService() != null && bleUser.getUsername() != null) { 370 | if (bleUser.isMyService()) { 371 | bleUser.setIdentified(true); 372 | } 373 | } 374 | 375 | 376 | if (bleUser.isIdentified()) { 377 | /// great! we know everything we need to about this service. just update the rssi and time and we're done 378 | Log.v(TAG, device.getAddress() + " - device is identified"); 379 | } else if (bleUser.isMyService() == null) { 380 | // ok, maybe we know the username but we dont know if it's our service, so connect to gatt and check. 381 | if (mGattManager == null) 382 | mGattManager = new GattManager(mContext, mUUID, this); 383 | 384 | mGattManager.identify(device); 385 | } else if (!bleUser.isMyService()) { 386 | /// Ok, this isn't our service, we don't care about it. 387 | Log.v(TAG, device.getAddress() + " - device not our service"); 388 | } 389 | 390 | bleUser.setRssi(rssi); 391 | bleUser.setUpdateTime(new Date().getTime()); 392 | } 393 | 394 | @Override 395 | public void onScanFailed(int errorCode) { 396 | 397 | } 398 | 399 | @Override 400 | public void didIdentify(BluetoothDevice device, String username, ParcelUuid uuid) { 401 | BLEUser bleUser = userForDevice(device); 402 | bleUser.setUsername(username); 403 | bleUser.setIdentified(true); 404 | bleUser.setIsMyService(true); 405 | updateList(true); 406 | } 407 | 408 | 409 | @Override 410 | public void failedToMatchService(BluetoothDevice device) { 411 | BLEUser bleUser = userForDevice(device); 412 | bleUser.setIsMyService(false); 413 | } 414 | 415 | 416 | //***BEGIN GETTERS AND SETTERS** 417 | public String getUsername() { 418 | return mUsername; 419 | } 420 | public ParcelUuid getUUID() { 421 | return mUUID; 422 | } 423 | public Boolean getPaused() { 424 | return mPaused; 425 | } 426 | public Boolean getShouldDiscover() { 427 | return mShouldDiscover; 428 | } 429 | public Boolean getShouldAdvertise() { 430 | return mShouldAdvertise; 431 | } 432 | public Boolean getShouldDisableAndroidLScanner() { 433 | return mDisableAndroidLScanner; 434 | } 435 | public Integer getUserTimeoutInterval() { 436 | return mUserTimeoutInterval; 437 | } 438 | public void setUserTimeoutInterval(Integer mUserTimeoutInterval) { 439 | this.mUserTimeoutInterval = mUserTimeoutInterval; 440 | } 441 | public Map getUsersMap() { 442 | return mUsersMap; 443 | } 444 | public Integer getScanForSeconds() { 445 | return mScanForSeconds; 446 | } 447 | public Integer getWaitForSeconds() { 448 | return mWaitForSeconds; 449 | } 450 | 451 | public void setShouldDisableAndroidLScanner(Boolean disableAndroidLScanner) { 452 | this.mDisableAndroidLScanner = disableAndroidLScanner; 453 | } 454 | public void setScanForSeconds(Integer scanForSeconds) { 455 | this.mScanForSeconds = scanForSeconds; 456 | startDetectionCycling(); 457 | } 458 | 459 | public void setWaitForSeconds(Integer waitForSeconds) { 460 | this.mWaitForSeconds = waitForSeconds; 461 | startDetectionCycling(); 462 | } 463 | 464 | private BluetoothAdapter getBluetoothAdapter() { 465 | if (mBluetoothAdapter == null) { 466 | BluetoothManager manager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE); 467 | mBluetoothAdapter = manager.getAdapter(); 468 | } 469 | 470 | return mBluetoothAdapter; 471 | } 472 | 473 | } 474 | --------------------------------------------------------------------------------