瀏覽代碼

Merge branch 'v1.1.0' of git.atmob.com:Atmob-Flutter/location2025 into v1.1.0

“HeShaoZe” 4 月之前
父節點
當前提交
7588b1380f
共有 42 個文件被更改,包括 4711 次插入71 次删除
  1. 0 0
      assets/anim/location_analyse_robot.mp4
  2. 2824 0
      assets/anim/location_label.json
  3. 4 0
      assets/string/base/string.xml
  4. 9 0
      lib/data/api/atmob_api.dart
  5. 75 0
      lib/data/api/atmob_api.g.dart
  6. 15 0
      lib/data/api/request/electric_request.dart
  7. 69 0
      lib/data/api/request/electric_request.g.dart
  8. 16 0
      lib/data/api/request/location_phone_event_request.dart
  9. 73 0
      lib/data/api/request/location_phone_event_request.g.dart
  10. 22 0
      lib/data/bean/phone_event_bean.dart
  11. 19 0
      lib/data/bean/phone_event_bean.g.dart
  12. 11 4
      lib/data/repositories/account_repository.dart
  13. 20 1
      lib/data/repositories/config_repository.dart
  14. 1 1
      lib/data/repositories/friends_repository.dart
  15. 160 0
      lib/data/repositories/phone_event_repository.dart
  16. 11 8
      lib/di/get_it.config.dart
  17. 5 0
      lib/dialog/account_replace_dialog.dart
  18. 1 1
      lib/module/analyse/location_analyse_controller.dart
  19. 9 0
      lib/module/analyse/location_analyse_page.dart
  20. 18 0
      lib/module/permission/permission_setting_controller.dart
  21. 4 0
      lib/module/permission/permission_setting_page.dart
  22. 7 4
      lib/module/track/track_day_detail/track_day_detail_controller.dart
  23. 14 11
      lib/resource/assets.gen.dart
  24. 6 0
      lib/resource/string.gen.dart
  25. 0 5
      lib/utils/common_expand.dart
  26. 20 4
      plugins/mobile_use_statistics/android/build.gradle
  27. 二進制
      plugins/mobile_use_statistics/android/gradle/wrapper/gradle-wrapper.jar
  28. 7 0
      plugins/mobile_use_statistics/android/gradle/wrapper/gradle-wrapper.properties
  29. 249 0
      plugins/mobile_use_statistics/android/gradlew
  30. 92 0
      plugins/mobile_use_statistics/android/gradlew.bat
  31. 10 1
      plugins/mobile_use_statistics/android/src/main/AndroidManifest.xml
  32. 70 25
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java
  33. 123 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/AppUsageInfo.java
  34. 118 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/DeviceUsageInfo.java
  35. 118 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/FlutterUsageHelper.java
  36. 208 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/UsageStats.java
  37. 97 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/AppUsageUtil.java
  38. 88 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/LogUtil.java
  39. 100 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/ParamUtil.java
  40. 3 3
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics.dart
  41. 4 2
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics_method_channel.dart
  42. 11 1
      pubspec.yaml

assets/video/location_analyse_robot.mp4 → assets/anim/location_analyse_robot.mp4


文件差異過大導致無法顯示
+ 2824 - 0
assets/anim/location_label.json


+ 4 - 0
assets/string/base/string.xml

@@ -348,4 +348,8 @@
 
     <string name="main_today_track_loading">正在加载中...</string>
     <string name="main_today_track_normal_point">暂无异常</string>
+    <string name="permission_phone_screen_setting">手机屏幕使用时长</string>
+    <string name="permission_phone_screen_setting_subtitle">
+        获取权限,保证您的信息定位更精准,预测更加准确。
+    </string>
 </resources>

+ 9 - 0
lib/data/api/atmob_api.dart

@@ -4,8 +4,10 @@ import 'package:location/base/base_response.dart';
 import 'package:location/data/api/request/add_friend_request.dart';
 import 'package:location/data/api/request/configs_request.dart';
 import 'package:location/data/api/request/contact_request.dart';
+import 'package:location/data/api/request/electric_request.dart';
 import 'package:location/data/api/request/friends_list_request.dart';
 import 'package:location/data/api/request/friends_operation_request.dart';
+import 'package:location/data/api/request/location_phone_event_request.dart';
 import 'package:location/data/api/request/login_request.dart';
 import 'package:location/data/api/request/member_list_request.dart';
 import 'package:location/data/api/request/message_request.dart';
@@ -236,4 +238,11 @@ abstract class AtmobApi {
   Future<BaseResponse<TrackDailyActionResponse>> trackDailyAction(
       @Body() TrackDailyActionRequest request,
       @DioOptions() RequestOptions options);
+
+  @POST("/s/v1/location/phone/event")
+  Future<BaseResponse> locationPhoneEvent(
+      @Body() LocationPhoneEventRequest request);
+
+  @POST("/s/v1/user/electric/report")
+  Future<BaseResponse> electricReport(@Body() ElectricRequest request);
 }

+ 75 - 0
lib/data/api/atmob_api.g.dart

@@ -1778,6 +1778,81 @@ class _AtmobApi implements AtmobApi {
     return _value;
   }
 
