|
| 1 | +package io.justdice.usagestats |
| 2 | + |
| 3 | +import android.annotation.TargetApi |
| 4 | +import android.app.AppOpsManager |
| 5 | +import android.app.AppOpsManager.MODE_ALLOWED |
| 6 | +import android.app.AppOpsManager.OPSTR_GET_USAGE_STATS |
| 7 | +import android.app.usage.EventStats |
| 8 | +import android.app.usage.NetworkStats |
| 9 | +import android.app.usage.NetworkStatsManager |
| 10 | +import android.app.usage.UsageEvents |
| 11 | +import android.app.usage.UsageStats |
| 12 | +import android.app.usage.UsageStatsManager |
| 13 | +import android.content.Context |
| 14 | +import android.content.Intent |
| 15 | +import android.content.pm.PackageManager |
| 16 | +import android.net.ConnectivityManager |
| 17 | +import android.net.Uri |
| 18 | +import android.os.Build |
| 19 | +import android.os.Process |
| 20 | +import android.os.RemoteException |
| 21 | +import androidx.annotation.RequiresApi |
| 22 | +import com.facebook.react.bridge.Promise |
| 23 | +import com.facebook.react.bridge.ReactApplicationContext |
| 24 | +import com.facebook.react.bridge.WritableMap |
| 25 | +import com.facebook.react.bridge.WritableNativeArray |
| 26 | +import com.facebook.react.bridge.WritableNativeMap |
| 27 | +import com.facebook.react.common.MapBuilder |
| 28 | +import io.justdice.usagestats.model.AppData |
| 29 | +import io.justdice.usagestats.utils.UsageUtils |
| 30 | +import io.justdice.usagestats.utils.UsageUtils.humanReadableMillis |
| 31 | + |
| 32 | +/** |
| 33 | + * Shared implementation class containing all native logic. |
| 34 | + * Not a React module itself — logic is exposed through the arch-specific wrappers. |
| 35 | + */ |
| 36 | +class UsageStatsManagerModuleImpl(private val reactContext: ReactApplicationContext) { |
| 37 | + |
| 38 | + @RequiresApi(Build.VERSION_CODES.M) |
| 39 | + private var networkStatsManager: NetworkStatsManager? = |
| 40 | + reactContext.getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager |
| 41 | + |
| 42 | + fun getConstants(): Map<String, Any>? { |
| 43 | + val constants: MutableMap<String, Any> = MapBuilder.newHashMap() |
| 44 | + constants["INTERVAL_WEEKLY"] = UsageStatsManager.INTERVAL_WEEKLY |
| 45 | + constants["INTERVAL_MONTHLY"] = UsageStatsManager.INTERVAL_MONTHLY |
| 46 | + constants["INTERVAL_YEARLY"] = UsageStatsManager.INTERVAL_YEARLY |
| 47 | + constants["INTERVAL_DAILY"] = UsageStatsManager.INTERVAL_DAILY |
| 48 | + constants["INTERVAL_BEST"] = UsageStatsManager.INTERVAL_BEST |
| 49 | + constants["TYPE_WIFI"] = ConnectivityManager.TYPE_WIFI |
| 50 | + constants["TYPE_MOBILE"] = ConnectivityManager.TYPE_MOBILE |
| 51 | + constants["TYPE_MOBILE_AND_WIFI"] = Int.MAX_VALUE |
| 52 | + return constants |
| 53 | + } |
| 54 | + |
| 55 | + private fun packageExists(packageName: String): Boolean { |
| 56 | + return try { |
| 57 | + reactContext.packageManager.getApplicationInfo(packageName, 0) |
| 58 | + true |
| 59 | + } catch (e: PackageManager.NameNotFoundException) { |
| 60 | + false |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + fun showUsageAccessSettings(packageName: String) { |
| 65 | + val intent = Intent(android.provider.Settings.ACTION_USAGE_ACCESS_SETTINGS) |
| 66 | + if (packageExists(packageName)) { |
| 67 | + intent.data = Uri.fromParts("package", packageName, null) |
| 68 | + } |
| 69 | + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |
| 70 | + reactContext.startActivity(intent) |
| 71 | + } |
| 72 | + |
| 73 | + // interval is Double to match codegen spec (TurboModules codegen uses Double for numeric types) |
| 74 | + fun queryUsageStats(interval: Double, startTime: Double, endTime: Double, promise: Promise) { |
| 75 | + val packageManager: PackageManager = reactContext.packageManager |
| 76 | + val result: WritableNativeArray = WritableNativeArray() |
| 77 | + val usageStatsManager = |
| 78 | + reactContext.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager |
| 79 | + val queryUsageStats: List<UsageStats> = |
| 80 | + usageStatsManager.queryUsageStats(interval.toInt(), startTime.toLong(), endTime.toLong()) |
| 81 | + |
| 82 | + for (us in queryUsageStats) { |
| 83 | + // On API 29+ totalTimeInForeground is always 0 due to OS restrictions. |
| 84 | + // Use totalTimeVisible (API 29+) as the primary metric, fall back to totalTimeInForeground. |
| 85 | + val rawTime: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| 86 | + us.totalTimeVisible |
| 87 | + } else { |
| 88 | + us.totalTimeInForeground |
| 89 | + } |
| 90 | + |
| 91 | + if (rawTime > 0) { |
| 92 | + val usageStats: WritableMap = WritableNativeMap() |
| 93 | + usageStats.putString("packageName", us.packageName) |
| 94 | + val totalTimeInSeconds = rawTime / 1000.0 |
| 95 | + usageStats.putDouble("totalTimeInForeground", totalTimeInSeconds) |
| 96 | + usageStats.putDouble("totalTimeVisible", rawTime.toDouble()) |
| 97 | + usageStats.putDouble("firstTimeStamp", us.firstTimeStamp.toDouble()) |
| 98 | + usageStats.putDouble("lastTimeStamp", us.lastTimeStamp.toDouble()) |
| 99 | + usageStats.putDouble("lastTimeUsed", us.lastTimeUsed.toDouble()) |
| 100 | + usageStats.putBoolean("isSystem", UsageUtils.isSystemApp(packageManager, us.packageName)) |
| 101 | + usageStats.putString( |
| 102 | + "appName", |
| 103 | + UsageUtils.parsePackageName(packageManager, us.packageName.toString()).toString() |
| 104 | + ) |
| 105 | + result.pushMap(usageStats) |
| 106 | + } |
| 107 | + } |
| 108 | + promise.resolve(result) |
| 109 | + } |
| 110 | + |
| 111 | + @RequiresApi(Build.VERSION_CODES.LOLLIPOP_MR1) |
| 112 | + fun queryAndAggregateUsageStats(startTime: Double, endTime: Double, promise: Promise) { |
| 113 | + val packageManager: PackageManager = reactContext.packageManager |
| 114 | + val result: WritableNativeArray = WritableNativeArray() |
| 115 | + val usageStatsManager = |
| 116 | + reactContext.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager |
| 117 | + val queryUsageStats: MutableMap<String, UsageStats>? = |
| 118 | + usageStatsManager.queryAndAggregateUsageStats(startTime.toLong(), endTime.toLong()) |
| 119 | + |
| 120 | + if (queryUsageStats != null) { |
| 121 | + for (us in queryUsageStats.values) { |
| 122 | + // On API 29+ totalTimeInForeground is always 0 due to OS restrictions. |
| 123 | + val rawTime: Long = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| 124 | + us.totalTimeVisible |
| 125 | + } else { |
| 126 | + us.totalTimeInForeground |
| 127 | + } |
| 128 | + |
| 129 | + if (rawTime > 0) { |
| 130 | + val usageStats: WritableMap = WritableNativeMap() |
| 131 | + usageStats.putString("packageName", us.packageName) |
| 132 | + val totalTimeInSeconds = rawTime.toDouble() / 1000 |
| 133 | + usageStats.putDouble("totalTimeInForeground", totalTimeInSeconds) |
| 134 | + usageStats.putDouble("totalTimeVisible", rawTime.toDouble()) |
| 135 | + usageStats.putDouble("firstTimeStamp", us.firstTimeStamp.toDouble()) |
| 136 | + usageStats.putDouble("lastTimeStamp", us.lastTimeStamp.toDouble()) |
| 137 | + usageStats.putDouble("lastTimeUsed", us.lastTimeUsed.toDouble()) |
| 138 | + usageStats.putInt("describeContents", us.describeContents()) |
| 139 | + usageStats.putBoolean("isSystem", UsageUtils.isSystemApp(packageManager, us.packageName.toString())) |
| 140 | + usageStats.putString( |
| 141 | + "appName", |
| 142 | + UsageUtils.parsePackageName(packageManager, us.packageName.toString()).toString() |
| 143 | + ) |
| 144 | + result.pushMap(usageStats) |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + promise.resolve(result) |
| 149 | + } |
| 150 | + |
| 151 | + fun writeToWritableMap(mutableList: MutableList<AppData>): WritableMap { |
| 152 | + val writableMap: WritableMap = WritableNativeMap() |
| 153 | + for ((index, appData) in mutableList.withIndex()) { |
| 154 | + val appDataMap: WritableMap = WritableNativeMap() |
| 155 | + appDataMap.putString("name", appData.mName) |
| 156 | + appDataMap.putString("packageName", appData.mPackageName) |
| 157 | + appDataMap.putDouble("eventTime", appData.mEventTime.toDouble()) |
| 158 | + appDataMap.putDouble("usageTime", appData.mUsageTime.toDouble()) |
| 159 | + appDataMap.putString("humanReadableUsageTime", humanReadableMillis(appData.mUsageTime)) |
| 160 | + appDataMap.putInt("eventType", appData.mEventType) |
| 161 | + appDataMap.putInt("count", appData.mCount) |
| 162 | + appDataMap.putBoolean("isSystem", appData.mIsSystem) |
| 163 | + writableMap.putMap(index.toString(), appDataMap) |
| 164 | + } |
| 165 | + return writableMap |
| 166 | + } |
| 167 | + |
| 168 | + fun queryEvents(startTime: Double, endTime: Double, promise: Promise) { |
| 169 | + val result = WritableNativeArray() |
| 170 | + val manager = |
| 171 | + reactContext.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager |
| 172 | + val events: UsageEvents = manager.queryEvents(startTime.toLong(), endTime.toLong()) |
| 173 | + val event = UsageEvents.Event() |
| 174 | + while (events.hasNextEvent()) { |
| 175 | + events.getNextEvent(event) |
| 176 | + val _event: WritableMap = WritableNativeMap() |
| 177 | + _event.putInt("eventType", event.eventType) |
| 178 | + _event.putDouble("timeStamp", event.timeStamp.toDouble()) |
| 179 | + _event.putString("packageName", event.packageName) |
| 180 | + result.pushMap(_event) |
| 181 | + } |
| 182 | + promise.resolve(result) |
| 183 | + } |
| 184 | + |
| 185 | + // interval is Double to match codegen spec |
| 186 | + @RequiresApi(Build.VERSION_CODES.P) |
| 187 | + fun queryEventsStats(interval: Double, startTime: Double, endTime: Double, promise: Promise) { |
| 188 | + val result: WritableMap = WritableNativeMap() |
| 189 | + val usageStatsManager = |
| 190 | + reactContext.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager |
| 191 | + val queryUsageStats: MutableList<EventStats>? = |
| 192 | + usageStatsManager.queryEventStats(interval.toInt(), startTime.toLong(), endTime.toLong()) |
| 193 | + if (queryUsageStats != null) { |
| 194 | + for (us in queryUsageStats) { |
| 195 | + val usageStats: WritableMap = WritableNativeMap() |
| 196 | + usageStats.putDouble("firstTimeStamp", us.firstTimeStamp.toDouble()) |
| 197 | + usageStats.putDouble("lastTimeStamp", us.lastTimeStamp.toDouble()) |
| 198 | + usageStats.putDouble("lastTimeUsed", us.totalTime.toDouble()) |
| 199 | + usageStats.putInt("describeContents", us.describeContents()) |
| 200 | + result.putMap(us.eventType.toString(), usageStats) |
| 201 | + } |
| 202 | + } |
| 203 | + promise.resolve(result) |
| 204 | + } |
| 205 | + |
| 206 | + fun checkForPermission(promise: Promise) { |
| 207 | + val appOps = |
| 208 | + reactContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager |
| 209 | + val mode: Int = |
| 210 | + appOps.checkOpNoThrow( |
| 211 | + OPSTR_GET_USAGE_STATS, |
| 212 | + Process.myUid(), |
| 213 | + reactContext.packageName |
| 214 | + ) |
| 215 | + promise.resolve(mode == MODE_ALLOWED) |
| 216 | + } |
| 217 | + |
| 218 | + @TargetApi(Build.VERSION_CODES.M) |
| 219 | + private fun getDataUsage( |
| 220 | + networkType: Int, |
| 221 | + subscriberId: String?, |
| 222 | + packageUid: Int, |
| 223 | + startTime: Long, |
| 224 | + endTime: Long |
| 225 | + ): Double { |
| 226 | + var currentDataUsage = 0.0 |
| 227 | + try { |
| 228 | + val networkStatsByApp = |
| 229 | + networkStatsManager?.querySummary(networkType, subscriberId, startTime, endTime)!! |
| 230 | + do { |
| 231 | + val bucket = NetworkStats.Bucket() |
| 232 | + networkStatsByApp.getNextBucket(bucket) |
| 233 | + if (bucket.uid == packageUid) { |
| 234 | + currentDataUsage += bucket.rxBytes + bucket.txBytes |
| 235 | + } |
| 236 | + } while (networkStatsByApp.hasNextBucket()) |
| 237 | + } catch (e: RemoteException) { |
| 238 | + e.printStackTrace() |
| 239 | + } |
| 240 | + return currentDataUsage |
| 241 | + } |
| 242 | + |
| 243 | + // networkType is Double to match codegen spec |
| 244 | + fun getAppDataUsage( |
| 245 | + packageName: String, |
| 246 | + networkType: Double, |
| 247 | + startTime: Double, |
| 248 | + endTime: Double, |
| 249 | + promise: Promise |
| 250 | + ) { |
| 251 | + val uid = getAppUid(packageName) |
| 252 | + val netType = networkType.toInt() |
| 253 | + when { |
| 254 | + netType == ConnectivityManager.TYPE_MOBILE -> promise.resolve( |
| 255 | + getDataUsage(ConnectivityManager.TYPE_MOBILE, null, uid, startTime.toLong(), endTime.toLong()) |
| 256 | + ) |
| 257 | + netType == ConnectivityManager.TYPE_WIFI -> promise.resolve( |
| 258 | + getDataUsage(ConnectivityManager.TYPE_WIFI, "", uid, startTime.toLong(), endTime.toLong()) |
| 259 | + ) |
| 260 | + else -> promise.resolve( |
| 261 | + getDataUsage(ConnectivityManager.TYPE_MOBILE, "", uid, startTime.toLong(), endTime.toLong()) + |
| 262 | + getDataUsage(ConnectivityManager.TYPE_WIFI, "", uid, startTime.toLong(), endTime.toLong()) |
| 263 | + ) |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + private fun getAppUid(packageName: String): Int { |
| 268 | + return try { |
| 269 | + reactContext.packageManager.getApplicationInfo(packageName, 0).uid |
| 270 | + } catch (e: PackageManager.NameNotFoundException) { |
| 271 | + 0 |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + companion object { |
| 276 | + const val MODULE_NAME = "JustDiceReactNativeUsageStats" |
| 277 | + } |
| 278 | +} |
| 279 | + |
| 280 | +internal class ClonedEvent(event: UsageEvents.Event) { |
| 281 | + var packageName: String |
| 282 | + var eventClass: String |
| 283 | + var timeStamp: Long |
| 284 | + var eventType: Int |
| 285 | + |
| 286 | + init { |
| 287 | + packageName = event.packageName |
| 288 | + eventClass = event.className |
| 289 | + timeStamp = event.timeStamp |
| 290 | + eventType = event.eventType |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | + |
| 295 | + |
0 commit comments