diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 5d74c4d0..523a9f8e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { id("com.google.gms.google-services") version "4.4.1" // Add the version here id("dev.flutter.flutter-gradle-plugin") id("com.huawei.agconnect") + id("kotlin-parcelize") // id("com.mapbox.gradle.application") // id("com.mapbox.gradle.plugins.ndk") } @@ -25,7 +26,7 @@ android { defaultConfig { applicationId = "com.ejada.hmg" // minSdk = 24 - minSdk = 26 + minSdk = 29 targetSdk = 36 compileSdk = 36 // targetSdk = flutter.targetSdkVersion @@ -167,6 +168,7 @@ dependencies { implementation(files("libs/PenNavUI.aar")) implementation(files("libs/Penguin.aar")) implementation(files("libs/PenguinRenderer.aar")) + api(files("libs/samsung-health-data-api.aar")) implementation("com.github.kittinunf.fuel:fuel:2.3.1") implementation("com.github.kittinunf.fuel:fuel-android:2.3.1") diff --git a/android/app/src/main/kotlin/com/ejada/hmg/MainActivity.kt b/android/app/src/main/kotlin/com/ejada/hmg/MainActivity.kt index b81a6fb0..8aac0a9d 100644 --- a/android/app/src/main/kotlin/com/ejada/hmg/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ejada/hmg/MainActivity.kt @@ -9,6 +9,7 @@ import android.view.WindowManager import androidx.annotation.NonNull; import androidx.annotation.RequiresApi import com.ejada.hmg.penguin.PenguinInPlatformBridge +import com.ejada.hmg.samsung_watch.SamsungWatch import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugins.GeneratedPluginRegistrant @@ -23,6 +24,7 @@ class MainActivity: FlutterFragmentActivity() { this.window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) PenguinInPlatformBridge(flutterEngine, this).create() + SamsungWatch(flutterEngine, this) } override fun onRequestPermissionsResult( diff --git a/android/app/src/main/kotlin/com/ejada/hmg/samsung_watch/SamsungWatch.kt b/android/app/src/main/kotlin/com/ejada/hmg/samsung_watch/SamsungWatch.kt new file mode 100644 index 00000000..e0bd2bac --- /dev/null +++ b/android/app/src/main/kotlin/com/ejada/hmg/samsung_watch/SamsungWatch.kt @@ -0,0 +1,125 @@ +package com.ejada.hmg.samsung_watch + + + +import com.ejada.hmg.MainActivity +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.ejada.hmg.penguin.PenguinView +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import com.ejada.hmg.PermissionManager.HostNotificationPermissionManager +import com.ejada.hmg.PermissionManager.HostBgLocationManager +import com.ejada.hmg.PermissionManager.HostGpsStateManager +import com.samsung.android.sdk.health.data.HealthDataService +import com.samsung.android.sdk.health.data.HealthDataStore +import com.samsung.android.sdk.health.data.permission.AccessType +import com.samsung.android.sdk.health.data.permission.Permission +import com.samsung.android.sdk.health.data.request.DataTypes +import com.samsung.android.sdk.health.data.request.LocalTimeFilter +import com.samsung.android.sdk.health.data.request.Ordering +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.LocalTime + +class SamsungWatch( + private var flutterEngine: FlutterEngine, + private var mainActivity: MainActivity +) { + + private lateinit var channel: MethodChannel + private lateinit var dataStore: HealthDataStore + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val TAG = "SamsungWatch" + companion object { + private const val CHANNEL = "samsung_watch" + + } + init{ + create() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun create() { + Log.d(TAG, "create: is called") +// openTok = OpenTok(mainActivity, flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "init" -> { + Log.d(TAG, "onMethodCall: init called") + dataStore = HealthDataService.getStore(mainActivity) + result.success("initialized") + } + + "getPermission"->{ + if(!this::dataStore.isInitialized) + result.error("DataStoreNotInitialized", "Please call init before requesting permissions", null) + val permSet = setOf( + Permission.of(DataTypes.HEART_RATE, AccessType.READ), + Permission.of(DataTypes.STEPS, AccessType.READ), + Permission.of(DataTypes.BLOOD_OXYGEN, AccessType.READ), + Permission.of(DataTypes.ACTIVITY_SUMMARY, AccessType.READ), + Permission.of(DataTypes.SLEEP, AccessType.READ), +// Permission.of(DataTypes.SKIN_TEMPERATURE, AccessType.READ), +// Permission.of(DataTypes.NUTRITION, AccessType.READ), + + ) + scope.launch { + try { + var granted = dataStore.getGrantedPermissions(permSet) + + if(granted.containsAll(granted)) + result.success("Permission Granted") // adapt result as needed + + granted = dataStore.requestPermissions(permSet, mainActivity) + + if(granted.containsAll(granted)) + result.success("Permission Granted") // adapt result as needed + result.error("PermissionError", "Permission Not Granted", null) // adapt result as needed + } catch (e: Exception) { + Log.e(TAG, "create: getPermission failed", e) + result.error("PermissionError", e.message, null) + } + } + } + + "getHeartRate"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT) + val localTimeFilter = LocalTimeFilter.of(dateTime, dateTime.minusDays(365)) + val readRequest = DataTypes.HEART_RATE.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val heartRateList = dataStore.readData(readRequest).dataList + Log.d(TAG, "create: heart rate data count: ${heartRateList}") + + } + } + + + "closeCoroutineScope"->{ + destroy() + result.success("Coroutine Scope Cancelled") + } + + else -> { + result.notImplemented() + } + } + } + } + + fun destroy() { + scope.cancel() + } + +} diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index c08cdda9..df6fdf7c 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -683,7 +683,7 @@ const DASHBOARD = 'Services/Patients.svc/REST/PatientDashboard'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/features/smartwatch_health_data/health_provider.dart b/lib/features/smartwatch_health_data/health_provider.dart index 0acb25bb..662b8141 100644 --- a/lib/features/smartwatch_health_data/health_provider.dart +++ b/lib/features/smartwatch_health_data/health_provider.dart @@ -1,6 +1,13 @@ import 'package:flutter/foundation.dart'; import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; + +import '../../core/dependencies.dart'; +import '../../presentation/smartwatches/smart_watch_activity.dart' show SmartWatchActivity; +import '../../services/navigation_service.dart' show NavigationService; class HealthProvider with ChangeNotifier { final HealthService _healthService = HealthService(); @@ -10,13 +17,14 @@ class HealthProvider with ChangeNotifier { String selectedTimeRange = '7D'; int selectedTabIndex = 0; - String selectedWatchType = 'apple'; + SmartWatchTypes? selectedWatchType ; String selectedWatchURL = 'assets/images/png/smartwatches/apple-watch-5.jpg'; - setSelectedWatchType(String type, String imageURL) { + setSelectedWatchType(SmartWatchTypes type, String imageURL) { selectedWatchType = type; selectedWatchURL = imageURL; notifyListeners(); + _healthService.addWatchHelper(type); } void onTabChanged(int index) { @@ -91,4 +99,27 @@ class HealthProvider with ChangeNotifier { return DateTime.now().subtract(const Duration(days: 7)); } } + + void initDevice() async { + LoaderBottomSheet.showLoader(); + notifyListeners(); + final result = await _healthService.initDevice(); + isLoading = false; + LoaderBottomSheet.hideLoader(); + if (result.isError) { + error = 'Error initializing device: ${result.asError}'; + } else { + getIt.get().pushPage(page: SmartWatchActivity()); + print('Device initialized successfully'); + } + notifyListeners(); + } + + void getHeartRate() async{ + isLoading = true; + notifyListeners(); + final result = await _healthService.getHeartRate(); + isLoading = false; + notifyListeners(); + } } diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart index d3815b3b..b8ffbc99 100644 --- a/lib/features/smartwatch_health_data/health_service.dart +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -1,9 +1,14 @@ +import 'dart:async'; import 'dart:io'; import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart'; import 'package:permission_handler/permission_handler.dart'; import 'health_utils.dart'; +import 'package:async/async.dart'; class HealthService { static final HealthService _instance = HealthService._internal(); @@ -14,6 +19,8 @@ class HealthService { final Health health = Health(); + WatchHelper? watchHelper; + final List _healthMetrics = [ HealthDataType.HEART_RATE, // HealthDataType.STEPS, @@ -161,4 +168,23 @@ class HealthService { return []; } } + + void addWatchHelper(SmartWatchTypes watchType){ + watchHelper = CreateWatchHelper.getWatchName(watchType) ; + } + + Future> initDevice() async { + if(watchHelper == null){ + return Result.error('No watch helper found'); + } + return await watchHelper!.initDevice(); + } + + FutureOr getHeartRate() async { + if (watchHelper == null) { + print('No watch helper found'); + return; + } + await watchHelper!.getHeartRate(); + } } diff --git a/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart b/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart new file mode 100644 index 00000000..ee3a3e83 --- /dev/null +++ b/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart @@ -0,0 +1,31 @@ + +import 'package:async/async.dart'; +import 'package:flutter/services.dart'; +class SamsungPlatformChannel { + final MethodChannel _channel = MethodChannel('samsung_watch'); + Future> initDevice() async { + try{ + await _channel.invokeMethod('init'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + + Future> getRequestedPermission() async { + try{ + await _channel.invokeMethod('getPermission'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + Future> getHeartRate() async { + try{ + await _channel.invokeMethod('getHeartRate'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart new file mode 100644 index 00000000..9bc6d84d --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart @@ -0,0 +1,14 @@ +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/samsung_health.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart'; + +class CreateWatchHelper { + static WatchHelper getWatchName(SmartWatchTypes watchType) { + switch(watchType){ + case SmartWatchTypes.samsung: + return SamsungHealth(); + default: + return SamsungHealth(); + } + } +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart b/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart new file mode 100644 index 00000000..69d0b88a --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:async/src/result/result.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart' show WatchHelper; + +class SamsungHealth extends WatchHelper { + + final SamsungPlatformChannel platformChannel = SamsungPlatformChannel(); + + @override + FutureOr getHeartRate() async { + await platformChannel.getHeartRate(); + } + + @override + Future> initDevice() async { + var result = await platformChannel.initDevice(); + if(result.isError){ + return result; + } + return await platformChannel.getRequestedPermission(); + } + +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart new file mode 100644 index 00000000..faa98d39 --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart @@ -0,0 +1,7 @@ +import 'dart:async'; +import 'package:async/async.dart'; +abstract class WatchHelper { + Future> initDevice(); + FutureOr getHeartRate(); + +} \ No newline at end of file diff --git a/lib/presentation/smartwatches/smartwatch_home_page.dart b/lib/presentation/smartwatches/smartwatch_home_page.dart index 050cbf7d..70cab3d9 100644 --- a/lib/presentation/smartwatches/smartwatch_home_page.dart +++ b/lib/presentation/smartwatches/smartwatch_home_page.dart @@ -84,7 +84,7 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("apple", "assets/images/png/smartwatches/apple-watch-5.jpg"); + context.read().setSelectedWatchType(SmartWatchTypes.apple, "assets/images/png/smartwatches/apple-watch-5.jpg"); getIt.get().pushPage(page: SmartwatchInstructionsPage( smartwatchDetails: SmartwatchDetails(SmartWatchTypes.apple, "assets/images/png/smartwatches/apple-watch-5.jpg", @@ -117,7 +117,7 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("samsung", "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg"); + context.read().setSelectedWatchType(SmartWatchTypes.samsung, "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg"); getIt.get().pushPage(page: SmartwatchInstructionsPage( smartwatchDetails: SmartwatchDetails(SmartWatchTypes.samsung, "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", @@ -149,7 +149,7 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("huawei", "assets/images/png/smartwatches/Huawei_Watch.png"); + context.read().setSelectedWatchType(SmartWatchTypes.huawei, "assets/images/png/smartwatches/Huawei_Watch.png"); getIt.get().pushPage(page: SmartwatchInstructionsPage( smartwatchDetails: SmartwatchDetails(SmartWatchTypes.huawei, "assets/images/png/smartwatches/Huawei_Watch.png", @@ -182,7 +182,7 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("whoop", "assets/images/png/smartwatches/Whoop_Watch.png"); + context.read().setSelectedWatchType(SmartWatchTypes.whoop, "assets/images/png/smartwatches/Whoop_Watch.png"); getIt.get().pushPage(page: SmartwatchInstructionsPage( smartwatchDetails: SmartwatchDetails(SmartWatchTypes.whoop, "assets/images/png/smartwatches/Whoop_Watch.png", diff --git a/lib/presentation/smartwatches/smartwatch_instructions_page.dart b/lib/presentation/smartwatches/smartwatch_instructions_page.dart index 949b460f..fa5f3135 100644 --- a/lib/presentation/smartwatches/smartwatch_instructions_page.dart +++ b/lib/presentation/smartwatches/smartwatch_instructions_page.dart @@ -12,8 +12,10 @@ import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; import '../../core/utils/utils.dart'; +import '../../features/smartwatch_health_data/health_provider.dart' show HealthProvider; class SmartwatchInstructionsPage extends StatelessWidget { final SmartwatchDetails smartwatchDetails; @@ -34,7 +36,7 @@ class SmartwatchInstructionsPage extends StatelessWidget { child: CustomButton( text: LocaleKeys.getStarted.tr(context: context), onPressed: () { - getIt.get().pushPage(page: SmartWatchActivity()); + context.read().initDevice(); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor,