Pārlūkot izejas kodu

[new]增加手机解锁屏以及手机电量事件上报功能

zk 9 mēneši atpakaļ
vecāks
revīzija
e672b4c782
34 mainītis faili ar 4286 papildinājumiem un 41 dzēšanām
  1. 0 0
      assets/anim/location_analyse_robot.mp4
  2. 2824 0
      assets/anim/location_label.json
  3. 9 0
      lib/data/api/atmob_api.dart
  4. 75 0
      lib/data/api/atmob_api.g.dart
  5. 15 0
      lib/data/api/request/electric_request.dart
  6. 69 0
      lib/data/api/request/electric_request.g.dart
  7. 16 0
      lib/data/api/request/location_phone_event_request.dart
  8. 73 0
      lib/data/api/request/location_phone_event_request.g.dart
  9. 22 0
      lib/data/bean/phone_event_bean.dart
  10. 19 0
      lib/data/bean/phone_event_bean.g.dart
  11. 11 4
      lib/data/repositories/account_repository.dart
  12. 20 1
      lib/data/repositories/config_repository.dart
  13. 1 1
      lib/data/repositories/friends_repository.dart
  14. 160 0
      lib/data/repositories/phone_event_repository.dart
  15. 11 8
      lib/di/get_it.config.dart
  16. 1 1
      lib/module/analyse/location_analyse_controller.dart
  17. 9 0
      lib/module/analyse/location_analyse_page.dart
  18. 18 0
      lib/module/permission/permission_setting_controller.dart
  19. 7 4
      lib/module/track/track_day_detail/track_day_detail_controller.dart
  20. 14 11
      lib/resource/assets.gen.dart
  21. 6 0
      lib/resource/string.gen.dart
  22. 0 5
      lib/utils/common_expand.dart
  23. 1 1
      plugins/mobile_use_statistics/android/build.gradle
  24. 1 0
      plugins/mobile_use_statistics/android/src/main/AndroidManifest.xml
  25. 37 2
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java
  26. 123 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/AppUsageInfo.java
  27. 118 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/bean/DeviceUsageInfo.java
  28. 118 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/FlutterUsageHelper.java
  29. 208 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/core/UsageStats.java
  30. 97 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/AppUsageUtil.java
  31. 88 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/LogUtil.java
  32. 100 0
      plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/utils/ParamUtil.java
  33. 4 2
      plugins/mobile_use_statistics/lib/src/mobile_use_statistics_method_channel.dart
  34. 11 1
      pubspec.yaml

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


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2824 - 0
assets/anim/location_label.json


+ 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>(),

+ 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);
+    }
+  }
 }

+ 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() {

+ 1 - 1
plugins/mobile_use_statistics/android/build.gradle

@@ -45,7 +45,7 @@ android {
     }
 
     defaultConfig {
-        minSdk = 21
+        minSdk = 22
     }
 
     dependencies {

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

@@ -1,4 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.atmob.mobile_use_statistics">
 
 

+ 37 - 2
plugins/mobile_use_statistics/android/src/main/java/com/atmob/mobile_use_statistics/MobileUseStatisticsPlugin.java

@@ -1,12 +1,19 @@
 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;
@@ -15,7 +22,7 @@ import io.flutter.plugin.common.MethodChannel.Result;
 /**
  * MobileUseStatisticsPlugin
  */
-public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandler {
+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
@@ -24,8 +31,11 @@ public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandl
 
     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);
@@ -35,7 +45,11 @@ public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandl
     public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
         String method = call.method;
         if (Objects.equals(method, "requestUseStatisticsPermission")) {
-            UsageStatsUtil.requestUseStatisticsPermission(context, call, result);
+            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();
         }
@@ -45,4 +59,25 @@ public class MobileUseStatisticsPlugin implements FlutterPlugin, MethodCallHandl
     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;
+    }
+}

+ 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