mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-18 06:54:24 +01:00
Migration to new BLEK library (#143)
* Changed view. * Clear messages. * Clear messages. * Add or delete configuration. * Fixed configuration picker. * Edit configuration. * Create new macro. * removed unnecessary resource files. * Fixed running macro command. * Delete macro * Edit macro * Changed to peripheral name. * Show peripheral name. * Fixed Eol tab design. * Removed icon resource * String changes * Removed any permission from home view. * Clear device after disconnection. * 1 line app bar * Changed missing services text. * Throughput service view changes. * Throughput service fixes. * Removed unused resources. * Fixed Health temperature profile. * Show heart rate. * Fixed hrs view. * Show heart rate data from left to right in the chart. * Changed chart color, solid, and scroll to see history. * Horizontal grid hidden, in case needed. * HTS view update * Changed padding. * Removed circular icon background. * Updated Battery level view. * Updated hrs body sensor location. * Moved ui mappers into view. * Updated gls view. * Changed focus color. * Fixed issue with job. * Fixed bps. * Added Blood pressure feature uuid. * Added blood pressure feature data. * Added rscs feature data. * Fixed cscs view. * Show supported features. * Fixed ui * Suspend the service discovery for GLS and CGMS until bonding is completed. * Added suspend on the function level. * Bonding state check only to cgms service * Removed stacktrace print. * Make cgms record available within a scrollable box * Changed to gray color. * removed padding * Fix height for output section. * onExpand click event. * Added todo for 9th item. * Removed unused code block. * When in focus, reduce the hint text alpha value. * Show empty text error. * Clear focus on tap outside. * Add border when focused. * Propagate focus changes. * CGM graph * Added sample of one to many uart configuration database. * Added device and configuration entities. * Fixed issue with only showing last item from the list. * Changed configuration database irrespective of device address. * File rename. * Added last configuration datastore. * Check if configuration name is unique * Removed Macro text. * Included x and y axis data points. * Added channel sounding service uuid. * Upgraded agp version to 2.7. * Added channel sounding manager. * Downgraded datastore preference to 1.1.4. * Changed to nordic colors. * Added ranging permission. * channel sounding repository * channel sounding service data * channel sounding profile * channel sounding profile in viewmodel * channel sounding manager class * channel sounding testing * CS service characteristics * Create bonding before channel sounding connection. * Clean up. * Added LBS profile * Read/write data to LBS * LBS ui events * LBS service * LBS profile * LBS ui * Agp upgrade * Fixed LBS profile * Removed focus * Changed macro size to 9 * Changed macro color * Show macro in bottom sheet * View refactoring * Added Blek dependency * Added utils dependency * rename * Removed unused event * reorganization * uart macro view update * background color update * different color for input and output message type * Changed to uart event * removed duplicate * rename * auto scroll to new record * removed unused dependency * Fixed crash with ChannelSoundingManager injection. * Require bonding only if it has bonding information * Changed disconnection * CGMS graph * changes in the home view * Home view fixes * changed color * Show MacroEol character in the input message. * Home view icon fixes. * Cadence data parser fixes * Fixed CSC settings view. * Fixed rscs view * hiding graphs until its finished * Removed duplicate * Fixed RSCS view * Fixed notification icon * fixed csc module name * Fixed icon cutoff * Fixed CSCDataParser * Fixed CGMS profile * Fixed GLS view * Fixed GLS strings * Fixed HTS view * Fixed HTS view * title change * Added hts timestamp * Deleted verbose text * UART: changed macro/configuration to preset * UART: fixed input text field * UART: removed expandable/collapsable preset * UART: added extra warning to delete action * UART: don't trim message end. * UART: message section * UART: configuration fixed * UART: configuration fixed * Fix crash when disconnecting before MTU change completes * Disabled incomplete PRX profile * Moved non-composable lambdas to parameters * refactoring display text * Fixed channel sounding screen * Disconnect on missing services before navigation * Fixed label name * Tailored disconnection message. * Tailored disconnection message. * Moved profile file to utils * App analytics events and modes * Integrated analytics with the profile actions. * Show only first non-battery service if multiple services are present. * Fixed window insets for camera notch. * Fixed glucose measurement context. * Fixed glucose concentration unit. * Fixed duplicate analytics update. * rename * refactoring text * Handled disconnecting event. * Replaced with LazyColumn * Fixed window insets * Replaced TitleAppBar with NordicAppBar * Show device address * Show multiple service names if available. * Fixed padding * BPS: Fixed waiting for measurement view. * BPS: view * GLS: Fixed padding * Ui: Fixed dialog * RSCS: fixed distance formatting error * CGMS: ui consistency * DFS: ui fixes * Replaced local scanner with common library scanner. * Fixed padding * reorganization * Removed previous uart module * Text with animated three dots * HTS: text fixes * formatting texts * changed text style * fixed string * Fixed HRS, not completed * DFS: fixed ui * HRS: graph fixes * UART: scroll up when keyboard is visible * Uart input: Add focus * Uart fix: input text field * UART: created rememberImeState * HRS: heart rate ui fixes * profile view scrollable fix * DFS: ui fixes * Fixed logger * Check if the battery characteristics supports NOTIFY or INDICATE property * Dependency update * Changed background color * cleanup * Fixed distance measurement data update. * Filtered devices with testing address * Added preview data * Fixed section view * Fixed elevation view * Removed duplicate views * Fixes control points * String fixes * Elevation view fixes * Range slider view update * Fixed DFS views * Fixed DFS ui * Fixed DFS views * Separated views * Separated profile viewmodel into individual profile view models. * AGP upgrade * Job canceled and make jobs null on clear * Profile name update * Request maximum MTU size only if it is not already set. * Fixed null pointer exception * Battery characteristics read property check * Fixed early mtu request * Removed garbage states * Removed logs * Removed multiple vertical scroll * Fixed padding * Ui fixes * File reorganization * Fixed previous configuration not loading on reconnection * Removed unused files * Dependency update * Renamed module name * Removed unused dependencies * Added param * Removed unused code block * Code optimization * Removed unused file * Readme update * Hide Channel sounding until implementation is complete * Handled initial state closed * revert changes * Added library as module placeholder * Fixed multiple flows for the same peripheral * Request mtu size only when needed * Readme update
This commit is contained in:
@@ -11,10 +11,12 @@ It contains applications demonstrating standard Bluetooth LE profiles:
|
||||
* **Health Thermometer Monitor**,
|
||||
* **Glucose Monitor**,
|
||||
* **Continuous Glucose Monitor**,
|
||||
* **Proximity Monitor**
|
||||
* **Universal Asynchronous Receiver/Transmitter (UART)**,
|
||||
* **Throughput**,
|
||||
* **Direction Finder**,
|
||||
* **Blinky (LBS) Service**
|
||||
|
||||
Since version 1.10.0 the *nRF Toolbox* also supports the **Nordic UART Service** which may be used
|
||||
for bidirectional text communication between devices.
|
||||
**_NOTE:_** The Proximity profile is not included in this version of the app. If you need it, please download the previous version.
|
||||
|
||||
### How to import to Android Studio
|
||||
|
||||
|
||||
@@ -31,9 +31,10 @@
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.application.compose)
|
||||
alias(libs.plugins.nordic.hilt)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Release")) {
|
||||
if (getGradle().startParameter.taskRequests.toString().contains("Release")) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply(plugin = "com.google.firebase.crashlytics")
|
||||
}
|
||||
@@ -43,44 +44,31 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//Hilt requires to implement every module in the main app module
|
||||
//https://github.com/google/dagger/issues/2123
|
||||
implementation(project(":profile_bps"))
|
||||
implementation(project(":profile_csc"))
|
||||
implementation(project(":profile_cgms"))
|
||||
implementation(project(":profile_gls"))
|
||||
implementation(project(":profile_hrs"))
|
||||
implementation(project(":profile_hts"))
|
||||
implementation(project(":profile_prx"))
|
||||
implementation(project(":profile_rscs"))
|
||||
|
||||
implementation(project(":profile_uart"))
|
||||
|
||||
implementation(project(":lib_analytics"))
|
||||
implementation(project(":profile-parsers"))
|
||||
implementation(project(":profile_manager"))
|
||||
implementation(project(":profile"))
|
||||
implementation(project(":profile_data"))
|
||||
implementation(project(":lib_ui"))
|
||||
implementation(project(":lib_utils"))
|
||||
implementation(project(":lib_service"))
|
||||
implementation(project(":lib_scanner"))
|
||||
|
||||
implementation(libs.nordic.core)
|
||||
implementation(libs.nordic.theme)
|
||||
implementation(libs.nordic.navigation)
|
||||
implementation(libs.nordic.blek.uiscanner)
|
||||
implementation(libs.nordic.theme)
|
||||
implementation(libs.nordic.logger)
|
||||
implementation(libs.nordic.permissions.ble)
|
||||
implementation(libs.nordic.analytics)
|
||||
|
||||
implementation(libs.nordic.blek.client)
|
||||
implementation(libs.nordic.ui)
|
||||
implementation(libs.nordic.core)
|
||||
implementation(libs.nordic.scanner.ble)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
|
||||
// Timber & SLF4J
|
||||
implementation (libs.slf4j.timber)
|
||||
implementation(libs.nordic.log.timber)
|
||||
|
||||
implementation(libs.nordic.blek.client.android)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import no.nordicsemi.android.analytics.AppAnalytics
|
||||
import no.nordicsemi.android.analytics.AppOpenEvent
|
||||
import no.nordicsemi.android.gls.GLSServer
|
||||
import no.nordicsemi.android.uart.UartServer
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -46,20 +44,11 @@ class NrfToolboxApplication : Application() {
|
||||
@Inject
|
||||
lateinit var analytics: AppAnalytics
|
||||
|
||||
@Inject
|
||||
lateinit var uartServer: UartServer
|
||||
|
||||
@Inject
|
||||
lateinit var glsServer: GLSServer
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
analytics.logEvent(AppOpenEvent)
|
||||
|
||||
uartServer.start(this)
|
||||
glsServer.start(this)
|
||||
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
|
||||
<queries>
|
||||
<package android:name="no.nordicsemi.android.dfu" />
|
||||
|
||||
|
||||
@@ -31,45 +31,10 @@
|
||||
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import no.nordicsemi.android.bps.view.BPSScreen
|
||||
import no.nordicsemi.android.cgms.view.CGMScreen
|
||||
import no.nordicsemi.android.common.navigation.createSimpleDestination
|
||||
import no.nordicsemi.android.common.navigation.defineDestination
|
||||
import no.nordicsemi.android.csc.view.CSCScreen
|
||||
import no.nordicsemi.android.gls.main.view.GLSScreen
|
||||
import no.nordicsemi.android.hrs.view.HRSScreen
|
||||
import no.nordicsemi.android.hts.view.HTSScreen
|
||||
import no.nordicsemi.android.nrftoolbox.view.HomeScreen
|
||||
import no.nordicsemi.android.prx.view.PRXScreen
|
||||
import no.nordicsemi.android.rscs.view.RSCSScreen
|
||||
import no.nordicsemi.android.toolbox.scanner.ScannerDestination
|
||||
import no.nordicsemi.android.uart.view.UARTScreen
|
||||
import no.nordicsemi.android.nrftoolbox.view.HomeView
|
||||
|
||||
val HomeDestinationId = createSimpleDestination("home-destination")
|
||||
|
||||
val HomeDestinations = listOf(
|
||||
defineDestination(HomeDestinationId) { HomeScreen() },
|
||||
ScannerDestination
|
||||
)
|
||||
|
||||
val CSCDestinationId = createSimpleDestination("csc-destination")
|
||||
val HRSDestinationId = createSimpleDestination("hrs-destination")
|
||||
val HTSDestinationId = createSimpleDestination("hts-destination")
|
||||
val GLSDestinationId = createSimpleDestination("gls-destination")
|
||||
val BPSDestinationId = createSimpleDestination("bps-destination")
|
||||
val PRXDestinationId = createSimpleDestination("prx-destination")
|
||||
val RSCSDestinationId = createSimpleDestination("rscs-destination")
|
||||
val CGMSDestinationId = createSimpleDestination("cgms-destination")
|
||||
val UARTDestinationId = createSimpleDestination("uart-destination")
|
||||
|
||||
val ProfileDestinations = listOf(
|
||||
defineDestination(CSCDestinationId) { CSCScreen() },
|
||||
defineDestination(HRSDestinationId) { HRSScreen() },
|
||||
defineDestination(HTSDestinationId) { HTSScreen() },
|
||||
defineDestination(GLSDestinationId) { GLSScreen() },
|
||||
defineDestination(BPSDestinationId) { BPSScreen() },
|
||||
defineDestination(PRXDestinationId) { PRXScreen() },
|
||||
defineDestination(RSCSDestinationId) { RSCSScreen() },
|
||||
defineDestination(CGMSDestinationId) { CGMScreen() },
|
||||
defineDestination(UARTDestinationId) { UARTScreen() },
|
||||
)
|
||||
val HomeDestinations = defineDestination(HomeDestinationId) { HomeView() }
|
||||
|
||||
@@ -42,17 +42,11 @@ import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionRequestDia
|
||||
import no.nordicsemi.android.common.navigation.NavigationView
|
||||
import no.nordicsemi.android.common.theme.NordicActivity
|
||||
import no.nordicsemi.android.common.theme.NordicTheme
|
||||
import no.nordicsemi.android.gls.GLSDestination
|
||||
import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals
|
||||
import no.nordicsemi.android.toolbox.scanner.ScannerDestination
|
||||
import javax.inject.Inject
|
||||
import no.nordicsemi.android.toolbox.profile.ProfileDestination
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : NordicActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var activitySignals: ActivitySignals
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -62,7 +56,7 @@ class MainActivity : NordicActivity() {
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
NavigationView(HomeDestinations + ProfileDestinations + ScannerDestination + GLSDestination)
|
||||
NavigationView(HomeDestinations + ScannerDestination + ProfileDestination)
|
||||
}
|
||||
|
||||
AnalyticsPermissionRequestDialog()
|
||||
@@ -70,8 +64,4 @@ class MainActivity : NordicActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activitySignals.onResume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.common.navigation.createDestination
|
||||
import no.nordicsemi.android.common.navigation.defineDestination
|
||||
import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel
|
||||
import no.nordicsemi.android.common.scanner.DeviceSelected
|
||||
import no.nordicsemi.android.common.scanner.ScannerScreen
|
||||
import no.nordicsemi.android.common.scanner.ScanningCancelled
|
||||
import no.nordicsemi.android.common.scanner.data.OnlyNearby
|
||||
import no.nordicsemi.android.common.scanner.data.OnlyWithNames
|
||||
import no.nordicsemi.android.common.scanner.rememberFilterState
|
||||
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
||||
import no.nordicsemi.kotlin.ble.client.android.ScanResult
|
||||
|
||||
val ScannerDestinationId = createDestination<Unit, ScanResult>("ble-scanner")
|
||||
|
||||
val ScannerDestination = defineDestination(ScannerDestinationId) {
|
||||
val navigationVM = hiltViewModel<SimpleNavigationViewModel>()
|
||||
|
||||
ScannerScreen(
|
||||
cancellable = true,
|
||||
state = rememberFilterState(
|
||||
dynamicFilters = listOf(
|
||||
OnlyNearby(),
|
||||
OnlyWithNames(),
|
||||
)
|
||||
),
|
||||
onResultSelected = {
|
||||
when (it) {
|
||||
is DeviceSelected -> {
|
||||
navigationVM.navigateTo(ProfileDestinationId, it.scanResult.peripheral.address)
|
||||
{
|
||||
popUpTo(ScannerDestinationId.toString()) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScanningCancelled -> {
|
||||
navigationVM.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
package no.nordicsemi.android.nrftoolbox.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
@Module
|
||||
@@ -12,5 +13,5 @@ import kotlinx.coroutines.SupervisorJob
|
||||
class ApplicationScopeModule {
|
||||
|
||||
@Provides
|
||||
fun applicationScope() = CoroutineScope(SupervisorJob())
|
||||
fun applicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package no.nordicsemi.android.nrftoolbox.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.native
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CentralManagerModule {
|
||||
|
||||
@Provides
|
||||
fun provideCentralManager(
|
||||
@ApplicationContext context: Context,
|
||||
scope: CoroutineScope
|
||||
): CentralManager {
|
||||
return CentralManager.Factory.native(context, scope)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.repository
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ActivitySignals @Inject constructor() {
|
||||
|
||||
private val _onResumeTrigger = MutableStateFlow(false)
|
||||
val state = _onResumeTrigger.asStateFlow()
|
||||
|
||||
fun onResume() {
|
||||
_onResumeTrigger.value = !_onResumeTrigger.value
|
||||
}
|
||||
}
|
||||
@@ -1,120 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.view
|
||||
|
||||
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.Spacer
|
||||
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.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
|
||||
@Composable
|
||||
fun FeatureButton(
|
||||
internal fun FeatureButton(
|
||||
@DrawableRes iconId: Int,
|
||||
@StringRes nameCode: Int,
|
||||
@StringRes name: Int,
|
||||
isRunning: Boolean? = null,
|
||||
@StringRes description: Int? = null,
|
||||
@StringRes description: Int,
|
||||
profileNames: List<String> = listOf(stringResource(description)),
|
||||
deviceName: String?,
|
||||
deviceAddress: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
OutlinedCard(onClick = onClick) {
|
||||
OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val color = if (isRunning == true) {
|
||||
colorResource(id = R.color.nordicGrass)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(iconId),
|
||||
contentDescription = stringResource(id = name),
|
||||
contentScale = ContentScale.Crop,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary),
|
||||
contentDescription = stringResource(id = description),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.padding(16.dp)
|
||||
.size(40.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(id = name),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
text = deviceName ?: stringResource(R.string.unknown_device),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = profileNames.joinToString(", "),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
description?.let {
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(id = it),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = deviceAddress,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,5 +79,64 @@ fun FeatureButton(
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FeatureButtonPreview() {
|
||||
FeatureButton(R.drawable.ic_csc, R.string.csc_module, R.string.csc_module_full) { }
|
||||
FeatureButton(
|
||||
R.drawable.ic_csc,
|
||||
R.string.csc_module_full,
|
||||
listOf("Cycling Speed and Cadence", "Cycling Speed Sensor"),
|
||||
"Testing peripheral",
|
||||
deviceAddress = "AA:BB:CC:DD:EE:FF",
|
||||
) { }
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FeatureButton(
|
||||
iconId: ImageVector,
|
||||
@StringRes description: Int,
|
||||
profileNames: List<String> = listOf(stringResource(description)),
|
||||
deviceName: String?,
|
||||
deviceAddress: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Image(
|
||||
imageVector = iconId,
|
||||
contentDescription = deviceAddress,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = deviceName ?: stringResource(R.string.unknown_device),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = profileNames.joinToString(", "),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = deviceAddress,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,263 +1,344 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.view
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Lightbulb
|
||||
import androidx.compose.material.icons.filled.SocialDistance
|
||||
import androidx.compose.material.icons.filled.SyncAlt
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import no.nordicsemi.android.analytics.Link
|
||||
import no.nordicsemi.android.analytics.Profile
|
||||
import no.nordicsemi.android.analytics.ProfileOpenEvent
|
||||
import no.nordicsemi.android.nrftoolbox.BPSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.BuildConfig
|
||||
import no.nordicsemi.android.nrftoolbox.CGMSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.CSCDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.GLSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.HRSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.HTSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.PRXDestinationId
|
||||
import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionButton
|
||||
import no.nordicsemi.android.common.ui.view.NordicAppBar
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
import no.nordicsemi.android.nrftoolbox.RSCSDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.UARTDestinationId
|
||||
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
|
||||
import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
|
||||
private const val DFU_PACKAGE_NAME = "no.nordicsemi.android.dfu"
|
||||
private const val DFU_LINK = "https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu"
|
||||
|
||||
private const val LOGGER_PACKAGE_NAME = "no.nordicsemi.android.log"
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val viewModel: HomeViewModel = hiltViewModel()
|
||||
internal fun HomeView() {
|
||||
val viewModel = hiltViewModel<HomeViewModel>()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val onEvent: (UiEvent) -> Unit = { viewModel.onClickEvent(it) }
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||
topBar = {
|
||||
TitleAppBar(stringResource(id = R.string.app_name))
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
NordicAppBar(
|
||||
title = { Text(stringResource(id = R.string.app_name)) },
|
||||
showBackButton = false,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AnalyticsPermissionButton()
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { onEvent(UiEvent.OnConnectDeviceClick) },
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp, end = 8.dp, start = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add device from scanner"
|
||||
)
|
||||
Text(text = stringResource(R.string.connect_device))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// Get notch padding for devices with a display cutout (notch)
|
||||
val notchPadding = WindowInsets.displayCutout
|
||||
.union(WindowInsets(left = 8.dp, right = 8.dp, top = 8.dp, bottom = 8.dp))
|
||||
.only(WindowInsetsSides.Horizontal)
|
||||
.asPaddingValues()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.padding(paddingValues),
|
||||
contentPadding = notchPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
item {
|
||||
// Show the title at the top
|
||||
Text(
|
||||
text = stringResource(id = R.string.viewmodel_profiles),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.connected_devices),
|
||||
modifier = Modifier
|
||||
.alpha(0.5f)
|
||||
.padding(start = 16.dp),
|
||||
)
|
||||
if (state.connectedDevices.isNotEmpty()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
state.connectedDevices.values.forEach { (peripheral, services) ->
|
||||
// Skip if no services
|
||||
if (services.isEmpty()) return@forEach
|
||||
// Case 1: If only one service, show it directly like battery service
|
||||
if (services.size == 1 && services.first().profile == Profile.BATTERY) {
|
||||
FeatureButton(
|
||||
iconId = R.drawable.ic_battery,
|
||||
description = R.string.battery_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
services.first().profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
// Case 2: Show the first *non-Battery* profile.
|
||||
// This ensures only one service is shown per peripheral when multiple services are available.
|
||||
services.firstOrNull { it.profile != Profile.BATTERY }
|
||||
?.let { serviceManager ->
|
||||
when (serviceManager.profile) {
|
||||
Profile.HRS -> FeatureButton(
|
||||
iconId = R.drawable.ic_hrs,
|
||||
description = R.string.hrs_module_full,
|
||||
deviceName = peripheral.name,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
deviceAddress = peripheral.address,
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.HTS -> FeatureButton(
|
||||
iconId = R.drawable.ic_hts,
|
||||
description = R.string.hts_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
FeatureButton(R.drawable.ic_gls, R.string.gls_module, R.string.gls_module_full) {
|
||||
viewModel.openProfile(GLSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.GLS))
|
||||
}
|
||||
Profile.BPS -> FeatureButton(
|
||||
iconId = R.drawable.ic_bps,
|
||||
description = R.string.bps_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.GLS -> FeatureButton(
|
||||
iconId = R.drawable.ic_gls,
|
||||
description = R.string.gls_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
FeatureButton(R.drawable.ic_bps, R.string.bps_module, R.string.bps_module_full) {
|
||||
viewModel.openProfile(BPSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.BPS))
|
||||
}
|
||||
Profile.CGM -> FeatureButton(
|
||||
iconId = R.drawable.ic_cgm,
|
||||
description = R.string.cgm_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.RSCS -> FeatureButton(
|
||||
iconId = R.drawable.ic_rscs,
|
||||
description = R.string.rscs_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.service_profiles),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Profile.DFS -> FeatureButton(
|
||||
iconId = R.drawable.ic_distance,
|
||||
description = R.string.direction_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.CSC -> FeatureButton(
|
||||
iconId = R.drawable.ic_csc,
|
||||
description = R.string.csc_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_csc,
|
||||
R.string.csc_module,
|
||||
R.string.csc_module_full,
|
||||
state.isCSCModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(CSCDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.CSC))
|
||||
}
|
||||
Profile.THROUGHPUT -> {
|
||||
FeatureButton(
|
||||
iconId = Icons.Default.SyncAlt,
|
||||
description = R.string.throughput_module,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.UART -> {
|
||||
FeatureButton(
|
||||
iconId = R.drawable.ic_uart,
|
||||
description = R.string.uart_module_full,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_hrs,
|
||||
R.string.hrs_module,
|
||||
R.string.hrs_module_full,
|
||||
state.isHRSModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(HRSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.HRS))
|
||||
}
|
||||
Profile.CHANNEL_SOUNDING -> {
|
||||
FeatureButton(
|
||||
iconId = Icons.Default.SocialDistance,
|
||||
description = R.string.channel_sounding_module,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Profile.LBS -> {
|
||||
FeatureButton(
|
||||
iconId = Icons.Default.Lightbulb,
|
||||
description = R.string.lbs_blinky_module,
|
||||
deviceName = peripheral.name,
|
||||
deviceAddress = peripheral.address,
|
||||
profileNames = services.map { it.profile.toString() },
|
||||
onClick = {
|
||||
onEvent(
|
||||
UiEvent.OnDeviceClick(
|
||||
peripheral.address,
|
||||
serviceManager.profile
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_hts,
|
||||
R.string.hts_module,
|
||||
R.string.hts_module_full,
|
||||
state.isHTSModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(HTSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.HTS))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_rscs,
|
||||
R.string.rscs_module,
|
||||
R.string.rscs_module_full,
|
||||
state.isRSCSModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(RSCSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.RSCS))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_cgm,
|
||||
R.string.cgm_module,
|
||||
R.string.cgm_module_full,
|
||||
state.isCGMModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(CGMSDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.CGMS))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_prx,
|
||||
R.string.prx_module,
|
||||
R.string.prx_module_full,
|
||||
state.isPRXModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(PRXDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.PRX))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.utils_services),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_uart,
|
||||
R.string.uart_module,
|
||||
R.string.uart_module_full,
|
||||
state.isUARTModuleRunning
|
||||
) {
|
||||
viewModel.openProfile(UARTDestinationId)
|
||||
viewModel.logEvent(ProfileOpenEvent(Profile.UART))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
val packageManger = context.packageManager
|
||||
|
||||
val description = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME)?.let {
|
||||
R.string.dfu_module_info
|
||||
} ?: R.string.dfu_module_install
|
||||
|
||||
FeatureButton(R.drawable.ic_dfu, R.string.dfu_module, R.string.dfu_module_full, null, description) {
|
||||
val intent = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME)
|
||||
if (intent != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
uriHandler.openUri(DFU_LINK)
|
||||
else -> {
|
||||
// TODO: Add more profiles
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.logEvent(ProfileOpenEvent(Link.DFU))
|
||||
} else {
|
||||
NoConnectedDeviceView()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val loggerDescription = packageManger.getLaunchIntentForPackage(LOGGER_PACKAGE_NAME)?.let {
|
||||
R.string.logger_module_info
|
||||
} ?: R.string.dfu_module_install
|
||||
|
||||
FeatureButton(
|
||||
R.drawable.ic_logger,
|
||||
R.string.logger_module,
|
||||
R.string.logger_module_full,
|
||||
null,
|
||||
loggerDescription
|
||||
) {
|
||||
viewModel.openLogger()
|
||||
viewModel.logEvent(ProfileOpenEvent(Link.LOGGER))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = BuildConfig.VERSION_NAME,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item {
|
||||
Links { onEvent(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.view
|
||||
|
||||
data class HomeViewState(
|
||||
val isCSCModuleRunning: Boolean = false,
|
||||
val isHRSModuleRunning: Boolean = false,
|
||||
val isHTSModuleRunning: Boolean = false,
|
||||
val isRSCSModuleRunning: Boolean = false,
|
||||
val isPRXModuleRunning: Boolean = false,
|
||||
val isCGMModuleRunning: Boolean = false,
|
||||
val isUARTModuleRunning: Boolean = false,
|
||||
val refreshToggle: Boolean = false
|
||||
) {
|
||||
|
||||
fun copyWithRefresh(): HomeViewState {
|
||||
return copy(refreshToggle = !refreshToggle)
|
||||
}
|
||||
}
|
||||
103
app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt
Normal file
103
app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package no.nordicsemi.android.nrftoolbox.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent
|
||||
|
||||
@Composable
|
||||
internal fun Links(onEvent: (UiEvent) -> Unit) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.links),
|
||||
modifier = Modifier
|
||||
.alpha(0.5f)
|
||||
.padding(start = 16.dp),
|
||||
)
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))
|
||||
.clickable { onEvent(UiEvent.OnGitHubClick) }
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Code,
|
||||
contentDescription = stringResource(R.string.github_repo),
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.github_repo),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp))
|
||||
.clickable { onEvent(UiEvent.OnNordicDevZoneClick) }
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Language,
|
||||
contentDescription = stringResource(R.string.nordic_dev_zone),
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.nordic_dev_zone),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LinksPreview() {
|
||||
Links { }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package no.nordicsemi.android.nrftoolbox.view
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.common.theme.NordicTheme
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
|
||||
@Composable
|
||||
internal fun NoConnectedDeviceView() {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val scale by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = 1.1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
)
|
||||
)
|
||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_notification_icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.device_not_connected_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.device_not_connected_message),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun NoConnectedDeviceViewPreview() {
|
||||
NordicTheme {
|
||||
NoConnectedDeviceView()
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.view
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionButton
|
||||
import no.nordicsemi.android.nrftoolbox.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TitleAppBar(text: String) {
|
||||
TopAppBar(
|
||||
title = { Text(text, maxLines = 2) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primary,
|
||||
containerColor = colorResource(id = R.color.appBarColor),
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
actions = {
|
||||
AnalyticsPermissionButton()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,123 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.nrftoolbox.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import no.nordicsemi.android.analytics.AppAnalytics
|
||||
import no.nordicsemi.android.analytics.Link
|
||||
import no.nordicsemi.android.analytics.ProfileOpenEvent
|
||||
import no.nordicsemi.android.cgms.repository.CGMRepository
|
||||
import no.nordicsemi.android.common.logger.LoggerLauncher
|
||||
import no.nordicsemi.android.common.navigation.DestinationId
|
||||
import no.nordicsemi.android.common.navigation.Navigator
|
||||
import no.nordicsemi.android.csc.repository.CSCRepository
|
||||
import no.nordicsemi.android.hrs.service.HRSRepository
|
||||
import no.nordicsemi.android.hts.repository.HTSRepository
|
||||
import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals
|
||||
import no.nordicsemi.android.nrftoolbox.view.HomeViewState
|
||||
import no.nordicsemi.android.prx.repository.PRXRepository
|
||||
import no.nordicsemi.android.rscs.repository.RSCSRepository
|
||||
import no.nordicsemi.android.uart.repository.UARTRepository
|
||||
import no.nordicsemi.android.nrftoolbox.ScannerDestinationId
|
||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
||||
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val navigationManager: Navigator,
|
||||
private val activitySignals: ActivitySignals,
|
||||
cgmRepository: CGMRepository,
|
||||
cscRepository: CSCRepository,
|
||||
hrsRepository: HRSRepository,
|
||||
htsRepository: HTSRepository,
|
||||
prxRepository: PRXRepository,
|
||||
rscsRepository: RSCSRepository,
|
||||
uartRepository: UARTRepository,
|
||||
private val analytics: AppAnalytics
|
||||
) : ViewModel() {
|
||||
internal data class HomeViewState(
|
||||
val connectedDevices: Map<String, Pair<Peripheral, List<ServiceManager>>> = emptyMap(),
|
||||
)
|
||||
|
||||
private const val GITHUB_REPO_URL = "https://github.com/NordicSemiconductor/Android-nRF-Toolbox.git"
|
||||
private const val NORDIC_DEV_ZONE_URL = "https://devzone.nordicsemi.com/"
|
||||
|
||||
@HiltViewModel
|
||||
internal class HomeViewModel @Inject constructor(
|
||||
private val navigator: Navigator,
|
||||
deviceRepository: DeviceRepository,
|
||||
private val analytics: AppAnalytics,
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(HomeViewState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
cgmRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isCGMModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
cscRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isCSCModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
hrsRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isHRSModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
htsRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isHTSModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
prxRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isPRXModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
rscsRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isRSCSModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
uartRepository.isRunning.onEach {
|
||||
_state.value = _state.value.copy(isUARTModuleRunning = it)
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
activitySignals.state.onEach {
|
||||
_state.value = _state.value.copyWithRefresh()
|
||||
// Observe connected devices from the repository
|
||||
deviceRepository.connectedDevices.onEach { devices ->
|
||||
_state.update { currentState ->
|
||||
currentState.copy(connectedDevices = devices)
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun openProfile(destination: DestinationId<Unit, Unit>) {
|
||||
navigationManager.navigateTo(destination)
|
||||
fun onClickEvent(event: UiEvent) {
|
||||
when (event) {
|
||||
UiEvent.OnConnectDeviceClick -> navigator.navigateTo(ScannerDestinationId)
|
||||
is UiEvent.OnDeviceClick -> {
|
||||
// Log the event for analytics.
|
||||
analytics.logEvent(ProfileOpenEvent(event.profile))
|
||||
|
||||
navigator.navigateTo(
|
||||
ProfileDestinationId, event.deviceAddress
|
||||
)
|
||||
}
|
||||
|
||||
UiEvent.OnGitHubClick -> {
|
||||
// Log the event for analytics.
|
||||
analytics.logEvent(ProfileOpenEvent(Link.GITHUB))
|
||||
navigator.open(GITHUB_REPO_URL.toUri())
|
||||
}
|
||||
|
||||
UiEvent.OnNordicDevZoneClick -> {
|
||||
// Log the event for analytics.
|
||||
analytics.logEvent(ProfileOpenEvent(Link.DEV_ACADEMY))
|
||||
navigator.open(NORDIC_DEV_ZONE_URL.toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openLogger() {
|
||||
LoggerLauncher.launch(context, null)
|
||||
}
|
||||
|
||||
fun logEvent(event: ProfileOpenEvent) {
|
||||
analytics.logEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package no.nordicsemi.android.nrftoolbox.viewmodel
|
||||
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
|
||||
/**
|
||||
* HomeViewEvent is a sealed interface that represents the events that can be emitted by the Home view.
|
||||
*/
|
||||
sealed interface UiEvent {
|
||||
|
||||
/** OnConnectDeviceClick event that is emitted when the user clicks on the Connect Device button. */
|
||||
data object OnConnectDeviceClick : UiEvent
|
||||
|
||||
/** OnDeviceClick event is emitted when the user clicks on a connected device. */
|
||||
data class OnDeviceClick(val deviceAddress: String, val profile: Profile) : UiEvent
|
||||
|
||||
/**
|
||||
* OnGitHubClick event is emitted when the user clicks on the GitHub repository option.
|
||||
*/
|
||||
data object OnGitHubClick : UiEvent
|
||||
|
||||
/** OnNordicDevZoneClick event is emitted when the user clicks on the Nordic DevZone option. */
|
||||
data object OnNordicDevZoneClick : UiEvent
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<!--
|
||||
~ Copyright (c) 2022, 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.
|
||||
-->
|
||||
|
||||
<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="M386.1,310.1c0,-21.9 -5.5,-43.7 -15.9,-63c-0.2,-0.4 -0.5,-0.9 -0.7,-1.3c-2.4,-4.3 -5,-8.4 -7.8,-12.4C311,152.8 276,96.7 271.7,89.2c-4.3,-8.4 -12.5,-14.3 -22.1,-15.4c-11,-1.3 -21.7,3.9 -27.5,13.3L133,230.6c-4.8,6.4 -8.9,13.2 -12.5,20.3c-0.1,0.2 -0.2,0.4 -0.3,0.7c-9.1,18.4 -13.6,38.1 -13.6,58.6c0,74.9 62.7,135.9 139.8,135.9S386.1,385 386.1,310.1zM163.5,310.1c0,-11.8 2.7,-23.1 7.9,-33.6c0,0 0,0 0,-0.1c2.2,-4.3 4.8,-8.6 7.8,-12.5c0.5,-0.7 1,-1.4 1.5,-2.2l65.4,-105.3c4,6.3 8.5,13.6 13.8,22.1c19.9,31.8 42.3,67.5 53.9,85.9c0.3,0.5 0.6,1 0.9,1.4c1.8,2.5 3.5,5.2 5,7.9c0,0 0,0 0,0c0,0 0,0.1 0,0.1c6.2,11.3 9.3,23.5 9.3,36.3c0,43.5 -37.1,78.9 -82.8,78.9S163.5,353.6 163.5,310.1z" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M975.7,103c-50.2,-48.2 -130.2,-46.5 -178.4,3.7L228.8,699.8c-10.9,11.4 -10.5,29.4 0.8,40.3l49.9,47.9c-1.7,1.2 -3.4,2.7 -4.8,4.3l-91.4,102.9c-10.5,11.8 -9.4,29.8 2.4,40.2c5.4,4.8 12.2,7.2 18.9,7.2c7.9,0 15.7,-3.2 21.3,-9.6l91.4,-102.9c1,-1.1 1.8,-2.2 2.6,-3.4l50.7,48.7c5.3,5.1 12.4,7.9 19.7,7.9c0.2,0 0.4,0 0.6,0c7.6,-0.2 14.8,-3.3 20,-8.8l568.4,-593c0,0 0.1,-0.1 0.1,-0.1C1027.6,231.2 1025.9,151.2 975.7,103zM938.3,241.9C938.3,241.9 938.3,241.9 938.3,241.9L389.5,814.5l-99.8,-95.8l548.8,-572.5c26.4,-27.5 70.3,-28.4 97.8,-2C963.8,170.5 964.7,214.3 938.3,241.9z" />
|
||||
</vector>
|
||||
@@ -32,33 +32,68 @@
|
||||
<resources>
|
||||
<string name="csc_module">CSC</string>
|
||||
<string name="csc_module_full">Cyclic Speed and Cadence</string>
|
||||
|
||||
<string name="hrs_module">HRS</string>
|
||||
<string name="hrs_module_full">Heart Rate</string>
|
||||
|
||||
<string name="gls_module">GLS</string>
|
||||
<string name="gls_module_full">Glucose</string>
|
||||
|
||||
<string name="hts_module">HTS</string>
|
||||
<string name="hts_module_full">Health Thermometer</string>
|
||||
|
||||
<string name="bps_module">BPS</string>
|
||||
<string name="bps_module_full">Blood Pressure</string>
|
||||
|
||||
<string name="rscs_module">RSCS</string>
|
||||
<string name="rscs_module_full">Running Speed and Cadence</string>
|
||||
|
||||
<string name="prx_module">PRX</string>
|
||||
<string name="prx_module_full">Proximity</string>
|
||||
|
||||
<string name="cgm_module">CGMS</string>
|
||||
<string name="cgm_module_full">Continuous Glucose</string>
|
||||
|
||||
<string name="uart_module">UART</string>
|
||||
<string name="uart_module_full">Universal Asynchronous Receiver/Transmitter (UART)</string>
|
||||
|
||||
<string name="dfu_module">DFU</string>
|
||||
<string name="dfu_module_full">Device Firmware Update</string>
|
||||
<string name="dfu_module_info">Open DFU application.</string>
|
||||
<string name="dfu_module_install">Download from Google Play.</string>
|
||||
|
||||
<string name="logger_module">nRF Logger</string>
|
||||
<string name="logger_module_full">nRF Logger</string>
|
||||
<string name="logger_module_info">Open nRF Logger application.</string>
|
||||
|
||||
<string name="direction_module">DF</string>
|
||||
<string name="direction_module_full">Direction Finder</string>
|
||||
|
||||
<string name="viewmodel_profiles">ViewModel profiles</string>
|
||||
<string name="service_profiles">Service profiles</string>
|
||||
<string name="utils_services">Utils services</string>
|
||||
|
||||
<string name="running_profile_icon">Icon indicating if the profile is running</string>
|
||||
|
||||
<string name="battery_module">BATTERY</string>
|
||||
<string name="battery_module_full">Battery</string>
|
||||
|
||||
<string name="channel_sounding_module"> CHANNEL SOUNDING</string>
|
||||
|
||||
<string name="throughput_module">Throughput</string>
|
||||
<string name="unknown_device">Unknown Device</string>
|
||||
|
||||
<string name="lbs_blinky_module">LBS/Blinky</string>
|
||||
|
||||
<string name="device_not_connected_title">NO DEVICES CONNECTED</string>
|
||||
<string name="device_not_connected_message">Tap Connect device button to begin.</string>
|
||||
|
||||
<string name="connect_device">Connect Device</string>
|
||||
<string name="connected_devices">Connected devices</string>
|
||||
|
||||
<string name="github_repo">Source code (GitHub)</string>
|
||||
<string name="nordic_dev_zone">Help (Nordic DevZone)</string>
|
||||
|
||||
<string name="links">Links</string>
|
||||
|
||||
</resources>
|
||||
|
||||
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -28,10 +28,9 @@
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
#Mon Feb 14 14:46:55 CET 2022
|
||||
#Thu Jul 03 10:42:00 CEST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -38,6 +38,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib_utils"))
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
|
||||
@@ -31,24 +31,20 @@
|
||||
|
||||
package no.nordicsemi.android.analytics
|
||||
|
||||
enum class Profile(val displayName: String) {
|
||||
BPS("BPS"),
|
||||
CGMS("CGMS"),
|
||||
CSC("CSC"),
|
||||
GLS("GLS"),
|
||||
HRS("HRS"),
|
||||
HTS("HTS"),
|
||||
PRX("PRX"),
|
||||
RSCS("RSCS"),
|
||||
UART("UART");
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the different links that can be used in the application.
|
||||
*/
|
||||
enum class Link(val displayName: String) {
|
||||
DFU("DFU"),
|
||||
LOGGER("LOGGER");
|
||||
LOGGER("LOGGER"),
|
||||
GITHUB("GITHUB"),
|
||||
DEV_ACADEMY("DEV_ACADEMY"),
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the mode of the UART service.
|
||||
* Used to determine how the UART service should behave.
|
||||
*/
|
||||
enum class UARTMode(val displayName: String) {
|
||||
MACRO("MACRO"),
|
||||
PRESET("PRESET"),
|
||||
TEXT("TEXT")
|
||||
}
|
||||
@@ -32,15 +32,39 @@
|
||||
package no.nordicsemi.android.analytics
|
||||
|
||||
import android.os.Bundle
|
||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||
|
||||
/**
|
||||
* Base class for Firebase Analytics events.
|
||||
*/
|
||||
sealed class FirebaseEvent(val eventName: String, val params: Bundle?)
|
||||
|
||||
/**
|
||||
* Represents an event that is logged when the app is opened.
|
||||
* This event does not carry any additional parameters.
|
||||
*/
|
||||
data object AppOpenEvent : FirebaseEvent("APP_OPEN", null)
|
||||
|
||||
/**
|
||||
* Represents an event that is logged when profile is opened.
|
||||
* This event can be created with a [Profile] or a [Link].
|
||||
*/
|
||||
class ProfileOpenEvent : FirebaseEvent {
|
||||
|
||||
constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.displayName))
|
||||
/**
|
||||
* Creates a new instance of [ProfileOpenEvent] with the given profile.
|
||||
* The profile's string representation is used as a parameter.
|
||||
*
|
||||
* @param profile The profile that was opened.
|
||||
*/
|
||||
constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.toString()))
|
||||
|
||||
/**
|
||||
* Creates a new instance of [ProfileOpenEvent] with the given link.
|
||||
* The link's display name is used as a parameter.
|
||||
*
|
||||
* @param link The link that was opened.
|
||||
*/
|
||||
constructor(link: Link) : super(EVENT_NAME, createBundle(link.displayName))
|
||||
|
||||
companion object {
|
||||
@@ -48,11 +72,12 @@ class ProfileOpenEvent : FirebaseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileConnectedEvent : FirebaseEvent {
|
||||
|
||||
constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.displayName))
|
||||
|
||||
constructor(link: Link) : super(EVENT_NAME, createBundle(link.displayName))
|
||||
/**
|
||||
* Represents an event that is logged when a profile is connected.
|
||||
* This event can be created with a [Profile] or a [Link].
|
||||
*/
|
||||
class ProfileConnectedEvent(profile: Profile) :
|
||||
FirebaseEvent(EVENT_NAME, createBundle(profile.toString())) {
|
||||
|
||||
companion object {
|
||||
private const val EVENT_NAME = "PROFILE_CONNECTED"
|
||||
@@ -61,13 +86,28 @@ class ProfileConnectedEvent : FirebaseEvent {
|
||||
|
||||
const val PROFILE_PARAM_KEY = "PROFILE_NAME"
|
||||
|
||||
/**
|
||||
* Creates a [Bundle] with the given profile name.
|
||||
*
|
||||
* @param name The name of the profile to be included in the bundle.
|
||||
* @return A [Bundle] containing the profile name.
|
||||
*/
|
||||
private fun createBundle(name: String): Bundle {
|
||||
return Bundle().apply { putString(PROFILE_PARAM_KEY, name) }
|
||||
}
|
||||
|
||||
sealed class UARTAnalyticsEvent(eventName: String, params: Bundle?) : FirebaseEvent(eventName, params)
|
||||
/**
|
||||
* Represents an event related to UART (Universal Asynchronous Receiver-Transmitter) analytics.
|
||||
*/
|
||||
sealed class UARTAnalyticsEvent(eventName: String, params: Bundle?) :
|
||||
FirebaseEvent(eventName, params)
|
||||
|
||||
class UARTSendAnalyticsEvent(mode: UARTMode) : UARTAnalyticsEvent("UART_SEND_EVENT", createParams(mode)) {
|
||||
/**
|
||||
* Represents an event that is logged when a UART message is send or received.
|
||||
* This event can be created with a [UARTMode].
|
||||
*/
|
||||
class UARTSendAnalyticsEvent(mode: UARTMode) :
|
||||
UARTAnalyticsEvent("UART_SEND_EVENT", createParams(mode)) {
|
||||
|
||||
companion object {
|
||||
fun createParams(mode: UARTMode) = Bundle().apply {
|
||||
@@ -76,6 +116,14 @@ class UARTSendAnalyticsEvent(mode: UARTMode) : UARTAnalyticsEvent("UART_SEND_EVE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an event that is logged when a UART preset configuration is created.
|
||||
* This event can be created with a [UARTMode].
|
||||
*/
|
||||
class UARTCreateConfiguration : UARTAnalyticsEvent("UART_CREATE_CONF", null)
|
||||
|
||||
/**
|
||||
* Represents an event that is logged when a UART preset configuration is changed.
|
||||
* This event does not carry any additional parameters.
|
||||
*/
|
||||
class UARTChangeConfiguration : UARTAnalyticsEvent("UART_CHANGE_CONF", null)
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "no.nordicsemi.android.toolbox.scanner"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.nordic.navigation)
|
||||
|
||||
implementation(libs.nordic.blek.uiscanner)
|
||||
implementation(libs.nordic.blek.scanner)
|
||||
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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 *;
|
||||
#}
|
||||
@@ -1,29 +0,0 @@
|
||||
package no.nordicsemi.android.toolbox.scanner
|
||||
|
||||
import android.os.ParcelUuid
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.common.navigation.createDestination
|
||||
import no.nordicsemi.android.common.navigation.defineDestination
|
||||
import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
import no.nordicsemi.android.kotlin.ble.ui.scanner.DeviceSelected
|
||||
import no.nordicsemi.android.kotlin.ble.ui.scanner.ScannerScreen
|
||||
import no.nordicsemi.android.kotlin.ble.ui.scanner.ScanningCancelled
|
||||
|
||||
val ScannerDestinationId = createDestination<ParcelUuid, ServerDevice>("uiscanner-destination")
|
||||
|
||||
val ScannerDestination = defineDestination(ScannerDestinationId) {
|
||||
val navigationViewModel = hiltViewModel<SimpleNavigationViewModel>()
|
||||
|
||||
val arg = navigationViewModel.parameterOf(ScannerDestinationId)
|
||||
|
||||
ScannerScreen(
|
||||
uuid = arg,
|
||||
onResult = {
|
||||
when (it) {
|
||||
is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.scanResults.device)
|
||||
ScanningCancelled -> navigationViewModel.navigateUp()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -40,11 +39,16 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib_ui"))
|
||||
|
||||
implementation(libs.nordic.blek.uiscanner)
|
||||
implementation(libs.nordic.blek.core)
|
||||
implementation(project(":lib_utils"))
|
||||
implementation(project(":profile_manager"))
|
||||
|
||||
implementation(libs.nordic.logger)
|
||||
implementation(libs.nordic.log.timber)
|
||||
implementation(libs.nordic.blek.client.android)
|
||||
|
||||
implementation(libs.androidx.lifecycle.service)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
implementation(libs.androidx.core)
|
||||
|
||||
implementation(libs.slf4j.timber)
|
||||
}
|
||||
|
||||
@@ -30,13 +30,4 @@
|
||||
~ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||
|
||||
</manifest>
|
||||
<manifest/>
|
||||
@@ -1,3 +0,0 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
class DisconnectAndStopEvent
|
||||
@@ -54,11 +54,19 @@ abstract class NotificationService : LifecycleService() {
|
||||
|
||||
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()
|
||||
stopForegroundService()
|
||||
stopSelf()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
// This method is called when user removed the app from recent app list.
|
||||
// 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service as a foreground service
|
||||
*/
|
||||
@@ -77,7 +85,7 @@ abstract class NotificationService : LifecycleService() {
|
||||
/**
|
||||
* Stops the service as a foreground service
|
||||
*/
|
||||
private fun stopForegroundService() {
|
||||
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) {
|
||||
@@ -85,6 +93,7 @@ abstract class NotificationService : LifecycleService() {
|
||||
} else {
|
||||
cancelNotification()
|
||||
}
|
||||
stopSelf() // Ensure the service stops when it's no longer needed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,9 +111,9 @@ abstract class NotificationService : LifecycleService() {
|
||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(messageResId, "Device"))
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setColor(ContextCompat.getColor(this, R.color.md_theme_primary))
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
class OpenLoggerEvent
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.service
|
||||
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
|
||||
const val DEVICE_DATA = "device-data"
|
||||
|
||||
interface ServiceManager {
|
||||
|
||||
fun <T> startService(service: Class<T>, device: ServerDevice)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class ServiceManagerHiltModule {
|
||||
|
||||
@Provides
|
||||
fun createServiceManager(
|
||||
@ApplicationContext
|
||||
context: Context,
|
||||
): ServiceManager {
|
||||
return ServiceManagerImpl(context)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
|
||||
import javax.inject.Inject
|
||||
|
||||
class ServiceManagerImpl @Inject constructor(
|
||||
@ApplicationContext
|
||||
private val context: Context
|
||||
): ServiceManager {
|
||||
|
||||
override fun <T> startService(service: Class<T>, device: ServerDevice) {
|
||||
val intent = Intent(context, service).apply {
|
||||
putExtra(DEVICE_DATA, device)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package no.nordicsemi.android.service.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import no.nordicsemi.android.service.profile.ProfileServiceManager
|
||||
import no.nordicsemi.android.service.profile.ProfileServiceManagerImp
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ProfileServiceManagerImpModule {
|
||||
|
||||
@Provides
|
||||
fun provideServiceManager(
|
||||
@ApplicationContext context: Context
|
||||
): ProfileServiceManager = ProfileServiceManagerImp(context)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package no.nordicsemi.android.service.profile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.android.log.timber.nRFLoggerTree
|
||||
import no.nordicsemi.android.service.NotificationService
|
||||
import no.nordicsemi.android.service.R
|
||||
import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID
|
||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManagerFactory
|
||||
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager.ConnectionOptions
|
||||
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.core.BondState
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
import no.nordicsemi.kotlin.ble.core.Manager
|
||||
import no.nordicsemi.kotlin.ble.core.Phy
|
||||
import no.nordicsemi.kotlin.ble.core.PhyOption
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class ProfileService : NotificationService() {
|
||||
|
||||
@Inject
|
||||
lateinit var centralManager: CentralManager
|
||||
private var logger: nRFLoggerTree? = null
|
||||
private val binder = LocalBinder()
|
||||
|
||||
private val _connectedDevices =
|
||||
MutableStateFlow<Map<String, Pair<Peripheral, List<ServiceManager>>>>(emptyMap())
|
||||
private val _isMissingServices = MutableStateFlow(false)
|
||||
private val _disconnectionReason = MutableStateFlow<DeviceDisconnectionReason?>(null)
|
||||
|
||||
private val connectionJobs = mutableMapOf<String, Job>()
|
||||
private val serviceHandlingJob = mutableMapOf<String, Job>()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Observe the Bluetooth state
|
||||
centralManager.state.onEach { state ->
|
||||
if (state == Manager.State.POWERED_OFF) {
|
||||
_disconnectionReason.tryEmit(CustomReason(DisconnectReason.BLUETOOTH_OFF))
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
intent?.getStringExtra(DEVICE_ADDRESS)?.let { deviceAddress ->
|
||||
initLogger(deviceAddress)
|
||||
initiateConnection(deviceAddress)
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder(), ServiceApi {
|
||||
override val connectedDevices: Flow<Map<String, Pair<Peripheral, List<ServiceManager>>>>
|
||||
get() = _connectedDevices.asSharedFlow()
|
||||
|
||||
override val isMissingServices: Flow<Boolean>
|
||||
get() = _isMissingServices.asStateFlow()
|
||||
|
||||
override val disconnectionReason: Flow<DeviceDisconnectionReason?>
|
||||
get() = _disconnectionReason.asStateFlow()
|
||||
|
||||
override suspend fun getMaxWriteValue(address: String, writeType: WriteType): Int? {
|
||||
val peripheral = getPeripheralById(address) ?: return null
|
||||
if (!peripheral.isConnected) return null
|
||||
|
||||
return try {
|
||||
peripheral.requestHighestValueLength()
|
||||
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
||||
peripheral.setPreferredPhy(Phy.PHY_LE_2M, Phy.PHY_LE_2M, PhyOption.S2)
|
||||
peripheral.maximumWriteValueLength(writeType)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Failed to configure $address for MTU change with reason: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createBonding(address: String) {
|
||||
val peripheral = getPeripheralById(address)
|
||||
peripheral?.bondState
|
||||
?.onEach { state ->
|
||||
if (state == BondState.NONE) {
|
||||
peripheral.createBond()
|
||||
}
|
||||
}
|
||||
?.filter { it == BondState.BONDED }
|
||||
?.first() // suspend until bonded
|
||||
}
|
||||
|
||||
override fun getPeripheralById(address: String?): Peripheral? =
|
||||
address?.let { centralManager.getPeripheralById(it) }
|
||||
|
||||
override fun disconnect(deviceAddress: String) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
getPeripheralById(deviceAddress)
|
||||
?.let { peripheral ->
|
||||
if (peripheral.isConnected) peripheral.disconnect()
|
||||
handleDisconnection(deviceAddress)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Couldn't disconnect from the $deviceAddress")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConnectionState(address: String): Flow<ConnectionState>? {
|
||||
val peripheral = getPeripheralById(address) ?: return null
|
||||
return peripheral.state.also { stateFlow ->
|
||||
connectionJobs[address]?.cancel()
|
||||
val job = stateFlow.onEach { state ->
|
||||
when (state) {
|
||||
ConnectionState.Connected -> {
|
||||
_isMissingServices.tryEmit(false)
|
||||
// Discover services if not already discovered
|
||||
if (_connectedDevices.value[address] == null) {
|
||||
discoverServices(peripheral)
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionState.Connecting -> _disconnectionReason.tryEmit(null)
|
||||
is ConnectionState.Disconnected -> {
|
||||
_disconnectionReason.tryEmit(StateReason(state.reason))
|
||||
}
|
||||
|
||||
ConnectionState.Closed -> return@onEach
|
||||
|
||||
ConnectionState.Disconnecting -> {
|
||||
connectionJobs[address]?.cancel()
|
||||
handleDisconnection(address)
|
||||
}
|
||||
}
|
||||
}.onCompletion {
|
||||
connectionJobs[address]?.cancel()
|
||||
connectionJobs.remove(address)
|
||||
}.launchIn(lifecycleScope)
|
||||
connectionJobs[address] = job
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the peripheral and observe its state.
|
||||
*/
|
||||
private fun initiateConnection(deviceAddress: String) {
|
||||
centralManager.getPeripheralById(deviceAddress)?.let { peripheral ->
|
||||
lifecycleScope.launch { connectPeripheral(peripheral) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun connectPeripheral(peripheral: Peripheral) {
|
||||
runCatching {
|
||||
centralManager.connect(peripheral, options = ConnectionOptions.Direct())
|
||||
}.onFailure { exception ->
|
||||
Timber.e(exception, "Could not connect to the ${peripheral.address}")
|
||||
stopForegroundService() // Stop service if connection fails
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover services and characteristics for the connected [peripheral].
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
private fun discoverServices(peripheral: Peripheral) {
|
||||
val discoveredServices = mutableListOf<ServiceManager>()
|
||||
serviceHandlingJob[peripheral.address]?.cancel()
|
||||
val job = peripheral.services().onEach { remoteServices ->
|
||||
remoteServices?.forEach { remoteService ->
|
||||
val serviceManager = ServiceManagerFactory.createServiceManager(remoteService.uuid)
|
||||
serviceManager?.let { manager ->
|
||||
Timber.tag("DiscoverServices").i("${manager.profile}")
|
||||
discoveredServices.add(manager)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val requiresBonding =
|
||||
remoteService.uuid == CGMS_SERVICE_UUID.toKotlinUuid() && peripheral.hasBondInformation
|
||||
|
||||
if (requiresBonding) {
|
||||
peripheral.bondState
|
||||
.onEach { if (it == BondState.NONE) peripheral.createBond() }
|
||||
.filter { it == BondState.BONDED }
|
||||
.first()
|
||||
}
|
||||
|
||||
manager.observeServiceInteractions(
|
||||
peripheral.address,
|
||||
remoteService,
|
||||
this
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("ObserveServices").e(e)
|
||||
handleDisconnection(peripheral.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
discoveredServices.isEmpty() -> {
|
||||
if (remoteServices?.isNotEmpty() == true) {
|
||||
_isMissingServices.tryEmit(true)
|
||||
serviceHandlingJob[peripheral.address]?.cancel()
|
||||
serviceHandlingJob.remove(peripheral.address)
|
||||
}
|
||||
}
|
||||
|
||||
peripheral.isConnected -> {
|
||||
_isMissingServices.tryEmit(false)
|
||||
updateConnectedDevices(peripheral, discoveredServices)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onCompletion {
|
||||
serviceHandlingJob[peripheral.address]?.cancel()
|
||||
serviceHandlingJob.remove(peripheral.address)
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
serviceHandlingJob[peripheral.address] = job
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the connected devices with the latest state.
|
||||
*/
|
||||
private fun updateConnectedDevices(peripheral: Peripheral, handlers: List<ServiceManager>) {
|
||||
_connectedDevices.update {
|
||||
it.toMutableMap().apply { this[peripheral.address] = peripheral to handlers }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle disconnection and cleanup for the given peripheral.
|
||||
*/
|
||||
private fun handleDisconnection(peripheral: String) {
|
||||
val currentDevices = _connectedDevices.value.toMutableMap()
|
||||
currentDevices[peripheral]?.let {
|
||||
currentDevices.remove(peripheral)
|
||||
_connectedDevices.tryEmit(currentDevices)
|
||||
}
|
||||
clearJobs(peripheral)
|
||||
clearFlags()
|
||||
stopServiceIfNoDevices()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any active jobs for connection and service handling.
|
||||
*/
|
||||
private fun clearJobs(peripheral: String) {
|
||||
connectionJobs[peripheral]?.cancel()
|
||||
connectionJobs.remove(peripheral)
|
||||
|
||||
serviceHandlingJob[peripheral]?.cancel()
|
||||
serviceHandlingJob.remove(peripheral)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the service if no devices are connected.
|
||||
*/
|
||||
private fun stopServiceIfNoDevices() {
|
||||
if (_connectedDevices.value.isEmpty()) {
|
||||
stopForegroundService()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the logger for the specified device.
|
||||
*/
|
||||
private fun initLogger(device: String) {
|
||||
logger?.let { Timber.uproot(it) }
|
||||
logger = nRFLoggerTree(this, this.getString(R.string.app_name), device)
|
||||
.also { Timber.plant(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Uproot the logger and clear the logger instance.
|
||||
*/
|
||||
private fun uprootLogger() {
|
||||
logger?.let { Timber.uproot(it) }
|
||||
logger = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the missing services and battery level flags.
|
||||
*/
|
||||
private fun clearFlags() {
|
||||
_isMissingServices.tryEmit(false)
|
||||
uprootLogger()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package no.nordicsemi.android.service.profile
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
const val DEVICE_ADDRESS = "deviceAddress"
|
||||
|
||||
sealed interface ProfileServiceManager {
|
||||
suspend fun bindService(): ServiceApi
|
||||
fun unbindService()
|
||||
fun connectToPeripheral(deviceAddress: String)
|
||||
}
|
||||
|
||||
internal class ProfileServiceManagerImp @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ProfileServiceManager {
|
||||
private var serviceConnection: ServiceConnection? = null
|
||||
private var api: ServiceApi? = null
|
||||
|
||||
override suspend fun bindService(): ServiceApi = suspendCancellableCoroutine { continuation ->
|
||||
val intent = Intent(context, ProfileService::class.java)
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
api = service as ServiceApi
|
||||
continuation.resume(api!!) { _, _, _ -> }
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(p0: ComponentName?) {
|
||||
continuation.resumeWithException(Exception("Service disconnected"))
|
||||
}
|
||||
|
||||
override fun onBindingDied(p0: ComponentName?) {
|
||||
continuation.resumeWithException(Exception("Service binding died"))
|
||||
}
|
||||
}.apply {
|
||||
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindService() {
|
||||
serviceConnection?.let { context.unbindService(it) }
|
||||
}
|
||||
|
||||
override fun connectToPeripheral(deviceAddress: String) {
|
||||
val intent = Intent(context, ProfileService::class.java)
|
||||
intent.putExtra(DEVICE_ADDRESS, deviceAddress)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package no.nordicsemi.android.service.profile
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
|
||||
/** Device disconnection reason. */
|
||||
sealed interface DeviceDisconnectionReason
|
||||
|
||||
/** Includes the [ConnectionState.Disconnected.Reason]. */
|
||||
data class StateReason(val reason: ConnectionState.Disconnected.Reason) : DeviceDisconnectionReason
|
||||
|
||||
/** Includes the custom made [DisconnectReason] to include other disconnection reasons which are not included in the [ConnectionState.Disconnected.Reason]. */
|
||||
data class CustomReason(val reason: DisconnectReason) :
|
||||
DeviceDisconnectionReason
|
||||
|
||||
interface ServiceApi {
|
||||
|
||||
/** Flow of connected devices. */
|
||||
val connectedDevices: Flow<Map<String, Pair<Peripheral, List<ServiceManager>>>>
|
||||
|
||||
/** Missing services flag. */
|
||||
val isMissingServices: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Get the peripheral by its [address].
|
||||
*
|
||||
* @return the peripheral instance.
|
||||
*/
|
||||
fun getPeripheralById(address: String?): Peripheral?
|
||||
|
||||
/**
|
||||
* Disconnect the device with the given [deviceAddress].
|
||||
*
|
||||
* @param deviceAddress the device address.
|
||||
*/
|
||||
fun disconnect(deviceAddress: String)
|
||||
|
||||
/**
|
||||
* Get the connection state of the device with the given [address].
|
||||
*
|
||||
* @return the connection state flow.
|
||||
*/
|
||||
fun getConnectionState(address: String): Flow<ConnectionState>?
|
||||
|
||||
/**
|
||||
* Get the disconnection reason of the device with the given address.
|
||||
*
|
||||
* @return the disconnection reason flow.
|
||||
*/
|
||||
val disconnectionReason: Flow<DeviceDisconnectionReason?>
|
||||
|
||||
/**
|
||||
* Request maximum write value length.
|
||||
* For [WriteType.WITHOUT_RESPONSE] it is equal to *ATT MTU - 3 bytes*.
|
||||
*/
|
||||
suspend fun getMaxWriteValue(
|
||||
address: String,
|
||||
writeType: WriteType = WriteType.WITHOUT_RESPONSE
|
||||
): Int?
|
||||
|
||||
suspend fun createBonding(
|
||||
address: String
|
||||
)
|
||||
|
||||
}
|
||||
14
lib_storage/build.gradle.kts
Normal file
14
lib_storage/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
alias(libs.plugins.ksp)
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "no.nordicsemi.android.toolbox.lib.storage"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
}
|
||||
21
lib_storage/module-rules.pro
Normal file
21
lib_storage/module-rules.pro
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
@@ -1,4 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
<manifest/>
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.toolbox.lib.storage
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "configurations")
|
||||
data class ConfigurationEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "_id") val _id: Int?,
|
||||
@ColumnInfo(name = "name") val name: String,
|
||||
@ColumnInfo(name = "xml") val xml: String,
|
||||
@ColumnInfo(name = "deleted", defaultValue = "0") val deleted: Int
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package no.nordicsemi.android.toolbox.lib.storage
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface ConfigurationsDao {
|
||||
@Query("SELECT * FROM configurations")
|
||||
fun getAllConfigurations(): Flow<List<ConfigurationEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertConfiguration(configuration: ConfigurationEntity): Long
|
||||
|
||||
@Query("DELETE FROM configurations WHERE name = :configurationName")
|
||||
suspend fun deleteConfiguration(configurationName: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package no.nordicsemi.android.toolbox.lib.storage
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [ConfigurationEntity::class],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
internal abstract class ConfigurationDatabase : RoomDatabase() {
|
||||
abstract fun configurationDao(): ConfigurationsDao
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package no.nordicsemi.android.toolbox.lib.storage
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// Empty implementation, because the schema isn't changing.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package no.nordicsemi.android.toolbox.lib.storage.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import no.nordicsemi.android.toolbox.lib.storage.ConfigurationDatabase
|
||||
import no.nordicsemi.android.toolbox.lib.storage.ConfigurationsDao
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DaoHiltModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideDeviceDao(db: ConfigurationDatabase): ConfigurationsDao {
|
||||
return db.configurationDao()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.uart
|
||||
package no.nordicsemi.android.toolbox.lib.storage.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
@@ -7,8 +7,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import no.nordicsemi.android.uart.db.ConfigurationsDatabase
|
||||
import no.nordicsemi.android.uart.db.MIGRATION_1_2
|
||||
import no.nordicsemi.android.toolbox.lib.storage.ConfigurationDatabase
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -17,10 +16,11 @@ class DbHiltModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase {
|
||||
internal fun provideDeviceDB(@ApplicationContext context: Context): ConfigurationDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
ConfigurationsDatabase::class.java, "toolbox_uart.db"
|
||||
).addMigrations(MIGRATION_1_2).build()
|
||||
ConfigurationDatabase::class.java,
|
||||
"toolbox_uart.db"
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,22 +31,18 @@
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "no.nordicsemi.android.ui"
|
||||
|
||||
testOptions {
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.nordic.theme)
|
||||
implementation(libs.nordic.ui)
|
||||
implementation(libs.nordic.logger)
|
||||
|
||||
implementation(libs.nordic.blek.client)
|
||||
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.iconsExtended)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun AnimatedThreeDots(
|
||||
modifier: Modifier = Modifier,
|
||||
dotSize: Dp = 8.dp
|
||||
) {
|
||||
val dotCount = 3
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
|
||||
val dotAlphas = List(dotCount) { index ->
|
||||
infiniteTransition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 500,
|
||||
delayMillis = index * 200,
|
||||
easing = LinearEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
dotAlphas.forEach { alpha ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(dotSize)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Gray.copy(alpha = alpha.value))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextWithAnimatedDots(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
dotSize: Dp = 2.dp,
|
||||
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
textAlign: TextAlign = TextAlign.Center
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
textAlign = textAlign,
|
||||
style = textStyle,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
AnimatedThreeDots(
|
||||
modifier = modifier.padding(bottom = 4.dp),
|
||||
dotSize = dotSize
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun createCircleTransition(
|
||||
isInAccessibilityMode: Boolean,
|
||||
duration: Int
|
||||
): CircleTransitionState {
|
||||
val transition = updateTransition(targetState = isInAccessibilityMode, label = "Transition")
|
||||
return CircleTransitionState(
|
||||
dotRadius = transition.animateDp(
|
||||
label = "Dot Radius",
|
||||
transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) }
|
||||
) { if (it) 10.dp else 5.dp },
|
||||
circleWidth = transition.animateDp(
|
||||
label = "Circle Width",
|
||||
transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) }
|
||||
) { if (it) 8.dp else 5.dp },
|
||||
circleColor = transition.animateColor(
|
||||
label = "Circle Color",
|
||||
transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) }
|
||||
) { if (it) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.primaryContainer },
|
||||
dotColor = transition.animateColor(
|
||||
label = "Dot Color",
|
||||
transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) }
|
||||
) { if (it) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary }
|
||||
)
|
||||
}
|
||||
|
||||
data class CircleTransitionState(
|
||||
val dotRadius: State<Dp>,
|
||||
val circleWidth: State<Dp>,
|
||||
val circleColor: State<Color>,
|
||||
val dotColor: State<Color>,
|
||||
) {
|
||||
fun toggleAccessibilityMode() {
|
||||
dotRadius.value
|
||||
}
|
||||
}
|
||||
|
||||
data class LinearTransitionState(
|
||||
val border: State<Dp>,
|
||||
val height: State<Dp>,
|
||||
val radius: State<Dp>,
|
||||
val color: State<Color>,
|
||||
val inactiveColor: State<Color>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun createLinearTransition(
|
||||
isInAccessibilityMode: Boolean,
|
||||
duration: Int,
|
||||
): LinearTransitionState {
|
||||
val transition = updateTransition(targetState = isInAccessibilityMode, label = "Transition")
|
||||
return LinearTransitionState(
|
||||
border = transition.animateDp(
|
||||
label = "Border",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) 3.dp else 0.dp },
|
||||
|
||||
height = transition.animateDp(
|
||||
label = "Height",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) 30.dp else 25.dp },
|
||||
radius = transition.animateDp(
|
||||
label = "Radius",
|
||||
transitionSpec = { TweenSpec(duration / 2, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) 4.dp else 8.dp },
|
||||
color = transition.animateColor(
|
||||
label = "Color",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) {
|
||||
if (it) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary
|
||||
},
|
||||
inactiveColor = transition.animateColor(
|
||||
label = "In-active color",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) {
|
||||
if (it) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.onPrimary
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun createAngularTransition(
|
||||
isInAccessibilityMode: Boolean,
|
||||
duration: Int,
|
||||
): ChartTransition {
|
||||
val transition = updateTransition(
|
||||
targetState = isInAccessibilityMode,
|
||||
label = "Accessibility transition"
|
||||
)
|
||||
return ChartTransition(
|
||||
height = transition.animateDp(
|
||||
label = "Height",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) 100.dp else 50.dp },
|
||||
avgLineWidth = transition.animateDp(
|
||||
label = "Average Line Width",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) 8.dp else 2.dp },
|
||||
chartColor = transition.animateColor(
|
||||
label = "Chart Color",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary },
|
||||
avgLineColor = transition.animateColor(
|
||||
label = "Average Line Color",
|
||||
transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) }
|
||||
) { if (it) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiary }
|
||||
)
|
||||
}
|
||||
|
||||
data class ChartTransition(
|
||||
val height: State<Dp>,
|
||||
val avgLineWidth: State<Dp>,
|
||||
val chartColor: State<Color>,
|
||||
val avgLineColor: State<Color>,
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.ui.view
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import no.nordicsemi.android.ui.R
|
||||
|
||||
@Composable
|
||||
fun BatteryLevelView(batteryLevel: Int) {
|
||||
ScreenSection {
|
||||
KeyValueField(
|
||||
stringResource(id = R.string.field_battery),
|
||||
"$batteryLevel%"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
inline fun <reified T> DropdownView(
|
||||
items: List<T>,
|
||||
label: String,
|
||||
placeholder: String,
|
||||
defaultSelectedItem: T? = null,
|
||||
isError: Boolean = false,
|
||||
errorMessage: String = "",
|
||||
crossinline onItemSelected: (T) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedText by rememberSaveable { mutableStateOf(defaultSelectedItem) }
|
||||
|
||||
Box {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }) {
|
||||
OutlinedTextField(
|
||||
value = selectedText?.toString() ?: placeholder,
|
||||
onValueChange = { }, // No need to handle since it's readOnly
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
placeholder = { Text(text = placeholder) },
|
||||
label = { Text(text = label) },
|
||||
isError = isError,
|
||||
supportingText = {
|
||||
if (isError) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
// Animated dropdown menu
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
items.forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(it.toString()) },
|
||||
onClick = {
|
||||
selectedText = it
|
||||
expanded = false
|
||||
onItemSelected(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DropdownViewPreview() {
|
||||
val items = listOf("Item 1", "Item 2", "Item 3")
|
||||
DropdownView(
|
||||
items = items,
|
||||
label = "Label",
|
||||
placeholder = "Placeholder",
|
||||
defaultSelectedItem = items[0],
|
||||
onItemSelected = {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun FeatureSupported(
|
||||
text: String,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RectangleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeatureSupportedPreview() {
|
||||
FeatureSupported("Instantaneous stride length measurement supported")
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun KeyValueColumn(
|
||||
value: String,
|
||||
key: String,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalSpacing: Dp = 8.dp,
|
||||
keyStyle: TextStyle?= null
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(verticalSpacing),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = key,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = keyStyle ?: MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun KeyValueColumnPreview() {
|
||||
KeyValueColumn(
|
||||
value = "Sample Value",
|
||||
key = "Sample Key",
|
||||
// keyStyle = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyValueColumn(
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
key: @Composable (() -> Unit)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
key()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyValueColumnReverse(
|
||||
value: String,
|
||||
key: String,
|
||||
modifier: Modifier = Modifier,
|
||||
verticalSpacing: Dp = 8.dp,
|
||||
keyStyle: TextStyle? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp),
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(verticalSpacing),
|
||||
horizontalAlignment = Alignment.End,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = key,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = keyStyle ?: MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyValueColumnReverse(
|
||||
value: String,
|
||||
modifier: Modifier = Modifier,
|
||||
key: @Composable (() -> Unit)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp),
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.End,
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
key()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
@@ -8,9 +9,15 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ScreenSection(content: @Composable () -> Unit) {
|
||||
fun ScreenSection(
|
||||
modifier: Modifier = Modifier.padding(16.dp),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
OutlinedCard {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun SectionRow(
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -31,36 +31,42 @@
|
||||
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import no.nordicsemi.android.ui.R
|
||||
|
||||
@Composable
|
||||
fun SectionTitle(
|
||||
@DrawableRes resId: Int,
|
||||
title: String,
|
||||
menu: @Composable (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier.fillMaxWidth()
|
||||
modifier: Modifier = Modifier.fillMaxWidth(),
|
||||
menu: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
@@ -70,26 +76,106 @@ fun SectionTitle(
|
||||
Image(
|
||||
painter = painterResource(id = resId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(8.dp)
|
||||
.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Start,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
menu?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionTitle(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
modifier: Modifier = Modifier.fillMaxWidth(),
|
||||
menu: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Image(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary),
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
menu?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SuppressLint("ModifierParameter")
|
||||
fun SectionTitle(
|
||||
@DrawableRes resId: Int,
|
||||
title: String,
|
||||
modifier: Modifier = Modifier.fillMaxWidth(),
|
||||
rotateArrow: Float? = null,
|
||||
iconBackground: Color = MaterialTheme.colorScheme.secondary
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = resId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary),
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
|
||||
)
|
||||
Spacer(modifier = Modifier.padding(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
rotateArrow?.let {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Image(
|
||||
Icons.Default.ArrowDropDown,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.rotate(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun SectionTitle_Preview() {
|
||||
SectionTitle(
|
||||
resId = R.drawable.ic_records,
|
||||
title = stringResource(id = R.string.back_screen),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { },
|
||||
rotateArrow = 0f
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionTitle(
|
||||
icon: ImageVector,
|
||||
@@ -104,20 +190,15 @@ fun SectionTitle(
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondary,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(8.dp)
|
||||
.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private class PlaceholderTransformation(private val placeholder: String) : VisualTransformation {
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
return placeholderFilter(placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
fun placeholderFilter(placeholder: String): TransformedText {
|
||||
|
||||
val numberOffsetTranslator = object : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return TransformedText(AnnotatedString(placeholder), numberOffsetTranslator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose view to input text in OutlinedTextField.
|
||||
*/
|
||||
@Composable
|
||||
fun TextInputField(
|
||||
modifier: Modifier = Modifier,
|
||||
input: String,
|
||||
label: String,
|
||||
hint: String? = null,
|
||||
placeholder: String = "",
|
||||
errorMessage: String = "",
|
||||
errorState: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
onUpdate: (String) -> Unit
|
||||
) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = if (input.isEmpty()) 0.5f else LocalContentColor.current.alpha
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = input,
|
||||
onValueChange = { onUpdate(it) },
|
||||
visualTransformation = if (input.isEmpty())
|
||||
PlaceholderTransformation(placeholder) else VisualTransformation.None,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
label = { Text(text = label) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
)
|
||||
},
|
||||
supportingText = {
|
||||
if (errorState) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.alpha(1f)
|
||||
)
|
||||
}
|
||||
} else if (hint != null) {
|
||||
Text(
|
||||
text = hint,
|
||||
modifier = Modifier.alpha(0.38f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(textColor),
|
||||
isError = errorState,
|
||||
)
|
||||
}
|
||||
@@ -31,10 +31,9 @@
|
||||
|
||||
package no.nordicsemi.android.ui.view
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -48,21 +47,24 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
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 no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
|
||||
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus
|
||||
import no.nordicsemi.android.common.theme.NordicTheme
|
||||
import no.nordicsemi.android.ui.R
|
||||
|
||||
private const val TOP_APP_BAR_TITLE = "Nordic_Appbar"
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CloseIconAppBar(text: String, onClick: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text, maxLines = 2) },
|
||||
title = { Text(text, maxLines = 1) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primary,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
containerColor = colorResource(id = R.color.appBarColor),
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
@@ -78,29 +80,41 @@ fun CloseIconAppBar(text: String, onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CloseIconAppBarPreview() {
|
||||
NordicTheme {
|
||||
CloseIconAppBar(TOP_APP_BAR_TITLE) {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) {
|
||||
fun LoggerBackIconAppBar(
|
||||
text: String,
|
||||
onBackClick: () -> Unit,
|
||||
onLoggerClick: () -> Unit
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(text, maxLines = 2) },
|
||||
title = { Text(text, maxLines = 1) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primary,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
containerColor = colorResource(id = R.color.appBarColor),
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onClick() }) {
|
||||
IconButton(onClick = { onBackClick() }) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
contentDescription = stringResource(id = R.string.back_screen),
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { onClick() }) {
|
||||
IconButton(onClick = { onLoggerClick() }) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_logger),
|
||||
contentDescription = stringResource(id = R.string.open_logger),
|
||||
@@ -112,14 +126,22 @@ fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LoggerBackIconAppBarPreview() {
|
||||
NordicTheme {
|
||||
LoggerBackIconAppBar(TOP_APP_BAR_TITLE, {}) {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BackIconAppBar(text: String, onClick: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text, maxLines = 2) },
|
||||
title = { Text(text, maxLines = 1) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primary,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
containerColor = colorResource(id = R.color.appBarColor),
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
@@ -127,7 +149,7 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) {
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onClick() }) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
contentDescription = stringResource(id = R.string.back_screen),
|
||||
)
|
||||
@@ -136,6 +158,14 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BackIconAppBarPreview() {
|
||||
NordicTheme {
|
||||
BackIconAppBar(TOP_APP_BAR_TITLE) {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoggerIconAppBar(
|
||||
@@ -145,10 +175,10 @@ fun LoggerIconAppBar(
|
||||
onLoggerClick: () -> Unit
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(text, maxLines = 2) },
|
||||
title = { Text(text, maxLines = 1) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.primary,
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
containerColor = colorResource(id = R.color.appBarColor),
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
@@ -156,7 +186,7 @@ fun LoggerIconAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onClick() }) {
|
||||
Icon(
|
||||
Icons.Default.ArrowBack,
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
contentDescription = stringResource(id = R.string.back_screen),
|
||||
)
|
||||
@@ -184,23 +214,10 @@ fun LoggerIconAppBar(
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ProfileAppBar(
|
||||
deviceName: String?,
|
||||
connectionState: GattConnectionStateWithStatus?,
|
||||
@StringRes
|
||||
title: Int,
|
||||
navigateUp: () -> Unit,
|
||||
disconnect: () -> Unit,
|
||||
openLogger: () -> Unit
|
||||
) {
|
||||
if (deviceName?.isNotBlank() == true) {
|
||||
if (connectionState?.state == GattConnectionState.STATE_DISCONNECTING || connectionState?.state == GattConnectionState.STATE_DISCONNECTED) {
|
||||
LoggerBackIconAppBar(deviceName, openLogger)
|
||||
} else {
|
||||
LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger)
|
||||
}
|
||||
} else {
|
||||
BackIconAppBar(stringResource(id = title), navigateUp)
|
||||
private fun LoggerIconAppBarPreview() {
|
||||
NordicTheme {
|
||||
LoggerIconAppBar(TOP_APP_BAR_TITLE, {}, {}) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package no.nordicsemi.android.ui.view.animate
|
||||
|
||||
import androidx.compose.animation.core.EaseInOutCubic
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AnimatedHeart(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// Infinite transition for pulsing animation
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
|
||||
val scale by infiniteTransition.animateFloat(
|
||||
initialValue = 1f,
|
||||
targetValue = 1.15f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(600, easing = EaseInOutCubic),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Favorite,
|
||||
contentDescription = "heart icon",
|
||||
modifier = modifier
|
||||
.size(28.dp)
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale
|
||||
),
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
@@ -32,9 +32,6 @@
|
||||
package no.nordicsemi.android.ui.view.dialog
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
||||
@Composable
|
||||
fun String.toAnnotatedString() = buildAnnotatedString {
|
||||
append(this@toAnnotatedString)
|
||||
}
|
||||
fun Boolean.toBooleanText(): String = if (this) "YES" else "NO"
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.ui.view.dialog
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.ui.R
|
||||
|
||||
@Composable
|
||||
fun StringListDialog(config: StringListDialogConfig) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { config.onResult(FlowCanceled) },
|
||||
title = { Text(text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString()) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
config.items.forEachIndexed { i, entry ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { config.onResult(ItemSelectedResult(i)) }
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
config.leftIcon?.let {
|
||||
Image(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
painter = painterResource(it),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
contentDescription = "Content image",
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = entry,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { config.onResult(FlowCanceled) }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.ui.view.dialog
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
data class StringListDialogConfig(
|
||||
val title: AnnotatedString? = null,
|
||||
@DrawableRes
|
||||
val leftIcon: Int? = null,
|
||||
val items: List<String> = emptyList(),
|
||||
val onResult: (StringListDialogResult) -> Unit
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.ui.view.dialog
|
||||
|
||||
sealed class StringListDialogResult
|
||||
|
||||
data class ItemSelectedResult(val index: Int): StringListDialogResult()
|
||||
|
||||
object FlowCanceled : StringListDialogResult()
|
||||
@@ -0,0 +1,80 @@
|
||||
package no.nordicsemi.android.ui.view.internal
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HourglassTop
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.common.ui.view.CircularIcon
|
||||
import no.nordicsemi.android.ui.R
|
||||
import no.nordicsemi.android.ui.view.TextWithAnimatedDots
|
||||
|
||||
@Composable
|
||||
fun DeviceConnectingView(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.(PaddingValues) -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 460.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularIcon(imageVector = Icons.Default.HourglassTop)
|
||||
|
||||
TextWithAnimatedDots(
|
||||
text = stringResource(id = R.string.device_connecting),
|
||||
textStyle = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
|
||||
TextWithAnimatedDots(
|
||||
text = stringResource(id = R.string.device_connecting_des),
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content(PaddingValues(top = 16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun DeviceConnectingView_Preview() {
|
||||
MaterialTheme {
|
||||
DeviceConnectingView { padding ->
|
||||
Button(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package no.nordicsemi.android.ui.view.internal
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DeveloperBoardOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
enum class DisconnectReason {
|
||||
USER, UNKNOWN, LINK_LOSS, MISSING_SERVICE, BLUETOOTH_OFF
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceDisconnectedView(
|
||||
reason: DisconnectReason,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.(PaddingValues) -> Unit = {},
|
||||
) {
|
||||
val disconnectedReason = when (reason) {
|
||||
DisconnectReason.USER -> "Device disconnected successfully."
|
||||
DisconnectReason.UNKNOWN -> "Oops...! Connection went on a coffee break."
|
||||
DisconnectReason.LINK_LOSS -> "Device signal has been lost."
|
||||
DisconnectReason.MISSING_SERVICE -> "The peripheral has services that aren't supported in the nRF Toolbox."
|
||||
DisconnectReason.BLUETOOTH_OFF -> "Bluetooth adapter is turned off."
|
||||
}
|
||||
|
||||
DeviceDisconnectedView(
|
||||
disconnectedReason = disconnectedReason,
|
||||
modifier = modifier,
|
||||
content = content,
|
||||
isMissingService = reason == DisconnectReason.MISSING_SERVICE
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceDisconnectedView(
|
||||
disconnectedReason: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isMissingService: Boolean = false,
|
||||
content: @Composable ColumnScope.(PaddingValues) -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 460.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeveloperBoardOff,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (isMissingService) "No supported services" else "Device disconnected",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = disconnectedReason,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content(PaddingValues(top = 16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun DeviceDisconnectedViewPreview() {
|
||||
MaterialTheme {
|
||||
DeviceDisconnectedView(
|
||||
reason = DisconnectReason.UNKNOWN,
|
||||
content = { padding ->
|
||||
Button(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
Text(text = "Retry")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package no.nordicsemi.android.ui.view.internal
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.BluetoothSearching
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.common.ui.view.WarningView
|
||||
import no.nordicsemi.android.ui.R
|
||||
|
||||
@Composable
|
||||
fun EmptyView(
|
||||
@StringRes title: Int,
|
||||
@StringRes hint: Int,
|
||||
) {
|
||||
WarningView(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
imageVector = Icons.AutoMirrored.Filled.BluetoothSearching,
|
||||
title = stringResource(title).uppercase(),
|
||||
hint = stringResource(hint).uppercase(),
|
||||
hintTextAlign = TextAlign.Justify,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun EmptyViewPreview() {
|
||||
EmptyView(
|
||||
R.string.app_name,
|
||||
R.string.app_name,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package no.nordicsemi.android.ui.view.internal
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LoadingView() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.fillMaxSize()
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LoadingViewPreview() {
|
||||
LoadingView()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package no.nordicsemi.android.ui.view.internal
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HourglassTop
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.common.ui.view.CircularIcon
|
||||
import no.nordicsemi.android.ui.R
|
||||
import no.nordicsemi.android.ui.view.TextWithAnimatedDots
|
||||
|
||||
@Composable
|
||||
fun ServiceDiscoveryView(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.(PaddingValues) -> Unit = {}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 460.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularIcon(imageVector = Icons.Default.HourglassTop)
|
||||
|
||||
TextWithAnimatedDots(
|
||||
text = stringResource(id = R.string.discovering_services),
|
||||
textStyle = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
TextWithAnimatedDots(
|
||||
text = stringResource(id = R.string.discovering_services_des),
|
||||
textStyle = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content(PaddingValues(top = 16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun ServiceDiscoveryViewPreview() {
|
||||
MaterialTheme {
|
||||
ServiceDiscoveryView(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) { padding ->
|
||||
Button(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(padding)
|
||||
) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
lib_ui/src/main/res/drawable/ic_battery.xml
Normal file
22
lib_ui/src/main/res/drawable/ic_battery.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="330dp"
|
||||
android:height="205dp"
|
||||
android:viewportWidth="330"
|
||||
android:viewportHeight="205">
|
||||
<path
|
||||
android:pathData="M126.36,33.18h77.27v152.87h-77.27z"
|
||||
android:strokeWidth="8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#00a1c5"/>
|
||||
<path
|
||||
android:pathData="M148.06,19.04h33.88v14.13h-33.88z"
|
||||
android:strokeWidth="8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#00a1c5"/>
|
||||
<path
|
||||
android:pathData="M177.88,72.65l-25.76,36.96l25.76,0l-25.76,39.2"
|
||||
android:strokeWidth="8"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#00a1c5"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -30,8 +30,8 @@
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
@@ -34,7 +34,11 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M18.66,2C18.4,2 18.16,2.09 17.97,2.28L16.13,4.13L19.88,7.88L21.72,6.03C22.11,5.64 22.11,5 21.72,4.63L19.38,2.28C19.18,2.09 18.91,2 18.66,2M3.28,4L2,5.28L8.5,11.75L4,16.25V20H7.75L12.25,15.5L18.72,22L20,20.72L13.5,14.25L9.75,10.5L3.28,4M15.06,5.19L11.03,9.22L14.78,12.97L18.81,8.94L15.06,5.19Z"/>
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M9,7.27c0,-0.51 -0.13,-1.02 -0.37,-1.48c-0.01,-0.01 -0.01,-0.02 -0.02,-0.03c-0.06,-0.1 -0.12,-0.2 -0.18,-0.29C7.3,3.5 6.48,2.27 6.38,2.09c-0.1,-0.2 -0.29,-0.34 -0.52,-0.36c-0.26,-0.03 -0.51,0.09 -0.65,0.31L3.13,5.43c-0.11,0.15 -0.21,0.32 -0.3,0.49c-0.01,0.01 -0.01,0.01 -0.01,0.02c-0.21,0.44 -0.32,0.9 -0.32,1.38c0,1.77 1.48,3.21 3.29,3.21s3.29,-1.45 3.29,-3.21zM3.83,7.27c0,-0.28 0.06,-0.54 0.19,-0.79c0,0 0,0 0,-0.01c0.05,-0.1 0.11,-0.2 0.19,-0.29c0.01,-0.02 0.02,-0.03 0.03,-0.05l1.55,-2.5c0.1,0.15 0.2,0.33 0.33,0.53c0.47,0.76 1.01,1.61 1.28,2.05c0.01,0.01 0.01,0.02 0.02,0.04c0.04,0.06 0.08,0.13 0.12,0.2c0,0 0,0 0,0c0,0 0,0.01 0,0.01c0.14,0.26 0.21,0.54 0.21,0.84c0,1 -0.86,1.82 -1.91,1.82s-1.91,-0.82 -1.91,-1.82z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M22.875,2.344c-1.144,-1.1 -2.966,-1.062 -4.067,0.085L5.356,16.406c-0.248,0.259 -0.24,0.668 0.018,0.914l1.2,1.153c-0.041,0.027 -0.08,0.063 -0.112,0.101l-2.195,2.472c-0.252,0.282 -0.225,0.712 0.054,0.961c0.123,0.11 0.277,0.165 0.429,0.165c0.179,0 0.356,-0.073 0.483,-0.22l2.195,-2.472c0.024,-0.027 0.041,-0.053 0.06,-0.082l1.217,1.169c0.127,0.123 0.297,0.191 0.47,0.191c0.006,0 0.014,0 0.019,0c0.182,-0.004 0.355,-0.08 0.48,-0.2l13.645,-14.234c0,0 0.003,-0.003 0.003,-0.003C23.84,4.579 23.799,3.445 22.875,2.344zM21.993,5.654C21.993,5.654 21.993,5.654 21.993,5.654L9.127,19.539l-2.363,-2.268l12.866,-13.417c0.619,-0.645 1.647,-0.665 2.294,-0.047C22.563,4.019 22.583,5.048 21.993,5.654z" />
|
||||
</vector>
|
||||
@@ -32,5 +32,6 @@
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/md_theme_primary"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -33,7 +33,6 @@
|
||||
<resources>
|
||||
<string name="app_name">nRF Toolbox</string>
|
||||
|
||||
<string name="dialog">Dialog</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
||||
<string name="go_up">Back</string>
|
||||
@@ -44,4 +43,10 @@
|
||||
|
||||
<string name="disconnect">Disconnect</string>
|
||||
<string name="field_battery">Battery</string>
|
||||
|
||||
<string name="discovering_services">Discovering services</string>
|
||||
<string name="discovering_services_des">Please wait</string>
|
||||
|
||||
<string name="device_connecting">Connecting</string>
|
||||
<string name="device_connecting_des">Please wait</string>
|
||||
</resources>
|
||||
@@ -1,47 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "no.nordicsemi.android.utils"
|
||||
namespace = "no.nordicsemi.android.toolbox.lib.utils"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.nordic.navigation)
|
||||
|
||||
implementation(libs.nordic.blek.uiscanner)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
}
|
||||
implementation(libs.nordic.log.timber)
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.kts.
|
||||
# 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
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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
|
||||
@@ -1,35 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2022, 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.
|
||||
-->
|
||||
|
||||
<manifest>
|
||||
|
||||
</manifest>
|
||||
<manifest/>
|
||||
@@ -0,0 +1,7 @@
|
||||
package no.nordicsemi.android.toolbox.lib.utils
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
fun Throwable.logAndReport() {
|
||||
Timber.e(this)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package no.nordicsemi.android.toolbox.lib.utils
|
||||
|
||||
enum class Profile {
|
||||
BPS,
|
||||
CGM,
|
||||
CHANNEL_SOUNDING,
|
||||
CSC,
|
||||
DFS,
|
||||
GLS,
|
||||
HRS,
|
||||
HTS,
|
||||
LBS,
|
||||
RSCS,
|
||||
|
||||
// PRX, TODO: PRX is not implemented yet, it will be added in the future.
|
||||
BATTERY,
|
||||
THROUGHPUT,
|
||||
UART;
|
||||
|
||||
override fun toString(): String =
|
||||
when (this) {
|
||||
BPS -> "Blood Pressure"
|
||||
CGM -> "Continuous Glucose Monitoring"
|
||||
CHANNEL_SOUNDING -> "Channel Sounding"
|
||||
CSC -> "Cycling Speed and Cadence"
|
||||
DFS -> "Direction Finder Service"
|
||||
GLS -> "Glucose"
|
||||
HRS -> "Heart Rate Sensor"
|
||||
HTS -> "Health Thermometer"
|
||||
LBS -> "Blinky/LED Button Service"
|
||||
RSCS -> "Running Speed and Cadence Sensor"
|
||||
BATTERY -> "Battery Service"
|
||||
THROUGHPUT -> "Throughput Service"
|
||||
UART -> "UART Service"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package no.nordicsemi.android.toolbox.lib.utils.spec
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
|
||||
val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
|
||||
val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
||||
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
|
||||
val DF_SERVICE_UUID: UUID = UUID.fromString("21490000-494a-4573-98af-f126af76f490")
|
||||
val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
||||
val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
|
||||
val PRX_SERVICE_UUID: UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb")
|
||||
val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB")
|
||||
val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
|
||||
val BATTERY_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
|
||||
val THROUGHPUT_SERVICE_UUID: UUID = UUID.fromString("0483DADD-6C9D-6CA9-5D41-03AD4FFF4ABB")
|
||||
val CHANNEL_SOUND_SERVICE_UUID: UUID = UUID.fromString("0000185B-0000-1000-8000-00805F9B34FB")
|
||||
val LBS_SERVICE_UUID: UUID = UUID.fromString("00001523-1212-EFDE-1523-785FEABCD123")
|
||||
@@ -1,4 +1,4 @@
|
||||
package no.nordicsemi.android.utils
|
||||
package no.nordicsemi.android.toolbox.lib.utils
|
||||
|
||||
suspend fun tryOrLog(block: suspend () -> Unit) {
|
||||
try {
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022, 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.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val String.Companion.EMPTY
|
||||
get() = ""
|
||||
|
||||
fun Context.isServiceRunning(serviceClassName: String): Boolean {
|
||||
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val services = activityManager.getRunningServices(Integer.MAX_VALUE)
|
||||
return services.find { it.service.className == serviceClassName } != null
|
||||
}
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, t ->
|
||||
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchWithCatch(block: suspend CoroutineScope.() -> Unit) =
|
||||
launch(SupervisorJob() + exceptionHandler) {
|
||||
block()
|
||||
}
|
||||
11
permissions-ranging/build.gradle.kts
Normal file
11
permissions-ranging/build.gradle.kts
Normal file
@@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
alias(libs.plugins.nordic.feature)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "no.nordicsemi.android.permissions_ranging"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.accompanist.permissions)
|
||||
}
|
||||
21
permissions-ranging/module-rules.pro
Normal file
21
permissions-ranging/module-rules.pro
Normal file
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
6
permissions-ranging/src/main/AndroidManifest.xml
Normal file
6
permissions-ranging/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.RANGING" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,42 @@
|
||||
package no.nordicsemi.android.permissions_ranging
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason
|
||||
import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState
|
||||
import no.nordicsemi.android.permissions_ranging.view.RangingPermissionRequestView
|
||||
import no.nordicsemi.android.permissions_ranging.viewmodel.RangingPermissionViewModel
|
||||
|
||||
@Composable
|
||||
fun RequestRangingPermission(
|
||||
onChanged: (Boolean) -> Unit = {},
|
||||
content: @Composable (Boolean) -> Unit,
|
||||
) {
|
||||
val permissionViewModel = hiltViewModel<RangingPermissionViewModel>()
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
|
||||
val state by activity?.let { permissionViewModel.requestRangingPermission(it) }!!
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
|
||||
LaunchedEffect(state) {
|
||||
onChanged(state is RangingPermissionState.Available)
|
||||
}
|
||||
|
||||
when (val s = state) {
|
||||
is RangingPermissionState.Available -> content(true)
|
||||
is RangingPermissionState.NotAvailable -> {
|
||||
when (s.reason) {
|
||||
RangingNotAvailableReason.NOT_AVAILABLE -> RangingPermissionRequestView(content)
|
||||
RangingNotAvailableReason.PERMISSION_DENIED -> content(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package no.nordicsemi.android.permissions_ranging.repository
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import no.nordicsemi.android.permissions_ranging.utils.LocalDataProvider
|
||||
import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason
|
||||
import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState
|
||||
import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionUtils
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val REFRESH_PERMISSIONS =
|
||||
"no.nordicsemi.android.permissions_ranging.repository.REFRESH_RANGING_PERMISSIONS"
|
||||
private const val RANGING_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
@Singleton
|
||||
internal class RangingStateManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val dataProvider = LocalDataProvider(context)
|
||||
private val utils = RangingPermissionUtils(context, dataProvider)
|
||||
|
||||
fun rangingPermissionState(activity: Activity) = callbackFlow {
|
||||
trySend(getRangingPermissionState())
|
||||
|
||||
val rangingStateChangeHandler = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
trySend(getRangingPermissionState())
|
||||
}
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
rangingStateChangeHandler,
|
||||
IntentFilter(),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf("android.permission.RANGING"),
|
||||
RANGING_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
|
||||
awaitClose {
|
||||
|
||||
context.unregisterReceiver(rangingStateChangeHandler)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun refreshRangingPermissionState() {
|
||||
val intent = Intent(REFRESH_PERMISSIONS)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
fun markRangingPermissionAsRequested() {
|
||||
dataProvider.isRangingPermissionRequested = true
|
||||
}
|
||||
|
||||
fun isRangingPermissionDenied(): Boolean {
|
||||
return utils.isRangingPermissionDenied()
|
||||
}
|
||||
|
||||
private fun getRangingPermissionState(): RangingPermissionState {
|
||||
return when {
|
||||
!utils.isRangingPermissionAvailable -> RangingPermissionState.NotAvailable(
|
||||
RangingNotAvailableReason.NOT_AVAILABLE
|
||||
)
|
||||
|
||||
utils.isRangingPermissionAvailable && !utils.isRangingPermissionGranted -> RangingPermissionState.NotAvailable(
|
||||
RangingNotAvailableReason.PERMISSION_DENIED
|
||||
)
|
||||
|
||||
else -> RangingPermissionState.Available
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package no.nordicsemi.android.permissions_ranging.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.edit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val SHARED_PREFS_NAME = "SHARED_PREFS_RANGING"
|
||||
private const val PREFS_PERMISSION_REQUESTED = "ranging_permission_requested"
|
||||
|
||||
@Singleton
|
||||
internal class LocalDataProvider @Inject constructor(
|
||||
private val context: Context,
|
||||
) {
|
||||
private val sharedPrefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val isBaklavaOrAbove: Boolean
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA)
|
||||
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA
|
||||
|
||||
/**
|
||||
* The first time an app requests a permission there is no 'Don't Allow' checkbox and
|
||||
* [ActivityCompat.shouldShowRequestPermissionRationale] returns false.
|
||||
*/
|
||||
var isRangingPermissionRequested: Boolean
|
||||
get() = sharedPrefs.getBoolean(PREFS_PERMISSION_REQUESTED, false)
|
||||
set(value) {
|
||||
sharedPrefs.edit { putBoolean(PREFS_PERMISSION_REQUESTED, value) }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user