first commit
This commit is contained in:
31
android/app/src/main/AndroidManifest.xml
Normal file
31
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<application
|
||||
android:label="AutoTime Tracker"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.autotime.tracker
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
// MainActivity 实现
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.autotime.tracker
|
||||
|
||||
import android.app.AppOpsManager
|
||||
import android.app.usage.UsageStats
|
||||
import android.app.usage.UsageStatsManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.*
|
||||
|
||||
class TimeTrackingPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
private lateinit var context: Context
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "autotime_tracker/time_tracking")
|
||||
channel.setMethodCallHandler(this)
|
||||
context = flutterPluginBinding.applicationContext
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"hasPermission" -> hasPermission(result)
|
||||
"requestPermission" -> requestPermission(result)
|
||||
"getAppUsage" -> getAppUsage(call, result)
|
||||
"startBackgroundTracking" -> startBackgroundTracking(result)
|
||||
"stopBackgroundTracking" -> stopBackgroundTracking(result)
|
||||
"isBackgroundTrackingActive" -> isBackgroundTrackingActive(result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permission Methods
|
||||
|
||||
private fun hasPermission(result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode = appOps.checkOpNoThrow(
|
||||
AppOpsManager.OPSTR_GET_USAGE_STATS,
|
||||
android.os.Process.myUid(),
|
||||
context.packageName
|
||||
)
|
||||
result.success(mode == AppOpsManager.MODE_ALLOWED)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermission(result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
result.success(true)
|
||||
} else {
|
||||
result.error(
|
||||
"UNSUPPORTED_VERSION",
|
||||
"Usage Stats API requires Android 5.0 (API 21) or later",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Usage Methods
|
||||
|
||||
private fun getAppUsage(call: MethodCall, result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
result.error(
|
||||
"UNSUPPORTED_VERSION",
|
||||
"Usage Stats API requires Android 5.0 (API 21) or later",
|
||||
null
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val args = call.arguments as? Map<*, *>
|
||||
val startTimeMs = args?.get("startTime") as? Long
|
||||
val endTimeMs = args?.get("endTime") as? Long
|
||||
|
||||
if (startTimeMs == null || endTimeMs == null) {
|
||||
result.error("INVALID_ARGUMENTS", "Invalid arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (!hasUsageStatsPermission()) {
|
||||
result.error(
|
||||
"PERMISSION_DENIED",
|
||||
"Usage Stats permission not granted",
|
||||
null
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val usageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
|
||||
val startTime = startTimeMs
|
||||
val endTime = endTimeMs
|
||||
|
||||
// 查询应用使用统计
|
||||
val stats = usageStatsManager.queryUsageStats(
|
||||
UsageStatsManager.INTERVAL_DAILY,
|
||||
startTime,
|
||||
endTime
|
||||
)
|
||||
|
||||
if (stats == null || stats.isEmpty()) {
|
||||
result.success(emptyList<Map<String, Any>>())
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 Flutter 可用的格式
|
||||
val appUsageList = mutableListOf<Map<String, Any>>()
|
||||
|
||||
// 按包名聚合数据
|
||||
val packageMap = mutableMapOf<String, UsageStats>()
|
||||
for (stat in stats) {
|
||||
val packageName = stat.packageName
|
||||
val existing = packageMap[packageName]
|
||||
|
||||
if (existing == null || stat.totalTimeInForeground > existing.totalTimeInForeground) {
|
||||
packageMap[packageName] = stat
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为列表
|
||||
for ((packageName, stat) in packageMap) {
|
||||
val appName = getAppName(packageName)
|
||||
|
||||
appUsageList.add(mapOf(
|
||||
"packageName" to packageName,
|
||||
"appName" to appName,
|
||||
"startTime" to stat.firstTimeStamp,
|
||||
"endTime" to stat.lastTimeUsed,
|
||||
"duration" to (stat.totalTimeInForeground / 1000).toInt(), // 转换为秒
|
||||
"deviceUnlockCount" to 0 // Android 不直接提供此数据
|
||||
))
|
||||
}
|
||||
|
||||
result.success(appUsageList)
|
||||
}
|
||||
|
||||
private fun hasUsageStatsPermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode = appOps.checkOpNoThrow(
|
||||
AppOpsManager.OPSTR_GET_USAGE_STATS,
|
||||
android.os.Process.myUid(),
|
||||
context.packageName
|
||||
)
|
||||
return mode == AppOpsManager.MODE_ALLOWED
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getAppName(packageName: String): String {
|
||||
return try {
|
||||
val packageManager = context.packageManager
|
||||
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
packageManager.getApplicationLabel(applicationInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
packageName
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Background Tracking Methods
|
||||
|
||||
private fun startBackgroundTracking(result: MethodChannel.Result) {
|
||||
// Android 后台追踪实现
|
||||
// 可以使用 WorkManager 或 Foreground Service
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun stopBackgroundTracking(result: MethodChannel.Result) {
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun isBackgroundTrackingActive(result: MethodChannel.Result) {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user