├── .gitignore ├── .idea ├── assetWizardSettings.xml ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── compiler.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── za │ │ └── co │ │ └── mitchwongho │ │ └── example │ │ └── esp32 │ │ └── alerts │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── za │ │ │ └── co │ │ │ └── mitchwongho │ │ │ └── example │ │ │ └── esp32 │ │ │ └── alerts │ │ │ ├── MainActivity.kt │ │ │ ├── app │ │ │ ├── ForegroundService.kt │ │ │ ├── MainApplication.kt │ │ │ ├── NotificationListener.kt │ │ │ ├── OnBootReceiver.kt │ │ │ └── SettingsActivity.kt │ │ │ └── ble │ │ │ ├── LEManager.kt │ │ │ └── LeManagerCallbacks.kt │ └── res │ │ ├── drawable-hdpi │ │ └── ic_stat_espressif.png │ │ ├── drawable-mdpi │ │ └── ic_stat_espressif.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ └── ic_stat_espressif.png │ │ ├── drawable-xxhdpi │ │ └── ic_stat_espressif.png │ │ ├── drawable-xxxhdpi │ │ └── ic_stat_espressif.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── menu │ │ └── main_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── preferences.xml │ └── test │ └── java │ └── za │ └── co │ └── mitchwongho │ └── example │ └── esp32 │ └── alerts │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | 47 | # External native build folder generated in Android Studio 2.2 and later 48 | .externalNativeBuild 49 | 50 | # Freeline 51 | freeline.py 52 | freeline/ 53 | freeline_project_description.json 54 | 55 | ### Android Patch ### 56 | gen-external-apklibs 57 | 58 | # End of https://www.gitignore.io/api/android -------------------------------------------------------------------------------- /.idea/assetWizardSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 138 | 139 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Class structureJava 36 | 37 | 38 | Code maturity issuesJava 39 | 40 | 41 | Java 42 | 43 | 44 | Java language level migration aidsJava 45 | 46 | 47 | Javadoc issuesJava 48 | 49 | 50 | Performance issuesJava 51 | 52 | 53 | TestNG 54 | 55 | 56 | Threading issuesJava 57 | 58 | 59 | 60 | 61 | Android 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 76 | 77 | 1.7 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP-Alerts-for-Android 2 | 3 | This project is the Android companion app for the [Read Phone Notifications using ESP](https://www.hackster.io/mitchwongho/read-phone-notifications-using-esp-eb0ad4) Hackster.io 4 | and Hackaday.io projects. The ESP32 source code can be found [here](https://github.com/mitchwongho/ESP-Alerts-for-Arduino) 5 | 6 | ## Dependencies 7 | 8 | This projec has a dependency on the Nordic Semiconductor `Android-BLE-Library` [repository](https://github.com/NordicSemiconductor/Android-BLE-Library) that should be cloned along side this project's folder. 9 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-android-extensions' 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | android { 12 | compileSdkVersion 28 13 | defaultConfig { 14 | applicationId "za.co.mitchwongho.example.esp32.alerts" 15 | minSdkVersion 21 16 | targetSdkVersion 28 17 | versionCode 2 18 | versionName "2.0.0" 19 | multiDexEnabled true 20 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | dexOptions { 29 | javaMaxHeapSize "4g" 30 | } 31 | compileOptions { 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation 'com.android.support:multidex:1.0.3' 40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 42 | implementation 'com.android.support:appcompat-v7:28.0.0' 43 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 44 | implementation 'com.android.support:design:28.0.0' 45 | implementation 'com.jakewharton.timber:timber:4.7.1' 46 | implementation project(':ble') 47 | testImplementation 'junit:junit:4.12' 48 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 49 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 50 | } 51 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/za/co/mitchwongho/example/esp32/alerts/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("za.co.mitchwongho.example.esp32.alerts", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts 2 | 3 | import android.content.DialogInterface 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.preference.PreferenceManager 9 | import android.provider.Settings 10 | import android.support.design.widget.FloatingActionButton 11 | import android.support.v4.app.NotificationManagerCompat 12 | import android.support.v7.app.AlertDialog 13 | import android.support.v7.app.AppCompatActivity 14 | import android.view.Menu 15 | import android.view.MenuItem 16 | import timber.log.Timber 17 | import za.co.mitchwongho.example.esp32.alerts.app.ForegroundService 18 | import za.co.mitchwongho.example.esp32.alerts.app.MainApplication 19 | import za.co.mitchwongho.example.esp32.alerts.app.SettingsActivity 20 | import java.util.* 21 | 22 | class MainActivity : AppCompatActivity() { 23 | 24 | lateinit var fab: FloatingActionButton 25 | lateinit var menu: Menu 26 | var alertDialog: AlertDialog? = null 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_main) 31 | 32 | fab = findViewById(R.id.fab) 33 | 34 | PreferenceManager.setDefaultValues(this, R.xml.preferences, false) 35 | 36 | fab.setOnClickListener({ view -> 37 | 38 | val enabled = NotificationManagerCompat.getEnabledListenerPackages(this).contains(BuildConfig.APPLICATION_ID) 39 | Timber.d("Notification Listener Enabled $enabled") 40 | 41 | if (alertDialog == null || !(alertDialog!!.isShowing)) { 42 | if (enabled) { 43 | 44 | // lookup installed apps 45 | val installedApps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA) 46 | installedApps.sortWith(kotlin.Comparator { a, b -> 47 | val nameA = packageManager.getApplicationLabel(a).toString() 48 | val nameB = packageManager.getApplicationLabel(b).toString() 49 | nameA.compareTo(nameB) 50 | }) 51 | val names: Array = installedApps.map { applicationInfo -> packageManager.getApplicationLabel(applicationInfo).toString() }.toTypedArray() 52 | 53 | val prefsAllowedPackages: MutableSet = MainApplication.sharedPrefs.getStringSet(MainApplication.PREFS_KEY_ALLOWED_PACKAGES, mutableSetOf()) 54 | val checkedItems: BooleanArray = BooleanArray(installedApps.size) 55 | for (i in names.indices) { 56 | checkedItems[i] = prefsAllowedPackages.contains(installedApps[i].packageName) 57 | } 58 | 59 | val modifiedList: ArrayList = arrayListOf() 60 | modifiedList.addAll(prefsAllowedPackages) 61 | 62 | // show Apps 63 | val builder: AlertDialog.Builder = AlertDialog.Builder(this) 64 | .setTitle(R.string.choose_app) 65 | .setPositiveButton(android.R.string.ok, DialogInterface.OnClickListener { dialogInterface, i -> 66 | // commit 67 | MainApplication.sharedPrefs.edit().putStringSet(MainApplication.PREFS_KEY_ALLOWED_PACKAGES, modifiedList.toSet()).commit(); 68 | }) 69 | .setNegativeButton(android.R.string.cancel, DialogInterface.OnClickListener { dialogInterface, i -> 70 | // close without commit 71 | }) 72 | .setMultiChoiceItems(names, checkedItems, DialogInterface.OnMultiChoiceClickListener { dialogInterface, position, checked -> 73 | if (checked) { 74 | modifiedList.add(installedApps[position].packageName) 75 | } else { 76 | modifiedList.remove(installedApps[position].packageName) 77 | } 78 | }) 79 | .setOnDismissListener { dialogInterface -> alertDialog = null } 80 | .setOnCancelListener { dialogInterface -> alertDialog = null } 81 | alertDialog = builder.create() 82 | alertDialog!!.show() 83 | } else { 84 | val builder: AlertDialog.Builder = AlertDialog.Builder(this) 85 | .setTitle(R.string.choose_app) 86 | .setMessage("Looks like you must first grant this app access to notifications. Do you want to continue?") 87 | .setNegativeButton(android.R.string.no, null) 88 | .setPositiveButton(android.R.string.yes, DialogInterface.OnClickListener { dialogInterface: DialogInterface?, i: Int -> 89 | if (!enabled) { 90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { 91 | startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) 92 | } else { 93 | startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) 94 | } 95 | } 96 | }) 97 | .setOnDismissListener { dialogInterface -> alertDialog = null } 98 | .setOnCancelListener { dialogInterface -> alertDialog = null } 99 | alertDialog = builder.create() 100 | alertDialog!!.show() 101 | 102 | } 103 | } 104 | }) 105 | 106 | } 107 | 108 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 109 | menuInflater.inflate(R.menu.main_menu, menu) 110 | this.menu = menu 111 | return super.onCreateOptionsMenu(menu) 112 | } 113 | 114 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 115 | return when (item.itemId) { 116 | R.id.menu_item_prefs -> { 117 | showPreferences() 118 | true 119 | } 120 | R.id.menu_item_kill -> { 121 | stopService(Intent(this, ForegroundService::class.java)) 122 | item.setVisible(false) 123 | menu.findItem(R.id.menu_item_start)?.setVisible(true) 124 | true 125 | } 126 | R.id.menu_item_start -> { 127 | startService(Intent(this, ForegroundService::class.java)) 128 | item.setVisible(false) 129 | menu.findItem(R.id.menu_item_kill)?.setVisible(true) 130 | true 131 | } 132 | else -> super.onOptionsItemSelected(item) 133 | } 134 | 135 | } 136 | 137 | override fun onStart() { 138 | super.onStart() 139 | startService(Intent(this, ForegroundService::class.java)) 140 | Timber.w("onStart") 141 | } 142 | 143 | override fun onDestroy() { 144 | super.onDestroy() 145 | // stop the service 146 | val isRunAsAService = PreferenceManager.getDefaultSharedPreferences(this) 147 | .getBoolean(SettingsActivity.PREF_KEY_RUN_AS_A_SERVICE, false) 148 | Timber.w("onDestroy {isService=$isRunAsAService}") 149 | if (!isRunAsAService) { 150 | stopService(Intent(this, ForegroundService::class.java)) 151 | } 152 | } 153 | 154 | fun showPreferences() { 155 | startActivity(Intent(this, SettingsActivity::class.java)) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/app/ForegroundService.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.app 2 | 3 | import android.annotation.TargetApi 4 | import android.app.* 5 | import android.bluetooth.BluetoothAdapter 6 | import android.bluetooth.BluetoothDevice 7 | import android.bluetooth.BluetoothManager 8 | import android.content.BroadcastReceiver 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.content.IntentFilter 12 | import android.net.Uri 13 | import android.os.Build 14 | import android.os.IBinder 15 | import android.preference.PreferenceManager 16 | import android.support.v4.app.NotificationCompat 17 | import android.support.v4.content.LocalBroadcastManager 18 | import no.nordicsemi.android.ble.BleManager 19 | import timber.log.Timber 20 | import za.co.mitchwongho.example.esp32.alerts.BuildConfig 21 | import za.co.mitchwongho.example.esp32.alerts.MainActivity 22 | import za.co.mitchwongho.example.esp32.alerts.R 23 | import za.co.mitchwongho.example.esp32.alerts.ble.LEManager 24 | import za.co.mitchwongho.example.esp32.alerts.ble.LeManagerCallbacks 25 | import java.text.DateFormat 26 | import java.text.SimpleDateFormat 27 | import java.util.* 28 | 29 | /** 30 | * 31 | */ 32 | class ForegroundService : Service() { 33 | 34 | companion object { 35 | val NOTIFICATION_DISPLAY_TIMEOUT = 2 * 60 * 1000 //2 minutes 36 | val SERVICE_ID = 9001 37 | val NOTIFICATION_CHANNEL = BuildConfig.APPLICATION_ID 38 | val VESPA_DEVICE_ADDRESS = "00:00:00:00:00:00"//""24:0A:C4:13:58:EA" // <--- YOUR ESP32 MAC address here 39 | val formatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) 40 | } 41 | 42 | private var startId = 0; 43 | lateinit var bleManager: BleManager 44 | var lastPost: Long = 0L 45 | /** 46 | * Called by the system when the service is first created. Do not call this method directly. 47 | */ 48 | override fun onCreate() { 49 | super.onCreate() 50 | initNotificationChannel() 51 | Timber.w("onCreate") 52 | val remoteMacAddress = PreferenceManager.getDefaultSharedPreferences(this) 53 | .getString(SettingsActivity.PREF_KEY_REMOTE_MAC_ADDRESS, VESPA_DEVICE_ADDRESS) 54 | 55 | val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 56 | val leDevice = bluetoothManager.adapter.getRemoteDevice(remoteMacAddress) 57 | bleManager = LEManager(this) 58 | bleManager.setGattCallbacks(bleManagerCallback) 59 | if (bluetoothManager.adapter.state == BluetoothAdapter.STATE_ON) { 60 | bleManager.connect(leDevice).enqueue() 61 | } 62 | 63 | val intentFilter = IntentFilter(NotificationListener.EXTRA_ACTION) 64 | LocalBroadcastManager.getInstance(this).registerReceiver(localReceiver, intentFilter) 65 | 66 | registerReceiver(tickReceiver, IntentFilter(Intent.ACTION_TIME_TICK)) 67 | registerReceiver(bluetoothReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) 68 | } 69 | 70 | @TargetApi(Build.VERSION_CODES.O) 71 | private fun initNotificationChannel() { 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 73 | val notificationMgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 74 | val notificationChannel = NotificationChannel(NOTIFICATION_CHANNEL, BuildConfig.APPLICATION_ID, NotificationManager.IMPORTANCE_LOW) 75 | notificationChannel.description = getString(R.string.channel_desc) 76 | notificationChannel.enableLights(false) 77 | notificationChannel.enableVibration(false) 78 | notificationMgr.createNotificationChannel(notificationChannel) 79 | } 80 | } 81 | 82 | override fun onDestroy() { 83 | Timber.w("onDestroy") 84 | startId = 0 85 | bleManager.close() 86 | LocalBroadcastManager.getInstance(this).unregisterReceiver(localReceiver) 87 | unregisterReceiver(tickReceiver) 88 | unregisterReceiver(bluetoothReceiver) 89 | (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancelAll() 90 | 91 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 92 | val notificationMgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 93 | notificationMgr.deleteNotificationChannel(NOTIFICATION_CHANNEL) 94 | } 95 | super.onDestroy() 96 | } 97 | 98 | /** 99 | * Create/Update the notification 100 | */ 101 | private fun notify(contentText: String): Notification { 102 | // Launch the MainAcivity when user taps on the Notification 103 | val pendingIntent = PendingIntent.getActivity(this, 0 104 | , Intent(this, MainActivity::class.java) 105 | , PendingIntent.FLAG_UPDATE_CURRENT) 106 | 107 | val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL) 108 | .setSmallIcon(R.drawable.ic_stat_espressif) 109 | .setContentTitle(getString(R.string.app_name)) 110 | .setContentText(contentText) 111 | .setContentIntent(pendingIntent) 112 | .setSound(Uri.EMPTY) 113 | .setOnlyAlertOnce(true) 114 | .build() 115 | (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(SERVICE_ID, notification) 116 | return notification 117 | } 118 | 119 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 120 | 121 | Timber.w("onStartCommand {intent=${intent != null},flags=$flags,startId=$startId}") 122 | if (intent == null || this.startId != 0) { 123 | //service restarted 124 | Timber.w("onStartCommand - already running") 125 | } else { 126 | //started by intent or pending intent 127 | this.startId = startId 128 | val notification = notify("Scanning...") 129 | startForeground(SERVICE_ID, notification) 130 | } 131 | return START_STICKY 132 | } 133 | 134 | override fun onBind(intent: Intent?): IBinder? { 135 | return null 136 | } 137 | 138 | val bleManagerCallback: LeManagerCallbacks = object : LeManagerCallbacks() { 139 | /** 140 | * Called when the device has been connected. This does not mean that the application may start communication. 141 | * A service discovery will be handled automatically after this call. Service discovery 142 | * may ends up with calling [.onServicesDiscovered] or 143 | * [.onDeviceNotSupported] if required services have not been found. 144 | * @param device the device that got connected 145 | */ 146 | override fun onDeviceConnected(device: BluetoothDevice) { 147 | super.onDeviceConnected(device) 148 | Timber.d("onDeviceConnected ${device.name}") 149 | notify("Connected to ${device.name}") 150 | } 151 | 152 | /** 153 | * Called when the Android device started connecting to given device. 154 | * The [.onDeviceConnected] will be called when the device is connected, 155 | * or [.onError] in case of error. 156 | * @param device the device that got connected 157 | */ 158 | override fun onDeviceConnecting(device: BluetoothDevice) { 159 | super.onDeviceConnecting(device) 160 | Timber.d("Connecting to ${if (device.name.isNullOrEmpty()) "device" else device.name}") 161 | notify("Connecting to ${if (device.name.isNullOrEmpty()) "device" else device.name}") 162 | } 163 | 164 | /** 165 | * Called when user initialized disconnection. 166 | * @param device the device that gets disconnecting 167 | */ 168 | override fun onDeviceDisconnecting(device: BluetoothDevice) { 169 | super.onDeviceDisconnecting(device) 170 | Timber.d("Disconnecting from ${device.name}") 171 | notify("Disconnecting from ${device.name}") 172 | } 173 | 174 | /** 175 | * Called when the device has disconnected (when the callback returned 176 | * [BluetoothGattCallback.onConnectionStateChange] with state DISCONNECTED), 177 | * but ONLY if the [BleManager.shouldAutoConnect] method returned false for this device when it was connecting. 178 | * Otherwise the [.onLinklossOccur] method will be called instead. 179 | * @param device the device that got disconnected 180 | */ 181 | override fun onDeviceDisconnected(device: BluetoothDevice) { 182 | super.onDeviceDisconnected(device) 183 | Timber.d("Disconnected from ${device.name}") 184 | notify("Disconnected from ${device.name}") 185 | } 186 | 187 | /** 188 | * This callback is invoked when the Ble Manager lost connection to a device that has been connected 189 | * with autoConnect option (see [BleManager.shouldAutoConnect]. 190 | * Otherwise a [.onDeviceDisconnected] method will be called on such event. 191 | * @param device the device that got disconnected due to a link loss 192 | */ 193 | override fun onLinkLossOccurred(device: BluetoothDevice) { 194 | super.onLinkLossOccurred(device) 195 | Timber.d("Lost link to ${device.name}") 196 | notify("Lost link to ${device.name}") 197 | } 198 | 199 | 200 | override fun onError(device: BluetoothDevice, message: String, errorCode: Int) { 201 | super.onError(device, message, errorCode) 202 | Timber.w("Error ${device.name}") 203 | stopSelf(startId) 204 | } 205 | 206 | 207 | } 208 | 209 | var tickReceiver: BroadcastReceiver = object : BroadcastReceiver() { 210 | override fun onReceive(context: Context, intent: Intent?) { 211 | if (System.currentTimeMillis() - lastPost > NOTIFICATION_DISPLAY_TIMEOUT) { 212 | (bleManager as LEManager).writeTimeAndBatt(formatter.format(Date())) 213 | } 214 | } 215 | } 216 | 217 | var localReceiver: BroadcastReceiver = object : BroadcastReceiver() { 218 | override fun onReceive(context: Context?, intent: Intent?) { 219 | if (bleManager.isConnected && intent != null) { 220 | Timber.d("onReceive") 221 | val notificationId = intent.getIntExtra(NotificationListener.EXTRA_NOTIFICATION_ID_INT, 0) 222 | val notificationAppName = intent.getStringExtra(NotificationListener.EXTRA_APP_NAME) 223 | val notificationTitle = intent.getStringExtra(NotificationListener.EXTRA_TITLE) 224 | val notificationBody = intent.getStringExtra(NotificationListener.EXTRA_BODY) 225 | val notificationTimestamp = intent.getLongExtra(NotificationListener.EXTRA_TIMESTAMP_LONG, 0) 226 | val notificationDismissed = intent.getBooleanExtra(NotificationListener.EXTRA_NOTIFICATION_DISMISSED, true) 227 | // 228 | if (notificationDismissed) { 229 | val success = (bleManager as LEManager).writeTimeAndBatt(formatter.format(Date())) 230 | lastPost = notificationTimestamp 231 | Timber.d("writeTime {success=$success}") 232 | } else { 233 | val buffer = StringBuffer(256) 234 | buffer.append(notificationTitle) 235 | buffer.append(":\"") 236 | buffer.append(notificationBody) 237 | buffer.append("\" via ") 238 | buffer.append(notificationAppName).append(" @ ") 239 | buffer.append(formatter.format(Date(notificationTimestamp))) 240 | val success = (bleManager as LEManager).writeNotification(buffer.substring(0, Math.min(buffer.length, 256))) 241 | lastPost = notificationTimestamp 242 | Timber.d("writeMessage {success=$success}") 243 | } 244 | } 245 | } 246 | } 247 | 248 | val bluetoothReceiver = object : BroadcastReceiver() { 249 | override fun onReceive(context: Context, intent: Intent?) { 250 | if (intent?.action?.equals(BluetoothAdapter.ACTION_STATE_CHANGED) == true) { 251 | val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) 252 | when (state) { 253 | BluetoothAdapter.STATE_ON -> { 254 | // TODO: 2018/01/03 connect to remote 255 | val remoteMacAddress = PreferenceManager.getDefaultSharedPreferences(context) 256 | .getString(SettingsActivity.PREF_KEY_REMOTE_MAC_ADDRESS, VESPA_DEVICE_ADDRESS) 257 | val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 258 | val leDevice = bluetoothManager.adapter.getRemoteDevice(remoteMacAddress) 259 | 260 | bleManager = LEManager(context) 261 | bleManager.setGattCallbacks(bleManagerCallback) 262 | bleManager.connect(leDevice) 263 | } 264 | BluetoothAdapter.STATE_TURNING_OFF -> { 265 | // TODO: 2018/01/03 close connections 266 | bleManager.disconnect() 267 | bleManager.close() 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/app/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.app 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.support.multidex.MultiDexApplication 6 | import timber.log.Timber 7 | import za.co.mitchwongho.example.esp32.alerts.BuildConfig 8 | 9 | /** 10 | * 11 | */ 12 | class MainApplication : MultiDexApplication() { 13 | 14 | companion object { 15 | val PREFS_KEY_ALLOWED_PACKAGES = "PREFS_KEY_ALLOWED_PACKAGES" 16 | lateinit var sharedPrefs: SharedPreferences 17 | } 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | 22 | sharedPrefs = getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) 23 | 24 | if (BuildConfig.DEBUG) { 25 | Timber.plant(Timber.DebugTree()) 26 | } 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/app/NotificationListener.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.app 2 | 3 | import android.content.Intent 4 | import android.content.pm.PackageManager 5 | import android.os.Bundle 6 | import android.service.notification.NotificationListenerService 7 | import android.service.notification.StatusBarNotification 8 | import android.support.v4.content.LocalBroadcastManager 9 | import android.text.SpannableString 10 | import android.util.Log 11 | import timber.log.Timber 12 | 13 | /** 14 | * 15 | */ 16 | class NotificationListener : NotificationListenerService() { 17 | 18 | companion object { 19 | val EXTRA_ACTION = "ESP" 20 | val EXTRA_NOTIFICATION_DISMISSED = "EXTRA_NOTIFICATION_DISMISSED" 21 | val EXTRA_APP_NAME = "EXTRA_APP_NAME" 22 | val EXTRA_NOTIFICATION_ID_INT = "EXTRA_NOTIFICATION_ID_INT" 23 | val EXTRA_TITLE = "EXTRA_TITLE" 24 | val EXTRA_BODY = "EXTRA_BODY" 25 | val EXTRA_TIMESTAMP_LONG = "EXTRA_TIMESTAMP_LONG" 26 | } 27 | 28 | /** 29 | * Implement this method to learn about new notifications as they are posted by apps. 30 | * 31 | * @param sbn A data structure encapsulating the original [android.app.Notification] 32 | * object as well as its identifying information (tag and id) and source 33 | * (package name). 34 | */ 35 | override fun onNotificationPosted(sbn: StatusBarNotification) { 36 | val notification = sbn.notification 37 | val ticker = notification?.tickerText 38 | val bundle: Bundle? = notification?.extras 39 | val titleObj = bundle?.get("android.title") 40 | val title: String 41 | when (titleObj) { 42 | is String -> title = titleObj 43 | is SpannableString -> title = titleObj.toString() 44 | else -> title = "undefined" 45 | } 46 | val body: String? = bundle?.getCharSequence("android.text").toString() 47 | 48 | val appInfo = applicationContext.packageManager.getApplicationInfo(sbn.packageName, PackageManager.GET_META_DATA) 49 | val appName = applicationContext.packageManager.getApplicationLabel(appInfo) 50 | Timber.d("onNotificationPosted {app=${appName},id=${sbn.id},ticker=$ticker,title=$title,body=$body,posted=${sbn.postTime},package=${sbn.packageName}}") 51 | 52 | val allowedPackages: MutableSet = MainApplication.sharedPrefs.getStringSet(MainApplication.PREFS_KEY_ALLOWED_PACKAGES, mutableSetOf()) 53 | 54 | // broadcast StatusBarNotication (exclude own notifications) 55 | if (sbn.id != ForegroundService.SERVICE_ID 56 | && allowedPackages.contains(sbn.packageName)) { 57 | val intent = Intent(EXTRA_ACTION) 58 | intent.putExtra(EXTRA_NOTIFICATION_ID_INT, sbn.id) 59 | intent.putExtra(EXTRA_APP_NAME, appName) 60 | intent.putExtra(EXTRA_TITLE, title) 61 | intent.putExtra(EXTRA_BODY, body) 62 | intent.putExtra(EXTRA_NOTIFICATION_DISMISSED, false) 63 | intent.putExtra(EXTRA_TIMESTAMP_LONG, sbn.postTime) 64 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent) 65 | } 66 | } 67 | 68 | override fun onNotificationRemoved(sbn: StatusBarNotification) { 69 | super.onNotificationRemoved(sbn) 70 | val notification = sbn.notification 71 | val ticker = notification?.tickerText 72 | val bundle: Bundle? = notification?.extras 73 | val titleObj = bundle?.get("android.title") 74 | val title: String 75 | when (titleObj) { 76 | is String -> title = titleObj 77 | is SpannableString -> title = titleObj.toString() 78 | else -> title = "undefined" 79 | } 80 | val body: String? = bundle?.getCharSequence("android.text").toString() 81 | 82 | val appInfo = applicationContext.packageManager.getApplicationInfo(sbn.packageName, PackageManager.GET_META_DATA) 83 | val appName = applicationContext.packageManager.getApplicationLabel(appInfo) 84 | Timber.d("onNotificationPosted {app=${appName},id=${sbn.id},ticker=$ticker,title=$title,body=$body,posted=${sbn.postTime},package=${sbn.packageName}}") 85 | 86 | val allowedPackages: MutableSet = MainApplication.sharedPrefs.getStringSet(MainApplication.PREFS_KEY_ALLOWED_PACKAGES, mutableSetOf()) 87 | 88 | Timber.d("onNotificationRemoved {app=${applicationContext.packageManager.getApplicationLabel(appInfo)},id=${sbn.id},ticker=$ticker,title=$title,body=$body,posted=${sbn.postTime},package=${sbn.packageName}}") 89 | 90 | // broadcast StatusBarNotication (exclude own notifications) 91 | if (sbn.id != ForegroundService.SERVICE_ID 92 | && allowedPackages.contains(sbn.packageName)) { 93 | val intent = Intent(EXTRA_ACTION) 94 | intent.putExtra(EXTRA_NOTIFICATION_ID_INT, sbn.id) 95 | intent.putExtra(EXTRA_APP_NAME, appName) 96 | intent.putExtra(EXTRA_TITLE, title) 97 | intent.putExtra(EXTRA_BODY, body) 98 | intent.putExtra(EXTRA_NOTIFICATION_DISMISSED, true) 99 | intent.putExtra(EXTRA_TIMESTAMP_LONG, sbn.postTime) 100 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent) 101 | } 102 | } 103 | 104 | 105 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/app/OnBootReceiver.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.app 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.preference.PreferenceManager 8 | 9 | /** 10 | * Invoked after the system boots up 11 | */ 12 | class OnBootReceiver : BroadcastReceiver() { 13 | override fun onReceive(context: Context, intent: Intent?) { 14 | val startAtBoot = PreferenceManager.getDefaultSharedPreferences(context) 15 | .getBoolean(SettingsActivity.PREF_KEY_START_AT_BOOT, false) 16 | if (startAtBoot) { 17 | val intent = Intent(context, ForegroundService::class.java) 18 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 19 | context.startForegroundService(intent) 20 | } else { 21 | context.startService(intent) 22 | } 23 | } 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/app/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.app 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.preference.Preference 6 | import android.preference.PreferenceFragment 7 | import android.preference.PreferenceManager 8 | import android.support.v7.app.AppCompatActivity 9 | import android.widget.Toast 10 | import za.co.mitchwongho.example.esp32.alerts.R 11 | import java.util.regex.Pattern 12 | 13 | /** 14 | * 15 | */ 16 | class SettingsActivity : AppCompatActivity() { 17 | 18 | companion object { 19 | val PREF_KEY_RUN_AS_A_SERVICE = "pref_as_bg_service" 20 | val PREF_KEY_REMOTE_MAC_ADDRESS = "pref_remote_mac_address" 21 | val PREF_KEY_START_AT_BOOT = "pref_start_at_boot" 22 | val PREF_KEY_FLIP_DISPLAY_VERTICALLY = "pref_flip_vertically" 23 | val MAC_PATTERN = Pattern.compile("^([A-F0-9]{2}[:]?){5}[A-F0-9]{2}$") 24 | 25 | class SettingsFragment : PreferenceFragment() { 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | addPreferencesFromResource(R.xml.preferences) 30 | // 31 | // apply persisted value 32 | val sharedPref = PreferenceManager.getDefaultSharedPreferences(activity) 33 | setRemoteMACAddressPrefSummary(sharedPref.getString(PREF_KEY_REMOTE_MAC_ADDRESS, "00:00:00:00:00:00")) 34 | // 35 | // validate updates and apply is valid 36 | findPreference(PREF_KEY_REMOTE_MAC_ADDRESS).setOnPreferenceChangeListener({ preference: Preference?, value: Any? -> 37 | val mac = (value as String).trim() 38 | if (MAC_PATTERN.matcher(mac).find()) { 39 | setRemoteMACAddressPrefSummary(mac) 40 | true 41 | } else { 42 | Toast.makeText(activity, R.string.mac_format_error, Toast.LENGTH_LONG).show() 43 | false 44 | } 45 | }) 46 | } 47 | 48 | fun setRemoteMACAddressPrefSummary(summary: String) { 49 | val pref = findPreference(PREF_KEY_REMOTE_MAC_ADDRESS) 50 | pref.summary = summary 51 | } 52 | } 53 | } 54 | 55 | override fun onCreate(savedInstanceState: Bundle?) { 56 | super.onCreate(savedInstanceState) 57 | fragmentManager.beginTransaction() 58 | .replace(android.R.id.content, SettingsActivity.Companion.SettingsFragment()) 59 | .commit() 60 | } 61 | 62 | override fun onStart() { 63 | super.onStart() 64 | stopService(Intent(this, ForegroundService::class.java)) 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/ble/LEManager.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.ble 2 | 3 | import android.bluetooth.BluetoothGatt 4 | import android.bluetooth.BluetoothGattCharacteristic 5 | import android.bluetooth.BluetoothGattService 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.os.BatteryManager 10 | import android.preference.PreferenceManager 11 | import no.nordicsemi.android.ble.BleManager 12 | import no.nordicsemi.android.ble.Request 13 | import no.nordicsemi.android.ble.callback.DataReceivedCallback 14 | import no.nordicsemi.android.ble.callback.FailCallback 15 | import no.nordicsemi.android.ble.callback.MtuCallback 16 | import no.nordicsemi.android.ble.callback.SuccessCallback 17 | import timber.log.Timber 18 | import za.co.mitchwongho.example.esp32.alerts.app.ForegroundService 19 | import za.co.mitchwongho.example.esp32.alerts.app.SettingsActivity 20 | import java.util.* 21 | 22 | /** 23 | * Implements BLEManager 24 | */ 25 | class LEManager : BleManager { 26 | 27 | var espDisplayMessageCharacteristic: BluetoothGattCharacteristic? = null 28 | var espDisplayTimeCharacteristic: BluetoothGattCharacteristic? = null 29 | var espDisplayOrientationCharacteristic: BluetoothGattCharacteristic? = null 30 | 31 | companion object { 32 | val MTU = 500 33 | val ESP_SERVICE_UUID = UUID.fromString("3db02924-b2a6-4d47-be1f-0f90ad62a048") 34 | val ESP_DISPLAY_MESSAGE_CHARACTERISITC_UUID = UUID.fromString("8d8218b6-97bc-4527-a8db-13094ac06b1d") 35 | val ESP_DISPLAY_TIME_CHARACTERISITC_UUID = UUID.fromString("b7b0a14b-3e94-488f-b262-5d584a1ef9e1") 36 | val ESP_DISPLAY_ORIENTATION_CHARACTERISITC_UUID = UUID.fromString("0070b87e-d825-43f5-be0c-7d86f75e4900") 37 | 38 | fun readBatteryLevel(context: Context): Int { 39 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) 40 | val batteryStatus: Intent? = context.registerReceiver(null, intentFilter) 41 | val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 42 | val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1 43 | 44 | val batteryLevelPercent: Int = ((level.toFloat() / scale.toFloat()) * 100f).toInt() 45 | Timber.d("readTimeAndBatt {level=$level,scale=$scale,batteryLevel=$batteryLevelPercent%}") 46 | return batteryLevelPercent 47 | } 48 | } 49 | 50 | constructor(context: Context) : super(context) 51 | 52 | /** 53 | * This method must return the gatt callback used by the manager. 54 | * This method must not create a new gatt callback each time it is being invoked, but rather return a single object. 55 | * 56 | * @return the gatt callback object 57 | */ 58 | override fun getGattCallback(): BleManagerGattCallback { 59 | return callback 60 | } 61 | 62 | /** 63 | * Write {@code message} to the remote device's characteristic 64 | */ 65 | fun writeNotification(message: String): Boolean { 66 | return write(message) 67 | } 68 | 69 | /** 70 | * Write {@code message} to the remote device's characteristic 71 | */ 72 | fun writeTimeAndBatt(message: String): Boolean { 73 | // 74 | // read battery level 75 | val batteryLevelPercent = Companion.readBatteryLevel(context) 76 | return writeTimeAndBatteryLevel(batteryLevelPercent, message) 77 | } 78 | 79 | private fun write(message: String): Boolean { 80 | Timber.d("write {connected=$isConnected,hasCharacteristic=${espDisplayMessageCharacteristic != null}}") 81 | return if (isConnected && espDisplayMessageCharacteristic != null) { 82 | requestMtu(MTU).enqueue() 83 | writeCharacteristic(espDisplayMessageCharacteristic, message.toByteArray()).enqueue() 84 | true 85 | } else { 86 | false 87 | } 88 | } 89 | private fun writeTimeAndBatteryLevel(battLevel: Int, message: String): Boolean { 90 | Timber.d("write {connected=$isConnected,hasCharacteristic=${espDisplayMessageCharacteristic != null}}") 91 | return if (isConnected && espDisplayTimeCharacteristic != null) { 92 | requestMtu(MTU).enqueue() 93 | writeCharacteristic(espDisplayTimeCharacteristic, (battLevel.toChar() + message).toByteArray()).enqueue() 94 | true 95 | } else { 96 | false 97 | } 98 | } 99 | 100 | fun applyDisplayVertifically(): Boolean { 101 | return if (isConnected && espDisplayOrientationCharacteristic != null) { 102 | val displayOrientation = PreferenceManager.getDefaultSharedPreferences(context) 103 | .getBoolean(SettingsActivity.PREF_KEY_FLIP_DISPLAY_VERTICALLY, false) 104 | val flag = if (displayOrientation) 1 else 2 105 | val barray = ByteArray(1) 106 | barray.set(0, flag.toByte()) 107 | writeCharacteristic(espDisplayOrientationCharacteristic, barray).enqueue() 108 | true 109 | } else { 110 | false 111 | } 112 | } 113 | 114 | /** 115 | * Returns whether to connect to the remote device just once (false) or to add the address to white list of devices 116 | * that will be automatically connect as soon as they become available (true). In the latter case, if 117 | * Bluetooth adapter is enabled, Android scans periodically for devices from the white list and if a advertising packet 118 | * is received from such, it tries to connect to it. When the connection is lost, the system will keep trying to reconnect 119 | * to it in. If true is returned, and the connection to the device is lost the [BleManagerCallbacks.onLinklossOccur] 120 | * callback is called instead of [BleManagerCallbacks.onDeviceDisconnected]. 121 | * 122 | * This feature works much better on newer Android phone models and many not work on older phones. 123 | * 124 | * This method should only be used with bonded devices, as otherwise the device may change it's address. 125 | * It will however work also with non-bonded devices with private static address. A connection attempt to 126 | * a device with private resolvable address will fail. 127 | * 128 | * The first connection to a device will always be created with autoConnect flag to false 129 | * (see [BluetoothDevice.connectGatt]). This is to make it quick as the 130 | * user most probably waits for a quick response. However, if this method returned true during first connection and the link was lost, 131 | * the manager will try to reconnect to it using [BluetoothGatt.connect] which forces autoConnect to true . 132 | * 133 | * @return autoConnect flag value 134 | */ 135 | override fun shouldAutoConnect(): Boolean { 136 | return true 137 | } 138 | 139 | /** 140 | * Implements GATTCallback methods 141 | */ 142 | private val callback: BleManagerGattCallback = object : BleManagerGattCallback() { 143 | /** 144 | * This method should return `true` when the gatt device supports the required services. 145 | * 146 | * @param gatt the gatt device with services discovered 147 | * @return `true` when the device has the required service 148 | */ 149 | override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { 150 | val gattService: BluetoothGattService? = gatt.getService(ESP_SERVICE_UUID) 151 | if (espDisplayMessageCharacteristic == null) { 152 | gattService?.getCharacteristic(ESP_DISPLAY_MESSAGE_CHARACTERISITC_UUID)?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE 153 | espDisplayMessageCharacteristic = gattService?.getCharacteristic(ESP_DISPLAY_MESSAGE_CHARACTERISITC_UUID) 154 | } 155 | if (espDisplayTimeCharacteristic == null) { 156 | espDisplayTimeCharacteristic = gattService?.getCharacteristic(ESP_DISPLAY_TIME_CHARACTERISITC_UUID) 157 | } 158 | if (espDisplayOrientationCharacteristic == null) { 159 | espDisplayOrientationCharacteristic = gattService?.getCharacteristic(ESP_DISPLAY_ORIENTATION_CHARACTERISITC_UUID) 160 | } 161 | return gattService != null 162 | && espDisplayMessageCharacteristic != null 163 | && espDisplayTimeCharacteristic != null 164 | && espDisplayOrientationCharacteristic != null 165 | } 166 | 167 | 168 | /** 169 | * This method should set up the request queue needed to initialize the profile. 170 | * Enabling Service Change indications for bonded devices is handled before executing this 171 | * queue. The queue may have requests that are not available, e.g. read an optional 172 | * service when it is not supported by the connected device. Such call will trigger 173 | * {@link Request#fail(FailCallback)}. 174 | *

