mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-20 07:54:20 +01:00
feature: Add new CSC Screen
This commit is contained in:
123
app/.gitignore
vendored
123
app/.gitignore
vendored
@@ -1,123 +0,0 @@
|
|||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/androidstudio
|
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio
|
|
||||||
|
|
||||||
### AndroidStudio ###
|
|
||||||
# Covers files to be ignored for android development using Android Studio.
|
|
||||||
|
|
||||||
# Built application files
|
|
||||||
*.apk
|
|
||||||
*.ap_
|
|
||||||
*.aab
|
|
||||||
|
|
||||||
# Files for the ART/Dalvik VM
|
|
||||||
*.dex
|
|
||||||
|
|
||||||
# Java class files
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Generated files
|
|
||||||
bin/
|
|
||||||
gen/
|
|
||||||
out/
|
|
||||||
|
|
||||||
# Gradle files
|
|
||||||
.gradle
|
|
||||||
.gradle/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# Signing files
|
|
||||||
.signing/
|
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
|
||||||
local.properties
|
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Android Studio
|
|
||||||
/*/build/
|
|
||||||
/*/local.properties
|
|
||||||
/*/out
|
|
||||||
/*/*/build
|
|
||||||
/*/*/production
|
|
||||||
captures/
|
|
||||||
.navigation/
|
|
||||||
*.ipr
|
|
||||||
*~
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# Keystore files
|
|
||||||
*.jks
|
|
||||||
*.keystore
|
|
||||||
|
|
||||||
# Google Services (e.g. APIs or Firebase)
|
|
||||||
# google-services.json
|
|
||||||
|
|
||||||
# Android Patch
|
|
||||||
gen-external-apklibs
|
|
||||||
|
|
||||||
# External native build folder generated in Android Studio 2.2 and later
|
|
||||||
.externalNativeBuild
|
|
||||||
|
|
||||||
# NDK
|
|
||||||
obj/
|
|
||||||
|
|
||||||
# IntelliJ IDEA
|
|
||||||
*.iml
|
|
||||||
*.iws
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# User-specific configurations
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# OS-specific files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Legacy Eclipse project files
|
|
||||||
.classpath
|
|
||||||
.project
|
|
||||||
.cproject
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
|
|
||||||
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
|
|
||||||
hs_err_pid*
|
|
||||||
|
|
||||||
## Plugin-specific files:
|
|
||||||
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/mongoSettings.xml
|
|
||||||
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
|
|
||||||
### AndroidStudio Patch ###
|
|
||||||
|
|
||||||
!/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/androidstudio
|
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 31
|
compileSdk android_api_version
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "no.nordicsemi.android.nrftoolbox"
|
applicationId "no.nordicsemi.android.nrftoolbox"
|
||||||
minSdk 21
|
minSdk android_min_api_version
|
||||||
targetSdk 31
|
targetSdk android_api_version
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
@@ -38,21 +40,32 @@ android {
|
|||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion compose_version
|
kotlinCompilerExtensionVersion compose_version
|
||||||
kotlinCompilerVersion '1.5.21'
|
kotlinCompilerVersion kotlin_version
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
resources {
|
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
|
||||||
}
|
}
|
||||||
|
hilt {
|
||||||
|
enableExperimentalClasspathAggregation = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
//Hilt requires to implement every module in the main app module
|
||||||
|
//https://github.com/google/dagger/issues/2123
|
||||||
|
implementation project(":feature_csc")
|
||||||
|
implementation project(":lib_broadcast")
|
||||||
|
implementation project(":lib_events")
|
||||||
|
implementation project(":lib_theme")
|
||||||
|
implementation project(":lib_scanner")
|
||||||
|
|
||||||
|
implementation libs.nordic.ble.common
|
||||||
|
|
||||||
|
implementation libs.bundles.hilt
|
||||||
|
kapt libs.bundles.hiltkapt
|
||||||
|
|
||||||
implementation libs.bundles.compose
|
implementation libs.bundles.compose
|
||||||
implementation libs.androidx.core
|
implementation libs.androidx.core
|
||||||
implementation libs.material
|
implementation libs.material
|
||||||
implementation libs.lifecycle
|
implementation libs.lifecycle.activity
|
||||||
|
implementation libs.compose.lifecycle
|
||||||
implementation libs.compose.activity
|
implementation libs.compose.activity
|
||||||
|
|
||||||
testImplementation libs.test.junit
|
testImplementation libs.test.junit
|
||||||
|
|||||||
21
app/proguard-rules.pro
vendored
21
app/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package="no.nordicsemi.android.nrftoolbox">
|
package="no.nordicsemi.android.nrftoolbox">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".NrfToolboxApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import no.nordicsemi.android.csc.CSCRoute
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = "home") {
|
||||||
|
composable("home") { HomeView(navController) }
|
||||||
|
composable("csc-route") { CSCRoute() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeView(navHostController: NavController) {
|
||||||
|
Column {
|
||||||
|
TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) })
|
||||||
|
|
||||||
|
FeatureButton(R.drawable.ic_csc, R.string.csc_module) { navHostController.navigate("csc-route") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { onClick() },
|
||||||
|
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(iconId),
|
||||||
|
contentDescription = stringResource(id = nameId),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.White)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = nameId),
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun DefaultPreview() {
|
||||||
|
HomeView(rememberNavController())
|
||||||
|
}
|
||||||
@@ -5,34 +5,21 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import androidx.compose.runtime.Composable
|
import no.nordicsemi.android.theme.TestTheme
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import no.nordicsemi.android.nrftoolbox.ui.theme.TestTheme
|
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
TestTheme {
|
TestTheme {
|
||||||
// A surface container using the 'background' color from the theme
|
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background) {
|
||||||
Greeting("Android")
|
HomeScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String) {
|
|
||||||
Text(text = "Hello $name!")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun DefaultPreview() {
|
|
||||||
TestTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class NrfToolboxApplication : Application() {
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
val Purple200 = Color(0xFFBB86FC)
|
|
||||||
val Purple500 = Color(0xFF6200EE)
|
|
||||||
val Purple700 = Color(0xFF3700B3)
|
|
||||||
val Teal200 = Color(0xFF03DAC5)
|
|
||||||
9
app/src/main/res/drawable/ic_csc.xml
Normal file
9
app/src/main/res/drawable/ic_csc.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="80dp"
|
||||||
|
android:height="80dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#00B3DC"
|
||||||
|
android:pathData="M757,436l-65,-207.4c-3.3,-10.4 -12.9,-17.5 -23.8,-17.5H564.8c-13.8,0 -24.9,11.2 -24.9,24.9c0,13.8 11.2,24.9 24.9,24.9h85l28.4,90.6l-242.5,55.2L414.3,310h16.4c13.8,0 24.9,-11.2 24.9,-24.9s-11.2,-24.9 -24.9,-24.9H295.8c-13.8,0 -24.9,11.2 -24.9,24.9s11.2,24.9 24.9,24.9h67.4l24,107.9l-78.3,17.8c-2.4,0.6 -4.7,1.5 -6.8,2.6c-11.2,-2.1 -22.7,-3.3 -34.6,-3.3c-101.7,0 -184.4,82.7 -184.4,184.4s82.7,184.4 184.4,184.4s184.4,-82.7 184.4,-184.4c0,-60.6 -29.4,-114.5 -74.8,-148.2l316.1,-72l12.2,38.8c-86,15.6 -151.4,91 -151.4,181.4c0,101.7 82.7,184.4 184.4,184.4s184.4,-82.7 184.4,-184.4C922.8,524 850,445.4 757,436zM402.1,619.4c0,74.2 -60.3,134.5 -134.5,134.5s-134.5,-60.3 -134.5,-134.5s60.3,-134.5 134.5,-134.5S402.1,545.2 402.1,619.4zM738.4,753.9c-74.2,0 -134.5,-60.3 -134.5,-134.5c0,-74.1 60.3,-134.4 134.4,-134.5c0,0 0.1,0 0.1,0c0,0 0.1,0 0.1,0c74.1,0.1 134.4,60.4 134.4,134.5C872.9,693.6 812.6,753.9 738.4,753.9z" />
|
||||||
|
</vector>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Test" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Test" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
<item name="colorOnPrimary">@color/colorOnPrimary</item>
|
||||||
<!-- Secondary brand color. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/colorSecondary</item>
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
<item name="colorOnSecondary">@color/colorOnSecondary</item>
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Test</string>
|
<string name="app_name">nRF Toolbox</string>
|
||||||
|
|
||||||
|
<string name="csc_module">CSC</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
compose_version = '1.0.2'
|
compose_version = '1.1.0-alpha03'
|
||||||
|
kotlin_version = '1.5.30'
|
||||||
|
android_api_version = 31
|
||||||
|
android_min_api_version = 21
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -9,8 +12,9 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.0.2"
|
classpath "com.android.tools.build:gradle:7.0.2"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE"
|
classpath "io.spring.gradle:dependency-management-plugin:1.0.11.RELEASE"
|
||||||
|
classpath "com.google.dagger:hilt-android-gradle-plugin:2.38.1"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
27
feature_csc/build.gradle
Normal file
27
feature_csc/build.gradle
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":lib_broadcast")
|
||||||
|
implementation project(":lib_events")
|
||||||
|
implementation project(":lib_theme")
|
||||||
|
implementation project(":lib_scanner")
|
||||||
|
|
||||||
|
implementation libs.nordic.ble.common
|
||||||
|
|
||||||
|
implementation libs.nordic.log
|
||||||
|
|
||||||
|
implementation libs.bundles.compose
|
||||||
|
implementation libs.androidx.core
|
||||||
|
implementation libs.material
|
||||||
|
implementation libs.lifecycle.activity
|
||||||
|
implementation libs.lifecycle.service
|
||||||
|
implementation libs.compose.lifecycle
|
||||||
|
implementation libs.compose.activity
|
||||||
|
|
||||||
|
testImplementation libs.test.junit
|
||||||
|
androidTestImplementation libs.android.test.junit
|
||||||
|
androidTestImplementation libs.android.test.espresso
|
||||||
|
androidTestImplementation libs.android.test.compose.ui
|
||||||
|
debugImplementation libs.android.test.compose.tooling
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.csc
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("no.nordicsemi.android.csc.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
feature_csc/src/main/AndroidManifest.xml
Normal file
16
feature_csc/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="no.nordicsemi.android.csc">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
tools:ignore="CoarseFineLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service android:name=".service.CSCService" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package no.nordicsemi.android.csc
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import no.nordicsemi.android.csc.view.CscScreen
|
||||||
|
import no.nordicsemi.android.scanner.ScannerRoute
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CSCRoute() {
|
||||||
|
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = "csc_screen") {
|
||||||
|
composable("csc_screen") { CscScreen(navController) }
|
||||||
|
composable("scanner-destination") { ScannerRoute(navController) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package no.nordicsemi.android.csc.batery
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.IntRange
|
||||||
|
import no.nordicsemi.android.ble.callback.DataReceivedCallback
|
||||||
|
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
|
||||||
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
import no.nordicsemi.android.csc.service.BatteryManagerCallbacks
|
||||||
|
import no.nordicsemi.android.log.LogContract
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Ble Manager with Battery Service support.
|
||||||
|
*
|
||||||
|
* @param <T> The profile callbacks type.
|
||||||
|
* @see BleManager
|
||||||
|
</T> */
|
||||||
|
abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) : LoggableBleManager<T>(context) {
|
||||||
|
|
||||||
|
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
/**
|
||||||
|
* Returns the last received Battery Level value.
|
||||||
|
* The value is set to null when the device disconnects.
|
||||||
|
* @return Battery Level value, in percent.
|
||||||
|
*/
|
||||||
|
/** Last received Battery Level value. */
|
||||||
|
var batteryLevel: Int? = null
|
||||||
|
private set
|
||||||
|
private val batteryLevelDataCallback: DataReceivedCallback =
|
||||||
|
object : BatteryLevelDataCallback() {
|
||||||
|
override fun onBatteryLevelChanged(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
@IntRange(from = 0, to = 100) batteryLevel: Int
|
||||||
|
) {
|
||||||
|
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
|
||||||
|
this@BatteryManager.batteryLevel = batteryLevel
|
||||||
|
mCallbacks!!.onBatteryLevelChanged(device, batteryLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
|
log(Log.WARN, "Invalid Battery Level data received: $data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readBatteryLevelCharacteristic() {
|
||||||
|
if (isConnected) {
|
||||||
|
readCharacteristic(batteryLevelCharacteristic)
|
||||||
|
.with(batteryLevelDataCallback)
|
||||||
|
.fail { device: BluetoothDevice?, status: Int ->
|
||||||
|
log(
|
||||||
|
Log.WARN,
|
||||||
|
"Battery Level characteristic not found"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableBatteryLevelCharacteristicNotifications() {
|
||||||
|
if (isConnected) {
|
||||||
|
// If the Battery Level characteristic is null, the request will be ignored
|
||||||
|
setNotificationCallback(batteryLevelCharacteristic)
|
||||||
|
.with(batteryLevelDataCallback)
|
||||||
|
enableNotifications(batteryLevelCharacteristic)
|
||||||
|
.done { device: BluetoothDevice? ->
|
||||||
|
log(
|
||||||
|
Log.INFO,
|
||||||
|
"Battery Level notifications enabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fail { device: BluetoothDevice?, status: Int ->
|
||||||
|
log(
|
||||||
|
Log.WARN,
|
||||||
|
"Battery Level characteristic not found"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables Battery Level notifications on the Server.
|
||||||
|
*/
|
||||||
|
fun disableBatteryLevelCharacteristicNotifications() {
|
||||||
|
if (isConnected) {
|
||||||
|
disableNotifications(batteryLevelCharacteristic)
|
||||||
|
.done { device: BluetoothDevice? ->
|
||||||
|
log(
|
||||||
|
Log.INFO,
|
||||||
|
"Battery Level notifications disabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
|
||||||
|
override fun initialize() {
|
||||||
|
readBatteryLevelCharacteristic()
|
||||||
|
enableBatteryLevelCharacteristicNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||||
|
val service = gatt.getService(BATTERY_SERVICE_UUID)
|
||||||
|
if (service != null) {
|
||||||
|
batteryLevelCharacteristic = service.getCharacteristic(
|
||||||
|
BATTERY_LEVEL_CHARACTERISTIC_UUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return batteryLevelCharacteristic != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeviceDisconnected() {
|
||||||
|
batteryLevelCharacteristic = null
|
||||||
|
batteryLevel = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Battery Service UUID. */
|
||||||
|
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
|
/** Battery Level characteristic UUID. */
|
||||||
|
private val BATTERY_LEVEL_CHARACTERISTIC_UUID =
|
||||||
|
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Nordic Semiconductor
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package no.nordicsemi.android.csc.batery
|
||||||
|
|
||||||
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
|
||||||
|
object CSCMeasurementParser {
|
||||||
|
|
||||||
|
private const val WHEEL_REV_DATA_PRESENT: Byte = 0x01 // 1 bit
|
||||||
|
private const val CRANK_REV_DATA_PRESENT: Byte = 0x02 // 1 bit
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun parse(data: Data): String {
|
||||||
|
var offset = 0
|
||||||
|
val flags = data.getByte(offset)!!.toInt() // 1 byte
|
||||||
|
offset += 1
|
||||||
|
val wheelRevPresent = flags and WHEEL_REV_DATA_PRESENT.toInt() > 0
|
||||||
|
val crankRevPreset = flags and CRANK_REV_DATA_PRESENT.toInt() > 0
|
||||||
|
var wheelRevolutions = 0
|
||||||
|
var lastWheelEventTime = 0
|
||||||
|
if (wheelRevPresent) {
|
||||||
|
wheelRevolutions = data.getIntValue(Data.FORMAT_UINT32, offset)!!
|
||||||
|
offset += 4
|
||||||
|
lastWheelEventTime = data.getIntValue(Data.FORMAT_UINT16, offset)!! // 1/1024 s
|
||||||
|
offset += 2
|
||||||
|
}
|
||||||
|
var crankRevolutions = 0
|
||||||
|
var lastCrankEventTime = 0
|
||||||
|
if (crankRevPreset) {
|
||||||
|
crankRevolutions = data.getIntValue(Data.FORMAT_UINT16, offset)!!
|
||||||
|
offset += 2
|
||||||
|
lastCrankEventTime = data.getIntValue(Data.FORMAT_UINT16, offset)!!
|
||||||
|
//offset += 2;
|
||||||
|
}
|
||||||
|
val builder = StringBuilder()
|
||||||
|
if (wheelRevPresent) {
|
||||||
|
builder.append("Wheel rev: ").append(wheelRevolutions).append(",\n")
|
||||||
|
builder.append("Last wheel event time: ").append(lastWheelEventTime).append(",\n")
|
||||||
|
}
|
||||||
|
if (crankRevPreset) {
|
||||||
|
builder.append("Crank rev: ").append(crankRevolutions).append(",\n")
|
||||||
|
builder.append("Last crank event time: ").append(lastCrankEventTime).append(",\n")
|
||||||
|
}
|
||||||
|
if (!wheelRevPresent && !crankRevPreset) {
|
||||||
|
builder.append("No wheel or crank data")
|
||||||
|
}
|
||||||
|
builder.setLength(builder.length - 2)
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package no.nordicsemi.android.csc.batery
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import no.nordicsemi.android.ble.BleManagerCallbacks
|
||||||
|
import no.nordicsemi.android.ble.LegacyBleManager
|
||||||
|
import no.nordicsemi.android.log.ILogSession
|
||||||
|
import no.nordicsemi.android.log.LogContract
|
||||||
|
import no.nordicsemi.android.log.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The manager that logs to nRF Logger. If nRF Logger is not installed, logs are ignored.
|
||||||
|
*
|
||||||
|
* @param <T> the callbacks class.
|
||||||
|
</T> */
|
||||||
|
abstract class LoggableBleManager<T : BleManagerCallbacks?>
|
||||||
|
/**
|
||||||
|
* The manager constructor.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* After constructing the manager, the callbacks object must be set with
|
||||||
|
* [.setGattCallbacks].
|
||||||
|
*
|
||||||
|
* @param context the context.
|
||||||
|
*/
|
||||||
|
(context: Context) : LegacyBleManager<T>(context) {
|
||||||
|
private var logSession: ILogSession? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the log session to log into.
|
||||||
|
*
|
||||||
|
* @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed.
|
||||||
|
*/
|
||||||
|
fun setLogger(session: ILogSession?) {
|
||||||
|
logSession = session
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun log(priority: Int, message: String) {
|
||||||
|
Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message)
|
||||||
|
Log.println(priority, "BleManager", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
|
import no.nordicsemi.android.ble.BleManagerCallbacks
|
||||||
|
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback
|
||||||
|
|
||||||
|
interface BatteryManagerCallbacks : BleManagerCallbacks, BatteryLevelCallback
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Nordic Semiconductor
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package no.nordicsemi.android.csc.service;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.lifecycle.LifecycleService;
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
|
import no.nordicsemi.android.ble.BleManager;
|
||||||
|
import no.nordicsemi.android.ble.BleManagerCallbacks;
|
||||||
|
import no.nordicsemi.android.ble.utils.ILogger;
|
||||||
|
import no.nordicsemi.android.csc.R;
|
||||||
|
import no.nordicsemi.android.csc.batery.LoggableBleManager;
|
||||||
|
import no.nordicsemi.android.log.ILogSession;
|
||||||
|
import no.nordicsemi.android.log.Logger;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public abstract class BleProfileService extends LifecycleService implements BleManagerCallbacks {
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static final String TAG = "BleProfileService";
|
||||||
|
|
||||||
|
public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE";
|
||||||
|
public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED";
|
||||||
|
public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY";
|
||||||
|
public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE";
|
||||||
|
@Deprecated
|
||||||
|
public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL";
|
||||||
|
public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS";
|
||||||
|
/**
|
||||||
|
* The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}.
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME";
|
||||||
|
public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE";
|
||||||
|
public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI";
|
||||||
|
public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE";
|
||||||
|
public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE";
|
||||||
|
public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY";
|
||||||
|
public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY";
|
||||||
|
@Deprecated
|
||||||
|
public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL";
|
||||||
|
public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE";
|
||||||
|
public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE";
|
||||||
|
|
||||||
|
public static final int STATE_LINK_LOSS = -1;
|
||||||
|
public static final int STATE_DISCONNECTED = 0;
|
||||||
|
public static final int STATE_CONNECTED = 1;
|
||||||
|
public static final int STATE_CONNECTING = 2;
|
||||||
|
public static final int STATE_DISCONNECTING = 3;
|
||||||
|
|
||||||
|
private LoggableBleManager<BleManagerCallbacks> bleManager;
|
||||||
|
private Handler handler;
|
||||||
|
|
||||||
|
protected boolean bound;
|
||||||
|
private boolean activityIsChangingConfiguration;
|
||||||
|
private BluetoothDevice bluetoothDevice;
|
||||||
|
private String deviceName;
|
||||||
|
private ILogSession logSession;
|
||||||
|
|
||||||
|
private final BroadcastReceiver bluetoothStateBroadcastReceiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
|
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
|
||||||
|
final ILogger logger = getBinder();
|
||||||
|
|
||||||
|
final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state);
|
||||||
|
logger.log(Log.DEBUG, stateString);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothAdapter.STATE_ON:
|
||||||
|
onBluetoothEnabled();
|
||||||
|
break;
|
||||||
|
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||||
|
case BluetoothAdapter.STATE_OFF:
|
||||||
|
onBluetoothDisabled();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String state2String(final int state) {
|
||||||
|
switch (state) {
|
||||||
|
case BluetoothAdapter.STATE_TURNING_ON:
|
||||||
|
return "TURNING ON";
|
||||||
|
case BluetoothAdapter.STATE_ON:
|
||||||
|
return "ON";
|
||||||
|
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||||
|
return "TURNING OFF";
|
||||||
|
case BluetoothAdapter.STATE_OFF:
|
||||||
|
return "OFF";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN (" + state + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public class LocalBinder extends Binder implements ILogger {
|
||||||
|
/**
|
||||||
|
* Disconnects from the sensor.
|
||||||
|
*/
|
||||||
|
public final void disconnect() {
|
||||||
|
final int state = bleManager.getConnectionState();
|
||||||
|
if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) {
|
||||||
|
bleManager.close();
|
||||||
|
onDeviceDisconnected(bluetoothDevice);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bleManager.disconnect().enqueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the bound activity if changing configuration or not.
|
||||||
|
* If <code>false</code>, we will turn off battery level notifications in onUnbind(..) method below.
|
||||||
|
*
|
||||||
|
* @param changing true if the bound activity is finishing
|
||||||
|
*/
|
||||||
|
public void setActivityIsChangingConfiguration(final boolean changing) {
|
||||||
|
activityIsChangingConfiguration = changing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device address
|
||||||
|
*
|
||||||
|
* @return device address
|
||||||
|
*/
|
||||||
|
public String getDeviceAddress() {
|
||||||
|
return bluetoothDevice.getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device name
|
||||||
|
*
|
||||||
|
* @return the device name
|
||||||
|
*/
|
||||||
|
public String getDeviceName() {
|
||||||
|
return deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Bluetooth device
|
||||||
|
*
|
||||||
|
* @return the Bluetooth device
|
||||||
|
*/
|
||||||
|
public BluetoothDevice getBluetoothDevice() {
|
||||||
|
return bluetoothDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the device is connected to the sensor.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
public boolean isConnected() {
|
||||||
|
return bleManager.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the connection state of given device.
|
||||||
|
*
|
||||||
|
* @return the connection state, as in {@link BleManager#getConnectionState()}.
|
||||||
|
*/
|
||||||
|
public int getConnectionState() {
|
||||||
|
return bleManager.getConnectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the log session that can be used to append log entries.
|
||||||
|
* The log session is created when the service is being created.
|
||||||
|
* The method returns <code>null</code> if the nRF Logger app was not installed.
|
||||||
|
*
|
||||||
|
* @return the log session
|
||||||
|
*/
|
||||||
|
public ILogSession getLogSession() {
|
||||||
|
return logSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void log(final int level, @NonNull final String message) {
|
||||||
|
Logger.log(logSession, level, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void log(final int level, final @StringRes int messageRes, final Object... params) {
|
||||||
|
Logger.log(logSession, level, messageRes, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a handler that is created in onCreate().
|
||||||
|
* The handler may be used to postpone execution of some operations or to run them in UI thread.
|
||||||
|
*/
|
||||||
|
protected Handler getHandler() {
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity.
|
||||||
|
*
|
||||||
|
* @return the service binder
|
||||||
|
*/
|
||||||
|
protected LocalBinder getBinder() {
|
||||||
|
// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
|
||||||
|
return new LocalBinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(final Intent intent) {
|
||||||
|
bound = true;
|
||||||
|
return getBinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onRebind(final Intent intent) {
|
||||||
|
bound = true;
|
||||||
|
|
||||||
|
if (!activityIsChangingConfiguration)
|
||||||
|
onRebind();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the activity has rebound to the service after being recreated.
|
||||||
|
* This method is not called when the activity was killed to be recreated when the phone orientation changed
|
||||||
|
* if prior to being killed called {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true.
|
||||||
|
*/
|
||||||
|
protected void onRebind() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final boolean onUnbind(final Intent intent) {
|
||||||
|
bound = false;
|
||||||
|
|
||||||
|
if (!activityIsChangingConfiguration)
|
||||||
|
onUnbind();
|
||||||
|
|
||||||
|
// We want the onRebind method be called if anything else binds to it again
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the activity has unbound from the service before being finished.
|
||||||
|
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
|
||||||
|
*/
|
||||||
|
protected void onUnbind() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
handler = new Handler();
|
||||||
|
|
||||||
|
// Initialize the manager
|
||||||
|
bleManager = initializeManager();
|
||||||
|
bleManager.setGattCallbacks(this);
|
||||||
|
|
||||||
|
// Register broadcast receivers
|
||||||
|
registerReceiver(bluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
|
||||||
|
|
||||||
|
// Service has now been created
|
||||||
|
onServiceCreated();
|
||||||
|
|
||||||
|
// Call onBluetoothEnabled if Bluetooth enabled
|
||||||
|
final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
if (bluetoothAdapter.isEnabled()) {
|
||||||
|
onBluetoothEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the service has been created, before the {@link #onBluetoothEnabled()} is called.
|
||||||
|
*/
|
||||||
|
protected void onServiceCreated() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Ble Manager responsible for connecting to a single device.
|
||||||
|
*
|
||||||
|
* @return a new BleManager object
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
protected abstract LoggableBleManager initializeManager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method returns whether autoConnect option should be used.
|
||||||
|
*
|
||||||
|
* @return true to use autoConnect feature, false (default) otherwise.
|
||||||
|
*/
|
||||||
|
protected boolean shouldAutoConnect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
||||||
|
if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS))
|
||||||
|
throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key");
|
||||||
|
|
||||||
|
final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI);
|
||||||
|
logSession = Logger.openSession(getApplicationContext(), logUri);
|
||||||
|
deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
|
||||||
|
|
||||||
|
Logger.i(logSession, "Service started");
|
||||||
|
|
||||||
|
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS);
|
||||||
|
bluetoothDevice = adapter.getRemoteDevice(deviceAddress);
|
||||||
|
|
||||||
|
bleManager.setLogger(logSession);
|
||||||
|
onServiceStarted();
|
||||||
|
bleManager.connect(bluetoothDevice)
|
||||||
|
.useAutoConnect(shouldAutoConnect())
|
||||||
|
.retry(3, 100)
|
||||||
|
.enqueue();
|
||||||
|
return START_REDELIVER_INTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the service has been started. The device name and address are set.
|
||||||
|
* The BLE Manager will try to connect to the device after this method finishes.
|
||||||
|
*/
|
||||||
|
protected void onServiceStarted() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTaskRemoved(final Intent rootIntent) {
|
||||||
|
super.onTaskRemoved(rootIntent);
|
||||||
|
// This method is called when user removed the app from Recents.
|
||||||
|
// By default, the service will be killed and recreated immediately after that.
|
||||||
|
// However, all managed devices will be lost and devices will be disconnected.
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
// Unregister broadcast receivers
|
||||||
|
unregisterReceiver(bluetoothStateBroadcastReceiver);
|
||||||
|
|
||||||
|
// shutdown the manager
|
||||||
|
bleManager.close();
|
||||||
|
Logger.i(logSession, "Service destroyed");
|
||||||
|
bleManager = null;
|
||||||
|
bluetoothDevice = null;
|
||||||
|
deviceName = null;
|
||||||
|
logSession = null;
|
||||||
|
handler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when Bluetooth Adapter has been disabled.
|
||||||
|
*/
|
||||||
|
protected void onBluetoothDisabled() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when Bluetooth Adapter has been enabled and
|
||||||
|
* after the service was created if Bluetooth Adapter was enabled at that moment.
|
||||||
|
* This method could initialize all Bluetooth related features, for example open the GATT server.
|
||||||
|
*/
|
||||||
|
protected void onBluetoothEnabled() {
|
||||||
|
// empty default implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceConnecting(@NonNull final BluetoothDevice device) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceConnected(@NonNull final BluetoothDevice device) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) {
|
||||||
|
// Notify user about changing the state to DISCONNECTING
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
|
||||||
|
* In that case the {@link #stopService()} method must be called when done.
|
||||||
|
*
|
||||||
|
* @return true (default) to automatically stop the service when device is disconnected. False otherwise.
|
||||||
|
*/
|
||||||
|
protected boolean stopWhenDisconnected() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceDisconnected(@NonNull final BluetoothDevice device) {
|
||||||
|
// Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above
|
||||||
|
|
||||||
|
// Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be
|
||||||
|
// invoked ONLY when user requested disconnection (using Disconnect button). If the device
|
||||||
|
// disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead.
|
||||||
|
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
|
||||||
|
if (stopWhenDisconnected())
|
||||||
|
stopService();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void stopService() {
|
||||||
|
// user requested disconnection. We must stop the service
|
||||||
|
Logger.v(logSession, "Stopping service...");
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLinkLossOccurred(@NonNull final BluetoothDevice device) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true);
|
||||||
|
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceReady(@NonNull final BluetoothDevice device) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_DEVICE_READY);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceNotSupported(@NonNull final BluetoothDevice device) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false);
|
||||||
|
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
|
||||||
|
// no need for disconnecting, it will be disconnected by the manager automatically
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBondingRequired(@NonNull final BluetoothDevice device) {
|
||||||
|
showToast(R.string.csc_bonding);
|
||||||
|
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBonded(@NonNull final BluetoothDevice device) {
|
||||||
|
showToast(R.string.csc_bonded);
|
||||||
|
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBondingFailed(@NonNull final BluetoothDevice device) {
|
||||||
|
showToast(R.string.csc_bonding_failed);
|
||||||
|
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_BOND_STATE);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) {
|
||||||
|
final Intent broadcast = new Intent(BROADCAST_ERROR);
|
||||||
|
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice);
|
||||||
|
broadcast.putExtra(EXTRA_ERROR_MESSAGE, message);
|
||||||
|
broadcast.putExtra(EXTRA_ERROR_CODE, errorCode);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
||||||
|
*
|
||||||
|
* @param messageResId an resource id of the message to be shown
|
||||||
|
*/
|
||||||
|
protected void showToast(final int messageResId) {
|
||||||
|
handler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
||||||
|
*
|
||||||
|
* @param message a message to be shown
|
||||||
|
*/
|
||||||
|
protected void showToast(final String message) {
|
||||||
|
handler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the log session that can be used to append log entries. The method returns <code>null</code> if the nRF Logger app was not installed. It is safe to use logger when
|
||||||
|
* {@link #onServiceStarted()} has been called.
|
||||||
|
*
|
||||||
|
* @return the log session
|
||||||
|
*/
|
||||||
|
protected ILogSession getLogSession() {
|
||||||
|
return logSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device address
|
||||||
|
*
|
||||||
|
* @return device address
|
||||||
|
*/
|
||||||
|
protected String getDeviceAddress() {
|
||||||
|
return bluetoothDevice.getAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Bluetooth device object
|
||||||
|
*
|
||||||
|
* @return bluetooth device
|
||||||
|
*/
|
||||||
|
protected BluetoothDevice getBluetoothDevice() {
|
||||||
|
return bluetoothDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the device name
|
||||||
|
*
|
||||||
|
* @return the device name
|
||||||
|
*/
|
||||||
|
protected String getDeviceName() {
|
||||||
|
return deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns <code>true</code> if the device is connected to the sensor.
|
||||||
|
*
|
||||||
|
* @return <code>true</code> if device is connected to the sensor, <code>false</code> otherwise
|
||||||
|
*/
|
||||||
|
protected boolean isConnected() {
|
||||||
|
return bleManager != null && bleManager.isConnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Nordic Semiconductor
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
||||||
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
import no.nordicsemi.android.csc.batery.BatteryManager
|
||||||
|
import no.nordicsemi.android.csc.batery.CSCMeasurementParser.parse
|
||||||
|
import no.nordicsemi.android.log.LogContract
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val SETTINGS_WHEEL_SIZE_DEFAULT = 2340
|
||||||
|
|
||||||
|
internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks?>(context) {
|
||||||
|
|
||||||
|
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
private var wheelSize = SETTINGS_WHEEL_SIZE_DEFAULT
|
||||||
|
|
||||||
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
|
return CSCManagerGattCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWheelSize(value: Int) {
|
||||||
|
wheelSize = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BluetoothGatt callbacks for connection/disconnection, service discovery,
|
||||||
|
* receiving indication, etc.
|
||||||
|
*/
|
||||||
|
private inner class CSCManagerGattCallback : BatteryManagerGattCallback() {
|
||||||
|
override fun initialize() {
|
||||||
|
super.initialize()
|
||||||
|
|
||||||
|
// CSC characteristic is required
|
||||||
|
setNotificationCallback(cscMeasurementCharacteristic)
|
||||||
|
.with(object : CyclingSpeedAndCadenceMeasurementDataCallback() {
|
||||||
|
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
|
log(LogContract.Log.Level.APPLICATION, "\"" + parse(data) + "\" received")
|
||||||
|
|
||||||
|
// Pass through received data
|
||||||
|
super.onDataReceived(device, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getWheelCircumference(): Float {
|
||||||
|
return wheelSize.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDistanceChanged(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
@FloatRange(from = 0.0) totalDistance: Float,
|
||||||
|
@FloatRange(from = 0.0) distance: Float,
|
||||||
|
@FloatRange(from = 0.0) speed: Float
|
||||||
|
) {
|
||||||
|
mCallbacks!!.onDistanceChanged(device, totalDistance, distance, speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCrankDataChanged(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
@FloatRange(from = 0.0) crankCadence: Float,
|
||||||
|
gearRatio: Float
|
||||||
|
) {
|
||||||
|
mCallbacks!!.onCrankDataChanged(device, crankCadence, gearRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidDataReceived(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
data: Data
|
||||||
|
) {
|
||||||
|
log(Log.WARN, "Invalid CSC Measurement data received: $data")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
enableNotifications(cscMeasurementCharacteristic).enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||||
|
val service = gatt.getService(CYCLING_SPEED_AND_CADENCE_SERVICE_UUID)
|
||||||
|
if (service != null) {
|
||||||
|
cscMeasurementCharacteristic = service.getCharacteristic(
|
||||||
|
CSC_MEASUREMENT_CHARACTERISTIC_UUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeviceDisconnected() {
|
||||||
|
super.onDeviceDisconnected()
|
||||||
|
cscMeasurementCharacteristic = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServicesInvalidated() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Cycling Speed and Cadence service UUID. */
|
||||||
|
val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID =
|
||||||
|
UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
|
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
||||||
|
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID =
|
||||||
|
UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Nordic Semiconductor
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
|
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback
|
||||||
|
|
||||||
|
internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015, Nordic Semiconductor
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
|
||||||
|
import no.nordicsemi.android.csc.R
|
||||||
|
import no.nordicsemi.android.csc.batery.LoggableBleManager
|
||||||
|
import no.nordicsemi.android.events.CrankDataChanged
|
||||||
|
import no.nordicsemi.android.events.OnBatteryLevelChanged
|
||||||
|
import no.nordicsemi.android.events.OnDistanceChangedEvent
|
||||||
|
import no.nordicsemi.android.log.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class CSCService : BleProfileService(), CSCManagerCallbacks {
|
||||||
|
private var manager: CSCManager? = null
|
||||||
|
|
||||||
|
@Inject lateinit var localBroadcast: BluetoothDataReadBroadcast
|
||||||
|
|
||||||
|
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks?> {
|
||||||
|
return CSCManager(this).also { manager = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
val filter = IntentFilter()
|
||||||
|
filter.addAction(ACTION_DISCONNECT)
|
||||||
|
registerReceiver(disconnectActionBroadcastReceiver, filter)
|
||||||
|
|
||||||
|
localBroadcast.wheelSize.onEach {
|
||||||
|
manager?.setWheelSize(it)
|
||||||
|
}.launchIn(GlobalScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
// when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService
|
||||||
|
cancelNotification()
|
||||||
|
unregisterReceiver(disconnectActionBroadcastReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRebind() {
|
||||||
|
stopForegroundService()
|
||||||
|
if (isConnected) {
|
||||||
|
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
|
||||||
|
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
|
||||||
|
manager!!.readBatteryLevelCharacteristic()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbind() {
|
||||||
|
// When we are connected, but the application is not open, we are not really interested in battery level notifications.
|
||||||
|
// But we will still be receiving other values, if enabled.
|
||||||
|
if (isConnected) manager!!.disableBatteryLevelCharacteristicNotifications()
|
||||||
|
startForegroundService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDistanceChanged(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
totalDistance: Float,
|
||||||
|
distance: Float,
|
||||||
|
speed: Float
|
||||||
|
) {
|
||||||
|
localBroadcast.offer(OnDistanceChangedEvent(bluetoothDevice, speed, distance, totalDistance))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCrankDataChanged(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
crankCadence: Float,
|
||||||
|
gearRatio: Float
|
||||||
|
) {
|
||||||
|
localBroadcast.offer(CrankDataChanged(bluetoothDevice, crankCadence.toInt(), gearRatio))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
||||||
|
localBroadcast.offer(OnBatteryLevelChanged(bluetoothDevice, batteryLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the service as a foreground service
|
||||||
|
*/
|
||||||
|
private fun startForegroundService() {
|
||||||
|
// when the activity closes we need to show the notification that user is connected to the peripheral sensor
|
||||||
|
// We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services
|
||||||
|
val notification = createNotification(R.string.csc_notification_connected_message, 0)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
} else {
|
||||||
|
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service as a foreground service
|
||||||
|
*/
|
||||||
|
private fun stopForegroundService() {
|
||||||
|
// when the activity rebinds to the service, remove the notification and stop the foreground service
|
||||||
|
// on devices running Android 8.0 (Oreo) or above
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
stopForeground(true)
|
||||||
|
} else {
|
||||||
|
cancelNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the notification
|
||||||
|
*
|
||||||
|
* @param messageResId the message resource id. The message must have one String parameter,<br></br>
|
||||||
|
* f.e. `<string name="name">%s is connected</string>`
|
||||||
|
* @param defaults
|
||||||
|
*/
|
||||||
|
private fun createNotification(messageResId: Int, defaults: Int): Notification {
|
||||||
|
TODO()
|
||||||
|
// final Intent parentIntent = new Intent(this, FeaturesActivity.class);
|
||||||
|
// parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
// final Intent targetIntent = new Intent(this, CSCActivity.class);
|
||||||
|
//
|
||||||
|
// final Intent disconnect = new Intent(ACTION_DISCONNECT);
|
||||||
|
// final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
//
|
||||||
|
// // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed
|
||||||
|
// final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
// final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL);
|
||||||
|
// builder.setContentIntent(pendingIntent);
|
||||||
|
// builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName()));
|
||||||
|
// builder.setSmallIcon(R.drawable.ic_stat_notify_csc);
|
||||||
|
// builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true);
|
||||||
|
// builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction));
|
||||||
|
//
|
||||||
|
// return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the existing notification. If there is no active notification this method does nothing
|
||||||
|
*/
|
||||||
|
private fun cancelNotification() {
|
||||||
|
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This broadcast receiver listens for [.ACTION_DISCONNECT] that may be fired by pressing Disconnect action button on the notification.
|
||||||
|
*/
|
||||||
|
private val disconnectActionBroadcastReceiver: BroadcastReceiver =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Logger.i(logSession, "[Notification] Disconnect action pressed")
|
||||||
|
if (isConnected) binder.disconnect() else stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT"
|
||||||
|
private const val NOTIFICATION_ID = 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import no.nordicsemi.android.events.EMPTY
|
||||||
|
|
||||||
|
internal sealed class CSCViewState {
|
||||||
|
|
||||||
|
fun ensureConnectedState(): CSCViewConnectedState {
|
||||||
|
return (this as? CSCViewConnectedState)
|
||||||
|
?: throw IllegalStateException("Wrong state. Device not connected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureDisconnectedState(): CSCViewNotConnectedState {
|
||||||
|
return (this as? CSCViewNotConnectedState)
|
||||||
|
?: throw IllegalStateException("Wrong state. Device should be connected.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO("Change to navigation")
|
||||||
|
internal data class CSCViewNotConnectedState(
|
||||||
|
val showScannerDialog: Boolean = false
|
||||||
|
) : CSCViewState()
|
||||||
|
|
||||||
|
internal data class CSCViewConnectedState(
|
||||||
|
val showDialog: Boolean = false,
|
||||||
|
val scanDevices: Boolean = false,
|
||||||
|
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||||
|
val speed: Float = 0f,
|
||||||
|
val cadence: Int = 0,
|
||||||
|
val distance: Float = 0f,
|
||||||
|
val totalDistance: Float = 0f,
|
||||||
|
val gearRatio: Float = 0f,
|
||||||
|
val batteryLevel: Int = 0,
|
||||||
|
val wheelSize: String = String.EMPTY
|
||||||
|
) : CSCViewState() {
|
||||||
|
|
||||||
|
fun displaySpeed(): String {
|
||||||
|
return speed.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayCadence(): String {
|
||||||
|
return cadence.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayDistance(): String {
|
||||||
|
return distance.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayTotalDistance(): String {
|
||||||
|
return totalDistance.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayBatteryLever(): String {
|
||||||
|
return batteryLevel.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
|
||||||
|
internal sealed class CSCViewEvent
|
||||||
|
|
||||||
|
internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent()
|
||||||
|
|
||||||
|
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
|
||||||
|
|
||||||
|
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
||||||
|
|
||||||
|
internal object OnDisconnectButtonClick : CSCViewEvent()
|
||||||
|
|
||||||
|
internal object OnConnectButtonClick : CSCViewEvent()
|
||||||
|
|
||||||
|
internal object OnMovedToScannerScreen : CSCViewEvent()
|
||||||
|
|
||||||
|
internal data class OnBluetoothDeviceSelected(val device: BluetoothDevice) : CSCViewEvent()
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import no.nordicsemi.android.csc.R
|
||||||
|
import no.nordicsemi.android.csc.service.CSCService
|
||||||
|
import no.nordicsemi.android.events.exhaustive
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun CscScreen(navController: NavController, viewModel: CscViewModel = hiltViewModel()) {
|
||||||
|
|
||||||
|
val secondScreenResult = navController.currentBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.getLiveData<BluetoothDevice>("result")?.observeAsState()
|
||||||
|
|
||||||
|
secondScreenResult?.value?.let {
|
||||||
|
viewModel.onEvent(OnBluetoothDeviceSelected(it))
|
||||||
|
|
||||||
|
val intent = Intent(LocalContext.current, CSCService::class.java).apply {
|
||||||
|
putExtra("no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS", it.address)
|
||||||
|
}
|
||||||
|
LocalContext.current.startService(intent)
|
||||||
|
|
||||||
|
navController.currentBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.set("result", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = viewModel.state.collectAsState().value
|
||||||
|
|
||||||
|
CSCView(navController, state) { viewModel.onEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CSCView(navController: NavController, state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
|
Column {
|
||||||
|
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is CSCViewConnectedState -> ConnectedView(state) { onEvent(it) }
|
||||||
|
is CSCViewNotConnectedState -> NotConnectedScreen(navController, state) {
|
||||||
|
onEvent(it)
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NotConnectedScreen(
|
||||||
|
navController: NavController,
|
||||||
|
state: CSCViewNotConnectedState,
|
||||||
|
onEvent: (CSCViewEvent) -> Unit
|
||||||
|
) {
|
||||||
|
if (state.showScannerDialog) {
|
||||||
|
navController.navigate("scanner-destination")
|
||||||
|
onEvent(OnMovedToScannerScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotConnectedView(onEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NotConnectedView(
|
||||||
|
onEvent: (CSCViewEvent) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.csc_no_connection))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(onClick = { onEvent(OnConnectButtonClick) }) {
|
||||||
|
Text(text = stringResource(id = R.string.csc_connect))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConnectedView(state: CSCViewConnectedState, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
|
if (state.showDialog) {
|
||||||
|
SelectWheelSizeDialog { onEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
WheelSizeView(state, onEvent)
|
||||||
|
|
||||||
|
SpeedUnitRadioGroup(state.selectedSpeedUnit) { onEvent(it) }
|
||||||
|
|
||||||
|
SensorsReadingView(state = state)
|
||||||
|
|
||||||
|
Button(onClick = { onEvent(OnDisconnectButtonClick) }) {
|
||||||
|
Text(text = stringResource(id = R.string.csc_disconnect))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun NotConnectedPreview() {
|
||||||
|
NotConnectedView { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ConnectedPreview() {
|
||||||
|
ConnectedView(CSCViewConnectedState()) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import no.nordicsemi.android.broadcast.BluetoothDataReadBroadcast
|
||||||
|
import no.nordicsemi.android.events.CSCServiceEvent
|
||||||
|
import no.nordicsemi.android.events.CrankDataChanged
|
||||||
|
import no.nordicsemi.android.events.OnBatteryLevelChanged
|
||||||
|
import no.nordicsemi.android.events.OnDistanceChangedEvent
|
||||||
|
import no.nordicsemi.android.events.exhaustive
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class CscViewModel @Inject constructor(
|
||||||
|
private val localBroadcast: BluetoothDataReadBroadcast
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val state = MutableStateFlow<CSCViewState>(CSCViewNotConnectedState())
|
||||||
|
|
||||||
|
init {
|
||||||
|
localBroadcast.events.onEach {
|
||||||
|
(it as? CSCServiceEvent)?.let { withContext(Dispatchers.Main) { consumeEvent(it) }}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun consumeEvent(event: CSCServiceEvent) {
|
||||||
|
val newValue = when (event) {
|
||||||
|
is CrankDataChanged -> createNewState(event)
|
||||||
|
is OnBatteryLevelChanged -> createNewState(event)
|
||||||
|
is OnDistanceChangedEvent -> createNewState(event)
|
||||||
|
}
|
||||||
|
state.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewState(event: CrankDataChanged): CSCViewConnectedState {
|
||||||
|
return state.value.ensureConnectedState().copy(
|
||||||
|
cadence = event.crankCadence,
|
||||||
|
gearRatio = event.gearRatio
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewState(event: OnBatteryLevelChanged): CSCViewConnectedState {
|
||||||
|
return state.value.ensureConnectedState().copy(
|
||||||
|
batteryLevel = event.batteryLevel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewState(event: OnDistanceChangedEvent): CSCViewConnectedState {
|
||||||
|
return state.value.ensureConnectedState().copy(
|
||||||
|
speed = event.speed,
|
||||||
|
distance = event.distance,
|
||||||
|
totalDistance = event.totalDistance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEvent(event: CSCViewEvent) {
|
||||||
|
when (event) {
|
||||||
|
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
||||||
|
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
||||||
|
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||||
|
OnDisconnectButtonClick -> TODO()
|
||||||
|
OnConnectButtonClick -> onConnectButtonClick()
|
||||||
|
OnMovedToScannerScreen -> onOnMovedToScannerScreen()
|
||||||
|
is OnBluetoothDeviceSelected -> onBluetoothDeviceSelected()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
||||||
|
state.tryEmit(state.value.ensureConnectedState().copy(selectedSpeedUnit = event.selectedSpeedUnit))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onShowDialogEvent() {
|
||||||
|
state.tryEmit(state.value.ensureConnectedState().copy(showDialog = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
||||||
|
localBroadcast.setWheelSize(event.wheelSize)
|
||||||
|
state.tryEmit(state.value.ensureConnectedState().copy(
|
||||||
|
showDialog = false,
|
||||||
|
wheelSize = event.wheelSizeDisplayInfo
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onConnectButtonClick() {
|
||||||
|
state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOnMovedToScannerScreen() {
|
||||||
|
state.tryEmit(state.value.ensureDisconnectedState().copy(showScannerDialog = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBluetoothDeviceSelected() {
|
||||||
|
state.tryEmit(CSCViewConnectedState())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import no.nordicsemi.android.csc.R
|
||||||
|
import no.nordicsemi.android.theme.Background
|
||||||
|
import no.nordicsemi.android.theme.TestTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SelectWheelSizeDialog(onEvent: (OnWheelSizeSelected) -> Unit) {
|
||||||
|
Dialog(onDismissRequest = {}) {
|
||||||
|
SelectWheelSizeView(onEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectWheelSizeView(onEvent: (OnWheelSizeSelected) -> Unit) {
|
||||||
|
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||||
|
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||||
|
|
||||||
|
Box(Modifier.padding(16.dp)) {
|
||||||
|
Column(modifier = Background.whiteRoundedCorners()) {
|
||||||
|
Text(text = "Wheel size")
|
||||||
|
wheelEntries.forEachIndexed { i, entry ->
|
||||||
|
Text(text = entry, modifier = Modifier.clickable {
|
||||||
|
onEvent(OnWheelSizeSelected(wheelValues[i].toInt(), entry))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun DefaultPreview() {
|
||||||
|
TestTheme {
|
||||||
|
SelectWheelSizeView { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import no.nordicsemi.android.csc.R
|
||||||
|
import no.nordicsemi.android.theme.Background
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SensorsReadingView(state: CSCViewConnectedState) {
|
||||||
|
Column {
|
||||||
|
Column(modifier = Background.whiteRoundedCorners()) {
|
||||||
|
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
|
||||||
|
KeyValueField(stringResource(id = R.string.scs_field_cadence), state.displayCadence())
|
||||||
|
KeyValueField(stringResource(id = R.string.scs_field_distance), state.displayDistance())
|
||||||
|
KeyValueField(
|
||||||
|
stringResource(id = R.string.scs_field_total_distance),
|
||||||
|
state.displayTotalDistance()
|
||||||
|
)
|
||||||
|
KeyValueField(stringResource(id = R.string.scs_field_gear_ratio), state.displaySpeed())
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Column(modifier = Background.whiteRoundedCorners()) {
|
||||||
|
KeyValueField(stringResource(id = R.string.scs_field_battery), state.displayBatteryLever())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KeyValueField(key: String, value: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = key)
|
||||||
|
Text(text = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun Preview() {
|
||||||
|
SensorsReadingView(CSCViewConnectedState())
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
internal enum class SpeedUnit {
|
||||||
|
M_S,
|
||||||
|
KM_H,
|
||||||
|
MPH
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.RadioButton
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SpeedUnitRadioGroup(
|
||||||
|
currentUnit: SpeedUnit,
|
||||||
|
onEvent: (OnSelectedSpeedUnitSelected) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
SpeedUnitRadioButton(currentUnit, SpeedUnit.KM_H, onEvent)
|
||||||
|
SpeedUnitRadioButton(currentUnit, SpeedUnit.MPH, onEvent)
|
||||||
|
SpeedUnitRadioButton(currentUnit, SpeedUnit.M_S, onEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SpeedUnitRadioButton(
|
||||||
|
selectedUnit: SpeedUnit,
|
||||||
|
displayedUnit: SpeedUnit,
|
||||||
|
onEvent: (OnSelectedSpeedUnitSelected) -> Unit
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
RadioButton(
|
||||||
|
selected = (selectedUnit == displayedUnit),
|
||||||
|
onClick = { onEvent(OnSelectedSpeedUnitSelected(displayedUnit)) }
|
||||||
|
)
|
||||||
|
Text(text = createSpeedUnitLabel(displayedUnit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun createSpeedUnitLabel(unit: SpeedUnit): String {
|
||||||
|
return when (unit) {
|
||||||
|
SpeedUnit.M_S -> "m/s"
|
||||||
|
SpeedUnit.KM_H -> "km/h"
|
||||||
|
SpeedUnit.MPH -> "mph"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import no.nordicsemi.android.csc.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun WheelSizeView(state: CSCViewConnectedState, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = state.wheelSize,
|
||||||
|
onValueChange = { },
|
||||||
|
enabled = false,
|
||||||
|
label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) },
|
||||||
|
trailingIcon = { EditIcon(onEvent = onEvent) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditIcon(onEvent: (CSCViewEvent) -> Unit) {
|
||||||
|
IconButton(onClick = { onEvent(OnShowEditWheelSizeDialogButtonClick) }) {
|
||||||
|
Icon(Icons.Filled.Edit, "Edit wheel size.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun WheelSizeViewPreview() {
|
||||||
|
WheelSizeView(CSCViewConnectedState()) { }
|
||||||
|
}
|
||||||
129
feature_csc/src/main/res/values/strings.xml
Normal file
129
feature_csc/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="csc_title">Cyclic and speed cadence</string>
|
||||||
|
|
||||||
|
<string name="csc_bonding">Bonding with the device…</string>
|
||||||
|
<string name="csc_bonded">The device is now bonded.</string>
|
||||||
|
<string name="csc_bonding_failed">Bonding failed.</string>
|
||||||
|
|
||||||
|
<string name="csc_disconnect">Disconnect</string>
|
||||||
|
<string name="csc_no_connection">No device connected</string>
|
||||||
|
<string name="csc_connect">Connect</string>
|
||||||
|
|
||||||
|
<string name="csc_notification_connected_message">%s is connected.</string>
|
||||||
|
|
||||||
|
<string name="scs_field_speed">Speed</string>
|
||||||
|
<string name="scs_field_cadence">Cadence</string>
|
||||||
|
<string name="scs_field_distance">Distance</string>
|
||||||
|
<string name="scs_field_total_distance">Total Distance</string>
|
||||||
|
<string name="scs_field_gear_ratio">Gear Ratio</string>
|
||||||
|
<string name="scs_field_battery">Battery</string>
|
||||||
|
|
||||||
|
<string name="scs_field_wheel_size">Wheel size</string>
|
||||||
|
|
||||||
|
<string-array name="wheel_entries">
|
||||||
|
<item>60–622</item>
|
||||||
|
<item>50–622</item>
|
||||||
|
<item>47–622</item>
|
||||||
|
<item>44–622</item>
|
||||||
|
<item>40–635</item>
|
||||||
|
<item>40–622</item>
|
||||||
|
<item>38–622</item>
|
||||||
|
<item>37–622</item>
|
||||||
|
<item>35–622</item>
|
||||||
|
<item>32–630</item>
|
||||||
|
<item>32–622</item>
|
||||||
|
<item>32–622</item>
|
||||||
|
<item>28–622</item>
|
||||||
|
<item>60–559</item>
|
||||||
|
<item>28–622</item>
|
||||||
|
<item>25–622</item>
|
||||||
|
<item>25–622</item>
|
||||||
|
<item>23–622</item>
|
||||||
|
<item>20–622</item>
|
||||||
|
<item>18–622</item>
|
||||||
|
<item>35–630</item>
|
||||||
|
<item>32–630</item>
|
||||||
|
<item>28–630</item>
|
||||||
|
<item>57–559</item>
|
||||||
|
<item>54–559</item>
|
||||||
|
<item>37–590</item>
|
||||||
|
<item>23–622</item>
|
||||||
|
<item>50–559</item>
|
||||||
|
<item>20–622</item>
|
||||||
|
<item>54–559</item>
|
||||||
|
<item>47–559</item>
|
||||||
|
<item>35–590</item>
|
||||||
|
<item>37–590</item>
|
||||||
|
<item>47–559</item>
|
||||||
|
<item>50–559</item>
|
||||||
|
<item>44–559</item>
|
||||||
|
<item>40–559</item>
|
||||||
|
<item>23–571</item>
|
||||||
|
<item>20–571</item>
|
||||||
|
<item>32–559</item>
|
||||||
|
<item>25–571</item>
|
||||||
|
<item>34–540</item>
|
||||||
|
<item>50–507</item>
|
||||||
|
<item>47–507</item>
|
||||||
|
<item>28–451</item>
|
||||||
|
<item>50–406</item>
|
||||||
|
<item>47–406</item>
|
||||||
|
<item>28–369</item>
|
||||||
|
<item>35–349</item>
|
||||||
|
<item>47–305</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="wheel_values">
|
||||||
|
<item>2340</item>
|
||||||
|
<item>2284</item>
|
||||||
|
<item>2268</item>
|
||||||
|
<item>2224</item>
|
||||||
|
<item>2265</item>
|
||||||
|
<item>2224</item>
|
||||||
|
<item>2180</item>
|
||||||
|
<item>2205</item>
|
||||||
|
<item>2168</item>
|
||||||
|
<item>2199</item>
|
||||||
|
<item>2174</item>
|
||||||
|
<item>2155</item>
|
||||||
|
<item>2149</item>
|
||||||
|
<item>2146</item>
|
||||||
|
<item>2136</item>
|
||||||
|
<item>2146</item>
|
||||||
|
<item>2105</item>
|
||||||
|
<item>2133</item>
|
||||||
|
<item>2114</item>
|
||||||
|
<item>2102</item>
|
||||||
|
<item>2169</item>
|
||||||
|
<item>2161</item>
|
||||||
|
<item>2155</item>
|
||||||
|
<item>2133</item>
|
||||||
|
<item>2114</item>
|
||||||
|
<item>2105</item>
|
||||||
|
<item>2097</item>
|
||||||
|
<item>2089</item>
|
||||||
|
<item>2086</item>
|
||||||
|
<item>2114</item>
|
||||||
|
<item>2070</item>
|
||||||
|
<item>2068</item>
|
||||||
|
<item>2105</item>
|
||||||
|
<item>2055</item>
|
||||||
|
<item>2089</item>
|
||||||
|
<item>2051</item>
|
||||||
|
<item>2026</item>
|
||||||
|
<item>1973</item>
|
||||||
|
<item>1954</item>
|
||||||
|
<item>1953</item>
|
||||||
|
<item>1952</item>
|
||||||
|
<item>1948</item>
|
||||||
|
<item>1910</item>
|
||||||
|
<item>1907</item>
|
||||||
|
<item>1618</item>
|
||||||
|
<item>1593</item>
|
||||||
|
<item>1590</item>
|
||||||
|
<item>1325</item>
|
||||||
|
<item>1282</item>
|
||||||
|
<item>1272</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.csc
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib_broadcast/build.gradle
Normal file
7
lib_broadcast/build.gradle
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":lib_events")
|
||||||
|
|
||||||
|
implementation libs.kotlin.coroutines
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.broadcast
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("no.nordicsemi.android.broadcast.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib_broadcast/src/main/AndroidManifest.xml
Normal file
4
lib_broadcast/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="no.nordicsemi.android.broadcast">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package no.nordicsemi.android.broadcast
|
||||||
|
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import no.nordicsemi.android.events.BluetoothReadDataEvent
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BluetoothDataReadBroadcast @Inject constructor() {
|
||||||
|
|
||||||
|
private val _event = MutableSharedFlow<BluetoothReadDataEvent>(
|
||||||
|
replay = 1,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val events: SharedFlow<BluetoothReadDataEvent> = _event
|
||||||
|
|
||||||
|
private val _wheelSize = MutableSharedFlow<Int>(
|
||||||
|
replay = 1,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val wheelSize: SharedFlow<Int> = _wheelSize
|
||||||
|
|
||||||
|
fun offer(newEvent: BluetoothReadDataEvent) {
|
||||||
|
_event.tryEmit(newEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setWheelSize(size: Int) {
|
||||||
|
_wheelSize.tryEmit(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.broadcast
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
lib_events/build.gradle
Normal file
3
lib_events/build.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.events
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("no.nordicsemi.android.events.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
lib_events/src/main/AndroidManifest.xml
Normal file
5
lib_events/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="no.nordicsemi.android.events">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package no.nordicsemi.android.events
|
||||||
|
|
||||||
|
sealed class BluetoothReadDataEvent
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package no.nordicsemi.android.events
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
sealed class CSCServiceEvent : BluetoothReadDataEvent(), Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class OnDistanceChangedEvent(
|
||||||
|
val bluetoothDevice: BluetoothDevice,
|
||||||
|
val speed: Float,
|
||||||
|
val distance: Float,
|
||||||
|
val totalDistance: Float
|
||||||
|
) : CSCServiceEvent()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class CrankDataChanged(
|
||||||
|
val bluetoothDevice: BluetoothDevice,
|
||||||
|
val crankCadence: Int,
|
||||||
|
val gearRatio: Float
|
||||||
|
) : CSCServiceEvent()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class OnBatteryLevelChanged(
|
||||||
|
val device: BluetoothDevice,
|
||||||
|
val batteryLevel: Int
|
||||||
|
) : CSCServiceEvent()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package no.nordicsemi.android.events
|
||||||
|
|
||||||
|
val <T> T.exhaustive
|
||||||
|
get() = this
|
||||||
|
|
||||||
|
val String.Companion.EMPTY
|
||||||
|
get() = ""
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.events
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib_scanner/build.gradle
Normal file
14
lib_scanner/build.gradle
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(":lib_theme")
|
||||||
|
implementation project(":lib_events")
|
||||||
|
|
||||||
|
implementation libs.material
|
||||||
|
implementation libs.google.permissions
|
||||||
|
|
||||||
|
implementation libs.bundles.compose
|
||||||
|
implementation libs.compose.lifecycle
|
||||||
|
implementation libs.compose.activity
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("no.nordicsemi.android.scanner.test", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib_scanner/src/main/AndroidManifest.xml
Normal file
15
lib_scanner/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="no.nordicsemi.android.scanner">
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
tools:ignore="CoarseFineLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal object HiltModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun createNordicBleScanner(): BluetoothAdapter? {
|
||||||
|
return BluetoothAdapter.getDefaultAdapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import no.nordicsemi.android.events.exhaustive
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ListOfDevicesScreen(onDeviceSelected: (BluetoothDevice) -> Unit) {
|
||||||
|
|
||||||
|
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
|
||||||
|
|
||||||
|
val result = viewModel.scannerResult.collectAsState().value
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is DeviceListResult -> DeviceListView(result.devices, onDeviceSelected)
|
||||||
|
is ScanningErrorResult -> ScanningErrorView()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@Composable
|
||||||
|
private fun DeviceListView(
|
||||||
|
devices: List<BluetoothDevice>,
|
||||||
|
onDeviceSelected: (BluetoothDevice) -> Unit
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(stringResource(id = R.string.scanner__list_of_devices))
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
itemsIndexed(devices) { _, device ->
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { onDeviceSelected(device) }
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(device.name ?: stringResource(id = R.string.scanner__no_name))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = device.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScanningErrorView() {
|
||||||
|
Text(text = stringResource(id = R.string.scanner__error))
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.le.ScanCallback
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
internal class NordicBleScanner @Inject constructor(private val bleAdapter: BluetoothAdapter?) {
|
||||||
|
|
||||||
|
val scannerResult = MutableStateFlow<ScanningResult>(DeviceListResult())
|
||||||
|
|
||||||
|
private var isScanning = false
|
||||||
|
|
||||||
|
private val scanner by lazy { bleAdapter?.bluetoothLeScanner }
|
||||||
|
private val devices = mutableListOf<BluetoothDevice>()
|
||||||
|
|
||||||
|
private val scanningCallback = object : ScanCallback() {
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult?) {
|
||||||
|
result?.device?.let { devices.addIfNotExist(it) }
|
||||||
|
scannerResult.value = DeviceListResult(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
scannerResult.value = ScanningErrorResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBluetoothStatus(): ScannerStatus {
|
||||||
|
return when {
|
||||||
|
bleAdapter == null -> ScannerStatus.NOT_AVAILABLE
|
||||||
|
bleAdapter.isEnabled -> ScannerStatus.ENABLED
|
||||||
|
else -> ScannerStatus.DISABLED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startScanning() {
|
||||||
|
if (isScanning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isScanning = true
|
||||||
|
scanner?.startScan(scanningCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopScanning() {
|
||||||
|
if (!isScanning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isScanning = false
|
||||||
|
scanner?.stopScan(scanningCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ScanningResult
|
||||||
|
|
||||||
|
data class DeviceListResult(val devices: List<BluetoothDevice> = emptyList()) : ScanningResult()
|
||||||
|
|
||||||
|
object ScanningErrorResult : ScanningResult()
|
||||||
|
|
||||||
|
private fun <T> MutableList<T>.addIfNotExist(value: T) {
|
||||||
|
if (!contains(value)) {
|
||||||
|
add(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import no.nordicsemi.android.events.exhaustive
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class NordicBleScannerViewModel @Inject constructor(
|
||||||
|
private val bleScanner: NordicBleScanner
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val state =
|
||||||
|
MutableStateFlow(NordicBleScannerState(scannerStatus = ScannerStatus.PERMISSION_REQUIRED))
|
||||||
|
|
||||||
|
val scannerResult = bleScanner.scannerResult
|
||||||
|
|
||||||
|
fun onEvent(event: ScannerViewEvent) {
|
||||||
|
when (event) {
|
||||||
|
ScannerViewEvent.PERMISSION_CHECKED -> onPermissionChecked()
|
||||||
|
ScannerViewEvent.BLUETOOTH_ENABLED -> onBluetoothEnabled()
|
||||||
|
ScannerViewEvent.ENABLE_SCANNING -> bleScanner.startScanning()
|
||||||
|
ScannerViewEvent.DISABLE_SCANNING -> bleScanner.stopScanning()
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPermissionChecked() {
|
||||||
|
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBluetoothEnabled() {
|
||||||
|
state.value = state.value.copy(scannerStatus = bleScanner.getBluetoothStatus())
|
||||||
|
bleScanner.startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ScannerViewEvent {
|
||||||
|
PERMISSION_CHECKED, BLUETOOTH_ENABLED, ENABLE_SCANNING, DISABLE_SCANNING
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class NordicBleScannerState(
|
||||||
|
val scannerStatus: ScannerStatus
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import no.nordicsemi.android.events.exhaustive
|
||||||
|
import no.nordicsemi.android.scanner.bluetooth.BluetoothNotAvailableScreen
|
||||||
|
import no.nordicsemi.android.scanner.bluetooth.BluetoothNotEnabledScreen
|
||||||
|
import no.nordicsemi.android.scanner.permissions.RequestPermissionScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScannerRoute(navController: NavController) {
|
||||||
|
val viewModel = hiltViewModel<NordicBleScannerViewModel>()
|
||||||
|
|
||||||
|
val scannerStatus = viewModel.state.collectAsState().value.scannerStatus
|
||||||
|
|
||||||
|
Column {
|
||||||
|
TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__devices_list)) })
|
||||||
|
ScannerScreen(navController, scannerStatus) { viewModel.onEvent(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ScannerScreen(
|
||||||
|
navController: NavController,
|
||||||
|
scannerStatus: ScannerStatus,
|
||||||
|
onEvent: (ScannerViewEvent) -> Unit
|
||||||
|
) {
|
||||||
|
when (scannerStatus) {
|
||||||
|
ScannerStatus.PERMISSION_REQUIRED -> RequestPermissionScreen { onEvent(ScannerViewEvent.PERMISSION_CHECKED) }
|
||||||
|
ScannerStatus.NOT_AVAILABLE -> BluetoothNotAvailableScreen()
|
||||||
|
ScannerStatus.DISABLED -> BluetoothNotEnabledScreen { onEvent(ScannerViewEvent.BLUETOOTH_ENABLED) }
|
||||||
|
ScannerStatus.ENABLED -> {
|
||||||
|
onEvent(ScannerViewEvent.ENABLE_SCANNING)
|
||||||
|
ListOfDevicesScreen {
|
||||||
|
navController.previousBackStackEntry
|
||||||
|
?.savedStateHandle
|
||||||
|
?.set("result", it)
|
||||||
|
navController.popBackStack()
|
||||||
|
onEvent(ScannerViewEvent.DISABLE_SCANNING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
enum class ScannerStatus {
|
||||||
|
PERMISSION_REQUIRED, ENABLED, DISABLED, NOT_AVAILABLE
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package no.nordicsemi.android.scanner.bluetooth
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BluetoothNotAvailableScreen() {
|
||||||
|
Text("Bluetooth not available.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BluetoothNotEnabledScreen(finish: () -> Unit) {
|
||||||
|
val contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
val launcher = rememberLauncherForActivityResult(contract = contract, onResult = {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(text = "Bluetooth not enabled.")
|
||||||
|
Text(text = "To enable Bluetooth please open settings.")
|
||||||
|
Button(onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }) {
|
||||||
|
Text(text = "Bluetooth not available.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package no.nordicsemi.android.scanner.permissions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.PermissionsRequired
|
||||||
|
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||||
|
import no.nordicsemi.android.scanner.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
internal fun RequestPermissionScreen(finish: () -> Unit) {
|
||||||
|
val permissionsState = rememberMultiplePermissionsState(listOf(
|
||||||
|
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
// android.Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
// android.Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
))
|
||||||
|
PermissionsRequired(
|
||||||
|
multiplePermissionsState = permissionsState,
|
||||||
|
permissionsNotGrantedContent = { PermissionNotGranted { permissionsState.launchMultiplePermissionRequest() } },
|
||||||
|
permissionsNotAvailableContent = { PermissionNotAvailable() }
|
||||||
|
) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PermissionNotGranted(onClick: () -> Unit) {
|
||||||
|
val doNotShowRationale = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (doNotShowRationale.value) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.scanner__feature_not_available))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.scanner__permission_rationale))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row {
|
||||||
|
Button(onClick = { onClick() }) {
|
||||||
|
Text(stringResource(id = R.string.scanner__button_ok))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Button(onClick = { doNotShowRationale.value = true }) {
|
||||||
|
Text(stringResource(id = R.string.scanner__button_nope))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PermissionNotAvailable() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.scanner__permission_denied))
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(onClick = { openPermissionSettings(context) }) {
|
||||||
|
Text(stringResource(id = R.string.scanner__open_settings))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPermissionSettings(context: Context) {
|
||||||
|
startActivity(
|
||||||
|
context,
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
|
Uri.fromParts("package", context.packageName, null)
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PermissionNotGrantedPreview() {
|
||||||
|
PermissionNotGranted { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun PermissionNotAvailablePreview() {
|
||||||
|
PermissionNotAvailable()
|
||||||
|
}
|
||||||
15
lib_scanner/src/main/res/values/strings.xml
Normal file
15
lib_scanner/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="scanner__devices_list">BLE devices</string>
|
||||||
|
|
||||||
|
<string name="scanner__permission_rationale">The location permission is required when using Bluetooth LE, because surrounding devices can expose user\'s location. Please grant the permission.</string>
|
||||||
|
<string name="scanner__permission_denied">Location permission denied. Please, grant us access on the Settings screen.</string>
|
||||||
|
<string name="scanner__button_ok">OK</string>
|
||||||
|
<string name="scanner__button_nope">Nope</string>
|
||||||
|
<string name="scanner__open_settings">Open settings</string>
|
||||||
|
<string name="scanner__feature_not_available">Feature not available</string>
|
||||||
|
|
||||||
|
<string name="scanner__list_of_devices">List of devices</string>
|
||||||
|
<string name="scanner__error">Scanning failed due to technical reason.</string>
|
||||||
|
<string name="scanner__no_name">Name: NONE</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package no.nordicsemi.android.scanner
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
lib_theme/build.gradle
Normal file
9
lib_theme/build.gradle
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apply from: rootProject.file("library.gradle")
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation libs.material
|
||||||
|
|
||||||
|
implementation libs.bundles.compose
|
||||||
|
implementation libs.compose.lifecycle
|
||||||
|
implementation libs.compose.activity
|
||||||
|
}
|
||||||
5
lib_theme/src/main/AndroidManifest.xml
Normal file
5
lib_theme/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="no.nordicsemi.android.theme">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package no.nordicsemi.android.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
object Background {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun whiteRoundedCorners(): Modifier {
|
||||||
|
return Modifier
|
||||||
|
.background(Color(0xffffffff))
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt
Normal file
26
lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package no.nordicsemi.android.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
object NordicColors {
|
||||||
|
val Primary = Color(0xFF00A9CE)
|
||||||
|
val PrimaryLight = Color(0xFF5fdbff)
|
||||||
|
val PrimaryDark = Color(0xFF007a9d)
|
||||||
|
val Secondary = Color(0xFF0077c8)
|
||||||
|
val SecondaryLight = Color(0xFF57c0e2)
|
||||||
|
val SecondaryDark = Color(0xFF004c97)
|
||||||
|
val Text = Color(0xFF00A9CE)
|
||||||
|
|
||||||
|
val NordicBlue = Color(0xFF00A9CE)
|
||||||
|
val NordicBlueDark = Color(0xFF0090B0)
|
||||||
|
val NordicSky = Color(0xFF6AD1E3)
|
||||||
|
val NordicBlueLate = Color(0xFF0033A0)
|
||||||
|
val NordicLake = Color(0xFF0077C8)
|
||||||
|
val NordicLightGray = Color(0xFFD9E1E2)
|
||||||
|
val NordicMediumGray = Color(0xFF768692)
|
||||||
|
val NordicDarkGray = Color(0xFF333F48)
|
||||||
|
val NordicGrass = Color(0xFFD0DF00)
|
||||||
|
val NordicSun = Color(0xFFFFCD00)
|
||||||
|
val NordicRed = Color(0xFFEE2F4E)
|
||||||
|
val NordicFall = Color(0xFFF58220)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox.ui.theme
|
package no.nordicsemi.android.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Shapes
|
import androidx.compose.material.Shapes
|
||||||
@@ -1,30 +1,37 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox.ui.theme
|
package no.nordicsemi.android.theme
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.darkColors
|
import androidx.compose.material.darkColors
|
||||||
import androidx.compose.material.lightColors
|
import androidx.compose.material.lightColors
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
//TODO
|
||||||
private val DarkColorPalette = darkColors(
|
private val DarkColorPalette = darkColors(
|
||||||
primary = Purple200,
|
primary = NordicColors.Primary,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = NordicColors.PrimaryDark,
|
||||||
secondary = Teal200
|
secondary = NordicColors.Secondary,
|
||||||
|
secondaryVariant = NordicColors.SecondaryDark,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onBackground = Color.Black,
|
||||||
|
onSurface = Color.Black,
|
||||||
|
background = Color.White,
|
||||||
|
surface = Color.White,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
primary = Purple500,
|
primary = NordicColors.Primary,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = NordicColors.PrimaryDark,
|
||||||
secondary = Teal200
|
secondary = NordicColors.Secondary,
|
||||||
|
secondaryVariant = NordicColors.SecondaryDark,
|
||||||
/* Other default colors to override
|
onSecondary = Color.White,
|
||||||
background = Color.White,
|
|
||||||
surface = Color.White,
|
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
onSecondary = Color.Black,
|
|
||||||
onBackground = Color.Black,
|
onBackground = Color.Black,
|
||||||
onSurface = Color.Black,
|
onSurface = Color.Black,
|
||||||
*/
|
background = Color.White,
|
||||||
|
surface = Color.White,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox.ui.theme
|
package no.nordicsemi.android.theme
|
||||||
|
|
||||||
import androidx.compose.material.Typography
|
import androidx.compose.material.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
9
lib_theme/src/main/res/values/colors.xml
Normal file
9
lib_theme/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#FF00A9CE</color>
|
||||||
|
<color name="colorPrimaryDark">#FF007a9d</color>
|
||||||
|
<color name="colorOnPrimary">#FFFFFFFF</color>
|
||||||
|
<color name="colorSecondary">#FF0077c8</color>
|
||||||
|
<color name="colorSecondaryDark">#FF004c97</color>
|
||||||
|
<color name="colorOnSecondary">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Test" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Test" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item>
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
<item name="colorOnPrimary">@color/colorOnPrimary</item>
|
||||||
<!-- Secondary brand color. -->
|
<!-- Secondary brand color. -->
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/colorSecondary</item>
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
<item name="colorOnSecondary">@color/colorOnSecondary</item>
|
||||||
<!-- Status bar color. -->
|
<!-- Status bar color. -->
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
59
library.gradle
Normal file
59
library.gradle
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'dagger.hilt.android.plugin'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk android_api_version
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk android_min_api_version
|
||||||
|
targetSdk android_api_version
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles "consumer-rules.pro"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There is an issue with recomposition performance when data objects doesn't belong to the module
|
||||||
|
* when composition is enabled, because those objects cannot be properly compared for the changes.
|
||||||
|
* Better to leave enabled for all modules.
|
||||||
|
*/
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion compose_version
|
||||||
|
kotlinCompilerVersion kotlin_version
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
|
||||||
|
hilt {
|
||||||
|
enableExperimentalClasspathAggregation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation libs.bundles.compose
|
||||||
|
|
||||||
|
implementation libs.bundles.hilt
|
||||||
|
kapt libs.bundles.hiltkapt
|
||||||
|
}
|
||||||
@@ -10,18 +10,39 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
libs {
|
libs {
|
||||||
version('compose', '1.0.2')
|
alias('nordic-ble-common').to('no.nordicsemi.android:ble-common:2.2.0')
|
||||||
|
alias('nordic-log').to('no.nordicsemi.android:log:2.3.0')
|
||||||
|
alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.5.0')
|
||||||
|
|
||||||
|
alias('material').to('com.google.android.material:material:1.4.0')
|
||||||
|
|
||||||
|
version('lifecycle', '2.3.1')
|
||||||
|
alias('lifecycle-activity').to('androidx.lifecycle', 'lifecycle-runtime-ktx').versionRef('lifecycle')
|
||||||
|
alias('lifecycle-service').to('androidx.lifecycle', 'lifecycle-service').versionRef('lifecycle')
|
||||||
|
|
||||||
alias('androidx-core').to('androidx.core:core-ktx:1.6.0')
|
alias('androidx-core').to('androidx.core:core-ktx:1.6.0')
|
||||||
alias('material').to('com.google.android.material:material:1.4.0')
|
|
||||||
alias('lifecycle').to('androidx.lifecycle:lifecycle-runtime-ktx:2.3.1')
|
|
||||||
alias('compose-activity').to('androidx.activity:activity-compose:1.3.1')
|
alias('compose-activity').to('androidx.activity:activity-compose:1.3.1')
|
||||||
|
alias('compose-lifecycle').to('androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07')
|
||||||
|
|
||||||
|
version('compose', '1.0.2')
|
||||||
|
alias('compose-livedata').to('androidx.compose.runtime', 'runtime-livedata').versionRef('compose')
|
||||||
alias('compose-ui').to('androidx.compose.ui', 'ui').versionRef('compose')
|
alias('compose-ui').to('androidx.compose.ui', 'ui').versionRef('compose')
|
||||||
alias('compose-material').to('androidx.compose.material', 'material').versionRef('compose')
|
alias('compose-material').to('androidx.compose.material', 'material').versionRef('compose')
|
||||||
alias('compose-tooling-preview').to('androidx.compose.ui', 'ui-tooling-preview').versionRef('compose')
|
alias('compose-tooling-preview').to('androidx.compose.ui', 'ui-tooling-preview').versionRef('compose')
|
||||||
|
alias('compose-navigation').to('androidx.navigation:navigation-compose:2.4.0-alpha09')
|
||||||
|
bundle('compose', ['compose-livedata', 'compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation'])
|
||||||
|
|
||||||
bundle('compose', ['compose-ui', 'compose-material', 'compose-tooling-preview'])
|
version('hilt', '2.38.1')
|
||||||
|
alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt')
|
||||||
|
alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt')
|
||||||
|
alias('hilt-compose').to('androidx.hilt:hilt-navigation-compose:1.0.0-alpha03')
|
||||||
|
alias('hilt-lifecycle').to('androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02')
|
||||||
|
alias('hilt-lifecyclecompiler').to('androidx.hilt:hilt-compiler:1.0.0-alpha02')
|
||||||
|
bundle('hilt', ['hilt-android', 'hilt-compose', 'hilt-lifecycle'])
|
||||||
|
bundle('hiltkapt', ['hilt-compiler', 'hilt-lifecyclecompiler'])
|
||||||
|
|
||||||
|
alias('kotlin-coroutines').to('org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2')
|
||||||
|
alias('google-permissions').to('com.google.accompanist:accompanist-permissions:0.18.0')
|
||||||
|
|
||||||
//-- Test ------------------------------------------------------------------------------
|
//-- Test ------------------------------------------------------------------------------
|
||||||
alias('test-junit').to('junit:junit:4.13.2')
|
alias('test-junit').to('junit:junit:4.13.2')
|
||||||
@@ -33,9 +54,15 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "Test"
|
rootProject.name = "Android-nRF-Toolbox"
|
||||||
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
include ':feature_csc'
|
||||||
|
include ':lib_broadcast'
|
||||||
|
include ':lib_events'
|
||||||
|
|
||||||
if (file('../Android-BLE-Library').exists()) {
|
if (file('../Android-BLE-Library').exists()) {
|
||||||
includeBuild('../Android-BLE-Library')
|
includeBuild('../Android-BLE-Library')
|
||||||
}
|
}
|
||||||
|
include ':lib_theme'
|
||||||
|
include ':lib_scanner'
|
||||||
|
|||||||
Reference in New Issue
Block a user