69 changed files with 4457 additions and 306 deletions
@ -0,0 +1,33 @@ |
|||
kotlin version: 2.1.20 |
|||
error message: java.lang.NoSuchMethodError: 'org.jetbrains.kotlin.config.LanguageVersionSettings org.jetbrains.kotlin.codegen.state.KotlinTypeMapper$Companion.getLANGUAGE_VERSION_SETTINGS_DEFAULT()' |
|||
at com.google.devtools.ksp.processing.impl.ResolverImpl.<init>(ResolverImpl.kt:147) |
|||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:231) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$7(KotlinToJVMBytecodeCompiler.kt:326) |
|||
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:317) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:154) |
|||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:75) |
|||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:167) |
|||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:36) |
|||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:113) |
|||
at org.jetbrains.kotlin.cli.common.CLICompiler.exec(CLICompiler.kt:337) |
|||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1700) |
|||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) |
|||
at java.base/java.lang.reflect.Method.invoke(Unknown Source) |
|||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source) |
|||
at java.base/java.security.AccessController.doPrivileged(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source) |
|||
at java.base/java.security.AccessController.doPrivileged(Unknown Source) |
|||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source) |
|||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) |
|||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) |
|||
at java.base/java.lang.Thread.run(Unknown Source) |
|||
|
|||
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,33 @@ |
|||
//
|
|||
// LNCTabbarController.h
|
|||
//
|
|||
//
|
|||
// Created by lanzhongping on 2020/11/4.
|
|||
// Copyright © 2020 linktop. All rights reserved.
|
|||
//
|
|||
|
|||
#import <UIKit/UIKit.h> |
|||
|
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
@interface HMTabbarController : UITabBarController |
|||
|
|||
|
|||
-(void)createTabs; |
|||
|
|||
/// 添加VC
|
|||
/// @param vc 控制器
|
|||
/// @param title tabbaritem标题
|
|||
/// @param imgNm tabbaritem普通图片
|
|||
/// @param selecImgNm tabbaritem选中图片
|
|||
- (void)addSubControllers:(UIViewController *)vc |
|||
Title:(NSString * __nullable)title |
|||
ImageName:(NSString *)imgNm |
|||
SelectImagName:(NSString *)selecImgNm; |
|||
|
|||
@property(copy, nonatomic) void (^viewdidAppearBLK)(void); |
|||
|
|||
|
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,16 @@ |
|||
//
|
|||
// LTSRingSDK+Desc.h
|
|||
// CareRingApp
|
|||
//
|
|||
// Created by Linktop on 2023/8/1.
|
|||
//
|
|||
|
|||
#import "LTSRingSDK.h" |
|||
|
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
@interface LTSRingSDK (Desc) |
|||
-(NSString *)cmdErrorDesc:(EXCUTED_CMD)cmd; |
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,65 @@ |
|||
// |
|||
// LTSRingSDK+Desc.m |
|||
// CareRingApp |
|||
// |
|||
// Created by Linktop on 2023/8/1. |
|||
// |
|||
|
|||
#import "LTSRingSDK+Desc.h" |
|||
|
|||
@implementation LTSRingSDK (Desc) |
|||
|
|||
-(NSString *)cmdErrorDesc:(EXCUTED_CMD)cmd |
|||
{ |
|||
NSString *dec = @""; |
|||
switch (cmd) { |
|||
case EXCUTED_CMD_SET_SPORT_MODE: |
|||
{ |
|||
dec = @"sport mode switch";//@"运动模式开关"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_SYNC_TIME: |
|||
{ |
|||
dec = @"time synchronization";//@"时间同步"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_GET_STEPS: |
|||
{ |
|||
dec = @"get steps";//@"获取计步"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_GET_TEMPERATURE: |
|||
{ |
|||
dec = @"get body temperature";//@"获取体温"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_HIS_DATA: |
|||
{ |
|||
dec = @"Historical data reporting";//@"历史数据"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_HIS_COUNT: |
|||
{ |
|||
dec = @"Number of historical data";//@"历史数据个数"; |
|||
} |
|||
break; |
|||
|
|||
case EXCUTED_CMD_SPORT_MODE: |
|||
{ |
|||
dec = @"sports mode";//@"运动模式"; |
|||
} |
|||
break; |
|||
case EXCUTED_CMD_CLEAR_HIS_DATA: |
|||
{ |
|||
dec = @"Clear device history";//@"清空设备历史记录"; |
|||
|
|||
} |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
|
|||
return dec; |
|||
} |
|||
|
|||
@end |
@ -0,0 +1,17 @@ |
|||
//
|
|||
// LoginVc.h
|
|||
// CareRingApp
|
|||
//
|
|||
// Created by Linktop on 2022/6/6.
|
|||
//
|
|||
|
|||
#import <UIKit/UIKit.h> |
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
@interface LoginVc : UIViewController |
|||
|
|||
@property(strong, nonatomic)NSString *cacheAccount; // 退出登录缓存使用
|
|||
|
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,18 @@ |
|||
//
|
|||
// MainNav.h
|
|||
//
|
|||
//
|
|||
// Created by Linktop on 2021/4/20.
|
|||
// Copyright © 2021 linktop. All rights reserved.
|
|||
//
|
|||
|
|||
#import <UIKit/UIKit.h> |
|||
|
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
@interface MainNav : UINavigationController |
|||
-(instancetype)initWithRootViewController:(UIViewController *)rootViewController ShowNavBar:(BOOL)show; |
|||
|
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,162 @@ |
|||
// |
|||
// NAVTemplateViewController.m |
|||
// |
|||
// |
|||
// Created by Linktop on 2021/4/15. |
|||
// Copyright © 2021 linktop. All rights reserved. |
|||
// |
|||
|
|||
#import "NAVTemplateViewController.h" |
|||
#import "ConfigModel.h" |
|||
#import "UILabel+LNCTitleStyle.h" |
|||
#import <QMUIKit/QMUIKit.h> |
|||
|
|||
@interface NAVTemplateViewController () |
|||
|
|||
@property(copy, nonatomic)void(^backBlk)(void); |
|||
|
|||
@end |
|||
|
|||
@implementation NAVTemplateViewController |
|||
|
|||
- (void)viewDidLoad { |
|||
[super viewDidLoad]; |
|||
self.view.backgroundColor = [UIColor blackColor]; |
|||
// Do any additional setup after loading the view. |
|||
self.navigationController.interactivePopGestureRecognizer.delegate = self; |
|||
|
|||
|
|||
|
|||
} |
|||
|
|||
#pragma mark -- navbar style |
|||
|
|||
-(void)arrowback:(void(^ __nullable)(void))backBlk |
|||
{ |
|||
|
|||
UIImage *image = [UIImage imageNamed:@"back_white"]; |
|||
QMUIButton *backBtn = [[QMUIButton alloc]init]; |
|||
[backBtn setImage:image forState:UIControlStateNormal]; |
|||
backBtn.frame = CGRectMake(0, 0, 50, 44); |
|||
[backBtn setTitle:@" " forState:UIControlStateNormal]; |
|||
backBtn.imagePosition = QMUIButtonImagePositionLeft; |
|||
[backBtn addTarget:self action:@selector(backBtn:) forControlEvents:UIControlEventTouchUpInside]; |
|||
|
|||
self.backBlk = backBlk; |
|||
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc]initWithCustomView:backBtn];; |
|||
|
|||
self.navigationItem.leftBarButtonItem = leftItem; |
|||
} |
|||
|
|||
-(void)cleanLeftBarButon |
|||
{ |
|||
|
|||
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc]initWithCustomView:[[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)] ];; |
|||
|
|||
self.navigationItem.leftBarButtonItem = leftItem; |
|||
} |
|||
|
|||
-(void)customNavStyleNormal:(NSString *)centerTitle BackBlk:( void(^ __nullable)(void))backBlk |
|||
{ |
|||
//navbar 背景 |
|||
|
|||
UINavigationBar *navBar = self.navigationController.navigationBar; |
|||
if (!navBar) { |
|||
return; |
|||
} |
|||
navBar.tintColor = [UIColor whiteColor]; |
|||
// self.title = centerTitle; |
|||
// [self gradientBackGround]; //渐变色背景 |
|||
//title |
|||
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 250, 44)]; |
|||
titleLabel.font = [UIFont fontWithName:@"ArialMT" size:19.0f]; |
|||
[titleLabel setTextColor:[UIColor whiteColor]]; |
|||
titleLabel.text = centerTitle; |
|||
titleLabel.textAlignment = NSTextAlignmentCenter; |
|||
|
|||
|
|||
self.navigationItem.titleView = titleLabel; |
|||
|
|||
//左按钮 |
|||
CGFloat bt_w = 50.0f; |
|||
UIButton * leftCustomBtn = [[UIButton alloc] initWithFrame:CGRectMake(0, 60, bt_w, 44)]; |
|||
[leftCustomBtn addTarget:self action:@selector(backBtn:) forControlEvents:UIControlEventTouchUpInside]; |
|||
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc] initWithCustomView:leftCustomBtn]; |
|||
if (backBlk != nil) { |
|||
UIImage *btnArrowImg = [UIImage imageNamed:@"back_white"]; |
|||
[leftCustomBtn setImage:btnArrowImg forState:UIControlStateNormal]; |
|||
CGFloat imageV_w = btnArrowImg.size.width; |
|||
leftCustomBtn.imageEdgeInsets = UIEdgeInsetsMake(0, imageV_w - bt_w, 0, 0); |
|||
} |
|||
|
|||
self.navigationItem.leftBarButtonItem = leftItem; |
|||
|
|||
self.backBlk = backBlk; |
|||
|
|||
// self.navigationController.interactivePopGestureRecognizer.enabled = YES; |
|||
// self.navigationController.interactivePopGestureRecognizer.delegate = (id)self; |
|||
} |
|||
|
|||
- (void)customNavBarView:(NSString *)leftBtnTitle |
|||
{ |
|||
|
|||
//navbar 背景 |
|||
UINavigationBar *navBar = self.navigationController.navigationBar; |
|||
if (!navBar) { |
|||
return; |
|||
} |
|||
// navBar.tintColor = [UIColor whiteColor]; |
|||
|
|||
// [self gradientBackGround]; //渐变色背景 |
|||
|
|||
//左按钮 |
|||
/* |
|||
UIButton * leftCustomBtn = [[UIButton alloc] initWithFrame:CGRectMake(0, 60, 100, 44)]; |
|||
[leftCustomBtn useNavTitleFont]; |
|||
[leftCustomBtn setTitle:leftBtnTitle forState:UIControlStateNormal]; |
|||
leftCustomBtn.enabled = NO; |
|||
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc] initWithCustomView:leftCustomBtn]; |
|||
|
|||
self.navigationItem.leftBarButtonItem = leftItem; |
|||
*/ |
|||
|
|||
UILabel * leftCustomLbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 60, 100, 44)]; |
|||
leftCustomLbl.text = leftBtnTitle; |
|||
leftCustomLbl.textAlignment = NSTextAlignmentLeft; |
|||
[leftCustomLbl userNavTitleFont:nil]; |
|||
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc] initWithCustomView:leftCustomLbl]; |
|||
self.navigationItem.leftBarButtonItem = leftItem; |
|||
|
|||
|
|||
|
|||
} |
|||
|
|||
#pragma mark --navbar按键响应 |
|||
- (void)backBtn:(id)sender { |
|||
if (self.backBlk) { |
|||
self.backBlk(); |
|||
} else { |
|||
[self.navigationController popViewControllerAnimated:YES]; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
//- (void)gradientBackGround { |
|||
// self.navigationController.navigationBar.translucent = NO; |
|||
// self.navigationController.navigationBar.barTintColor = MAIN_BG_COLOR; |
|||
// |
|||
// if (@available(iOS 15.0, *)) { |
|||
// UINavigationBarAppearance *appearance = [UINavigationBarAppearance new]; |
|||
// [appearance configureWithOpaqueBackground]; |
|||
// appearance.backgroundColor = MAIN_BG_COLOR; |
|||
// self.navigationController.navigationBar.standardAppearance = appearance; |
|||
// self.navigationController.navigationBar.scrollEdgeAppearance = self.navigationController.navigationBar.standardAppearance; |
|||
// } |
|||
// |
|||
// |
|||
//// self.navigationController.navigationBar.backgroundColor = [UIColor blackColor]; |
|||
//} |
|||
|
|||
|
|||
@end |
@ -0,0 +1,23 @@ |
|||
//
|
|||
// NSString+Check.h
|
|||
// CareRingApp
|
|||
//
|
|||
// Created by Linktop on 2022/8/23.
|
|||
//
|
|||
|
|||
#import <Foundation/Foundation.h> |
|||
|
|||
NS_ASSUME_NONNULL_BEGIN |
|||
|
|||
@interface NSString (Check) |
|||
|
|||
/// 检查是否包含非法字符 YES = 包含
|
|||
-(BOOL)isContainSpecialCharacters; |
|||
// yes = 复合规则
|
|||
-(BOOL)isValiadEmail; |
|||
|
|||
//-(int)transVersionToInt;
|
|||
-(BOOL)versionIsLowThan:(NSString *)remote; |
|||
@end |
|||
|
|||
NS_ASSUME_NONNULL_END |
@ -0,0 +1,67 @@ |
|||
// |
|||
// NSString+Check.m |
|||
// CareRingApp |
|||
// |
|||
// Created by Linktop on 2022/8/23. |
|||
// |
|||
|
|||
#import "NSString+Check.h" |
|||
|
|||
@implementation NSString (Check) |
|||
//非法字符 |
|||
-(BOOL)isContainSpecialCharacters { |
|||
|
|||
NSCharacterSet *nameCharacters = [[NSCharacterSet characterSetWithCharactersInString:@"_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"] invertedSet]; |
|||
NSRange userNameRange = [self rangeOfCharacterFromSet:nameCharacters]; |
|||
if (userNameRange.location != NSNotFound) { |
|||
NSLog(@"包含特殊字符"); |
|||
return YES; |
|||
} |
|||
return NO; |
|||
} |
|||
|
|||
-(BOOL)isValiadEmail |
|||
{ |
|||
|
|||
// ^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$ |
|||
NSString* emailRegu = @"^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"; |
|||
NSPredicate *numberPre = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",emailRegu]; |
|||
return [numberPre evaluateWithObject:self]; |
|||
|
|||
} |
|||
|
|||
-(int)transVersionToInt |
|||
{ |
|||
|
|||
NSArray<NSString *> *arr = [self componentsSeparatedByString:@"."]; |
|||
int res = 0; |
|||
NSMutableString *tempString = [NSMutableString new]; |
|||
for (int i = 0; i < arr.count; i++) { |
|||
[tempString appendFormat:@"%d", arr[i].intValue]; |
|||
|
|||
} |
|||
res = [tempString intValue]; |
|||
return res; |
|||
|
|||
} |
|||
|
|||
-(BOOL)versionIsLowThan:(NSString *)remote { |
|||
|
|||
NSArray<NSString *> *arrLocal = [self componentsSeparatedByString:@"."]; |
|||
NSArray<NSString *> *arrRemote = [remote componentsSeparatedByString:@"."]; |
|||
if (arrLocal.count != arrRemote.count) { |
|||
return NO; //格式不匹配 |
|||
} |
|||
BOOL isLow = NO; |
|||
for (int i = 0; i < arrLocal.count; i++) { |
|||
if ([arrLocal[i] intValue] < [arrRemote[i] intValue]) { |
|||
isLow = YES; |
|||
break; |
|||
} |
|||
|
|||
} |
|||
|
|||
return isLow; |
|||
} |
|||
|
|||
@end |
@ -0,0 +1,23 @@ |
|||
// |
|||
// PrefixHeader.pch |
|||
// sr01sdkProject |
|||
// |
|||
// Created by Linktop on 2022/5/30. |
|||
// |
|||
|
|||
#ifndef PrefixHeader_pch |
|||
#define PrefixHeader_pch |
|||
|
|||
//#import "TestUtils.h" |
|||
|
|||
// Include any system framework and library headers here that should be included in all compilation units. |
|||
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. |
|||
|
|||
#ifdef DEBUG |
|||
#define DebugNSLog(...) NSLog(__VA_ARGS__) |
|||
#else |
|||
#define DebugNSLog(...) |
|||
|
|||
#endif |
|||
|
|||
#endif /* PrefixHeader_pch */ |
@ -0,0 +1,23 @@ |
|||
// |
|||
// PrefixHeader.pch |
|||
// sr01sdkProject |
|||
// |
|||
// Created by Linktop on 2022/5/30. |
|||
// |
|||
|
|||
#ifndef PrefixHeader_pch |
|||
#define PrefixHeader_pch |
|||
|
|||
//#import "TestUtils.h" |
|||
|
|||
// Include any system framework and library headers here that should be included in all compilation units. |
|||
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. |
|||
|
|||
#ifdef DEBUG |
|||
#define DebugNSLog(...) NSLog(__VA_ARGS__) |
|||
#else |
|||
#define DebugNSLog(...) |
|||
|
|||
#endif |
|||
|
|||
#endif /* PrefixHeader_pch */ |
@ -0,0 +1,4 @@ |
|||
//
|
|||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
|||
//
|
|||
|
@ -0,0 +1,9 @@ |
|||
// |
|||
// temp.m |
|||
// iosApp |
|||
// |
|||
// Created by 安然雨声 on 2025/6/4. |
|||
// Copyright © 2025 orgName. All rights reserved. |
|||
// |
|||
|
|||
#import <Foundation/Foundation.h> |
@ -1 +1,3 @@ |
|||
language = Objective-C |
|||
package = com.whitefish.ring.objc |
|||
linkerOpts = -L../iosApp/iosApp/Libs -lRingSDK_2.0.2 |
|||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,58 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.app.Activity |
|||
import android.app.Application |
|||
import android.os.Bundle |
|||
import android.util.Log |
|||
|
|||
class ActivityLifecycleCb : Application.ActivityLifecycleCallbacks { |
|||
|
|||
/** |
|||
* 如果是从后台打开APP的,此标志意味着可以从设备拉数据 |
|||
* |
|||
* |
|||
var readDataFromDevice: Boolean = false |
|||
*/ |
|||
|
|||
private var flag = 0 |
|||
val activities = ArrayList<Activity>() |
|||
|
|||
/** |
|||
* 判断APP是否在前台运行 |
|||
* */ |
|||
val isAppForeground: Boolean get() = flag > 0 |
|||
var backgroundFlag = true |
|||
|
|||
val currAct: Activity? |
|||
get() = if (activities.isNotEmpty()) activities[activities.size - 1] else null |
|||
|
|||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { |
|||
activities.add(activity) |
|||
} |
|||
|
|||
override fun onActivityStarted(activity: Activity) { |
|||
flag++ |
|||
Log.i("ActivityLifecycleCb", "onActivityStarted - flag:$flag") |
|||
} |
|||
|
|||
override fun onActivityResumed(activity: Activity) { |
|||
} |
|||
|
|||
override fun onActivityPaused(activity: Activity) { |
|||
} |
|||
|
|||
override fun onActivityStopped(activity: Activity) { |
|||
flag-- |
|||
if (!isAppForeground) { |
|||
backgroundFlag = true |
|||
} |
|||
Log.i("ActivityLifecycleCb", "onActivityStopped - flag:$flag") |
|||
} |
|||
|
|||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { |
|||
} |
|||
|
|||
override fun onActivityDestroyed(activity: Activity) { |
|||
activities.remove(activity) |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.app.Application |
|||
import com.whitefish.app.bt.BleManager |
|||
import io.github.aakira.napier.DebugAntilog |
|||
import io.github.aakira.napier.Napier |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
|
|||
class Application: Application() { |
|||
val bleManager by lazy { |
|||
NexRingManager.init(this) |
|||
BleManager(this) |
|||
} |
|||
|
|||
val mActivityLifecycleCb = ActivityLifecycleCb() |
|||
|
|||
companion object { |
|||
@SuppressLint("StaticFieldLeak") |
|||
var INSTANTS: com.whitefish.ring.Application? = null |
|||
private set |
|||
|
|||
} |
|||
|
|||
override fun onCreate() { |
|||
super.onCreate() |
|||
INSTANTS = this |
|||
Napier.base(DebugAntilog()) |
|||
registerActivityLifecycleCallbacks(mActivityLifecycleCb) |
|||
} |
|||
} |
@ -0,0 +1,235 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.app.AlertDialog |
|||
import android.bluetooth.BluetoothManager |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import androidx.lifecycle.MutableLiveData |
|||
import com.whitefish.app.bt.BleDevice |
|||
import com.whitefish.ring.bt.OnBleConnectionListener |
|||
import com.whitefish.ring.bt.OnBleScanCallback |
|||
import com.whitefish.ring.bean.ui.Device |
|||
import com.whitefish.ring.device.IDeviceManager |
|||
import io.github.aakira.napier.Napier |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import lib.linktop.nexring.api.BATTERY_STATE_CHARGING |
|||
import lib.linktop.nexring.api.LOAD_DATA_EMPTY |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_COMPLETED |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_PROCESSING |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_START |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.OnSleepDataLoadListener |
|||
import lib.linktop.nexring.api.SleepData |
|||
|
|||
|
|||
class DeviceManager() : IDeviceManager(), OnBleConnectionListener, OnSleepDataLoadListener { |
|||
companion object{ |
|||
const val STATE_DEVICE_CHARGING = 1 |
|||
const val STATE_DEVICE_DISCHARGING = 0 |
|||
const val STATE_DEVICE_DISCONNECTED = -3 |
|||
const val STATE_DEVICE_CONNECTING = -2 |
|||
const val STATE_DEVICE_CONNECTED = -1 |
|||
} |
|||
private val context = Application.INSTANTS!! |
|||
private var isRegisterBattery = false |
|||
val batteryLevel = MutableLiveData(STATE_DEVICE_DISCONNECTED to 0) |
|||
private val sycProgress = MutableLiveData(0) |
|||
var isSyncingData: Boolean = false |
|||
// var homeViewModel: demo.linktop.nexring.ui.HomeViewModel? = null |
|||
// var workoutDetailViewModel: demo.linktop.nexring.ui.workout.WorkoutDetailViewModel? = null |
|||
|
|||
|
|||
init { |
|||
registerCb() |
|||
} |
|||
override fun onBleState(state: Int) { |
|||
bleStateListeners().forEach { |
|||
it.invoke(state) |
|||
} |
|||
when (state) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
isRegisterBattery = false |
|||
batteryLevel.postValue(STATE_DEVICE_DISCONNECTED to 0) |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
batteryLevel.postValue(STATE_DEVICE_CONNECTED to 0) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onBleReady() { |
|||
bleReadyStateFlow.value = true |
|||
postDelay { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.getBatteryInfo { |
|||
if (it.state == BATTERY_STATE_CHARGING) { |
|||
batteryLevel.postValue(STATE_DEVICE_CHARGING to 0) |
|||
} else { |
|||
batteryLevel.postValue(STATE_DEVICE_DISCHARGING to it.level) |
|||
} |
|||
if (!isRegisterBattery) { |
|||
isRegisterBattery = true |
|||
postDelay { |
|||
NexRingManager.get() |
|||
.sleepApi() |
|||
.syncDataFromDev() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onSyncDataFromDevice(state: Int, progress: Int) { |
|||
Napier.i( |
|||
"onSyncDataFromDevice state: $state, progress: $progress" |
|||
) |
|||
when (state) { |
|||
LOAD_DATA_EMPTY -> { |
|||
Napier.e("Empty data") |
|||
//TODO Callback when no data is received from the device. |
|||
} |
|||
|
|||
LOAD_DATA_STATE_START -> { |
|||
isSyncingData = true |
|||
sycProgress.postValue(progress) |
|||
} |
|||
|
|||
LOAD_DATA_STATE_PROCESSING -> sycProgress.postValue(progress) |
|||
LOAD_DATA_STATE_COMPLETED -> { |
|||
sycProgress.postValue(progress) |
|||
isSyncingData = false |
|||
//todo sync data complete |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onSyncDataError(errorCode: Int) { |
|||
context.cmdErrorTip(errorCode) |
|||
} |
|||
|
|||
override fun onOutputNewSleepData(sleepData: ArrayList<SleepData>?) { |
|||
sleepData.also { |
|||
if (it.isNullOrEmpty()) { |
|||
Napier.i( |
|||
"onOutputNewSleepData NULL" |
|||
) |
|||
} else { |
|||
Napier.i( |
|||
"onOutputNewSleepData size ${it.size}" |
|||
) |
|||
it.forEachIndexed { index, data -> |
|||
Napier.i( |
|||
"onOutputNewSleepData $index sleep from ${data.startTs} to ${data.endTs}" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun registerCb() { |
|||
context.bleManager.addOnBleConnectionListener(this) |
|||
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(this) |
|||
} |
|||
|
|||
fun unregisterCb() { |
|||
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(null) |
|||
context.bleManager.removeOnBleConnectionListener(this) |
|||
} |
|||
|
|||
|
|||
override fun connect(address: String) { |
|||
with(context.bleManager) { |
|||
when (bleState.value) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
batteryLevel.postValue(STATE_DEVICE_CONNECTING to 0) |
|||
if (!connect(address)) { |
|||
startScan( |
|||
20 * 1000L, |
|||
object : OnBleScanCallback { |
|||
override fun onScanning(result: BleDevice) { |
|||
if (result.device.address == address) { |
|||
connect(result.device) |
|||
} |
|||
} |
|||
|
|||
override fun onScanFinished() { |
|||
|
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
onBleState(bleState.value) |
|||
onBleReady() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun bind() { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.getBindState { |
|||
if (it) { |
|||
//todo bind dialog |
|||
// AlertDialog.Builder(this@DeviceActivity) |
|||
// .setCancelable(false) |
|||
// .setTitle(R.string.dialog_title_restricted_mode) |
|||
// .setMessage(R.string.dialog_msg_restricted_mode) |
|||
// .setNegativeButton(android.R.string.cancel) { _, _ -> |
|||
// |
|||
// }.setPositiveButton(android.R.string.ok) { _, _ -> |
|||
// Logger.i("reset device") |
|||
// deviceAdapter.clear() |
|||
// NexRingManager.get() |
|||
// .deviceApi() |
|||
// .factoryReset() |
|||
// switchUI(true) |
|||
// postDelay { |
|||
// Thread.sleep(200) |
|||
// DeviceManager.INSTANCE.scan(this@DeviceActivity) |
|||
// } |
|||
// isConnecting = false |
|||
// }.create().show() |
|||
} else { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.bind { |
|||
//todo bind result |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun startScan() { |
|||
val bluetoothAdapter = |
|||
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
if (bluetoothAdapter.isEnabled) { |
|||
if (!context.bleManager.isScanning) { |
|||
context.bleManager.startScan(20 * 1000L, |
|||
object : OnBleScanCallback { |
|||
@SuppressLint("MissingPermission") |
|||
override fun onScanning(result: BleDevice) { |
|||
Napier.i("scanned device:${result}") |
|||
val newDevices = arrayListOf<Device>().apply { |
|||
addAll(_deviceList.value) |
|||
add(Device(result.device.name,result.device.address)) |
|||
} |
|||
_deviceList.value = newDevices |
|||
} |
|||
|
|||
override fun onScanFinished() { |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun stopScan() { |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.os.Handler |
|||
import android.os.Looper |
|||
|
|||
private val uiHandler = Handler(Looper.getMainLooper()) |
|||
|
|||
fun postDelay(r: Runnable, delay: Long) = uiHandler.postDelayed(r, delay) |
|||
|
|||
fun postDelay(r: Runnable) = postDelay(r, 100L) |
|||
|
|||
fun post(r: Runnable) = uiHandler.post(r) |
|||
|
|||
fun Runnable.handlerPost() = post(this) |
|||
|
|||
fun Runnable.handlerPostDelay(delay: Long) = postDelay(this, delay) |
|||
|
|||
fun Runnable.handlerRemove() = uiHandler.removeCallbacks(this) |
@ -0,0 +1,112 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.Manifest |
|||
import android.annotation.SuppressLint |
|||
import android.app.AlertDialog |
|||
import android.bluetooth.BluetoothAdapter |
|||
import android.bluetooth.BluetoothManager |
|||
import android.content.ActivityNotFoundException |
|||
import android.content.Context |
|||
import android.content.Context.BLUETOOTH_SERVICE |
|||
import android.content.Intent |
|||
import android.content.pm.PackageManager |
|||
import android.location.LocationManager |
|||
import android.os.Build |
|||
import android.provider.Settings |
|||
import androidx.activity.result.ActivityResultLauncher |
|||
import androidx.core.app.ActivityCompat |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
|
|||
object PermissionManager { |
|||
var permissionChecker: ActivityResultLauncher<Array<String>>? = null |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun checkPermission(context: Context) { |
|||
context.apply { |
|||
val dinedPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
|||
checkDeniedPermissions( |
|||
this, |
|||
Manifest.permission.BLUETOOTH_SCAN, |
|||
Manifest.permission.BLUETOOTH_CONNECT |
|||
) |
|||
} else { |
|||
checkDeniedPermissions( |
|||
this, |
|||
Manifest.permission.ACCESS_FINE_LOCATION, |
|||
Manifest.permission.ACCESS_COARSE_LOCATION |
|||
) |
|||
} |
|||
if (dinedPermissions != null) { |
|||
permissionChecker?.launch(dinedPermissions) |
|||
return |
|||
}else{ |
|||
CoroutineScope(Dispatchers.IO).launch { |
|||
obtainDeviceManager().blePowerState.emit(true) |
|||
} |
|||
} |
|||
if (!locationServiceAllowed()) { |
|||
AlertDialog.Builder(this) |
|||
.setMessage(com.whitefish.ring.R.string.dialog_msg_turn_on_location_service) |
|||
.setCancelable(false) |
|||
.setPositiveButton(android.R.string.ok) { _, _ -> |
|||
goEnableLocationServicePage() |
|||
} |
|||
.create().show() |
|||
return |
|||
} |
|||
val bluetoothAdapter = |
|||
(getSystemService(BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
if (!bluetoothAdapter.isEnabled) { |
|||
startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun Context.locationServiceAllowed(): Boolean { |
|||
return if (Build.VERSION.SDK_INT in Build.VERSION_CODES.M..Build.VERSION_CODES.R) { |
|||
val manager = getSystemService(Context.LOCATION_SERVICE) as LocationManager |
|||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) manager.isLocationEnabled |
|||
else manager.isProviderEnabled(LocationManager.GPS_PROVIDER) |
|||
} else { |
|||
//Other versions do not need to turn on location services, |
|||
//so it can be considered that location services are turned on. |
|||
true |
|||
} |
|||
} |
|||
|
|||
private fun checkDeniedPermissions( |
|||
context: Context, |
|||
vararg permissions: String, |
|||
): Array<String>? { |
|||
val dinedPermissions: MutableList<String> = ArrayList() |
|||
for (permission in permissions) { |
|||
if (ActivityCompat.checkSelfPermission(context, permission) |
|||
!= PackageManager.PERMISSION_GRANTED |
|||
) { |
|||
dinedPermissions.add(permission) |
|||
} |
|||
} |
|||
return if (dinedPermissions.isEmpty()) null else dinedPermissions.toTypedArray() |
|||
} |
|||
|
|||
fun Context.goEnableLocationServicePage() { |
|||
val intent = Intent() |
|||
.setAction(Settings.ACTION_LOCATION_SOURCE_SETTINGS) |
|||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) |
|||
try { |
|||
startActivity(intent) |
|||
} catch (ex: ActivityNotFoundException) { |
|||
// The Android SDK doc says that the location settings activity |
|||
// may not be found. In that case show the general settings. |
|||
// General settings activity |
|||
intent.action = Settings.ACTION_SETTINGS |
|||
try { |
|||
startActivity(intent) |
|||
} catch (e: Exception) { |
|||
toast("Can not find the LOCATION setting page.") |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,49 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import android.Manifest |
|||
import android.annotation.SuppressLint |
|||
import android.app.AlertDialog |
|||
import android.bluetooth.BluetoothAdapter |
|||
import android.bluetooth.BluetoothManager |
|||
import android.content.ActivityNotFoundException |
|||
import android.content.Context |
|||
import android.content.Context.BLUETOOTH_SERVICE |
|||
import android.content.Intent |
|||
import android.content.pm.PackageManager |
|||
import android.location.LocationManager |
|||
import android.os.Build |
|||
import android.provider.Settings |
|||
import android.widget.Toast |
|||
import androidx.activity.result.ActivityResultLauncher |
|||
import androidx.activity.result.contract.ActivityResultContracts |
|||
import androidx.annotation.StringRes |
|||
import androidx.core.app.ActivityCompat |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
|
|||
fun Context.cmdErrorTip(code: Int) { |
|||
when (code) { |
|||
0 -> toast(R.string.cmd_execute_success) |
|||
1 -> toast(R.string.cmd_execute_failed_1) |
|||
2 -> toast(R.string.cmd_execute_failed_2) |
|||
3 -> toast(R.string.cmd_execute_failed_3) |
|||
4 -> toast(R.string.cmd_execute_failed_4) |
|||
5 -> toast(R.string.cmd_execute_failed_5) |
|||
6 -> toast(R.string.cmd_execute_failed_6) |
|||
} |
|||
} |
|||
|
|||
var toast: Toast? = null |
|||
|
|||
fun Context.toast(tip: String) { |
|||
toast?.cancel() |
|||
toast = Toast.makeText(this, tip, Toast.LENGTH_SHORT) |
|||
.apply { show() } |
|||
} |
|||
|
|||
fun Context.toast(@StringRes tip: Int) { |
|||
toast?.cancel() |
|||
toast = Toast.makeText(this, tip, Toast.LENGTH_SHORT) |
|||
.apply { show() } |
|||
} |
@ -0,0 +1,15 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
import android.bluetooth.BluetoothDevice |
|||
|
|||
data class BleDevice( |
|||
val device: BluetoothDevice, |
|||
val color: Int, |
|||
val size: Int, |
|||
val batteryState: Int? = null, |
|||
val batteryLevel: Int? = null, |
|||
/*val chipMode: Int = 0,*/ |
|||
val generation: Int? = null, |
|||
val sn: String? = null, |
|||
var rssi: Int, |
|||
) |
@ -0,0 +1,423 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.app.AlertDialog |
|||
import android.bluetooth.BluetoothDevice |
|||
import android.bluetooth.BluetoothGatt |
|||
import android.bluetooth.BluetoothGattDescriptor |
|||
import android.bluetooth.BluetoothManager |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.bluetooth.le.ScanCallback |
|||
import android.bluetooth.le.ScanResult |
|||
import android.bluetooth.le.ScanSettings |
|||
import android.content.Context |
|||
import android.content.pm.PackageManager |
|||
import android.os.Build |
|||
import com.whitefish.ring.Application |
|||
import com.whitefish.ring.R |
|||
import com.whitefish.ring.bt.OnBleConnectionListener |
|||
import com.whitefish.ring.bt.OnBleScanCallback |
|||
import com.whitefish.ring.handlerRemove |
|||
import com.whitefish.ring.post |
|||
import com.whitefish.ring.postDelay |
|||
import io.github.aakira.napier.Napier |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import lib.linktop.nexring.api.NexRingBluetoothGattCallback |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_DECRYPT |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_SN_NULL |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_START |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_SUCCESS |
|||
import lib.linktop.nexring.api.matchFromAdvertisementData |
|||
import lib.linktop.nexring.api.parseScanRecord |
|||
|
|||
|
|||
private const val OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS = 0 |
|||
private const val OEM_STEP_AUTHENTICATE_OEM = 1 |
|||
private const val OEM_STEP_TIMESTAMP_SYNC = 2 |
|||
private const val OEM_STEP_PROCESS_COMPLETED = 3 |
|||
|
|||
class BleManager(val app: Application) { |
|||
private val tag = "BleManager" |
|||
private val mBluetoothAdapter = |
|||
(app.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
|
|||
private val mOnBleConnectionListeners: MutableList<OnBleConnectionListener> = ArrayList() |
|||
|
|||
private var mOnBleScanCallback: OnBleScanCallback? = null |
|||
var bleGatt: BluetoothGatt? = null |
|||
private val scanDevMacList: MutableList<String> = ArrayList() |
|||
var isScanning = false |
|||
|
|||
private val mScanCallback = object : ScanCallback() { |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onScanResult(callbackType: Int, result: ScanResult) { |
|||
super.onScanResult(callbackType, result) |
|||
// loge( |
|||
// "JKL", |
|||
// "address ${result.device.address}, scanRecord.bytes ${result.scanRecord?.bytes.toByteArrayString()}" |
|||
// ) |
|||
synchronized(scanDevMacList) { |
|||
val scanRecord = result.scanRecord |
|||
if (scanRecord != null) { |
|||
val bytes = scanRecord.bytes |
|||
if (bytes.matchFromAdvertisementData()) { |
|||
val address = result.device.address |
|||
if (!scanDevMacList.contains(address).apply { |
|||
Napier.i{"scanDevMacList contains address($address) = ${!this}"} |
|||
}) { |
|||
val bleDevice = bytes.parseScanRecord().run { |
|||
BleDevice( |
|||
result.device, color, size, |
|||
batteryState, batteryLevel, |
|||
/*chipMode,*/ generation, sn, |
|||
result.rssi |
|||
) |
|||
} |
|||
scanDevMacList.add(address) |
|||
mOnBleScanCallback?.apply { |
|||
|
|||
post { |
|||
onScanning(bleDevice) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private val scanStopRunnable = Runnable { |
|||
cancelScan() |
|||
} |
|||
|
|||
var bleState = MutableStateFlow(0) |
|||
var connectedDevice: BluetoothDevice? = null |
|||
|
|||
private val _oemStepComplete = MutableStateFlow(false) |
|||
val oemStepComplete = _oemStepComplete.asStateFlow() |
|||
|
|||
private val mGattCallback = object : NexRingBluetoothGattCallback(NexRingManager.get()) { |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onConnectionStateChange( |
|||
gatt: BluetoothGatt, status: Int, newState: Int, |
|||
) { |
|||
super.onConnectionStateChange(gatt, status, newState) |
|||
Napier.i ( |
|||
"onConnectionStateChange->status:$status, newState:$newState" |
|||
) |
|||
when (newState) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
NexRingManager.get().apply { |
|||
setBleGatt(null) |
|||
unregisterRingService() |
|||
} |
|||
connectedDevice = null |
|||
gatt.close() |
|||
bleState.value = BluetoothProfile.STATE_DISCONNECTED |
|||
postBleState() |
|||
_oemStepComplete.value = false |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTING -> { |
|||
bleState.value = BluetoothProfile.STATE_CONNECTING |
|||
postBleState() |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
bleState.value = BluetoothProfile.STATE_CONNECTED |
|||
connectedDevice = gatt.device |
|||
postBleState() |
|||
// The default MTU for ATT in the core spec is 23 bytes, with 1 byte for ATT's Opcode, 2 bytes for ATT's Handle, and 20 bytes for GATT. |
|||
// So if you want to set 40, you should request the MTU to be set to 43. |
|||
gatt.requestMtu(40 + 3) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { |
|||
super.onMtuChanged(gatt, mtu, status) |
|||
when (status) { |
|||
BluetoothGatt.GATT_SUCCESS -> { |
|||
Napier.i{ "onMtuChanged success."} |
|||
gatt.discoverServices() |
|||
} |
|||
|
|||
BluetoothGatt.GATT_FAILURE -> { |
|||
Napier.i( "onMtuChanged failure.") |
|||
} |
|||
|
|||
else -> Napier.i("onMtuChanged unknown status $status.") |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { |
|||
super.onServicesDiscovered(gatt, status) |
|||
Napier.i( "onServicesDiscovered(), status:${status}") |
|||
// Refresh device cache. This is the safest place to initiate the procedure. |
|||
if (status == BluetoothGatt.GATT_SUCCESS) { |
|||
NexRingManager.get().setBleGatt(gatt) |
|||
Napier.i("onServicesDiscovered(), registerHealthData") |
|||
postDelay { |
|||
NexRingManager.get().registerRingService() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onDescriptorWrite( |
|||
gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, |
|||
) { |
|||
super.onDescriptorWrite(gatt, descriptor, status) |
|||
if (status == BluetoothGatt.GATT_SUCCESS && |
|||
NexRingManager.get().isRingServiceRegistered() |
|||
) { |
|||
// post { |
|||
// //you need to synchronize the timestamp with the device first after |
|||
// //the the service registration is successful. |
|||
// NexRingManager.get() |
|||
// .settingsApi() |
|||
// .timestampSync(System.currentTimeMillis()) { |
|||
// synchronized(mOnBleConnectionListeners) { |
|||
// mOnBleConnectionListeners.forEach { |
|||
// it.onBleReady() |
|||
// } |
|||
// } |
|||
// } |
|||
// } |
|||
OemAuthenticationProcess().start() |
|||
} |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission", "ObsoleteSdkInt") |
|||
private fun connectInterval(device: BluetoothDevice) { |
|||
Napier.i("connect gatt to ${device.address}") |
|||
bleGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
|||
// device.connectGatt(context, false, gattCallback) |
|||
device.connectGatt(app, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) |
|||
} else { |
|||
device.connectGatt(app, false, mGattCallback) |
|||
}.apply { connect() } |
|||
} |
|||
|
|||
fun isSupportBle(): Boolean = |
|||
// Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && |
|||
app.applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) |
|||
|
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun startScan(timeoutMillis: Long, callback: OnBleScanCallback) { |
|||
isScanning = true |
|||
mOnBleScanCallback = callback |
|||
scanDevMacList.clear() |
|||
val scanSettings = ScanSettings.Builder() |
|||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) |
|||
.build() |
|||
mBluetoothAdapter.bluetoothLeScanner.startScan(null, scanSettings, mScanCallback) |
|||
postDelay(scanStopRunnable, timeoutMillis) |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun cancelScan() { |
|||
if (isScanning) { |
|||
isScanning = false |
|||
mBluetoothAdapter.bluetoothLeScanner.stopScan(mScanCallback) |
|||
post { |
|||
mOnBleScanCallback?.onScanFinished() |
|||
mOnBleScanCallback = null |
|||
scanStopRunnable.handlerRemove() |
|||
} |
|||
} |
|||
scanDevMacList.clear() |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun connect(address: String): Boolean { |
|||
val remoteDevice = mBluetoothAdapter.getRemoteDevice(address) |
|||
Napier.i( "connect to remoteDevice by address, ${remoteDevice.name}") |
|||
return if (!remoteDevice.name.isNullOrEmpty()) { |
|||
connect(remoteDevice) |
|||
true |
|||
} else { |
|||
Napier.i("reject, because it cannot connect success.") |
|||
false |
|||
} |
|||
} |
|||
|
|||
fun connect(device: BluetoothDevice) { |
|||
val delayConnect = isScanning |
|||
cancelScan() |
|||
if (delayConnect) { |
|||
Napier.i( "connect to ${device.address}, delay 200L") |
|||
postDelay({ |
|||
Napier.i("delay finish, connect to ${device.address}") |
|||
connectInterval(device) |
|||
}, 200L) |
|||
} else { |
|||
Napier.i("connect to ${device.address} right now.") |
|||
connectInterval(device) |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun disconnect() { |
|||
bleGatt?.disconnect() |
|||
bleGatt = null |
|||
} |
|||
|
|||
|
|||
fun addOnBleConnectionListener(listener: OnBleConnectionListener) { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.add(listener) |
|||
} |
|||
} |
|||
|
|||
fun removeOnBleConnectionListener(listener: OnBleConnectionListener) { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.remove(listener) |
|||
} |
|||
} |
|||
|
|||
fun postBleState() { |
|||
post { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.forEach { |
|||
it.onBleState(bleState.value) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
inner class OemAuthenticationProcess : Thread() { |
|||
|
|||
private val innerTag = "OemAuthenticationProcess" |
|||
private val locked = Object() |
|||
private var step = OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS |
|||
|
|||
override fun run() { |
|||
while (step < OEM_STEP_PROCESS_COMPLETED) { |
|||
sleep(200L) |
|||
synchronized(locked) { |
|||
when (step) { |
|||
OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS -> { |
|||
Napier.i( "OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS") |
|||
NexRingManager.get().securityApi().checkOemAuthenticationStatus { |
|||
step = if (it) OEM_STEP_AUTHENTICATE_OEM else OEM_STEP_TIMESTAMP_SYNC |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
|
|||
OEM_STEP_AUTHENTICATE_OEM -> { |
|||
Napier.i( "OEM_STEP_AUTHENTICATE_OEM") |
|||
NexRingManager.get().securityApi().authenticateOem { result -> |
|||
when (result) { |
|||
OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 -> { |
|||
Napier.i( "OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_DECRYPT -> { |
|||
Napier.i( "OEM_AUTHENTICATION_FAILED_FOR_DECRYPT") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_SN_NULL -> { |
|||
Napier.i("OEM_AUTHENTICATION_FAILED_FOR_SN_NULL") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_START -> { |
|||
Napier.i( "OEM_AUTHENTICATION_START") |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_SUCCESS -> { |
|||
Napier.i( "OEM_AUTHENTICATION_SUCCESS") |
|||
step = OEM_STEP_TIMESTAMP_SYNC |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
OEM_STEP_TIMESTAMP_SYNC -> { |
|||
Napier.i( "OEM_STEP_TIMESTAMP_SYNC") |
|||
NexRingManager.get() |
|||
.settingsApi() |
|||
.timestampSync(System.currentTimeMillis()) { |
|||
Napier.i( "OEM_STEP_TIMESTAMP_SYNC result $it") |
|||
synchronized(mOnBleConnectionListeners) { |
|||
post { |
|||
mOnBleConnectionListeners.forEach { listener -> |
|||
listener.onBleReady() |
|||
} |
|||
} |
|||
} |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
locked.wait() |
|||
} |
|||
} |
|||
_oemStepComplete.value = true |
|||
Napier.i("OEM_STEP_PROCESS_COMPLETED") |
|||
} |
|||
} |
|||
|
|||
private fun Int.showOemAuthFailDialog() { |
|||
app.mActivityLifecycleCb.currAct.apply { |
|||
if (this != null) { |
|||
val message = when (this@showOemAuthFailDialog) { |
|||
OEM_AUTHENTICATION_FAILED_FOR_SN_NULL -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_sn_null) |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_DECRYPT -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_r1_to_r2) |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_check_r2) |
|||
} |
|||
|
|||
else -> "Unknown error." |
|||
} |
|||
runOnUiThread { |
|||
AlertDialog.Builder(this) |
|||
.setCancelable(false) |
|||
.setTitle(R.string.dialog_title_oem_auth_failed) |
|||
.setMessage(message) |
|||
.setPositiveButton(R.string.btn_label_disconnected) { _, _ -> |
|||
disconnect() |
|||
}.create().show() |
|||
} |
|||
} else disconnect() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
package com.whitefish.ring.bt |
|||
|
|||
interface OnBleConnectionListener { |
|||
|
|||
fun onBleState(state: Int) |
|||
|
|||
fun onBleReady() |
|||
} |
@ -0,0 +1,10 @@ |
|||
package com.whitefish.ring.bt |
|||
|
|||
import com.whitefish.app.bt.BleDevice |
|||
|
|||
interface OnBleScanCallback { |
|||
|
|||
fun onScanning(result: BleDevice) |
|||
|
|||
fun onScanFinished() |
|||
} |
@ -0,0 +1,3 @@ |
|||
package com.whitefish.ring.bean.ui |
|||
|
|||
data class Device (val name: String,val mac: String) |
@ -0,0 +1,30 @@ |
|||
package com.whitefish.ring.device |
|||
|
|||
import com.whitefish.ring.bean.ui.Device |
|||
import kotlinx.coroutines.flow.MutableSharedFlow |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asSharedFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
|
|||
abstract class IDeviceManager { |
|||
protected val _deviceList = MutableStateFlow<List<Device>>(emptyList()) |
|||
val deviceList = _deviceList.asStateFlow() |
|||
protected val _bleState = MutableStateFlow<Int>(-1) |
|||
val bleState = _bleState.asStateFlow() |
|||
val bleReadyStateFlow = MutableStateFlow(false) |
|||
val blePowerState = MutableStateFlow<Boolean>(false) // ios的蓝牙是懒加载的,安卓则无此特性 |
|||
|
|||
private val bleStateListeners = arrayListOf<(Int) -> Unit>() |
|||
fun bleStateListeners() = bleStateListeners |
|||
|
|||
|
|||
fun setOnBleStateChange(event: (Int) -> Unit) { |
|||
bleStateListeners.add(event) |
|||
} |
|||
|
|||
|
|||
abstract fun startScan() |
|||
abstract fun stopScan() |
|||
abstract fun connect(mac: String) |
|||
abstract fun bind() |
|||
} |
@ -0,0 +1,144 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun ConnectionGuideScreen( |
|||
onNextClick: () -> Unit = {} |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 标题 |
|||
Text( |
|||
text = "连接您的Acti戒指", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
|
|||
// 说明文字 |
|||
Text( |
|||
text = "将您的戒指连接到充电器,并继续下一步。请确保您的手机已启用蓝牙功能。", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center, |
|||
lineHeight = 24.sp, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 16.dp) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(60.dp)) |
|||
|
|||
// 戒指和充电器图片占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(280.dp) |
|||
.background( |
|||
Color.White, |
|||
RoundedCornerShape(20.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
verticalArrangement = Arrangement.Center |
|||
) { |
|||
// 戒指图标 |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 80.sp |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(20.dp)) |
|||
|
|||
// 连接线 |
|||
Box( |
|||
modifier = Modifier |
|||
.width(60.dp) |
|||
.height(4.dp) |
|||
.background( |
|||
Color(0xFF007AFF), |
|||
RoundedCornerShape(2.dp) |
|||
) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(20.dp)) |
|||
|
|||
// 充电器图标 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(80.dp) |
|||
.background( |
|||
Color(0xFF333333), |
|||
RoundedCornerShape(40.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "⚡", |
|||
fontSize = 40.sp, |
|||
color = Color.White |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 下一步按钮 |
|||
Button( |
|||
onClick = onNextClick, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = Color(0xFF007AFF), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "下一步", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun ConnectionGuideScreenPreview() { |
|||
ConnectionGuideScreen() |
|||
} |
@ -0,0 +1,150 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.Image |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.lazy.LazyColumn |
|||
import androidx.compose.foundation.lazy.items |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
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.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import androidx.lifecycle.viewmodel.compose.viewModel |
|||
import com.whitefish.ring.bean.ui.Device |
|||
import com.whitefish.ring.device.IDeviceManager |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun DeviceScreen(onBind:() -> Unit){ |
|||
val viewModel: DeviceViewModel = viewModel { DeviceViewModel() } |
|||
val uiState by viewModel.uiState.collectAsState() |
|||
val bindState by viewModel.manager.bleReadyStateFlow.collectAsState() |
|||
|
|||
if (bindState){ |
|||
onBind.invoke() |
|||
} |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 20.dp) |
|||
) { |
|||
// 标题 |
|||
Text( |
|||
text = "附近设备", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(top = 60.dp, bottom = 40.dp), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
// 设备列表 |
|||
LazyColumn( |
|||
verticalArrangement = Arrangement.spacedBy(16.dp), |
|||
modifier = Modifier.weight(1f) |
|||
) { |
|||
items(uiState.deviceList) { device -> |
|||
DeviceItem(device = device){ |
|||
viewModel.connect(it.mac) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 底部提示 |
|||
Text( |
|||
text = "连接失败?", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
modifier = Modifier |
|||
.align(Alignment.BottomCenter) |
|||
.padding(bottom = 21.dp) |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun DeviceItem(device: Device,onClick:(Device)-> Unit) { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(80.dp), |
|||
onClick = { |
|||
onClick.invoke(device) |
|||
}, |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = Color.White |
|||
), |
|||
elevation = CardDefaults.cardElevation( |
|||
defaultElevation = 2.dp |
|||
) |
|||
) { |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 20.dp, vertical = 16.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
// 设备图标占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(48.dp) |
|||
.clip(RoundedCornerShape(24.dp)) |
|||
.background(Color(0xFFE5E5E5)), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
// 这里可以放置实际的设备图标 |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 24.sp |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.width(16.dp)) |
|||
|
|||
// 设备信息 |
|||
Column( |
|||
modifier = Modifier.weight(1f) |
|||
) { |
|||
Text( |
|||
text = device.name, |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(4.dp)) |
|||
|
|||
Text( |
|||
text = "设备号:${device.mac}", |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF999999) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun Device(){ |
|||
DeviceScreen{ |
|||
|
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.runtime.getValue |
|||
import androidx.compose.runtime.mutableStateOf |
|||
import androidx.compose.runtime.remember |
|||
import androidx.compose.runtime.setValue |
|||
import androidx.lifecycle.ViewModel |
|||
import androidx.lifecycle.viewModelScope |
|||
import com.whitefish.ring.bean.ui.Device |
|||
import com.whitefish.ring.device.IDeviceManager |
|||
import com.whitefish.ring.obtainDeviceManager |
|||
import io.github.aakira.napier.Napier |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
|
|||
class DeviceViewModel: ViewModel() { |
|||
// var currentStep by remember { mutableStateOf(GuideStep.WELCOME) } |
|||
|
|||
class UiState( |
|||
val deviceList: List<Device> = emptyList() |
|||
) |
|||
|
|||
val manager = obtainDeviceManager() |
|||
private val _uiState = MutableStateFlow(UiState()) |
|||
val uiState = _uiState.asStateFlow() |
|||
|
|||
init { |
|||
Napier.i { "DeviceViewModel initializing..." } |
|||
|
|||
viewModelScope.launch { |
|||
|
|||
launch { |
|||
manager.deviceList.collectLatest { |
|||
Napier.i { "new device:${it}" } |
|||
_uiState.value = UiState(it) |
|||
} |
|||
} |
|||
|
|||
launch { |
|||
manager.blePowerState.collectLatest { |
|||
if (it){ |
|||
manager.startScan() |
|||
} |
|||
} |
|||
} |
|||
|
|||
launch { |
|||
manager.bleReadyStateFlow.collectLatest { |
|||
Napier.i { "ble ready:${it}" } |
|||
if (it){ |
|||
manager.bind() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun connect(mac: String){ |
|||
manager.connect(mac) |
|||
} |
|||
|
|||
} |
@ -0,0 +1,190 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.border |
|||
import androidx.compose.foundation.clickable |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun DominantHandScreen( |
|||
onNextClick: () -> Unit = {}, |
|||
onHandSelected: (Hand) -> Unit = {} |
|||
) { |
|||
var selectedHand by remember { mutableStateOf<Hand?>(null) } |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 标题 |
|||
Text( |
|||
text = "惯用手", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 副标题 |
|||
Text( |
|||
text = "请选择您的惯用手", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 选择区域 |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.spacedBy(24.dp) |
|||
) { |
|||
// 左手选项 |
|||
HandOptionCard( |
|||
hand = Hand.LEFT, |
|||
isSelected = selectedHand == Hand.LEFT, |
|||
onClick = { |
|||
selectedHand = Hand.LEFT |
|||
onHandSelected(Hand.LEFT) |
|||
}, |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
|
|||
// 右手选项 |
|||
HandOptionCard( |
|||
hand = Hand.RIGHT, |
|||
isSelected = selectedHand == Hand.RIGHT, |
|||
onClick = { |
|||
selectedHand = Hand.RIGHT |
|||
onHandSelected(Hand.RIGHT) |
|||
}, |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 下一步按钮 |
|||
Button( |
|||
onClick = onNextClick, |
|||
enabled = selectedHand != null, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = if (selectedHand != null) Color(0xFF007AFF) else Color(0xFFCCCCCC), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "下一步", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun HandOptionCard( |
|||
hand: Hand, |
|||
isSelected: Boolean, |
|||
onClick: () -> Unit, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Card( |
|||
modifier = modifier |
|||
.height(200.dp) |
|||
.clickable { onClick() }, |
|||
shape = RoundedCornerShape(20.dp), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = if (isSelected) Color(0xFFE3F2FD) else Color.White |
|||
), |
|||
elevation = CardDefaults.cardElevation( |
|||
defaultElevation = if (isSelected) 8.dp else 4.dp |
|||
) |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.then( |
|||
if (isSelected) { |
|||
Modifier.border( |
|||
width = 2.dp, |
|||
color = Color(0xFF007AFF), |
|||
shape = RoundedCornerShape(20.dp) |
|||
) |
|||
} else { |
|||
Modifier |
|||
} |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
verticalArrangement = Arrangement.Center |
|||
) { |
|||
// 手部图标占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(100.dp) |
|||
.background( |
|||
if (isSelected) Color(0xFF007AFF) else Color(0xFFE5E5E5), |
|||
RoundedCornerShape(50.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = if (hand == Hand.LEFT) "✋" else "🤚", |
|||
fontSize = 48.sp, |
|||
color = if (isSelected) Color.White else Color(0xFF666666) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(20.dp)) |
|||
|
|||
Text( |
|||
text = if (hand == Hand.LEFT) "左手" else "右手", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = if (isSelected) Color(0xFF007AFF) else Color(0xFF333333) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun DominantHandScreenPreview() { |
|||
DominantHandScreen() |
|||
} |
@ -0,0 +1,100 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Modifier |
|||
|
|||
enum class GuideStep { |
|||
WELCOME, |
|||
REGISTER, |
|||
CONNECTION_GUIDE, |
|||
DEVICE_LIST, |
|||
PERSONAL_INFO, |
|||
WEARING_FINGER, |
|||
DOMINANT_HAND, |
|||
|
|||
} |
|||
|
|||
@Composable |
|||
fun GuideNavigationScreen( |
|||
modifier: Modifier = Modifier, |
|||
onGuideComplete: () -> Unit = {}, |
|||
|
|||
) { |
|||
var currentStep by remember { mutableStateOf(GuideStep.WELCOME) } |
|||
|
|||
when (currentStep) { |
|||
GuideStep.WELCOME -> { |
|||
WelcomeScreen( |
|||
onStartClick = { |
|||
currentStep = GuideStep.REGISTER |
|||
} |
|||
) |
|||
} |
|||
|
|||
GuideStep.REGISTER -> { |
|||
RegisterScreen( |
|||
onLoginClick = { phoneNumber, verificationCode -> |
|||
// 这里可以添加登录验证逻辑 |
|||
if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) { |
|||
currentStep = GuideStep.CONNECTION_GUIDE |
|||
} |
|||
} |
|||
) |
|||
} |
|||
|
|||
GuideStep.CONNECTION_GUIDE -> { |
|||
ConnectionGuideScreen( |
|||
onNextClick = { |
|||
currentStep = GuideStep.DEVICE_LIST |
|||
} |
|||
) |
|||
} |
|||
|
|||
|
|||
GuideStep.DEVICE_LIST -> { |
|||
DeviceScreen{ |
|||
currentStep = GuideStep.PERSONAL_INFO |
|||
} |
|||
} |
|||
|
|||
GuideStep.PERSONAL_INFO -> { |
|||
PersonalInfoScreen( |
|||
onNextClick = { |
|||
currentStep = GuideStep.WEARING_FINGER |
|||
}, |
|||
onGenderClick = { |
|||
// 这里可以打开性别选择对话框或跳转到专门的性别选择页面 |
|||
}, |
|||
onBirthdayClick = { |
|||
// 这里可以打开日期选择器 |
|||
}, |
|||
onHeightClick = { |
|||
// 这里可以打开身高选择器 |
|||
} |
|||
) |
|||
} |
|||
|
|||
GuideStep.WEARING_FINGER -> { |
|||
WearingFingerScreen( |
|||
onNextClick = { |
|||
currentStep = GuideStep.DOMINANT_HAND |
|||
}, |
|||
onFingerSelected = { position -> |
|||
// 保存选择的佩戴位置 |
|||
} |
|||
) |
|||
} |
|||
|
|||
GuideStep.DOMINANT_HAND -> { |
|||
DominantHandScreen( |
|||
onNextClick = { |
|||
// 完成所有引导步骤 |
|||
onGuideComplete() |
|||
}, |
|||
onHandSelected = { hand -> |
|||
// 保存选择的惯用手 |
|||
} |
|||
) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,107 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.lazy.LazyColumn |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.unit.dp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun AllGuideScreensPreview() { |
|||
LazyColumn( |
|||
modifier = Modifier.fillMaxSize(), |
|||
verticalArrangement = Arrangement.spacedBy(16.dp), |
|||
contentPadding = PaddingValues(16.dp) |
|||
) { |
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
WelcomeScreen() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
RegisterScreen() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
ConnectionGuideScreen() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
SearchTip() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
DeviceScreen{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
PersonalInfoScreen() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
WearingFingerScreen() |
|||
} |
|||
} |
|||
|
|||
item { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(600.dp) |
|||
) { |
|||
DominantHandScreen() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun GuideNavigationPreview() { |
|||
GuideNavigationScreen() |
|||
} |
@ -0,0 +1,174 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.clickable |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
data class PersonalInfo( |
|||
val gender: String = "", |
|||
val birthday: String = "", |
|||
val height: String = "" |
|||
) |
|||
|
|||
@Composable |
|||
fun PersonalInfoScreen( |
|||
onNextClick: () -> Unit = {}, |
|||
onGenderClick: () -> Unit = {}, |
|||
onBirthdayClick: () -> Unit = {}, |
|||
onHeightClick: () -> Unit = {} |
|||
) { |
|||
var personalInfo by remember { mutableStateOf(PersonalInfo()) } |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp) |
|||
) { |
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 标题 |
|||
Text( |
|||
text = "个人信息完善", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 副标题 |
|||
Text( |
|||
text = "完善您的个人信息以便更好的服务", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(60.dp)) |
|||
|
|||
// 性别选项 |
|||
PersonalInfoItem( |
|||
title = "性别", |
|||
value = personalInfo.gender.ifEmpty { "" }, |
|||
onClick = onGenderClick |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 生日选项 |
|||
PersonalInfoItem( |
|||
title = "生日", |
|||
value = personalInfo.birthday.ifEmpty { "" }, |
|||
onClick = onBirthdayClick |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 身高选项 |
|||
PersonalInfoItem( |
|||
title = "身高", |
|||
value = personalInfo.height.ifEmpty { "" }, |
|||
onClick = onHeightClick |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 下一步按钮 |
|||
Button( |
|||
onClick = onNextClick, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = Color(0xFF007AFF), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "下一步", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun PersonalInfoItem( |
|||
title: String, |
|||
value: String, |
|||
onClick: () -> Unit |
|||
) { |
|||
Card( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.clickable { onClick() }, |
|||
shape = RoundedCornerShape(16.dp), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = Color.White |
|||
), |
|||
elevation = CardDefaults.cardElevation( |
|||
defaultElevation = 2.dp |
|||
) |
|||
) { |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 20.dp, vertical = 20.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Text( |
|||
text = title, |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
modifier = Modifier.weight(1f) |
|||
) |
|||
|
|||
if (value.isNotEmpty()) { |
|||
Text( |
|||
text = value, |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
modifier = Modifier.padding(end = 8.dp) |
|||
) |
|||
} |
|||
|
|||
// Icon( |
|||
// imageVector = Icons.Default.KeyboardArrowRight, |
|||
// contentDescription = "展开", |
|||
// tint = Color(0xFF999999), |
|||
// modifier = Modifier.size(24.dp) |
|||
// ) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun PersonalInfoScreenPreview() { |
|||
PersonalInfoScreen() |
|||
} |
@ -0,0 +1,214 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.foundation.text.KeyboardOptions |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.input.KeyboardType |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun RegisterScreen( |
|||
onLoginClick: (phoneNumber: String, verificationCode: String) -> Unit = { _, _ -> } |
|||
) { |
|||
var phoneNumber by remember { mutableStateOf("") } |
|||
var verificationCode by remember { mutableStateOf("") } |
|||
var isCodeSent by remember { mutableStateOf(false) } |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(120.dp)) |
|||
|
|||
// 戒指图片占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(160.dp) |
|||
.background( |
|||
Color(0xFFE5E5E5), |
|||
RoundedCornerShape(80.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 64.sp |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
|
|||
// 欢迎标题 |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Text( |
|||
text = "Hi,", |
|||
fontSize = 28.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
Text( |
|||
text = "欢迎来到Acti", |
|||
fontSize = 28.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(60.dp)) |
|||
|
|||
// 手机号输入框 |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
Text( |
|||
text = "手机号", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
modifier = Modifier.padding(bottom = 8.dp) |
|||
) |
|||
|
|||
OutlinedTextField( |
|||
value = phoneNumber, |
|||
onValueChange = { phoneNumber = it }, |
|||
placeholder = { |
|||
Text( |
|||
text = "请输入您的手机号", |
|||
color = Color(0xFF999999) |
|||
) |
|||
}, |
|||
modifier = Modifier.fillMaxWidth(), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedBorderColor = Color(0xFF007AFF), |
|||
unfocusedBorderColor = Color(0xFFE5E5E5), |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
), |
|||
singleLine = true |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 验证码输入框 |
|||
Column( |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
Text( |
|||
text = "验证码", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
modifier = Modifier.padding(bottom = 8.dp) |
|||
) |
|||
|
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
OutlinedTextField( |
|||
value = verificationCode, |
|||
onValueChange = { verificationCode = it }, |
|||
placeholder = { |
|||
Text( |
|||
text = "请输入验证码", |
|||
color = Color(0xFF999999) |
|||
) |
|||
}, |
|||
modifier = Modifier.weight(1f), |
|||
shape = RoundedCornerShape(12.dp), |
|||
colors = OutlinedTextFieldDefaults.colors( |
|||
focusedBorderColor = Color(0xFF007AFF), |
|||
unfocusedBorderColor = Color(0xFFE5E5E5), |
|||
focusedContainerColor = Color.White, |
|||
unfocusedContainerColor = Color.White |
|||
), |
|||
singleLine = true |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.width(12.dp)) |
|||
|
|||
TextButton( |
|||
onClick = { |
|||
if (phoneNumber.isNotEmpty()) { |
|||
isCodeSent = true |
|||
} |
|||
}, |
|||
enabled = phoneNumber.isNotEmpty() |
|||
) { |
|||
Text( |
|||
text = if (isCodeSent) "重新发送验证码" else "获取验证码", |
|||
fontSize = 14.sp, |
|||
color = if (phoneNumber.isNotEmpty()) Color(0xFF007AFF) else Color(0xFF999999) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 提示文字 |
|||
Text( |
|||
text = "未注册的手机号码会自动创建新账号", |
|||
fontSize = 12.sp, |
|||
color = Color(0xFF999999), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
|
|||
// 登录按钮 |
|||
Button( |
|||
onClick = { onLoginClick(phoneNumber, verificationCode) }, |
|||
enabled = phoneNumber.isNotEmpty() && verificationCode.isNotEmpty(), |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = if (phoneNumber.isNotEmpty() && verificationCode.isNotEmpty()) |
|||
Color(0xFF007AFF) else Color(0xFFCCCCCC), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "登录", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun RegisterScreenPreview() { |
|||
RegisterScreen() |
|||
} |
@ -0,0 +1,166 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.animation.core.* |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
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.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.text.style.TextDecoration |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun SearchTip( |
|||
onDeviceNotFoundClick: () -> Unit = {}, |
|||
onDeviceFound: () -> Unit = {} |
|||
) { |
|||
// 旋转动画 |
|||
val infiniteTransition = rememberInfiniteTransition() |
|||
val rotation by infiniteTransition.animateFloat( |
|||
initialValue = 0f, |
|||
targetValue = 360f, |
|||
animationSpec = infiniteRepeatable( |
|||
animation = tween(2000, easing = LinearEasing), |
|||
repeatMode = RepeatMode.Restart |
|||
) |
|||
) |
|||
|
|||
// 模拟搜索过程,5秒后跳转到设备列表 |
|||
LaunchedEffect(Unit) { |
|||
kotlinx.coroutines.delay(5000) |
|||
onDeviceFound() |
|||
} |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(120.dp)) |
|||
|
|||
// 标题 |
|||
Text( |
|||
text = "正在搜索设备...", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 搜索动画 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(200.dp) |
|||
.background( |
|||
Color.White, |
|||
RoundedCornerShape(100.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
// 外圆环 - 旋转动画 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(160.dp) |
|||
.rotate(rotation) |
|||
.background( |
|||
Color.Transparent |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
// 虚线圆环效果 |
|||
repeat(8) { index -> |
|||
Box( |
|||
modifier = Modifier |
|||
.size(8.dp) |
|||
.background( |
|||
Color(0xFF007AFF), |
|||
RoundedCornerShape(4.dp) |
|||
) |
|||
.offset( |
|||
x = (70 * kotlin.math.cos(index * 45.0 * kotlin.math.PI / 180)).dp, |
|||
y = (70 * kotlin.math.sin(index * 45.0 * kotlin.math.PI / 180)).dp |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 中心搜索图标 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(80.dp) |
|||
.background( |
|||
Color(0xFF007AFF), |
|||
RoundedCornerShape(40.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "🔍", |
|||
fontSize = 32.sp, |
|||
color = Color.White |
|||
) |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(60.dp)) |
|||
|
|||
// 进度指示器 |
|||
LinearProgressIndicator( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(4.dp), |
|||
color = Color(0xFF007AFF), |
|||
trackColor = Color(0xFFE5E5E5) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 提示文字 |
|||
Text( |
|||
text = "请确保戒指在充电状态并且靠近手机", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 找不到设备链接 |
|||
TextButton( |
|||
onClick = onDeviceNotFoundClick, |
|||
modifier = Modifier.padding(bottom = 40.dp) |
|||
) { |
|||
Text( |
|||
text = "找不到设备?", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF007AFF), |
|||
textDecoration = TextDecoration.Underline |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun SearchingScreenPreview() { |
|||
SearchTip() |
|||
} |
@ -0,0 +1,236 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.border |
|||
import androidx.compose.foundation.clickable |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
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.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
enum class Hand { |
|||
LEFT, RIGHT |
|||
} |
|||
|
|||
enum class Finger { |
|||
THUMB, INDEX, MIDDLE, RING, PINKY |
|||
} |
|||
|
|||
data class WearingPosition( |
|||
val hand: Hand, |
|||
val finger: Finger |
|||
) |
|||
|
|||
@Composable |
|||
fun WearingFingerScreen( |
|||
onNextClick: () -> Unit = {}, |
|||
onFingerSelected: (WearingPosition) -> Unit = {} |
|||
) { |
|||
var selectedPosition by remember { mutableStateOf<WearingPosition?>(null) } |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 标题 |
|||
Text( |
|||
text = "佩戴手指", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 副标题 |
|||
Text( |
|||
text = "请选择您佩戴Acti戒指的手指", |
|||
fontSize = 16.sp, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center, |
|||
modifier = Modifier.fillMaxWidth() |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(80.dp)) |
|||
|
|||
// 双手选择区域 |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceEvenly |
|||
) { |
|||
// 左手 |
|||
HandSelector( |
|||
hand = Hand.LEFT, |
|||
selectedPosition = selectedPosition, |
|||
onFingerClick = { finger -> |
|||
val position = WearingPosition(Hand.LEFT, finger) |
|||
selectedPosition = position |
|||
onFingerSelected(position) |
|||
} |
|||
) |
|||
|
|||
// 右手 |
|||
HandSelector( |
|||
hand = Hand.RIGHT, |
|||
selectedPosition = selectedPosition, |
|||
onFingerClick = { finger -> |
|||
val position = WearingPosition(Hand.RIGHT, finger) |
|||
selectedPosition = position |
|||
onFingerSelected(position) |
|||
} |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 下一步按钮 |
|||
Button( |
|||
onClick = onNextClick, |
|||
enabled = selectedPosition != null, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = if (selectedPosition != null) Color(0xFF007AFF) else Color(0xFFCCCCCC), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "下一步", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun HandSelector( |
|||
hand: Hand, |
|||
selectedPosition: WearingPosition?, |
|||
onFingerClick: (Finger) -> Unit |
|||
) { |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
// 手部图像占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(140.dp, 180.dp) |
|||
.background( |
|||
Color.White, |
|||
RoundedCornerShape(20.dp) |
|||
) |
|||
.border( |
|||
width = 2.dp, |
|||
color = Color(0xFFE5E5E5), |
|||
shape = RoundedCornerShape(20.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
// 简化的手部表示 |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
verticalArrangement = Arrangement.Center |
|||
) { |
|||
// 拇指 |
|||
FingerButton( |
|||
finger = Finger.THUMB, |
|||
isSelected = selectedPosition?.hand == hand && selectedPosition.finger == Finger.THUMB, |
|||
onClick = { onFingerClick(Finger.THUMB) }, |
|||
modifier = Modifier.offset(x = if (hand == Hand.LEFT) 20.dp else (-20).dp) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(8.dp)) |
|||
|
|||
// 其他四个手指 |
|||
Row( |
|||
horizontalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
listOf(Finger.INDEX, Finger.MIDDLE, Finger.RING, Finger.PINKY).forEach { finger -> |
|||
FingerButton( |
|||
finger = finger, |
|||
isSelected = selectedPosition?.hand == hand && selectedPosition.finger == finger, |
|||
onClick = { onFingerClick(finger) } |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 显示戒指图标在选中的手指上 |
|||
if (selectedPosition?.hand == hand) { |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 16.sp, |
|||
modifier = Modifier.padding(top = 4.dp) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 标签 |
|||
Text( |
|||
text = if (hand == Hand.LEFT) "左手" else "右手", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium, |
|||
color = Color(0xFF333333) |
|||
) |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
private fun FingerButton( |
|||
finger: Finger, |
|||
isSelected: Boolean, |
|||
onClick: () -> Unit, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Box( |
|||
modifier = modifier |
|||
.size(20.dp, 40.dp) |
|||
.clip(RoundedCornerShape(10.dp)) |
|||
.background( |
|||
if (isSelected) Color(0xFF007AFF) else Color(0xFFE5E5E5) |
|||
) |
|||
.clickable { onClick() }, |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
if (isSelected) { |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 12.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun WearingFingerScreenPreview() { |
|||
WearingFingerScreen() |
|||
} |
@ -0,0 +1,161 @@ |
|||
package com.whitefish.ring.ui.guide |
|||
|
|||
import androidx.compose.foundation.Image |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import org.jetbrains.compose.ui.tooling.preview.Preview |
|||
|
|||
@Composable |
|||
fun WelcomeScreen( |
|||
onStartClick: () -> Unit = {} |
|||
) { |
|||
var isChecked by remember { mutableStateOf(false) } |
|||
|
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.background(Color(0xFFF5F5F5)) |
|||
) { |
|||
Column( |
|||
modifier = Modifier |
|||
.fillMaxSize() |
|||
.padding(horizontal = 24.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Spacer(modifier = Modifier.height(120.dp)) |
|||
|
|||
// 主标题 |
|||
Text( |
|||
text = "Acti", |
|||
fontSize = 48.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color(0xFF333333), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 副标题 |
|||
Text( |
|||
text = "赋能每一个动作", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Normal, |
|||
color = Color(0xFF666666), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
// 英文副标题 |
|||
Text( |
|||
text = "Empower Every Move", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Normal, |
|||
color = Color(0xFF999999), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 戒指图片占位符 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(200.dp) |
|||
.background( |
|||
Color(0xFFE5E5E5), |
|||
RoundedCornerShape(16.dp) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "💍", |
|||
fontSize = 80.sp |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.weight(1f)) |
|||
|
|||
// 协议同意checkbox |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 16.dp), |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Checkbox( |
|||
checked = isChecked, |
|||
onCheckedChange = { isChecked = it }, |
|||
colors = CheckboxDefaults.colors( |
|||
checkedColor = Color(0xFF007AFF) |
|||
) |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
|
|||
Text( |
|||
text = "我已阅读并同意", |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF666666) |
|||
) |
|||
|
|||
Text( |
|||
text = "《用户协议》", |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF007AFF) |
|||
) |
|||
|
|||
Text( |
|||
text = "和", |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF666666) |
|||
) |
|||
|
|||
Text( |
|||
text = "《隐私政策》", |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF007AFF) |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 立即使用按钮 |
|||
Button( |
|||
onClick = onStartClick, |
|||
enabled = isChecked, |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(56.dp), |
|||
shape = RoundedCornerShape(28.dp), |
|||
colors = ButtonDefaults.buttonColors( |
|||
containerColor = if (isChecked) Color(0xFF007AFF) else Color(0xFFCCCCCC), |
|||
contentColor = Color.White |
|||
) |
|||
) { |
|||
Text( |
|||
text = "立即使用", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(40.dp)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Composable |
|||
@Preview |
|||
fun WelcomeScreenPreview() { |
|||
WelcomeScreen() |
|||
} |
@ -0,0 +1,189 @@ |
|||
package com.whitefish.ring |
|||
|
|||
import androidx.compose.ui.util.fastFirstOrNull |
|||
import com.whitefish.ring.bean.ui.Device |
|||
import com.whitefish.ring.device.IDeviceManager |
|||
import com.whitefish.ring.objc.CMD_EXECTE_ERROR_REASON |
|||
import com.whitefish.ring.objc.DeviceCenter |
|||
import com.whitefish.ring.objc.EXCUTED_CMD |
|||
import com.whitefish.ring.objc.FUNCTION_ERROR |
|||
import com.whitefish.ring.objc.LTSRingSDK |
|||
import com.whitefish.ring.objc.OusideBleDiscovery |
|||
import com.whitefish.ring.objc.SRBLeService |
|||
import com.whitefish.ring.objc.SRBleDataProtocalProtocol |
|||
import com.whitefish.ring.objc.SRBleScanProtocalProtocol |
|||
import com.whitefish.ring.objc.SRDeviceInfo |
|||
import io.github.aakira.napier.Napier |
|||
import kotlinx.cinterop.ExperimentalForeignApi |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.IO |
|||
import kotlinx.coroutines.launch |
|||
import platform.CoreBluetooth.CBManagerState |
|||
import platform.Foundation.NSNumber |
|||
import platform.darwin.NSInteger |
|||
import platform.darwin.NSObject |
|||
import platform.darwin.NSUInteger |
|||
|
|||
@OptIn(ExperimentalForeignApi::class) |
|||
class DeviceManager: IDeviceManager() { |
|||
private val manager = DeviceCenter.instance() |
|||
|
|||
private var iosBleList = arrayListOf<SRBLeService>() |
|||
private val scope = CoroutineScope(Dispatchers.IO) |
|||
|
|||
// 将delegate对象存储为强引用的成员变量,避免被垃圾回收 |
|||
private val scanDelegate = object : NSObject(), SRBleScanProtocalProtocol { |
|||
override fun srBleDidConnectPeripheral(service: SRBLeService) { |
|||
Napier.i { "srBleDidConnectPeripheral" } |
|||
} |
|||
|
|||
override fun srBleDidDisconnectPeripheral(service: SRBLeService) { |
|||
Napier.i { "srBleDidDisconnectPeripheral" } |
|||
} |
|||
|
|||
override fun srBlePowerStateChange(state: CBManagerState) { |
|||
Napier.i { "srBlePowerStateChange:${state}" } |
|||
if (state.toInt() == 5){ |
|||
scope.launch { |
|||
blePowerState.emit(true) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun srScanDeviceDidRefresh(perphelArray: List<*>) { |
|||
Napier.i { "srScanDeviceDidRefresh:${perphelArray}" } |
|||
iosBleList.clear() |
|||
val deviceList = perphelArray.map { |
|||
val device = it as SRBLeService |
|||
iosBleList.add(device) |
|||
Device(device.advDataLocalName.toString(),device.macAddress.toString()) |
|||
} |
|||
_deviceList.value = deviceList |
|||
} |
|||
} |
|||
|
|||
private val dataDelegate = object : NSObject(), SRBleDataProtocalProtocol { |
|||
override fun srBleDeviceDidReadyForReadAndWrite(service: SRBLeService) { |
|||
Napier.i { "srBleDeviceDidReadyForReadAndWrite" } |
|||
} |
|||
|
|||
override fun srBleRealtimeSpo(spo: NSNumber) { |
|||
Napier.i { "srBleRealtimeSpo" } |
|||
} |
|||
|
|||
override fun srBleRealtimeHeartRate(hr: NSNumber) { |
|||
} |
|||
|
|||
override fun srBleRealtimeHrv(hrv: NSNumber) { |
|||
Napier.i { "srBleRealtimeHrv" } |
|||
} |
|||
|
|||
override fun srBleDeviceBatteryLevel( |
|||
batteryLevel: NSUInteger, |
|||
IsCharging: Boolean |
|||
) { |
|||
Napier.i { "srBleDeviceBatteryLevel" } |
|||
} |
|||
|
|||
override fun srBleSN(sn: String) { |
|||
Napier.i { "srBleSN:${sn}" } |
|||
} |
|||
|
|||
override fun srBleDeviceInfo(devInfo: SRDeviceInfo) { |
|||
Napier.i { "srBleDeviceInfo:${devInfo}" } |
|||
} |
|||
|
|||
override fun srBleHistorySr03DataWithCurrentCount( |
|||
currentCount: NSInteger, |
|||
IsComplete: Boolean |
|||
) { |
|||
Napier.i { "srBleHistorySr03DataWithCurrentCount:${IsComplete}" } |
|||
} |
|||
|
|||
override fun srBleDeviceRealtimeSteps(steps: NSNumber) { |
|||
Napier.i { "srBleDeviceRealtimeSteps:${steps}" } |
|||
} |
|||
|
|||
override fun srBleDeviceRealtimeTemperature(temperature: NSNumber) { |
|||
Napier.i { "srBleDeviceRealtimeTemperature:${temperature}" } |
|||
} |
|||
|
|||
override fun srBleCmdExcute( |
|||
cmd: EXCUTED_CMD, |
|||
Succ: Boolean |
|||
) { |
|||
|
|||
} |
|||
|
|||
override fun srBleCmdExcute( |
|||
cmd: EXCUTED_CMD, |
|||
Succ: Boolean, |
|||
Reason: CMD_EXECTE_ERROR_REASON |
|||
) { |
|||
} |
|||
|
|||
override fun srBleHistoryDataCount(count: NSInteger) { |
|||
} |
|||
|
|||
override fun srBleHistoryDataProgress(percent: Float, IsComplete: Boolean) { |
|||
} |
|||
|
|||
override fun srBleHistoryDataTimeout() { |
|||
} |
|||
|
|||
override fun srBleIsbinded(isBinded: Boolean) { |
|||
} |
|||
|
|||
override fun srBleOEMAuthResult(authSucceddful: Boolean) { |
|||
bleReadyStateFlow.value = true |
|||
Napier.i { "srBleOEMAuthResult" } |
|||
} |
|||
|
|||
override fun srBleFunctionErrorCallBack( |
|||
error: FUNCTION_ERROR, |
|||
MehthodName: String |
|||
) { |
|||
} |
|||
} |
|||
|
|||
init { |
|||
initializeManager() |
|||
} |
|||
|
|||
private fun initializeManager() { |
|||
Napier.i { "DeviceManager initializing..." } |
|||
manager.registWithisCustomBleManage(true) |
|||
// 使用成员变量而不是匿名对象 |
|||
manager.appScanDelegate = scanDelegate |
|||
manager.appDataDelegate = dataDelegate |
|||
Napier.i { "DeviceManager delegates set: scan=${scanDelegate}, data=${dataDelegate}" } |
|||
} |
|||
|
|||
// 添加重新初始化方法,在需要时可以调用 |
|||
fun reinitialize() { |
|||
Napier.i { "DeviceManager reinitializing..." } |
|||
initializeManager() |
|||
} |
|||
|
|||
override fun startScan() { |
|||
Napier.i { "Starting scan, delegate: ${manager.appScanDelegate}" } |
|||
manager.startBleScan() |
|||
} |
|||
|
|||
override fun stopScan() { |
|||
manager.stopBleScan() |
|||
} |
|||
|
|||
override fun connect(mac: String) { |
|||
iosBleList.fastFirstOrNull { it.macAddress == mac }?.let { |
|||
manager.connectDevice(it) |
|||
Napier.i { "connect device:${it}" } |
|||
} |
|||
} |
|||
|
|||
override fun bind() { |
|||
Napier.i { "bind device:${manager.currentDevice()}" } |
|||
manager.bindCurrentDevice() |
|||
} |
|||
} |
@ -0,0 +1,225 @@ |
|||
<resources> |
|||
<string name="app_name">RingApp</string> |
|||
|
|||
<string name="launcher_title_text">Acti</string> |
|||
<string name="launcher_description_text">赋能每一个动作\nEmpower Every Move</string> |
|||
<string name="launcher_button_text">立即使用</string> |
|||
<string name="launcher_agreement_text">我已阅读井同意《用户隐私协议》和《用户注册协议》</string> |
|||
<string name="login_description">Hi,\n欢迎来到Acti</string> |
|||
<string name="login_phone_number">手机号</string> |
|||
<string name="login_phone_hint">请输入您的手机号</string> |
|||
<string name="login_code">验证码</string> |
|||
<string name="login_code_hint">请输入验证码</string> |
|||
<string name="login_unregister">未注册的手机号验证后自动注册登录</string> |
|||
<string name="login_button_text">登录</string> |
|||
<string name="connect_title">连接您的Acti戒指</string> |
|||
<string name="connect_description">将您的戒指连接到充电器,并继续下一步。请确保您的手机已启用蓝牙功能。</string> |
|||
<string name="connect_button">下一步</string> |
|||
<string name="devices_scan">正在搜索设备...</string> |
|||
<string name="devices_list">附近设备</string> |
|||
<string name="devices_can_not_find">找不到设备?</string> |
|||
<string name="devices_connect_fail">连接失败?</string> |
|||
|
|||
<string name="first_fragment_label">First Fragment</string> |
|||
<string name="second_fragment_label">Second Fragment</string> |
|||
<string name="next">Next</string> |
|||
<string name="previous">Previous</string> |
|||
<string name="heart_rate_value">%d 次/分</string> |
|||
<string name="label_no_data">无数据</string> |
|||
|
|||
<string name="dialog_msg_turn_on_location_service">为了可以扫描蓝牙设备,现在还需要开启位置服务。</string> |
|||
<string name="tip_ring_not_connected">您未连接戒指</string> |
|||
<string name="dialog_title_tip">提示</string> |
|||
|
|||
<string name="date_format">yyyy年MM月dd日</string> |
|||
<string name="date_format_ym">yyyy年MM月</string> |
|||
<string name="date_format_md">MM月dd日</string> |
|||
<string name="date_format_ymd">yyyy年MM月dd日</string> |
|||
<string name="label_today">今天</string> |
|||
<string name="label_yesterday">昨天</string> |
|||
|
|||
<!-- ************************************************************************************** --> |
|||
<string name="label_rhr">静息心率</string> |
|||
<string name="label_step">步数</string> |
|||
<string name="label_sleep_hr_dip">心率沉浸</string> |
|||
<string name="label_sleep_br">呼吸速率</string> |
|||
<string name="label_sleep_spo2">血氧饱和度</string> |
|||
<string name="label_text_sleep_duration">睡眠%s %s效率</string> |
|||
<string name="desc_sleep_state_wake">苏醒/被打扰时间 %s</string> |
|||
<string name="desc_sleep_state_rem">REM睡眠 %s</string> |
|||
<string name="desc_sleep_state_light">浅度睡眠 %s</string> |
|||
<string name="desc_sleep_state_deep">深度睡眠 %s</string> |
|||
<string name="desc_sleep_state_nap">零星小睡 %s</string> |
|||
<string name="time_format_hh_mm">%d小时%d分钟</string> |
|||
<string name="time_format_hh">%d小时</string> |
|||
<string name="time_format_mm">%d分钟</string> |
|||
<string name="label_sleep_session">睡眠分析</string> |
|||
<string name="label_sleep_session_num">睡眠分析%d</string> |
|||
<!-- ************************************************************************************** --> |
|||
<string name="tip_text_sleep_duration">睡眠%s %s效率</string> |
|||
<string name="tip_text_nap_duration">睡眠%s</string> |
|||
<string name="label_sleep_details">睡眠详情</string> |
|||
<string name="session_tab_sleep">睡眠</string> |
|||
<string name="session_tab_nap">零星小睡</string> |
|||
|
|||
<string name="title_available_devices">可用设备</string> |
|||
<string name="note_for_scan_devices">注意:未绑定的Ring在充电时才会广播,请充电,以便APP可以扫描到。</string> |
|||
<string name="tip_connecting">连接中…</string> |
|||
<string name="tip_wait_for_device_disconnected">请等待设备连接断开</string> |
|||
<string name="dialog_title_restricted_mode">受限模式</string> |
|||
<string name="dialog_msg_restricted_mode">已启用受限模式,只能恢复出厂设置和自检。</string> |
|||
|
|||
|
|||
|
|||
<string name="activity_title_others">其他操作</string> |
|||
<string name="btn_label_skin_temp">手指温度</string> |
|||
<string name="btn_label_factory_test">工厂测试</string> |
|||
<string name="btn_label_reboot">重启</string> |
|||
<string name="btn_label_reset">恢复出厂设置</string> |
|||
<string name="btn_label_unbind">解綁</string> |
|||
<string name="btn_label_shutdown">关机</string> |
|||
<string name="btn_label_device_info">设备信息</string> |
|||
<string name="btn_label_device_sn">设备SN</string> |
|||
<string name="btn_label_take_ppg_readings">获取PPG读数(血氧、心率)</string> |
|||
<string name="btn_label_take_ppg_readings_tip">仅心率</string> |
|||
<string name="btn_label_set_ppg_params">设置PPG参数</string> |
|||
<string name="et_hint_bo_measurement_interval">SpO₂测量间隔 (0, 5~360, 分钟)</string> |
|||
<string name="btn_label_spo_measure_interval">SpO₂测量间隔</string> |
|||
<string name="btn_label_ecg">心电图</string> |
|||
<string name="label_set_health_measurement_duration">设置心率&体温测量时长</string> |
|||
<string name="et_hint_set_health_measurement_duration">测量时长(10 ~ 180,秒)</string> |
|||
<string name="btn_commit">提交</string> |
|||
<string name="tip_for_empty_measurement_duration">请输入测量时长!</string> |
|||
<string name="label_off">关</string> |
|||
<string name="label_on">开</string> |
|||
<string name="set_to_dev_error">戒指断开连接,设置参数无法发送。</string> |
|||
|
|||
|
|||
<!-- ************************************************************************************** --> |
|||
<string name="btn_select_firmware_file">选择</string> |
|||
<string name="btn_query">查询</string> |
|||
<string name="label_select_fw_from_local">通过文件管理器APP从本地选择固件文件</string> |
|||
<string name="label_query_new_fw_from_server">从服务器查询并下载新的固件到本地</string> |
|||
<string name="tip_cannot_support_query_new_fw_from_server">您的设备暂未支持从服务器查询新的固件,缺少关键参数【%s】</string> |
|||
<string name="btn_upgrade">升级</string> |
|||
<string name="dfu_alert_no_file_browser_message">在您的设备上未找到文件浏览器应用程序。你想下载一个吗?</string> |
|||
<string name="ota_charging_mode_tip_for_wireless">该戒指为无线充电模式,请选择SR09W固件更新。</string> |
|||
<string name="ota_charging_mode_tip_for_nfc">该戒指为NFC充电模式,请选择SR09N固件更新。</string> |
|||
<string name="text_fw_already_the_last_ver">您的设备固件已经是最新版本:v%s.</string> |
|||
<string name="text_found_last_fw">查询到最新的固件版本:v%s,下载中…</string> |
|||
<string name="text_md5_matched">\nMD5匹配,正在保存文件到本地…</string> |
|||
<string name="text_md5_not_match">\nMD5不匹配!请检查。</string> |
|||
<string name="text_file_storage_path">\n文件存储路径:%s</string> |
|||
<string name="text_file_storage_error">\n文件存储出错:%s</string> |
|||
<string name="tip_empty_content_found">查询到空内容!</string> |
|||
<string name="tip_request_failed">请求失败,code = %d。</string> |
|||
<string name="btn_select_this_fw_file">选择此固件</string> |
|||
|
|||
<string name="dev_size">尺寸%d</string> |
|||
<string name="ring_color_deep_black">深黑色</string> |
|||
<string name="ring_color_sliver">银色</string> |
|||
<string name="ring_color_golden">金色</string> |
|||
<string name="ring_color_rose_gold">玫瑰金</string> |
|||
<string name="ring_color_gold_silver_mixed">金/银混色</string> |
|||
<string name="ring_color_purple_silver_mixed">紫/银混色</string> |
|||
<string name="ring_color_rose_gold_silver_mixed">玫瑰金/银混色</string> |
|||
<string name="battery_level_charging">充电中</string> |
|||
<string name="battery_level_discharging">放电中</string> |
|||
|
|||
<string name="dialog_title_oem_certification_failed">OEM验证失败</string> |
|||
<string name="dialog_msg_oem_certify_fail_cause_by_sn_null">设备序列号空,连接认证失败。</string> |
|||
<string name="dialog_msg_oem_certify_fail_cause_by_r1_to_r2">R1解密并生成R2失败。</string> |
|||
<string name="dialog_msg_oem_certify_fail_cause_by_certificate_r2">连接失败,请使用凌拓NexRing智能戒指。</string> |
|||
<string name="btn_label_disconnected">断开连接</string> |
|||
|
|||
<string name="tip_your_ring_do_not_support_workout_mode">您的戒指型号不支持【锻炼模式】!</string> |
|||
<string name="title_workout">锻炼</string> |
|||
<string name="title_workout_history">锻炼记录</string> |
|||
<string name="workout_detail_duration">锻炼时间</string> |
|||
<string name="workout_detail_interval">时间间隔</string> |
|||
<string name="label_hr_avg">平均心率</string> |
|||
<string name="label_hr_max">最高心率</string> |
|||
<string name="label_hr_min">最低心率</string> |
|||
<string name="tip_synchronizing_data">同步数据中…</string> |
|||
<string name="tip_no_hr_data_available">没有可用的心率数据!</string> |
|||
<string name="label_choose_a_sport">选择一项锻炼</string> |
|||
<string name="workout_type_walking">步行</string> |
|||
<string name="workout_type_indoor_running">室内跑步</string> |
|||
<string name="workout_type_outdoor_running">室外跑步</string> |
|||
<string name="workout_type_indoor_cycling">室内骑车</string> |
|||
<string name="workout_type_outdoor_cycling">室外骑车</string> |
|||
<string name="workout_type_mountain_biking">山地自行车</string> |
|||
<string name="workout_type_swimming">游泳</string> |
|||
<string name="label_add_details">添加详情</string> |
|||
<string name="workout_mode_btn_start">开始</string> |
|||
<string name="label_workout_mode_finish">已结束</string> |
|||
<string name="label_workout_mode_in_progress">进行中</string> |
|||
<string name="btn_end_early">提早结束</string> |
|||
<string name="dialog_msg_end_workout_mode_early">是否提早结束锻炼?</string> |
|||
<string name="tip_ring_busy_for_charging">戒指充电中,请佩戴后操作</string> |
|||
<string name="text_sec">%d 秒</string> |
|||
<string name="text_min">%d 分钟</string> |
|||
<string name="text_hour">%d 小时</string> |
|||
|
|||
<string name="cmd_execute_success">命令执行成功。</string> |
|||
<string name="cmd_execute_failed_1">命令执行失败。</string> |
|||
<string name="cmd_execute_failed_2">>命令执行失败。戒指连接时OEM验证未通过。</string> |
|||
<string name="cmd_execute_failed_3">>命令执行失败。戒指正在主动测量。</string> |
|||
<string name="cmd_execute_failed_4">>命令执行失败。戒指处于锻炼模式。</string> |
|||
<string name="cmd_execute_failed_5">>命令执行失败。戒指正在执行APP发起的测量。</string> |
|||
<string name="cmd_execute_failed_6">>命令执行失败。参数错误。</string> |
|||
|
|||
<string name="label_use_built_in_algo">使用内置算法</string> |
|||
<string name="label_output_raw_data">输出原始波形</string> |
|||
<string name="label_sampling_rate">采样率</string> |
|||
|
|||
<string name="input_interval_value_error_hint">请输入有效值</string> |
|||
|
|||
|
|||
<!-- ECG --> |
|||
<string name="tip_your_ring_do_not_support_ecg">您的戒指型号不支持【心电图测量】!</string> |
|||
<string name="btn_start">开始</string> |
|||
<string name="btn_stop">停止</string> |
|||
<string name="ecg_settings">心电图设置</string> |
|||
<string name="paper_speed">时间基准</string> |
|||
<string name="paper_speed_value">时间基准:%1$s</string> |
|||
<string name="gain">增益</string> |
|||
<string name="gain_value">增益:%1$s</string> |
|||
<string name="action_generate_pdf">生成PDF文件</string> |
|||
<string name="label_countdown">%d 秒</string> |
|||
<string name="title_param_settings_in_dev">设备端参数设置</string> |
|||
<string name="pdf_ecg_record_time_label">"'记录时间:'yyyy年MM月dd日 HH:mm"</string> |
|||
<string name="pdf_ecg_page_footer_label">%s, %s,导联I,512赫,Linktop NexRing</string> |
|||
<string name="pdf_data_measure_time_format">"yyyy.MM.dd HH:mm"</string> |
|||
<string name="pdf_file_name_format">"'%s' yyyy-MM-dd HH_mm'.pdf'"</string> |
|||
<string name="msg_tip_generating_pdf_file">正在生成PDF文件…</string> |
|||
<string name="tip_pls_take_an_ecg_first">请先测量一次心电图。</string> |
|||
<string name="label_ecg_pga_gain">心电图PGA增益(单位:V/V)</string> |
|||
<string name="title_wear_habit">您的戒指现在佩戴于</string> |
|||
<string name="item_wear_habit_lf">左手指</string> |
|||
<string name="item_wear_habit_rf">右手指</string> |
|||
<string name="tip_ecg_lead_on">将另一只手的手指搭在戒指上,以便形成导联并完成心电测量。</string> |
|||
<string name="ecg_hr_value">心率 %s BPM</string> |
|||
<string name="ecg_avg_hr_value">平均心率 %s BPM</string> |
|||
<string name="ecg_arrhythmia_sinus_rhythm">窦性心律</string> |
|||
<string name="ecg_arrhythmia_afib">房颤</string> |
|||
<string name="ecg_arrhythmia_low_hr">低心率</string> |
|||
<string name="ecg_arrhythmia_high_hr">高心率</string> |
|||
<string name="ecg_arrhythmia_inconclusive">不确定</string> |
|||
<string name="ecg_arrhythmia_poor_recording">记录结果不佳</string> |
|||
<string name="ecg_arrhythmia_no_result">无结果</string> |
|||
|
|||
<string name="permission_required">需要权限</string> |
|||
<string name="permission_bluetooth_rationale">需要蓝牙权限来扫描和连接设备。请在设置中授予权限。</string> |
|||
<string name="permission_bluetooth_denied">没有蓝牙权限,应用可能无法正常工作。您可以在系统设置中手动开启权限。</string> |
|||
|
|||
<string name="dialog_title_oem_auth_failed">OEM 认证失败</string> |
|||
<string name="dialog_msg_oem_auth_failed_cause_by_sn_null">设备序列号空,OEM 认证失败。</string> |
|||
<string name="dialog_msg_oem_auth_failed_cause_by_r1_to_r2">R1 解密并生成 R2 失败。</string> |
|||
<string name="dialog_msg_oem_auth_failed_cause_by_check_r2"> |
|||
您的戒指已开启 OEM 认证,但本 APP 设定的 OEM 字符串似乎与您的戒指所写入的不匹配, |
|||
请检查 Demo 工程的 `res/values/arrays.xml` 的 `oem_array` 中的字符串元素是否覆盖正确? |
|||
修改后请重新编译工程生成新的 APK 文件,安装后重试。 |
|||
如果仍然失败,请联系我们的技术支持。 |
|||
</string> |
|||
</resources> |
Loading…
Reference in new issue