175 | * This method is called from the main thread when the services has been discovered and 176 | * the device is supported (has required service). 177 | *

178 | * Remember to call {@link Request#enqueue()} for each request. 179 | *

180 | * A sample initialization should look like this: 181 | *

182 |          * @Override
183 |          * protected void initialize() {
184 |          *    requestMtu(MTU)
185 |          *       .with((device, mtu) -> {
186 |          *           ...
187 |          *       })
188 |          *       .enqueue();
189 |          *    setNotificationCallback(characteristic)
190 |          *       .with((device, data) -> {
191 |          *           ...
192 |          *       });
193 |          *    enableNotifications(characteristic)
194 |          *       .done(device -> {
195 |          *           ...
196 |          *       })
197 |          *       .fail((device, status) -> {
198 |          *           ...
199 |          *       })
200 |          *       .enqueue();
201 |          * }
202 |          * 
203 | */ 204 | override fun initialize() { 205 | Timber.i("Initialising...") 206 | 207 | enableNotifications(espDisplayTimeCharacteristic) 208 | .done(SuccessCallback { 209 | Timber.i("Successfully enabled DisplayMessageCharacteristic notifications") 210 | }) 211 | .fail { device, status -> 212 | Timber.w("Failed to enable DisplayMessageCharacteristic notifications") 213 | }.enqueue() 214 | enableIndications(espDisplayMessageCharacteristic) 215 | .done(SuccessCallback { 216 | Timber.i("Successfully wrote message") 217 | }) 218 | .fail(FailCallback { device, status -> 219 | Timber.w("Failed to write message to ${device.address} - status: ${status}") 220 | }) 221 | .enqueue() 222 | 223 | // requestMtu(MTU).enqueue() 224 | setNotificationCallback(espDisplayTimeCharacteristic) 225 | .with(DataReceivedCallback { device, data -> 226 | Timber.i("Data received from ${device.address}") 227 | }) 228 | enableNotifications(espDisplayTimeCharacteristic) 229 | .done(SuccessCallback { 230 | Timber.i("Successfully enabled DisplayTimeCharacteristic notifications") 231 | }) 232 | .fail { device, status -> 233 | Timber.w("Failed to enable DisplayTimeCharacteristic notifications") 234 | }.enqueue() 235 | enableIndications(espDisplayTimeCharacteristic) 236 | .done(SuccessCallback { 237 | Timber.i("Successfully wrote Time & Battery status") 238 | }) 239 | .fail(FailCallback { device, status -> 240 | Timber.w("Failed to write Time & Battery status to ${device.address} - status: ${status}") 241 | }).enqueue() 242 | 243 | val batteryLevelPercent = Companion.readBatteryLevel(context) 244 | writeTimeAndBatteryLevel(batteryLevelPercent, ForegroundService.formatter.format(Date())) 245 | } 246 | 247 | /** 248 | * This method should nullify all services and characteristics of the device. 249 | * It's called when the device is no longer connected, either due to user action 250 | * or a link loss. 251 | */ 252 | override fun onDeviceDisconnected() { 253 | espDisplayMessageCharacteristic = null 254 | espDisplayTimeCharacteristic = null 255 | espDisplayOrientationCharacteristic = null 256 | } 257 | } 258 | } -------------------------------------------------------------------------------- /app/src/main/java/za/co/mitchwongho/example/esp32/alerts/ble/LeManagerCallbacks.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts.ble 2 | 3 | import android.bluetooth.BluetoothDevice 4 | import no.nordicsemi.android.ble.BleManagerCallbacks 5 | import timber.log.Timber 6 | 7 | /** 8 | * Implements the BLEManager callback methods 9 | */ 10 | open class LeManagerCallbacks : BleManagerCallbacks { 11 | 12 | /** 13 | * Called when the Android device started connecting to given device. 14 | * The [.onDeviceConnected] will be called when the device is connected, 15 | * or [.onError] in case of error. 16 | * @param device the device that got connected 17 | */ 18 | override fun onDeviceConnecting(device: BluetoothDevice) { 19 | Timber.d("onDeviceConnecting {address=${device.address},name=${device.name}}") 20 | } 21 | 22 | /** 23 | * Called when the device has been connected. This does not mean that the application may start communication. 24 | * A service discovery will be handled automatically after this call. Service discovery 25 | * may ends up with calling [.onServicesDiscovered] or 26 | * [.onDeviceNotSupported] if required services have not been found. 27 | * @param device the device that got connected 28 | */ 29 | override fun onDeviceConnected(device: BluetoothDevice) { 30 | 31 | Timber.d("onDeviceConnected {address=${device.address},name=${device.name}}") 32 | } 33 | 34 | /** 35 | * Called when user initialized disconnection. 36 | * @param device the device that gets disconnecting 37 | */ 38 | override fun onDeviceDisconnecting(device: BluetoothDevice) { 39 | Timber.d("onDeviceDisconnecting {address=${device.address},name=${device.name}}") 40 | } 41 | 42 | /** 43 | * Called when the device has disconnected (when the callback returned 44 | * [BluetoothGattCallback.onConnectionStateChange] with state DISCONNECTED), 45 | * but ONLY if the [BleManager.shouldAutoConnect] method returned false for this device when it was connecting. 46 | * Otherwise the [.onLinklossOccur] method will be called instead. 47 | * @param device the device that got disconnected 48 | */ 49 | override fun onDeviceDisconnected(device: BluetoothDevice) { 50 | Timber.d("onDeviceDisconnected {address=${device.address},name=${device.name}}") 51 | } 52 | 53 | /** 54 | * This callback is invoked when the Ble Manager lost connection to a device that has been connected 55 | * with autoConnect option (see [BleManager.shouldAutoConnect]. 56 | * Otherwise a [.onDeviceDisconnected] method will be called on such event. 57 | * @param device the device that got disconnected due to a link loss 58 | */ 59 | override fun onLinkLossOccurred(device: BluetoothDevice) { 60 | Timber.d("onLinklossOccur {address=${device.address},name=${device.name}}") 61 | } 62 | 63 | /** 64 | * Called when service discovery has finished and primary services has been found. 65 | * This method is not called if the primary, mandatory services were not found during service discovery. 66 | * For example in the Blood Pressure Monitor, a Blood Pressure service is a primary service and 67 | * Intermediate Cuff Pressure service is a optional secondary service. 68 | * Existence of battery service is not notified by this call. 69 | * 70 | * After successful service discovery the service will initialize all services. 71 | * The [.onDeviceReady] method will be called when the initialization is complete. 72 | * 73 | * @param device the device which services got disconnected 74 | * @param optionalServicesFound 75 | * if `true` the secondary services were also found on the device. 76 | */ 77 | override fun onServicesDiscovered(device: BluetoothDevice, optionalServicesFound: Boolean) { 78 | Timber.d("onServiceDiscovered {address=${device.address},name=${device.name}}") 79 | } 80 | 81 | /** 82 | * Method called when all initialization requests has been completed. 83 | * @param device the device that get ready 84 | */ 85 | override fun onDeviceReady(device: BluetoothDevice) { 86 | Timber.d("onDeviceReady {address=${device.address},name=${device.name}}") 87 | } 88 | 89 | /** 90 | * This method should return true if Battery Level notifications should be enabled on the target device. 91 | * If there is no Battery Service, or the Battery Level characteristic does not have NOTIFY property, 92 | * this method will not be called for this device. 93 | * 94 | * This method may return true only if an activity is bound to the service (to display the information 95 | * to the user), always (e.g. if critical battery level is reported using notifications) or never, if 96 | * such information is not important or the manager wants to control Battery Level notifications on its own. 97 | * @param device target device 98 | * @return true to enabled battery level notifications after connecting to the device, false otherwise 99 | */ 100 | override fun shouldEnableBatteryLevelNotifications(device: BluetoothDevice): Boolean { 101 | // TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 102 | return false 103 | } 104 | 105 | /** 106 | * Called when battery value has been received from the device. 107 | * 108 | * @param value 109 | * the battery value in percent 110 | * @param device the device frm which the battery value has changed 111 | */ 112 | override fun onBatteryValueReceived(device: BluetoothDevice, value: Int) { 113 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 114 | } 115 | 116 | /** 117 | * Called when an [BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION] error occurred and the device bond state is NOT_BONDED 118 | * @param device the device that requires bonding 119 | */ 120 | override fun onBondingRequired(device: BluetoothDevice) { 121 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 122 | } 123 | 124 | /** 125 | * Called when the device has been successfully bonded. 126 | * @param device the device that got bonded 127 | */ 128 | override fun onBonded(device: BluetoothDevice) { 129 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 130 | } 131 | 132 | override fun onBondingFailed(device: BluetoothDevice) { 133 | TODO("not implemented") //To change body of created functions use File | Settings | File Templates. 134 | } 135 | 136 | /** 137 | * Called when a BLE error has occurred 138 | * 139 | * @param message 140 | * the error message 141 | * @param errorCode 142 | * the error code 143 | * @param device the device that caused an error 144 | */ 145 | override fun onError(device: BluetoothDevice, message: String, errorCode: Int) { 146 | Timber.e("onError {address=${device.address},name=${device.name},msg=${message},err=$errorCode}") 147 | } 148 | 149 | /** 150 | * Called when service discovery has finished but the main services were not found on the device. 151 | * @param device the device that failed to connect due to lack of required services 152 | */ 153 | override fun onDeviceNotSupported(device: BluetoothDevice) { 154 | Timber.d("onDeviceNotSupported {address=${device.address},name=${device.name}}") 155 | } 156 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat_espressif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/drawable-hdpi/ic_stat_espressif.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat_espressif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/drawable-mdpi/ic_stat_espressif.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat_espressif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/drawable-xhdpi/ic_stat_espressif.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_espressif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/drawable-xxhdpi/ic_stat_espressif.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_stat_espressif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/drawable-xxxhdpi/ic_stat_espressif.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 31 | 32 | 37 | 38 | 47 | 48 | 49 | 50 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #bf2a34 4 | #73181e 5 | #d9454f 6 | #cfcfcf 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ESP Alerts 3 | ESPNotification Listener 4 | Choose App 5 | Preferences 6 | Settings 7 | Run as a service 8 | Run this application as a service without keeping the app open 9 | Remote MAC address 10 | 00:00:00:00:00:00 11 | Notification access 12 | Grant access to status notifications 13 | Format must be A1:BB:3C:44:DE:9F 14 | Kill service 15 | Start service 16 | Start at boot 17 | Enable to automatically start the service after the operating system boots up 18 | Connection status notifications 19 | Flip display vertically 20 | Enable to flip the display vertically 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | 18 | 24 | -------------------------------------------------------------------------------- /app/src/test/java/za/co/mitchwongho/example/esp32/alerts/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package za.co.mitchwongho.example.esp32.alerts 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.21' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.2' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /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 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchwongho/ESP-Alerts-for-Android/00d16bd81ce51bb14bc0eaa187da1af8630aaebf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Mar 30 13:49:16 SAST 2019 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-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':ble' 3 | project(':ble').projectDir = file('../Android-BLE-Library/ble') --------------------------------------------------------------------------------