+  @override
+  Future<BaseResponse<dynamic>> locationPhoneEvent(
+      LocationPhoneEventRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<dynamic>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/location/phone/event',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<dynamic> _value;
+    try {
+      _value = BaseResponse<dynamic>.fromJson(
+        _result.data!,
+        (json) => json as dynamic,
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<dynamic>> electricReport(ElectricRequest request) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<dynamic>>(Options(
+      method: 'POST',
+      headers: _headers,
+      extra: _extra,
+    )
+        .compose(
+          _dio.options,
+          '/s/v1/user/electric/report',
+          queryParameters: queryParameters,
+          data: _data,
+        )
+        .copyWith(
+            baseUrl: _combineBaseUrls(
+          _dio.options.baseUrl,
+          baseUrl,
+        )));
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<dynamic> _value;
+    try {
+      _value = BaseResponse<dynamic>.fromJson(
+        _result.data!,
+        (json) => json as dynamic,
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
   RequestOptions newRequestOptions(Object? options) {
     if (options is RequestOptions) {
       return options as RequestOptions;

+ 15 - 0
lib/data/api/request/electric_request.dart

@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+
+part 'electric_request.g.dart';
+
+@JsonSerializable()
+class ElectricRequest extends AppBaseRequest {
+  @JsonKey(name: 'electric')
+  int electric;
+
+  ElectricRequest(this.electric);
+
+  @override
+  Map<String, dynamic> toJson() => _$ElectricRequestToJson(this);
+}

+ 69 - 0
lib/data/api/request/electric_request.g.dart

@@ -0,0 +1,69 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'electric_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ElectricRequest _$ElectricRequestFromJson(Map<String, dynamic> json) =>
+    ElectricRequest(
+      (json['electric'] as num).toInt(),
+    )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$ElectricRequestToJson(ElectricRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'electric': instance.electric,
+    };

+ 16 - 0
lib/data/api/request/location_phone_event_request.dart

@@ -0,0 +1,16 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:location/base/app_base_request.dart';
+import 'package:location/data/bean/phone_event_bean.dart';
+
+part 'location_phone_event_request.g.dart';
+
+@JsonSerializable()
+class LocationPhoneEventRequest extends AppBaseRequest {
+  @JsonKey(name: 'events')
+  List<PhoneEventBean> events;
+
+  LocationPhoneEventRequest(this.events);
+
+  @override
+  Map<String, dynamic> toJson() => _$LocationPhoneEventRequestToJson(this);
+}

+ 73 - 0
lib/data/api/request/location_phone_event_request.g.dart

@@ -0,0 +1,73 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'location_phone_event_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+LocationPhoneEventRequest _$LocationPhoneEventRequestFromJson(
+        Map<String, dynamic> json) =>
+    LocationPhoneEventRequest(
+      (json['events'] as List<dynamic>)
+          .map((e) => PhoneEventBean.fromJson(e as Map<String, dynamic>))
+          .toList(),
+    )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$LocationPhoneEventRequestToJson(
+        LocationPhoneEventRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'events': instance.events,
+    };

+ 22 - 0
lib/data/bean/phone_event_bean.dart

@@ -0,0 +1,22 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'phone_event_bean.g.dart';
+
+@JsonSerializable()
+class PhoneEventBean {
+  @JsonKey(name: 'startTime')
+  int startTime;
+
+  @JsonKey(name: 'endTime')
+  int endTime;
+
+  PhoneEventBean({
+    required this.startTime,
+    required this.endTime,
+  });
+
+  factory PhoneEventBean.fromJson(Map<String, dynamic> json) =>
+      _$PhoneEventBeanFromJson(json);
+
+  Map<String, dynamic> toJson() => _$PhoneEventBeanToJson(this);
+}

+ 19 - 0
lib/data/bean/phone_event_bean.g.dart

@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'phone_event_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PhoneEventBean _$PhoneEventBeanFromJson(Map<String, dynamic> json) =>
+    PhoneEventBean(
+      startTime: (json['startTime'] as num).toInt(),
+      endTime: (json['endTime'] as num).toInt(),
+    );
+
+Map<String, dynamic> _$PhoneEventBeanToJson(PhoneEventBean instance) =>
+    <String, dynamic>{
+      'startTime': instance.startTime,
+      'endTime': instance.endTime,
+    };

+ 11 - 4
lib/data/repositories/account_repository.dart

@@ -13,6 +13,7 @@ import 'package:location/data/consts/constants.dart';
 import 'package:location/data/consts/error_code.dart';
 import 'package:location/data/repositories/friends_repository.dart';
 import 'package:location/data/repositories/message_repository.dart';
+import 'package:location/data/repositories/phone_event_repository.dart';
 import 'package:location/data/repositories/urgent_contact_repository.dart';
 import 'package:location/di/get_it.dart';
 import 'package:location/push_notification/ios_push_notification_service.dart';
@@ -52,6 +53,7 @@ class AccountRepository {
   late final FriendsRepository friendsRepository;
   late final MessageRepository messageRepository;
   late final UrgentContactRepository urgentContactRepository;
+  late final PhoneEventRepository phoneEventRepository;
 
   final Rx<UserInfo> mineUserInfo = Rx<UserInfo>(UserInfo(
       id: Constants.mineLocationId,
@@ -63,6 +65,7 @@ class AccountRepository {
     friendsRepository = FriendsRepository.getInstance();
     messageRepository = MessageRepository.getInstance();
     urgentContactRepository = UrgentContactRepository.getInstance();
+    phoneEventRepository = PhoneEventRepository.getInstance();
 
     isLogin.bindStream(
       loginPhoneNum.map((value) {
@@ -133,8 +136,10 @@ class AccountRepository {
     messageRepository.refreshFriendWaitingCount();
     messageRepository.refreshUnreadMessage();
     urgentContactRepository.requestUrgentContactList();
-    //上传推送toekn
+    //上传推送token
     onRequestNotificationReport();
+    //上报事件
+    phoneEventRepository.startReportPhoneEvent();
   }
 
   void logout() {
@@ -154,6 +159,8 @@ class AccountRepository {
     friendsRepository.clearFriends();
     messageRepository.clearMessage();
     urgentContactRepository.clearContactList();
+
+    phoneEventRepository.stopReportPhoneEvent();
   }
 
   void refreshMemberStatus() {
@@ -195,8 +202,7 @@ class AccountRepository {
           permanent: response.permanent,
           trialed: response.trialed,
           avatar: response.avatar,
-          trialEndTimestamp: response.trialEndTimestamp
-      );
+          trialEndTimestamp: response.trialEndTimestamp);
       this.memberStatusInfo.value = memberStatusInfo;
       return response;
     });
@@ -236,7 +242,8 @@ class AccountRepository {
     var tokenStr = await IosPushNotificationService.getDeviceToken();
     print("tokenStrsfsdf---${tokenStr}");
     return atmobApi
-        .notificationReport(NotificationReportRequest(deviceToken: tokenStr as String))
+        .notificationReport(
+            NotificationReportRequest(deviceToken: tokenStr as String))
         .then(HttpHandler.handle(false))
         .then((response) {})
         .catchError((_) {});

+ 20 - 1
lib/data/repositories/config_repository.dart

@@ -6,6 +6,7 @@ import 'package:location/data/api/request/configs_request.dart';
 import 'package:location/data/api/request/upload_client_id_request.dart';
 import 'package:location/data/bean/config_bean.dart';
 import 'package:location/data/repositories/friends_repository.dart';
+import 'package:location/data/repositories/phone_event_repository.dart';
 import 'package:location/di/get_it.dart';
 import 'package:location/utils/async_util.dart';
 import 'package:location/utils/http_handler.dart';
@@ -18,6 +19,7 @@ class ConfigRepository {
   final String tag = 'ConfigRepository';
 
   static const String keyVirtualFriend = 'virtual_friend';
+  static const String keyClientScheduled = 'client_scheduled';
 
   bool? _isShowVirtualFriend;
 
@@ -51,14 +53,31 @@ class ConfigRepository {
           case keyVirtualFriend:
             _dealWithVirtualFriendSetting(item);
             break;
+          case keyClientScheduled:
+            _dealWithClientScheduledSetting(item);
+            break;
         }
       }
     });
   }
 
+  void _dealWithClientScheduledSetting(ConfigBean item) {
+    final cfg = item.value;
+    if (cfg == null || cfg.isEmpty == true) {
+      return;
+    }
+    if (cfg.containsKey("phoneEventMinutes")) {
+      final phoneEventMinutes = cfg["phoneEventMinutes"];
+      if (phoneEventMinutes is double && phoneEventMinutes > 0) {
+        PhoneEventRepository.getInstance()
+            .setTaskIntervalTime(Duration(minutes: phoneEventMinutes.toInt()));
+      }
+    }
+  }
+
   Future<ConfigsResponse> requestConfigsData() {
     return atmobApi
-        .getConfigs(ConfigsRequest([keyVirtualFriend]))
+        .getConfigs(ConfigsRequest([keyVirtualFriend, keyClientScheduled]))
         .then(HttpHandler.handle(true));
   }
 

+ 1 - 1
lib/data/repositories/friends_repository.dart

@@ -75,7 +75,7 @@ class FriendsRepository {
     refreshVirtualFuture?.cancel();
     refreshVirtualFuture = AsyncUtil.retry(
         () => _requestVirtualFriend(), Duration(seconds: 3),
-        maxRetry: 50);
+        maxRetry: 10);
   }
 
   Future<UserInfo> _requestVirtualFriend() {

+ 160 - 0
lib/data/repositories/phone_event_repository.dart

@@ -0,0 +1,160 @@
+import 'package:flutter/cupertino.dart';
+import 'package:injectable/injectable.dart';
+import 'package:location/data/api/atmob_api.dart';
+import 'package:location/di/get_it.dart';
+import 'package:location/utils/async_util.dart';
+import 'package:location/utils/date_util.dart';
+import 'package:mobile_use_statistics/flutter_mobile_statistics.dart';
+import 'package:battery_plus/battery_plus.dart';
+import '../../utils/atmob_log.dart';
+import '../../utils/http_handler.dart';
+import '../api/request/electric_request.dart';
+import '../api/request/location_phone_event_request.dart';
+import '../bean/phone_event_bean.dart';
+import 'account_repository.dart';
+import 'dart:async';
+
+@lazySingleton
+class PhoneEventRepository {
+  final AtmobApi atmobApi;
+
+  static final String tag = 'PhoneEventRepository';
+
+  Duration _taskIntervalTime = Duration(minutes: 5);
+  Timer? _reportTimer;
+
+  CancelableFuture? lockScreenCancelableFuture;
+  CancelableFuture? batteryCancelableFuture;
+
+  final battery = Battery();
+
+  PhoneEventRepository(this.atmobApi) {
+    startReportPhoneEvent();
+  }
+
+  static PhoneEventRepository getInstance() {
+    return getIt.get<PhoneEventRepository>();
+  }
+
+  static Future<bool> hasUseStatisticsPermission() {
+    return MobileUseStatistics.hasUseStatisticsPermission();
+  }
+
+  static Future<bool> requestUseStatisticsPermission() async {
+    final hasPermission =
+        await MobileUseStatistics.requestUseStatisticsPermission();
+    if (hasPermission) {
+      PhoneEventRepository.getInstance().startReportPhoneEvent();
+    }
+    return hasPermission;
+  }
+
+  void setTaskIntervalTime(Duration duration) {
+    _taskIntervalTime = duration;
+    if (_reportTimer != null && _reportTimer!.isActive) {
+      _reportTimer!.cancel();
+      _schedulePeriodicReport();
+    }
+  }
+
+  //开始事件上报 ,锁屏事件以及手机电量等
+  void startReportPhoneEvent() {
+    if (AccountRepository.token == null ||
+        AccountRepository.token?.isEmpty == true) {
+      return;
+    }
+    _runReportTasks();
+    _schedulePeriodicReport();
+  }
+
+  void _schedulePeriodicReport() {
+    _reportTimer = Timer.periodic(_taskIntervalTime, (_) => _runReportTasks());
+  }
+
+  void _runReportTasks() {
+    //上报手机锁屏事件上报
+    _startLockScreenEvent();
+    //上报手机电量
+    _startPhoneElectricEvent();
+  }
+
+  void _startPhoneElectricEvent() {
+    AtmobLog.d(tag, '_startPhoneElectricEvent');
+    batteryCancelableFuture?.cancel();
+    batteryCancelableFuture = AsyncUtil.retryWithExponentialBackoff(
+        () => _commitBatteryEvent(), 4,
+        initialInterval: Duration(seconds: 3));
+    batteryCancelableFuture!.catchError((error) {
+      AtmobLog.d(tag, ' startPhoneElectricEvent 重试最大次数 异常 error:$error');
+      _startPhoneElectricEvent();
+    });
+  }
+
+  Future<void> _commitBatteryEvent() async {
+    int batteryValue = await getBatteryLevel();
+    return _electricReport(batteryValue);
+  }
+
+  ///停止事件上报
+  ///
+  void stopReportPhoneEvent() {
+    AtmobLog.d(tag, 'stopReportPhoneEvent');
+    _reportTimer?.cancel();
+    lockScreenCancelableFuture?.cancel();
+    batteryCancelableFuture?.cancel();
+  }
+
+  void _startLockScreenEvent() async {
+    //判断是否有事件上报权限
+    if (!await hasUseStatisticsPermission()) {
+      return;
+    }
+    AtmobLog.d(tag, '_startLockScreenEvent');
+    lockScreenCancelableFuture?.cancel();
+    lockScreenCancelableFuture = AsyncUtil.retryWithExponentialBackoff(
+        () => _commitLockScreenEvent(), 4,
+        initialInterval: Duration(seconds: 3));
+    lockScreenCancelableFuture!.catchError((error) {
+      AtmobLog.d(tag, 'startLockScreenEvent 重试最大次数 异常 error:$error');
+      startReportPhoneEvent();
+    });
+  }
+
+  Future<void> _commitLockScreenEvent() async {
+    //收集手机事件数据上报
+    DateTime now = DateUtil.getNow();
+    final startTime =
+        DateUtil.getStartOfDayTimestamp(now).millisecondsSinceEpoch;
+    final endTime = DateUtil.getEndOfDayTimestamp(now).millisecondsSinceEpoch;
+    final eventList = await MobileUseStatistics.getLockScreenStatistics(
+        startTime: startTime, endTime: endTime);
+    final events = eventList
+        ?.map((event) =>
+            PhoneEventBean(startTime: event.startTime, endTime: event.endTime))
+        .toList();
+
+    if (events == null || events.isEmpty) {
+      return;
+    }
+    return _locationPhoneEvent(events);
+  }
+
+  // 获取电量百分比
+  Future<int> getBatteryLevel() async {
+    final level = await battery.batteryLevel;
+    debugPrint('Battery level: $level%');
+    return level;
+  }
+
+  Future<void> _locationPhoneEvent(List<PhoneEventBean> events) {
+    return atmobApi
+        .locationPhoneEvent(LocationPhoneEventRequest(events))
+        .then(HttpHandler.handle(true));
+  }
+
+  Future<void> _electricReport(int electric) {
+    return atmobApi
+        .electricReport(ElectricRequest(electric))
+        .then(HttpHandler.handle(true));
+  }
+}

+ 11 - 8
lib/di/get_it.config.dart

@@ -20,6 +20,7 @@ import '../data/repositories/contact_repository.dart' as _i850;
 import '../data/repositories/friends_repository.dart' as _i1053;
 import '../data/repositories/member_repository.dart' as _i814;
 import '../data/repositories/message_repository.dart' as _i791;
+import '../data/repositories/phone_event_repository.dart' as _i274;
 import '../data/repositories/track_repository.dart' as _i240;
 import '../data/repositories/urgent_contact_repository.dart' as _i983;
 import '../module/about/about_controller.dart' as _i256;
@@ -58,14 +59,14 @@ extension GetItInjectableX on _i174.GetIt {
       environmentFilter,
     );
     final networkModule = _$NetworkModule();
-    gh.factory<_i973.SplashController>(() => _i973.SplashController());
-    gh.factory<_i756.TrackDetailController>(
-        () => _i756.TrackDetailController());
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
-    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i108.PermissionSettingController>(
         () => _i108.PermissionSettingController());
+    gh.factory<_i973.SplashController>(() => _i973.SplashController());
+    gh.factory<_i756.TrackDetailController>(
+        () => _i756.TrackDetailController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.lazySingleton<_i220.AtmobLocationClient>(
         () => _i220.AtmobLocationClient());
@@ -79,12 +80,14 @@ extension GetItInjectableX on _i174.GetIt {
         .provideAtmobStreamApi(gh<_i361.Dio>(instanceName: 'stream')));
     gh.lazySingleton<_i20.AccountRepository>(
         () => _i20.AccountRepository(gh<_i243.AtmobApi>()));
-    gh.lazySingleton<_i1053.FriendsRepository>(
-        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i850.ContactRepository>(
         () => _i850.ContactRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i1053.FriendsRepository>(
+        () => _i1053.FriendsRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i791.MessageRepository>(
         () => _i791.MessageRepository(gh<_i243.AtmobApi>()));
+    gh.lazySingleton<_i274.PhoneEventRepository>(
+        () => _i274.PhoneEventRepository(gh<_i243.AtmobApi>()));
     gh.lazySingleton<_i983.UrgentContactRepository>(
         () => _i983.UrgentContactRepository(gh<_i243.AtmobApi>()));
     gh.factory<_i1008.LoginController>(
@@ -118,10 +121,10 @@ extension GetItInjectableX on _i174.GetIt {
               gh<_i983.UrgentContactRepository>(),
               gh<_i20.AccountRepository>(),
             ));
-    gh.factory<_i492.FriendSettingController>(
-        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.factory<_i897.AddFriendDialogController>(
         () => _i897.AddFriendDialogController(gh<_i1053.FriendsRepository>()));
+    gh.factory<_i492.FriendSettingController>(
+        () => _i492.FriendSettingController(gh<_i1053.FriendsRepository>()));
     gh.lazySingleton<_i814.MemberRepository>(() => _i814.MemberRepository(
           gh<_i243.AtmobApi>(),
           gh<_i20.AccountRepository>(),

+ 5 - 0
lib/dialog/account_replace_dialog.dart

@@ -2,6 +2,9 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import 'package:location/module/main/main_page.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/common_expand.dart';
 
@@ -103,6 +106,8 @@ class _AccountReplaceView extends StatelessWidget {
   }
 
   void onKnowClick() {
+    //清空至首页
+    Get.offAll(() => MainPage());
     AccountReplaceDialog.dismiss();
   }
 }

+ 1 - 1
lib/module/analyse/location_analyse_controller.dart

@@ -17,7 +17,7 @@ class LocationAnalyseController extends BaseController {
   void onInit() {
     super.onInit();
     bgController = VideoPlayerController.asset(
-      Assets.video.locationAnalyseRobot,
+      Assets.anim.locationAnalyseRobot,
     )
       ..setLooping(true)
       ..setVolume(0.0)

+ 9 - 0
lib/module/analyse/location_analyse_page.dart

@@ -8,6 +8,7 @@ import 'package:location/base/base_page.dart';
 import 'package:location/resource/colors.gen.dart';
 import 'package:location/router/app_pages.dart';
 import 'package:location/utils/common_expand.dart';
+import 'package:lottie/lottie.dart';
 import 'package:video_player/video_player.dart';
 import '../../resource/assets.gen.dart';
 import '../../widget/common_view.dart';
@@ -62,6 +63,14 @@ class LocationAnalysePage extends BasePage<LocationAnalyseController> {
       children: [
         Assets.images.bgLocationAnalyse.image(width: double.infinity),
         buildRobotView(),
+        Lottie.asset(
+          Assets.anim.locationLabel,
+          delegates: LottieDelegates(
+            values: [
+              ValueDelegate.text(['健身房', '健身房'], value: '占位符啊123')
+            ],
+          ),
+        )
       ],
     );
   }

+ 18 - 0
lib/module/permission/permission_setting_controller.dart

@@ -4,8 +4,10 @@ import 'package:flutter_tool_android/flutter_tool_android.dart';
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:location/base/base_controller.dart';
+import 'package:location/data/repositories/phone_event_repository.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/toast_util.dart';
+import 'package:mobile_use_statistics/flutter_mobile_statistics.dart';
 import '../../utils/permission_util.dart';
 
 @injectable
@@ -64,4 +66,20 @@ class PermissionSettingController extends BaseController {
   void openBackgroundRunSetting() async {
     FlutterToolAndroid.openAppBackgroundSetting();
   }
+
+  void openPhoneScreenSetting() async {
+    bool hasPermission =
+        await PhoneEventRepository.hasUseStatisticsPermission();
+    if (hasPermission) {
+      ToastUtil.show(StringName.permissionSettingSuccess);
+      return;
+    }
+    //打开app设置页
+    hasPermission = await PhoneEventRepository.requestUseStatisticsPermission();
+    if (hasPermission) {
+      ToastUtil.show(StringName.permissionSettingSuccess);
+    } else {
+      ToastUtil.show(StringName.permissionRequestFail);
+    }
+  }
 }

+ 4 - 0
lib/module/permission/permission_setting_page.dart

@@ -31,6 +31,10 @@ class PermissionSettingPage extends BasePage<PermissionSettingController> {
                 StringName.permissionLocationSettingSubtitle, () {
               controller.openLocationSetting();
             }),
+            buildPermissionItem(StringName.permissionPhoneScreenSetting,
+                StringName.permissionPhoneScreenSettingSubtitle, () {
+              controller.openPhoneScreenSetting();
+            }),
             Obx(() {
               return Visibility(
                 visible: controller.permissionShowBattery,

+ 7 - 4
lib/module/track/track_day_detail/track_day_detail_controller.dart

@@ -68,6 +68,7 @@ class TrackDayDetailController extends BaseController {
 
   final GraduallyController graduallyController = GraduallyController();
   StreamSubscription? _streamChatSubscription;
+  StreamSubscription? _currentTrackDaySubscription;
 
   final RxnString _summaryError = RxnString();
 
@@ -91,7 +92,8 @@ class TrackDayDetailController extends BaseController {
 
   @override
   void onReady() {
-    trackController.currentTrackDay.listen((dat) {
+    _currentTrackDaySubscription =
+        trackController.currentTrackDay.listen((dat) {
       if (dat == days) {
         _requestTrackData();
       }
@@ -105,6 +107,7 @@ class TrackDayDetailController extends BaseController {
       return;
     }
     requestTrackFuture?.cancel();
+    CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt);
     requestTrackFuture = AsyncUtil.waitForAll(
         [_requestTrackHistoryPoints(), _requestTrackDaily()]);
     requestTrackFuture!.then((_) {
@@ -171,6 +174,8 @@ class TrackDayDetailController extends BaseController {
         points.add(points.first);
       }
       trackController.showMapTrack(points, markers);
+    }).whenComplete(() {
+      CustomLoadingDialog.hide();
     });
   }
 
@@ -214,14 +219,12 @@ class TrackDayDetailController extends BaseController {
 
   Future<void> _requestTrackDaily() {
     _isRequested.value = false;
-    CustomLoadingDialog.show(loadingTxt: StringName.trackLoadingTxt);
     return trackRepository
         .trackDailyQuery(
             startTime: days.start,
             endTime: days.end,
             userId: trackController.userInfo?.id)
         .then((list) {
-      CustomLoadingDialog.hide();
       _isRequested.value = true;
       _trackNoData.value = (list == null || list.isEmpty == true);
       trackDailyList.assignAll(list ?? []);
@@ -232,7 +235,6 @@ class TrackDayDetailController extends BaseController {
       _dealPieChatData();
       _dealTrackExpandData();
     }).catchError((error) {
-      CustomLoadingDialog.hide();
       _isRequested.value = false;
       ErrorHandler.toastError(error);
     });
@@ -421,6 +423,7 @@ class TrackDayDetailController extends BaseController {
   void onClose() {
     super.onClose();
     _streamChatSubscription?.cancel();
+    _currentTrackDaySubscription?.cancel();
     graduallyController.dispose();
     requestTrackFuture?.cancel();
   }

+ 14 - 11
lib/resource/assets.gen.dart

@@ -9,6 +9,19 @@
 
 import 'package:flutter/widgets.dart';
 
+class $AssetsAnimGen {
+  const $AssetsAnimGen();
+
+  /// File path: assets/anim/location_analyse_robot.mp4
+  String get locationAnalyseRobot => 'assets/anim/location_analyse_robot.mp4';
+
+  /// File path: assets/anim/location_label.json
+  String get locationLabel => 'assets/anim/location_label.json';
+
+  /// List of all assets
+  List<String> get values => [locationAnalyseRobot, locationLabel];
+}
+
 class $AssetsImagesGen {
   const $AssetsImagesGen();
 
@@ -762,21 +775,11 @@ class $AssetsImagesGen {
       ];
 }
 
-class $AssetsVideoGen {
-  const $AssetsVideoGen();
-
-  /// File path: assets/video/location_analyse_robot.mp4
-  String get locationAnalyseRobot => 'assets/video/location_analyse_robot.mp4';
-
-  /// List of all assets
-  List<String> get values => [locationAnalyseRobot];
-}
-
 class Assets {
   const Assets._();
 
+  static const $AssetsAnimGen anim = $AssetsAnimGen();
   static const $AssetsImagesGen images = $AssetsImagesGen();
-  static const $AssetsVideoGen video = $AssetsVideoGen();
 }
 
 class AssetGenImage {

+ 6 - 0
lib/resource/string.gen.dart

@@ -291,6 +291,10 @@ class StringName {
   static String get trackDailySkipCallPhoneFail => 'track_daily_skip_call_phone_fail'.tr; // 跳转拨号界面失败
   static String get mainTodayTrackLoading => 'main_today_track_loading'.tr; // 正在加载中...
   static String get mainTodayTrackNormalPoint => 'main_today_track_normal_point'.tr; // 暂无异常
+  static String get permissionPhoneScreenSetting =>
+      'permission_phone_screen_setting'.tr; // 手机屏幕使用时长
+  static String get permissionPhoneScreenSettingSubtitle =>
+      'permission_phone_screen_setting_subtitle'.tr; // 获取权限,保证您的信息定位更精准,预测更加准确。
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -585,6 +589,8 @@ class StringMultiSource {
       'track_daily_skip_call_phone_fail': '跳转拨号界面失败',
       'main_today_track_loading': '正在加载中...',
       'main_today_track_normal_point': '暂无异常',
+      'permission_phone_screen_setting': '手机屏幕使用时长',
+      'permission_phone_screen_setting_subtitle': '获取权限,保证您的信息定位更精准,预测更加准确。',
     },
   };
 }

+ 0 - 5
lib/utils/common_expand.dart

@@ -26,11 +26,6 @@ extension DoubleExtension on double {
   }
 }
 
-extension FutureMap<T> on Future<T> {
-  Future<R> map<R>(R Function(T) transform) {
-    return then(transform);
-  }
-}
 
 extension TrimAllExtension on String {
   String trimAll() {

+ 20 - 4
plugins/mobile_use_statistics/android/build.gradle

@@ -8,7 +8,7 @@ buildscript {
     }
 
     dependencies {
-        classpath("com.android.tools.build:gradle:8.7.0")
+        classpath("com.android.tools.build:gradle:7.3.0")
     }
 }
 
@@ -19,6 +19,19 @@ rootProject.allprojects {
     }
 }
 
+
+// 加载 local.properties 文件
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withInputStream { stream ->
+        localProperties.load(stream)
+    }
+}
+
+// 读取变量
+def flutterSdk = localProperties.getProperty('flutter.sdk')
+
 apply plugin: "com.android.library"
 
 android {
@@ -32,12 +45,15 @@ android {
     }
 
     defaultConfig {
-        minSdk = 21
+        minSdk = 22
     }
 
     dependencies {
-        testImplementation("junit:junit:4.13.2")
-        testImplementation("org.mockito:mockito-core:5.0.0")
+        //flutter
+        compileOnly files("$flutterSdk/bin/cache/artifacts/engine/android-arm/flutter.jar")
+
+        //AndroidX
+        compileOnly "androidx.annotation:annotation:1.1.0"
     }
 
     testOptions {

二進制
plugins/mobile_use_statistics/android/gradle/wrapper/gradle-wrapper.jar


+ 7 - 0
plugins/mobile_use_statistics/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 249 - 0
plugins/mobile_use_statistics/android/gradlew

@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 92 - 0
plugins/mobile_use_statistics/android/gradlew.bat

@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 10 - 1
plugins/mobile_use_statistics/android/src/main/AndroidManifest.xml

@@ -1,2 +1,11 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.atmob.mobile_use_statistics"></manifest>
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.atmob.mobile_use_statistics">
+
+
+    <uses-permission
+        android:name="android.permission.PACKAGE_USAGE_STATS"
+        tools:ignore="ProtectedPermissions" />
+
+
+</manifest>

+ 70 - 25
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java

@@ -1,38 +1,83 @@
 package com.atmob.mobile_use_statistics;
 
+import android.app.Activity;
+import android.content.Context;
+
 import androidx.annotation.NonNull;
 
+import com.atmob.mobile_use_statistics.core.FlutterUsageHelper;
+import com.atmob.mobile_use_statistics.utils.LogUtil;
+
+import java.util.Objects;
+
+import io.flutter.BuildConfig;
 import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
 import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
 import io.flutter.plugin.common.MethodChannel.Result;
 
-/** MobileUseStatisticsPlugin */
-public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandler {
-  /// The MethodChannel that will the communication between Flutter and native Android
-  ///
-  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
-  /// when the Flutter Engine is detached from the Activity
-  private MethodChannel channel;
-
-  @Override
-  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
-    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mobile_use_statistics");
-    channel.setMethodCallHandler(this);
-  }
-
-  @Override
-  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
-    if (call.method.equals("getPlatformVersion")) {
-      result.success("Android " + android.os.Build.VERSION.RELEASE);
-    } else {
-      result.notImplemented();
+/**
+ * MobileUseStatisticsPlugin
+ */
+public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
+    /// The MethodChannel that will the communication between Flutter and native Android
+    ///
+    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+    /// when the Flutter Engine is detached from the Activity
+    private MethodChannel channel;
+
+    private Context context;
+
+    private Activity activity;
+
+    @Override
+    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+        LogUtil.setLogEnabled(BuildConfig.DEBUG);
+        context = flutterPluginBinding.getApplicationContext();
+        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "mobile_use_statistics");
+        channel.setMethodCallHandler(this);
+    }
+
+    @Override
+    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+        String method = call.method;
+        if (Objects.equals(method, "requestUseStatisticsPermission")) {
+            FlutterUsageHelper.requestUseStatisticsPermission(activity, result);
+        } else if (Objects.equals(method, "hasUseStatisticsPermission")) {
+            FlutterUsageHelper.hasUseStatisticsPermission(context, result);
+        } else if (Objects.equals(method, "getLockScreenStatistics")) {
+            FlutterUsageHelper.getLockScreenStatistics(activity, call, result);
+        } else {
+            result.notImplemented();
+        }
+    }
+
+    @Override
+    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+        channel.setMethodCallHandler(null);
     }
-  }
 
-  @Override
-  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
-    channel.setMethodCallHandler(null);
-  }
+    @Override
+    public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {
+        this.activity = activityPluginBinding.getActivity();
+
+    }
+
+    @Override
+    public void onDetachedFromActivityForConfigChanges() {
+
+    }
+
+    @Override
+    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) {
+
+    }
+
+    @Override
+    public void onDetachedFromActivity() {
+        this.activity = null;
+    }
 }

+ 123 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/AppUsageInfo.java

@@ -0,0 +1,123 @@
+package com.atmob.mobile_use_statistics.bean;
+
+import java.util.List;
+
+public class AppUsageInfo {
+    private final String packageName;
+    private final String appName;
+    private final List<Event> appUsageEvents;
+
+    private long totalUsedTime;
+    private long lastUsedStartTime;
+    private long lastUsedEndTime;
+    private long lastUsedTime;
+    private int launchCount;
+
+    public AppUsageInfo(String packageName, String appName, List<Event> appUsageEvents) {
+        this.packageName = packageName;
+        this.appName = appName;
+        this.appUsageEvents = appUsageEvents;
+        initializeUsageData();
+    }
+
+    private void initializeUsageData() {
+        if (appUsageEvents == null || appUsageEvents.isEmpty()) {
+            return;
+        }
+        for (Event appUsageEvent : appUsageEvents) {
+            long startTime = appUsageEvent.getStartTime();
+            long endTime = appUsageEvent.getEndTime();
+            long usedTime = appUsageEvent.getUsedTime();
+
+            totalUsedTime += usedTime;
+
+            if (lastUsedStartTime == 0 || startTime > lastUsedStartTime) {
+                lastUsedStartTime = startTime;
+            }
+            if (endTime > lastUsedEndTime) {
+                lastUsedEndTime = endTime;
+                lastUsedTime = endTime - startTime;
+            }
+        }
+        launchCount = appUsageEvents.size();
+    }
+
+    public String getPackageName() {
+        return packageName;
+    }
+
+    public String getAppName() {
+        return appName;
+    }
+
+    public long getTotalUsedTime() {
+        return totalUsedTime;
+    }
+
+    public long getLastUsedStartTime() {
+        return lastUsedStartTime;
+    }
+
+    public long getLastUsedEndTime() {
+        return lastUsedEndTime;
+    }
+
+    public long getLastUsedTime() {
+        return lastUsedTime;
+    }
+
+    public List<Event> getAppUsageEvents() {
+        return appUsageEvents;
+    }
+
+    public int getLaunchCount() {
+        return launchCount;
+    }
+
+    @Override
+    public String toString() {
+        return "AppUsageInfo{" +
+                "packageName='" + packageName + '\'' +
+                ", appName='" + appName + '\'' +
+                ", appUsageEvents=" + appUsageEvents +
+                ", totalUsedTime=" + totalUsedTime +
+                ", lastUsedStartTime=" + lastUsedStartTime +
+                ", lastUsedEndTime=" + lastUsedEndTime +
+                ", lastUsedTime=" + lastUsedTime +
+                ", launchCount=" + launchCount +
+                '}';
+    }
+
+    public static class Event {
+        private final long startTime;
+        private final long endTime;
+        private final long usedTime;
+
+        public Event(long startTime, long endTime) {
+            this.startTime = startTime;
+            this.endTime = endTime;
+            this.usedTime = endTime - startTime;
+        }
+
+        public long getStartTime() {
+            return startTime;
+        }
+
+        public long getEndTime() {
+            return endTime;
+        }
+
+        public long getUsedTime() {
+            return usedTime;
+        }
+
+        @Override
+        public String toString() {
+            return "Event{" +
+                    "startTime=" + startTime +
+                    ", endTime=" + endTime +
+                    ", usedTime=" + usedTime +
+                    '}';
+        }
+    }
+}

+ 118 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/DeviceUsageInfo.java

@@ -0,0 +1,118 @@
+package com.atmob.mobile_use_statistics.bean;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DeviceUsageInfo {
+    private long totalUsedTime;
+    private long lastUsedStartTime;
+    private long lastUsedEndTime;
+    private long lastUsedTime;
+    private int interactiveCount;
+
+    private final List<Event> events;
+
+    public DeviceUsageInfo(List<Event> events) {
+        this.events = events;
+        initializeUsageData();
+    }
+
+    private void initializeUsageData() {
+        if (events == null || events.isEmpty()) {
+            return;
+        }
+        for (Event event : events) {
+            long startTime = event.getStartTime();
+            long endTime = event.getEndTime();
+            long usedTime = event.getUsedTime();
+
+            totalUsedTime += usedTime;
+
+            if (lastUsedStartTime == 0 || startTime > lastUsedStartTime) {
+                lastUsedStartTime = startTime;
+            }
+            if (endTime > lastUsedEndTime) {
+                lastUsedEndTime = endTime;
+                lastUsedTime = endTime - startTime;
+            }
+        }
+        interactiveCount = events.size();
+    }
+
+    public long getTotalUsedTime() {
+        return totalUsedTime;
+    }
+
+    public long getLastUsedStartTime() {
+        return lastUsedStartTime;
+    }
+
+    public long getLastUsedEndTime() {
+        return lastUsedEndTime;
+    }
+
+    public long getLastUsedTime() {
+        return lastUsedTime;
+    }
+
+    public int getInteractiveCount() {
+        return interactiveCount;
+    }
+
+    public List<Event> getEvents() {
+        return events;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceUsageInfo{" +
+                "totalUsedTime=" + totalUsedTime +
+                ", lastUsedStartTime=" + lastUsedStartTime +
+                ", lastUsedEndTime=" + lastUsedEndTime +
+                ", lastUsedTime=" + lastUsedTime +
+                ", interactiveCount=" + interactiveCount +
+                ", events=" + events +
+                '}';
+    }
+
+    public static class Event {
+        private final long startTime;
+        private final long endTime;
+        private final long usedTime;
+
+        public Event(long startTime, long endTime) {
+            this.startTime = startTime;
+            this.endTime = endTime;
+            this.usedTime = endTime - startTime;
+        }
+
+        public long getStartTime() {
+            return startTime;
+        }
+
+        public long getEndTime() {
+            return endTime;
+        }
+
+        public long getUsedTime() {
+            return usedTime;
+        }
+
+        @Override
+        public String toString() {
+            return "Event{" +
+                    "startTime=" + startTime +
+                    ", endTime=" + endTime +
+                    ", usedTime=" + usedTime +
+                    '}';
+        }
+
+        public Map<String, Object> toMap() {
+            Map<String, Object> map = new HashMap<>();
+            map.put("startTime", startTime);
+            map.put("endTime", endTime);
+            return map;
+        }
+    }
+}

+ 118 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/FlutterUsageHelper.java

@@ -0,0 +1,118 @@
+package com.atmob.mobile_use_statistics.core;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.os.Looper;
+
+import com.atmob.mobile_use_statistics.bean.DeviceUsageInfo;
+import com.atmob.mobile_use_statistics.utils.LogUtil;
+import com.atmob.mobile_use_statistics.utils.ParamUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel.Result;
+
+public class FlutterUsageHelper {
+
+    private static final String TAG = "UsagePermissions";
+
+    public static void requestUseStatisticsPermission(Activity activity, Result result) {
+        UsageStats.requestPermission(activity);
+
+        LogUtil.d(TAG, "requestUseStatisticsPermission 申请使用统计权限");
+        Application application = activity.getApplication();
+        Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
+            @Override
+            public void onActivityResumed(Activity resumedActivity) {
+                boolean granted = UsageStats.hasPermission(resumedActivity);
+                LogUtil.d(TAG, "requestUseStatisticsPermission granted: " + granted);
+                if (granted) {
+                    result.success(true);
+                } else {
+                    result.success(false);
+                }
+                // 3. 注销监听器
+                application.unregisterActivityLifecycleCallbacks(this);
+            }
+
+            // 其他生命周期回调不处理
+            @Override
+            public void onActivityCreated(Activity a, android.os.Bundle b) {
+            }
+
+            @Override
+            public void onActivityStarted(Activity a) {
+            }
+
+            @Override
+            public void onActivityPaused(Activity a) {
+            }
+
+            @Override
+            public void onActivityStopped(Activity a) {
+            }
+
+            @Override
+            public void onActivitySaveInstanceState(Activity a, android.os.Bundle b) {
+            }
+
+            @Override
+            public void onActivityDestroyed(Activity a) {
+            }
+        };
+
+        application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
+    }
+
+
+    public static void hasUseStatisticsPermission(Context context, Result result) {
+        boolean hasPermission = UsageStats.hasPermission(context);
+        LogUtil.d(TAG, "hasUseStatisticsPermission: " + hasPermission);
+        if (hasPermission) {
+            result.success(true);
+        } else {
+            result.success(false);
+        }
+    }
+
+    public static void getLockScreenStatistics(Activity activity, MethodCall call, Result result) {
+        LogUtil.i(TAG, "getLockScreenStatistics===>" + call.arguments());
+        Map<String, Object> arguments = call.arguments();
+        if (arguments == null || arguments.isEmpty()) {
+            result.error("-1", "getLockScreenStatistics.arguments is empty", null);
+            return;
+        }
+        Long startTime = ParamUtil.getLong(arguments, "startTime");
+        Long endTime = ParamUtil.getLong(arguments, "endTime");
+        if (startTime == null || endTime == null) {
+            result.error("-1", "getLockScreenStatistics arguments must contain startTime and endTime", null);
+            return;
+        }
+        if (startTime > endTime) {
+            result.error("-1", "Invalid time range: startTime=" + startTime + " endTime=" + endTime, null);
+            return;
+        }
+
+        new Thread(() -> {
+            try {
+                //目前仅返回设备事件
+                List<DeviceUsageInfo.Event> resultEvent = UsageStats.queryAndAggregateAppUsageData(activity, startTime, endTime);
+                List<Map<String, Object>> eventMapList = new ArrayList<>();
+                if (resultEvent != null) {
+                    for (DeviceUsageInfo.Event event : resultEvent) {
+                        eventMapList.add(event.toMap());
+                    }
+                }
+                activity.runOnUiThread(() -> result.success(eventMapList));
+            } catch (Exception e) {
+                LogUtil.e(TAG, "getLockScreenStatistics error: " + e.getMessage());
+                activity.runOnUiThread(() -> result.error("-1", "getLockScreenStatistics error: " + e.getMessage(), null));
+            }
+        }).start();
+    }
+}

+ 208 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/UsageStats.java

@@ -0,0 +1,208 @@
+package com.atmob.mobile_use_statistics.core;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+
+import com.atmob.mobile_use_statistics.bean.DeviceUsageInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UsageStats {
+
+    private static final String TAG = "UsageStats";
+
+    public static void requestPermission(Context context) {
+        Intent intent = new Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS)
+                .setData(Uri.fromParts("package", context.getPackageName(), null));
+        if (!(context instanceof Activity)) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        context.startActivity(intent);
+    }
+
+
+    public static boolean hasPermission(Context context) {
+        boolean granted;
+        AppOpsManager appOps = (AppOpsManager) context
+                .getSystemService(Context.APP_OPS_SERVICE);
+        int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
+                android.os.Process.myUid(), context.getPackageName());
+
+        if (mode == AppOpsManager.MODE_DEFAULT) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                granted = (context.checkCallingOrSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
+            } else {
+                return true;
+            }
+        } else {
+            granted = (mode == AppOpsManager.MODE_ALLOWED);
+        }
+        return granted;
+    }
+
+    public static List<DeviceUsageInfo.Event> queryAndAggregateAppUsageData(Context context, long startTime, long endTime) {
+        UsageEvents usageEvents = queryAppUsageData(context, startTime, endTime);
+        if (usageEvents == null) {
+            return null;
+        }
+        // App usage tracking
+//        Map<String, AppUsageState> packageStateMap = new HashMap<>();
+        // Device usage tracking
+        DeviceUsageState deviceUsageState = new DeviceUsageState();
+
+        UsageEvents.Event event = new UsageEvents.Event();
+
+        while (usageEvents.getNextEvent(event)) {
+            String packageName = event.getPackageName();
+            long timeStamp = event.getTimeStamp();
+            int eventType = event.getEventType();
+
+            // Handle system events
+            if ("android".equals(packageName)) {
+                if (eventType == UsageEvents.Event.SCREEN_INTERACTIVE) {
+                    deviceUsageState.onScreenOn(timeStamp);
+                } else if (eventType == UsageEvents.Event.SCREEN_NON_INTERACTIVE) {
+                    deviceUsageState.onScreenOff(timeStamp);
+                }
+            }
+            // Handle app events
+//            else if (!AppUsageUtil.isSystemApp(context,packageName)) {
+//                AppUsageState state = packageStateMap.get(packageName);
+//                if (state == null) {
+//                    state = new AppUsageState();
+//                    packageStateMap.put(packageName, state);
+//                }
+//
+//                if (eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
+//                    state.onActivityResumed(timeStamp);
+//                } else if (eventType == UsageEvents.Event.ACTIVITY_PAUSED) {
+//                    state.onActivityPausedOrStop(timeStamp);
+//                } else if (eventType == UsageEvents.Event.ACTIVITY_STOPPED) {
+//                    state.onActivityPausedOrStop(timeStamp);
+//                } else if (eventType == 10) {
+//                    //NOTIFICATION_SEEN
+//                    LogUtil.d(TAG, "Notification seen for package: " + packageName + " at " + timeStamp);
+//                } else if (eventType == 12) {
+//                    //NOTIFICATION_INTERRUPTION
+//                    LogUtil.d(TAG, "Notification interruption for package: " + packageName + " at " + timeStamp);
+//                }
+//            }
+        }
+
+        deviceUsageState.finishIncompleteEvent(0);
+
+        // Build app usage info list
+//        List<AppUsageInfo> newAppUsageInfos = new ArrayList<>();
+//        for (Map.Entry<String, AppUsageState> entry : packageStateMap.entrySet()) {
+//            String packageName = entry.getKey();
+//            AppUsageState state = entry.getValue();
+//
+//            // Skip apps with no usage
+//            if (state.events.isEmpty()) {
+//                continue;
+//            }
+//
+//            // Get app name (cached)
+//            String appName = appNameCache.get(packageName);
+//            if (appName == null) {
+//                appName = AppUsageUtil.getAppName(packageName);
+//                appNameCache.put(packageName, appName);
+//            }
+//
+//            newAppUsageInfos.add(new AppUsageInfo(packageName, appName, state.events));
+//        }
+
+       return deviceUsageState.events;
+    }
+
+    private static UsageEvents queryAppUsageData(Context context, long begin, long end) {
+        UsageStatsManager manager =
+                (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+
+        return manager.queryEvents(begin, end);
+    }
+
+
+
+
+//    private static class AppUsageState {
+//        int count;
+//        long currentStartTime = -1; // -1 means no active event
+//        final List<AppUsageInfo.Event> events = new ArrayList<>();
+//
+//        void onActivityResumed(long timeStamp) {
+//            count++;
+//            if (count == 1) {
+//                // Transition from 0 to 1, start new event
+//                currentStartTime = timeStamp;
+//            }
+//        }
+//
+//        void onActivityPausedOrStop(long timeStamp) {
+//            if (count <= 0) {
+//                count = 0;
+//                return;
+//            }
+//
+//            count--;
+//            if (count == 0) {
+//                // Transition to 0, complete event
+//                events.add(new AppUsageInfo.Event(currentStartTime, timeStamp));
+//                currentStartTime = -1;
+//            }
+//        }
+//
+//        void finishIncompleteEvent(long endTime) {
+//            if (count > 0 && currentStartTime != -1) {
+//                events.add(new AppUsageInfo.Event(currentStartTime, endTime));
+//                currentStartTime = -1;
+//                count = 0;
+//            }
+//        }
+//    }
+
+    private static class DeviceUsageState {
+        int count;
+        long currentStartTime = -1; // -1 means no active event
+        final List<DeviceUsageInfo.Event> events = new ArrayList<>();
+
+        void onScreenOn(long timeStamp) {
+            count++;
+            if (count == 1) {
+                // Transition from 0 to 1, start new event
+                currentStartTime = timeStamp;
+            }
+        }
+
+        void onScreenOff(long timeStamp) {
+            if (count <= 0) {
+                count = 0;
+                return;
+            }
+
+            count--;
+            if (count == 0) {
+                // Transition to 0, complete event
+                events.add(new DeviceUsageInfo.Event(currentStartTime, timeStamp));
+                currentStartTime = -1;
+            }
+        }
+
+        void finishIncompleteEvent(long endTime) {
+            if (count > 0 && currentStartTime != -1) {
+                events.add(new DeviceUsageInfo.Event(currentStartTime, endTime));
+                currentStartTime = -1;
+                count = 0;
+            }
+        }
+    }
+}

+ 97 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/AppUsageUtil.java

@@ -0,0 +1,97 @@
+package com.atmob.mobile_use_statistics.utils;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class AppUsageUtil {
+    private static final Set<String> SYSTEM_PACKAGES = new HashSet<>(10);
+    private static final Map<String, ApplicationInfo> APPLICATION_INFO_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
+
+    public static boolean isSystemApp(Context context,String packageName) {
+        if (packageName == null || packageName.isEmpty()) {
+            return false;
+        }
+        if (SYSTEM_PACKAGES.contains(packageName)) {
+            return true;
+        }
+        if (Objects.equals(context.getPackageName(), packageName)) {
+            return false;
+        }
+        try {
+            ApplicationInfo applicationInfo = getApplicationInfo(context,packageName);
+            if (applicationInfo == null) {
+                return false;
+            }
+            boolean isSystemApp = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+            if (isSystemApp) {
+                SYSTEM_PACKAGES.add(packageName);
+                APPLICATION_INFO_CACHE.remove(packageName);
+            } else {
+                SYSTEM_PACKAGES.remove(packageName);
+                APPLICATION_INFO_CACHE.put(packageName, applicationInfo);
+            }
+            return isSystemApp;
+        } catch (Exception e) {
+            // If the package is not found, consider it not a system app
+            return false;
+        }
+    }
+
+    public static String getAppName(Context context,String packageName) {
+        if (packageName == null || packageName.isEmpty()) {
+            return "";
+        }
+        PackageManager packageManager = context.getPackageManager();
+        try {
+            ApplicationInfo applicationInfo = getApplicationInfo(context,packageName);
+            if (applicationInfo == null) {
+                return packageName;
+            }
+            return packageManager.getApplicationLabel(applicationInfo).toString();
+        } catch (Exception e) {
+            // If the package is not found, return null
+        }
+        return packageName;
+    }
+
+    public static Drawable getAppIcon(Context context,String packageName) {
+        if (packageName == null || packageName.isEmpty()) {
+            return null;
+        }
+        PackageManager packageManager = context.getPackageManager();
+        try {
+            ApplicationInfo applicationInfo = getApplicationInfo(context,packageName);
+            if (applicationInfo == null) {
+                return null;
+            }
+            return packageManager.getApplicationIcon(applicationInfo);
+        } catch (Exception e) {
+            // If the package is not found, return null
+            return null;
+        }
+    }
+
+    public static ApplicationInfo getApplicationInfo(Context context,String packageName) {
+        if (packageName == null || packageName.isEmpty()) {
+            return null;
+        }
+        try {
+            if (APPLICATION_INFO_CACHE.containsKey(packageName)) {
+                return APPLICATION_INFO_CACHE.get(packageName);
+            } else {
+                ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
+                APPLICATION_INFO_CACHE.put(packageName, applicationInfo);
+                return applicationInfo;
+            }
+        } catch (Exception e) {
+            // If the package is not found, return null
+            return null;
+        }
+    }
+}

+ 88 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/LogUtil.java

@@ -0,0 +1,88 @@
+package com.atmob.mobile_use_statistics.utils;
+
+import android.util.Log;
+
+public class LogUtil {
+
+    // 日志开关(默认开启)
+    private static boolean isLogEnabled = false;
+
+    private static final String TAG = "use_statistics_";
+
+    /**
+     * 设置日志开关
+     *
+     * @param enabled true 开启日志,false 关闭日志
+     */
+    public static void setLogEnabled(boolean enabled) {
+        isLogEnabled = enabled;
+    }
+
+    /**
+     * 打印 VERBOSE 级别日志
+     *
+     * @param msg 日志内容
+     */
+    public static void v(String tag, String msg) {
+        if (isLogEnabled) {
+            Log.v(TAG + tag, msg);
+        }
+    }
+
+    /**
+     * 打印 DEBUG 级别日志
+     *
+     * @param msg 日志内容
+     */
+    public static void d(String tag, String msg) {
+        if (isLogEnabled) {
+            Log.d(TAG + tag, msg);
+        }
+    }
+
+    /**
+     * 打印 INFO 级别日志
+     *
+     * @param msg 日志内容
+     */
+    public static void i(String tag, String msg) {
+        if (isLogEnabled) {
+            Log.i(TAG + tag, msg);
+        }
+    }
+
+    /**
+     * 打印 WARN 级别日志
+     *
+     * @param msg 日志内容
+     */
+    public static void w(String tag, String msg) {
+        if (isLogEnabled) {
+            Log.w(TAG + tag, msg);
+        }
+    }
+
+    /**
+     * 打印 ERROR 级别日志
+     *
+     * @param msg 日志内容
+     */
+    public static void e(String tag, String msg) {
+        if (isLogEnabled) {
+            Log.e(TAG + tag, msg);
+        }
+    }
+
+    /**
+     * 打印 ERROR 级别日志(带异常信息)
+     *
+     * @param msg 日志内容
+     * @param tr  异常信息
+     */
+    public static void e(String tag, String msg, Throwable tr) {
+        if (isLogEnabled) {
+            Log.e(TAG + tag, msg, tr);
+        }
+    }
+
+}

+ 100 - 0
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/ParamUtil.java

@@ -0,0 +1,100 @@
+package com.atmob.mobile_use_statistics.utils;
+
+import java.util.Map;
+
+import io.flutter.plugin.common.MethodCall;
+
+public class ParamUtil {
+
+
+    public static String getString(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        return (String) map.get(key);
+    }
+
+    public static Integer getInt(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (int) value;
+    }
+
+    public static Double getDouble(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (double) value;
+    }
+
+    public static double getDouble(Map<String, Object> map, String key, double defaultValue) {
+        Double d = getDouble(map, key);
+        if (d == null) {
+            return defaultValue;
+        }
+        return d;
+    }
+
+
+    public static Long getLong(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (long) value;
+    }
+
+    public static long getLong(Map<String, Object> map, String key, long defaultValue) {
+        Long l = getLong(map, key);
+        if (l == null) {
+            return defaultValue;
+        }
+        return l;
+    }
+
+    public static Boolean getBoolean(Map<String, Object> map, String key) {
+        if (map == null) {
+            return null;
+        }
+        if (!map.containsKey(key)) {
+            return null;
+        }
+        Object value = map.get(key);
+        if (value == null) {
+            return null;
+        }
+        return (boolean) value;
+    }
+
+    public static boolean getBoolean(Map<String, Object> map, String key, boolean defaultValue) {
+        Boolean b = getBoolean(map, key);
+        if (b == null) {
+            return defaultValue;
+        }
+        return b;
+    }
+}

+ 3 - 3
plugins/mobile_use_statistics/lib/src/mobile_use_statistics.dart

@@ -3,7 +3,7 @@ import 'package:mobile_use_statistics/src/event/event.dart';
 import 'mobile_use_statistics_platform_interface.dart';
 
 class MobileUseStatistics {
-  Future<List<Event>?> getLockScreenStatistics({
+  static Future<List<Event>?> getLockScreenStatistics({
     required int startTime,
     required int endTime,
   }) {
@@ -13,11 +13,11 @@ class MobileUseStatistics {
     );
   }
 
-  Future<bool> hasUseStatisticsPermission() {
+  static Future<bool> hasUseStatisticsPermission() {
     return MobileUseStatisticsPlatform.instance.hasUseStatisticsPermission();
   }
 
-  Future<bool> requestUseStatisticsPermission() {
+  static Future<bool> requestUseStatisticsPermission() {
     return MobileUseStatisticsPlatform.instance
         .requestUseStatisticsPermission();
   }

+ 4 - 2
plugins/mobile_use_statistics/lib/src/mobile_use_statistics_method_channel.dart

@@ -15,12 +15,14 @@ class MethodChannelMobileUseStatistics extends MobileUseStatisticsPlatform {
     required int startTime,
     required int endTime,
   }) async {
-    final result = await methodChannel.invokeMethod<List<dynamic>>(
+    final result = await methodChannel.invokeListMethod(
       'getLockScreenStatistics',
       <String, dynamic>{'startTime': startTime, 'endTime': endTime},
     );
 
-    return result?.map((e) => Event.fromJson(e as Map<String, dynamic>)).toList();
+    return result
+        ?.map((e) => Event.fromJson(Map<String, dynamic>.from(e as Map)))
+        .toList();
   }
 
   @override

+ 11 - 1
pubspec.yaml

@@ -148,6 +148,16 @@ dependencies:
   #视频播放器
   video_player: ^2.10.0
 
+  #手机使用情况
+  mobile_use_statistics:
+    path: plugins/mobile_use_statistics
+
+  #lottie
+  lottie: ^3.3.1
+
+  #获取手机电量
+  battery_plus: ^6.2.2
+
   ######################地图########################
   flutter_map:
     path: plugins/map
@@ -249,7 +259,7 @@ flutter:
 
   assets:
     - assets/images/
-    - assets/video/
+    - assets/anim/
 
   fonts:
     - family: OppoSans