@capgo/capacitor-android-usagestatsmanager 8.1.2 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ The only plugin exposing Android's **UsageStatsManager API** to Capacitor - this
15
15
 
16
16
  - **App usage tracking** - Monitor which apps users open and for how long
17
17
  - **Screen time analytics** - Build parental controls and digital wellbeing features
18
+ - **Exact-window events** - Query raw lifecycle events when you need foreground time clipped to a specific range
18
19
  - **Package information** - Query all installed apps on the device
19
20
  - **Time-based queries** - Get usage stats for any time range
20
21
 
@@ -74,6 +75,8 @@ npx cap sync
74
75
  <docgen-index>
75
76
 
76
77
  * [`queryAndAggregateUsageStats(...)`](#queryandaggregateusagestats)
78
+ * [`queryUsageStats(...)`](#queryusagestats)
79
+ * [`queryEvents(...)`](#queryevents)
77
80
  * [`isUsageStatsPermissionGranted()`](#isusagestatspermissiongranted)
78
81
  * [`openUsageStatsSettings()`](#openusagestatssettings)
79
82
  * [`queryAllPackages(...)`](#queryallpackages)
@@ -96,6 +99,12 @@ queryAndAggregateUsageStats(options: UsageStatsOptions) => Promise<Record<string
96
99
 
97
100
  Queries and aggregates usage stats for the given time range.
98
101
 
102
+ Android reads pre-aggregated daily/weekly/monthly/yearly buckets and sums
103
+ every bucket that intersects `[beginTime, endTime)`, without clipping to it.
104
+ `totalTimeInForeground` can therefore include usage from outside the window.
105
+ Use `queryUsageStats` for the unmerged per-interval buckets, or `queryEvents`
106
+ for timestamped lifecycle events in the requested range.
107
+
99
108
  | Param | Type | Description |
100
109
  | ------------- | --------------------------------------------------------------- | -------------------------------------- |
101
110
  | **`options`** | <code><a href="#usagestatsoptions">UsageStatsOptions</a></code> | - The time range options for the query |
@@ -107,6 +116,86 @@ Queries and aggregates usage stats for the given time range.
107
116
  --------------------
108
117
 
109
118
 
119
+ ### queryUsageStats(...)
120
+
121
+ ```typescript
122
+ queryUsageStats(options: QueryUsageStatsOptions) => Promise<QueryUsageStatsResult>
123
+ ```
124
+
125
+ Queries usage stats for the given interval type and time range.
126
+
127
+ Wraps Android `UsageStatsManager.queryUsageStats`. Unlike
128
+ `queryAndAggregateUsageStats`, this does not merge buckets: the result is
129
+ one <a href="#usagestats">`UsageStats`</a> object per package per overlapping interval. Android may
130
+ expand `[beginTime, endTime)` to the nearest whole interval period, so
131
+ `totalTimeInForeground` can include usage from outside the window.
132
+
133
+ On Android R (API 30) and above, the OS returns no data while the user is
134
+ locked; this plugin then resolves `{ stats: [] }`, which must not be treated
135
+ as zero foreground usage.
136
+
137
+ This uses the same `PACKAGE_USAGE_STATS` permission as
138
+ `queryAndAggregateUsageStats`.
139
+
140
+ | Param | Type | Description |
141
+ | ------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------ |
142
+ | **`options`** | <code><a href="#queryusagestatsoptions">QueryUsageStatsOptions</a></code> | - The interval type, time range, and optional package filter |
143
+
144
+ **Returns:** <code>Promise&lt;<a href="#queryusagestatsresult">QueryUsageStatsResult</a>&gt;</code>
145
+
146
+ **Since:** 8.1.3
147
+
148
+ --------------------
149
+
150
+
151
+ ### queryEvents(...)
152
+
153
+ ```typescript
154
+ queryEvents(options: QueryEventsOptions) => Promise<QueryEventsResult>
155
+ ```
156
+
157
+ Queries the raw usage event log for the given time range.
158
+
159
+ Returns lifecycle events whose timestamps fall in `[beginTime, endTime)`.
160
+ Android does not emit a synthetic event for "already in foreground at
161
+ beginTime" or "still in foreground at endTime". To measure duration across
162
+ those boundaries, pass an earlier `beginTime` as lookback and clip locally:
163
+ treat an unmatched pause as starting at the window start, and an unmatched
164
+ resume as ending at the window end.
165
+
166
+ Android retains events for only a few days. Older ranges may return an
167
+ incomplete or empty list; that is not the same as zero foreground usage.
168
+ On Android R (API 30) and above, the OS also returns no events while the
169
+ user is locked; this plugin then resolves `{ events: [] }`, which likewise
170
+ must not be treated as zero foreground usage.
171
+
172
+ Callers can sum resumed-to-paused intervals from the returned events.
173
+ Do not treat `DEVICE_SHUTDOWN` as a pause: it is a reset marker, not an
174
+ interval close. This uses the same `PACKAGE_USAGE_STATS` permission as
175
+ `queryAndAggregateUsageStats`.
176
+
177
+ Only lifecycle events are returned, to keep the bridge payload small:
178
+ - `1` — ACTIVITY_RESUMED / MOVE_TO_FOREGROUND
179
+ - `2` — ACTIVITY_PAUSED / MOVE_TO_BACKGROUND
180
+ - `23` — ACTIVITY_STOPPED
181
+ - `26` — DEVICE_SHUTDOWN (device-wide reset marker; still returned when
182
+ `packageName` is set. Android typically reports package `"android"`.
183
+ `packageName` is omitted if the OS does not attach one. The timestamp
184
+ is the last <a href="#usagestats">UsageStats</a> persist before shutdown, not the actual
185
+ power-off. Open resume events without a matching pause between this
186
+ marker and the next boot have unknown duration and must be ignored.)
187
+
188
+ | Param | Type | Description |
189
+ | ------------- | ----------------------------------------------------------------- | -------------------------------------------- |
190
+ | **`options`** | <code><a href="#queryeventsoptions">QueryEventsOptions</a></code> | - The time range and optional package filter |
191
+
192
+ **Returns:** <code>Promise&lt;<a href="#queryeventsresult">QueryEventsResult</a>&gt;</code>
193
+
194
+ **Since:** 8.1.3
195
+
196
+ --------------------
197
+
198
+
110
199
  ### isUsageStatsPermissionGranted()
111
200
 
112
201
  ```typescript
@@ -196,10 +285,76 @@ Usage statistics for an Android app.
196
285
 
197
286
  Options for querying usage statistics.
198
287
 
199
- | Prop | Type | Description |
200
- | --------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
201
- | **`beginTime`** | <code>number</code> | The inclusive beginning of the range of stats to include in the results. Defined in terms of "Unix time" |
202
- | **`endTime`** | <code>number</code> | The exclusive end of the range of stats to include in the results. Defined in terms of "Unix time" |
288
+ | Prop | Type | Description | Since |
289
+ | ----------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
290
+ | **`beginTime`** | <code>number</code> | The inclusive beginning of the range of stats to include in the results. Defined in terms of "Unix time" | |
291
+ | **`endTime`** | <code>number</code> | The exclusive end of the range of stats to include in the results. Defined in terms of "Unix time" | |
292
+ | **`packageName`** | <code>string</code> | Optional package name. When set, only stats for this package are returned. Omit to return stats for every package (previous behavior). An empty string is rejected. | 8.1.3 |
293
+
294
+
295
+ #### QueryUsageStatsResult
296
+
297
+ Result of a `queryUsageStats` call.
298
+
299
+ | Prop | Type | Description |
300
+ | ----------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
301
+ | **`stats`** | <code>UsageStats[]</code> | Usage stats buckets in the requested range, ordered as returned by the OS. The same package can appear more than once when multiple intervals overlap. |
302
+
303
+
304
+ #### QueryUsageStatsOptions
305
+
306
+ Options for querying per-interval usage statistics.
307
+
308
+ | Prop | Type | Description |
309
+ | ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
310
+ | **`intervalType`** | <code>number</code> | Interval type from `android.app.usage.UsageStatsManager`: - `0` — INTERVAL_DAILY - `1` — INTERVAL_WEEKLY - `2` — INTERVAL_MONTHLY - `3` — INTERVAL_YEARLY - `4` — INTERVAL_BEST |
311
+ | **`beginTime`** | <code>number</code> | The inclusive beginning of the range of stats to include in the results. Defined in terms of "Unix time" |
312
+ | **`endTime`** | <code>number</code> | The exclusive end of the range of stats to include in the results. Defined in terms of "Unix time" |
313
+ | **`packageName`** | <code>string</code> | Optional package name. When set, only stats for this package are returned. An empty string is rejected. |
314
+
315
+
316
+ #### QueryEventsResult
317
+
318
+ Result of a `queryEvents` call.
319
+
320
+ | Prop | Type | Description |
321
+ | ------------ | ------------------------- | ----------------------------------------------------------------------------- |
322
+ | **`events`** | <code>UsageEvent[]</code> | Lifecycle usage events in the requested range, ordered as returned by the OS. |
323
+
324
+
325
+ #### UsageEvent
326
+
327
+ Represents a single usage event.
328
+
329
+ `queryEvents` currently populates `className`, `timeStamp`, and `eventType`.
330
+ `packageName` is set when Android attaches one (DEVICE_SHUTDOWN usually uses
331
+ `"android"`) and omitted otherwise. Other fields remain optional for
332
+ compatibility.
333
+
334
+ | Prop | Type | Description |
335
+ | --------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
336
+ | **`packageName`** | <code>string</code> | Package name of the app. Omitted when Android does not attach a package. DEVICE_SHUTDOWN typically uses `"android"`. |
337
+ | **`className`** | <code>string</code> | Class name (might be null) |
338
+ | **`timeStamp`** | <code>number</code> | Timestamp in milliseconds since epoch |
339
+ | **`eventType`** | <code>number</code> | Event type constant from `android.app.usage.UsageEvents.Event`. `queryEvents` returns lifecycle types only: - `1` — ACTIVITY_RESUMED / MOVE_TO_FOREGROUND - `2` — ACTIVITY_PAUSED / MOVE_TO_BACKGROUND - `23` — ACTIVITY_STOPPED - `26` — DEVICE_SHUTDOWN (reset marker, not an interval close) |
340
+ | **`configuration`** | <code>any</code> | Configuration object (requires API 28+) |
341
+ | **`shortcutId`** | <code>string</code> | Shortcut ID (requires API 28+) |
342
+ | **`standbyBucket`** | <code>number</code> | App standby bucket (requires API 28+) |
343
+ | **`notificationChannelId`** | <code>string</code> | Notification channel ID (requires API 29+) |
344
+ | **`instanceId`** | <code>number</code> | Instance ID (requires API 30+) |
345
+ | **`taskRootPackageName`** | <code>string</code> | Task root package name (requires API 31+) |
346
+ | **`taskRootClassName`** | <code>string</code> | Task root class name (requires API 31+) |
347
+
348
+
349
+ #### QueryEventsOptions
350
+
351
+ Options for querying the raw usage event log.
352
+
353
+ | Prop | Type | Description |
354
+ | ----------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
355
+ | **`beginTime`** | <code>number</code> | The inclusive beginning of the range of events to include in the results. Defined in terms of "Unix time" |
356
+ | **`endTime`** | <code>number</code> | The exclusive end of the range of events to include in the results. Defined in terms of "Unix time" |
357
+ | **`packageName`** | <code>string</code> | Optional package name. When set, only events for this package are returned, plus device-wide `DEVICE_SHUTDOWN` events. An empty string is rejected. Keeps the Capacitor bridge payload small when you care about one app. |
203
358
 
204
359
 
205
360
  #### UsageStatsPermissionResult
@@ -1,6 +1,7 @@
1
1
  package ee.forgr.capacitor_android_usagestatsmanager;
2
2
 
3
3
  import android.app.AppOpsManager;
4
+ import android.app.usage.UsageEvents;
4
5
  import android.app.usage.UsageStats;
5
6
  import android.app.usage.UsageStatsManager;
6
7
  import android.content.Context;
@@ -17,6 +18,7 @@ import android.provider.Settings;
17
18
  import android.util.Base64;
18
19
  import android.util.Log;
19
20
  import androidx.annotation.NonNull;
21
+ import com.getcapacitor.JSArray;
20
22
  import com.getcapacitor.JSObject;
21
23
  import com.getcapacitor.Plugin;
22
24
  import com.getcapacitor.PluginCall;
@@ -31,8 +33,15 @@ import java.util.Map;
31
33
  @CapacitorPlugin(name = "CapacitorUsageStatsManager")
32
34
  public class CapacitorUsageStatsManagerPlugin extends Plugin {
33
35
 
34
- private final String pluginVersion = "8.1.2";
36
+ private final String pluginVersion = "8.2.0";
35
37
 
38
+ /**
39
+ * Queries and aggregates usage stats for the given time range.
40
+ * Optional {@code packageName} limits the result to one package; an empty
41
+ * string is rejected. Omit the field to return every package.
42
+ *
43
+ * @param call Capacitor plugin call with beginTime, endTime, and optional packageName
44
+ */
36
45
  @PluginMethod
37
46
  public void queryAndAggregateUsageStats(final PluginCall call) {
38
47
  // I cannot use the primitive long here because it would create an NPE
@@ -54,13 +63,23 @@ public class CapacitorUsageStatsManagerPlugin extends Plugin {
54
63
  return;
55
64
  }
56
65
 
66
+ // optional filter, avoids pushing every app's stats through the bridge
67
+ final String packageFilter = call.getString("packageName");
68
+ if (packageFilter != null && packageFilter.isEmpty()) {
69
+ call.reject("packageName is empty");
70
+ return;
71
+ }
72
+
57
73
  try {
58
74
  final UsageStatsManager usageStatsManager = (UsageStatsManager) this.getContext().getSystemService(Context.USAGE_STATS_SERVICE);
59
75
  final Map<String, UsageStats> response = usageStatsManager.queryAndAggregateUsageStats(beginTime, endTime);
60
76
  final JSObject finalResponse = new JSObject();
61
77
  for (final Map.Entry<String, UsageStats> stat : response.entrySet()) {
62
78
  final String key = stat.getKey();
63
- final JSObject encodedValue = getJsObject(stat);
79
+ if (packageFilter != null && !packageFilter.equals(key)) {
80
+ continue;
81
+ }
82
+ final JSObject encodedValue = getJsObject(stat.getValue());
64
83
  finalResponse.put(key, encodedValue);
65
84
  }
66
85
  call.resolve(finalResponse);
@@ -73,6 +92,193 @@ public class CapacitorUsageStatsManagerPlugin extends Plugin {
73
92
  }
74
93
  }
75
94
 
95
+ /**
96
+ * Queries usage stats for the given interval type and time range.
97
+ * Optional {@code packageName} limits the result to one package; an empty
98
+ * string is rejected. Resolves {@code stats: []} when Android returns null
99
+ * because the user is locked.
100
+ *
101
+ * @param call Capacitor plugin call with intervalType, beginTime, endTime,
102
+ * and optional packageName
103
+ */
104
+ @PluginMethod
105
+ public void queryUsageStats(final PluginCall call) {
106
+ final Integer intervalType = call.getInt("intervalType");
107
+ if (intervalType == null) {
108
+ call.reject("intervalType is null");
109
+ return;
110
+ }
111
+ if (
112
+ intervalType != UsageStatsManager.INTERVAL_DAILY &&
113
+ intervalType != UsageStatsManager.INTERVAL_WEEKLY &&
114
+ intervalType != UsageStatsManager.INTERVAL_MONTHLY &&
115
+ intervalType != UsageStatsManager.INTERVAL_YEARLY &&
116
+ intervalType != UsageStatsManager.INTERVAL_BEST
117
+ ) {
118
+ call.reject("intervalType is invalid");
119
+ return;
120
+ }
121
+
122
+ final Long beginTime = call.getLong("beginTime");
123
+ if (beginTime == null) {
124
+ call.reject("beginTime is null");
125
+ return;
126
+ }
127
+
128
+ final Long endTime = call.getLong("endTime");
129
+ if (endTime == null) {
130
+ call.reject("endTime is null");
131
+ return;
132
+ }
133
+
134
+ if (!this.checkUsageStatsPermission(this.getContext())) {
135
+ call.reject("Not allowed to query usage stats");
136
+ return;
137
+ }
138
+
139
+ final String packageFilter = call.getString("packageName");
140
+ if (packageFilter != null && packageFilter.isEmpty()) {
141
+ call.reject("packageName is empty");
142
+ return;
143
+ }
144
+
145
+ try {
146
+ final UsageStatsManager usageStatsManager = (UsageStatsManager) this.getContext().getSystemService(Context.USAGE_STATS_SERVICE);
147
+ final List<UsageStats> usageStats = usageStatsManager.queryUsageStats(intervalType, beginTime, endTime);
148
+ if (usageStats == null) {
149
+ // Android R+: queryUsageStats returns null when the user is locked.
150
+ final JSObject ret = new JSObject();
151
+ ret.put("stats", new JSArray());
152
+ call.resolve(ret);
153
+ return;
154
+ }
155
+
156
+ final JSArray result = new JSArray();
157
+ for (final UsageStats stat : usageStats) {
158
+ if (stat == null) {
159
+ continue;
160
+ }
161
+ final String packageName = stat.getPackageName();
162
+ if (packageName == null) {
163
+ continue;
164
+ }
165
+ if (packageFilter != null && !packageFilter.equals(packageName)) {
166
+ continue;
167
+ }
168
+ result.put(getJsObject(stat));
169
+ }
170
+
171
+ final JSObject ret = new JSObject();
172
+ ret.put("stats", result);
173
+ call.resolve(ret);
174
+ } catch (Throwable t) {
175
+ Log.e("CapacitorUsageStatsManager", "Error during queryUsageStats", t);
176
+ StringWriter sw = new StringWriter();
177
+ PrintWriter pw = new PrintWriter(sw);
178
+ t.printStackTrace(pw);
179
+ call.reject("Error during queryUsageStats: " + sw);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Queries the raw usage event log for {@code beginTime}..{@code endTime}.
185
+ * Returns lifecycle events only. Optional {@code packageName} filters the
186
+ * result; an empty string is rejected. {@code DEVICE_SHUTDOWN} is always
187
+ * included because it is a device-wide reset marker. Resolves {@code events: []} when
188
+ * Android returns null because the user is locked.
189
+ *
190
+ * @param call Capacitor plugin call with beginTime, endTime, and optional packageName
191
+ */
192
+ @PluginMethod
193
+ public void queryEvents(final PluginCall call) {
194
+ final Long beginTime = call.getLong("beginTime");
195
+ if (beginTime == null) {
196
+ call.reject("beginTime is null");
197
+ return;
198
+ }
199
+
200
+ final Long endTime = call.getLong("endTime");
201
+ if (endTime == null) {
202
+ call.reject("endTime is null");
203
+ return;
204
+ }
205
+
206
+ if (!this.checkUsageStatsPermission(this.getContext())) {
207
+ call.reject("Not allowed to query usage stats");
208
+ return;
209
+ }
210
+
211
+ // optional filter, avoids pushing every app's events through the bridge
212
+ final String packageFilter = call.getString("packageName");
213
+ if (packageFilter != null && packageFilter.isEmpty()) {
214
+ call.reject("packageName is empty");
215
+ return;
216
+ }
217
+
218
+ try {
219
+ final UsageStatsManager usageStatsManager = (UsageStatsManager) this.getContext().getSystemService(Context.USAGE_STATS_SERVICE);
220
+ final UsageEvents events = usageStatsManager.queryEvents(beginTime, endTime);
221
+ if (events == null) {
222
+ // Android R+: queryEvents returns null when the user is locked.
223
+ final JSObject ret = new JSObject();
224
+ ret.put("events", new JSArray());
225
+ call.resolve(ret);
226
+ return;
227
+ }
228
+ final UsageEvents.Event event = new UsageEvents.Event();
229
+ final JSArray result = new JSArray();
230
+
231
+ while (events.hasNextEvent()) {
232
+ events.getNextEvent(event);
233
+
234
+ final int type = event.getEventType();
235
+ // ACTIVITY_RESUMED/PAUSED share values with pre-API-29 MOVE_TO_FOREGROUND/BACKGROUND (1/2).
236
+ if (
237
+ type != UsageEvents.Event.ACTIVITY_RESUMED &&
238
+ type != UsageEvents.Event.ACTIVITY_PAUSED &&
239
+ type != UsageEvents.Event.ACTIVITY_STOPPED &&
240
+ type != UsageEvents.Event.DEVICE_SHUTDOWN
241
+ ) {
242
+ continue;
243
+ }
244
+
245
+ final String eventPackage = event.getPackageName();
246
+ // DEVICE_SHUTDOWN is a device-wide reset marker. Android usually sets
247
+ // package "android"; keep it even when filtering to one app.
248
+ if (type != UsageEvents.Event.DEVICE_SHUTDOWN) {
249
+ if (eventPackage == null) {
250
+ continue;
251
+ }
252
+ if (packageFilter != null && !packageFilter.equals(eventPackage)) {
253
+ continue;
254
+ }
255
+ }
256
+
257
+ final JSObject e = new JSObject();
258
+ if (eventPackage != null) {
259
+ e.put("packageName", eventPackage);
260
+ }
261
+ final String className = event.getClassName();
262
+ if (className != null) {
263
+ e.put("className", className);
264
+ }
265
+ e.put("timeStamp", event.getTimeStamp());
266
+ e.put("eventType", type);
267
+ result.put(e);
268
+ }
269
+
270
+ final JSObject ret = new JSObject();
271
+ ret.put("events", result);
272
+ call.resolve(ret);
273
+ } catch (Throwable t) {
274
+ Log.e("CapacitorUsageStatsManager", "Error during queryEvents", t);
275
+ StringWriter sw = new StringWriter();
276
+ PrintWriter pw = new PrintWriter(sw);
277
+ t.printStackTrace(pw);
278
+ call.reject("Error during queryEvents: " + sw);
279
+ }
280
+ }
281
+
76
282
  @PluginMethod
77
283
  public void isUsageStatsPermissionGranted(final PluginCall call) {
78
284
  try {
@@ -181,8 +387,7 @@ public class CapacitorUsageStatsManagerPlugin extends Plugin {
181
387
  }
182
388
 
183
389
  @NonNull
184
- private static JSObject getJsObject(Map.Entry<String, UsageStats> stat) {
185
- final UsageStats value = stat.getValue();
390
+ private static JSObject getJsObject(final UsageStats value) {
186
391
  final JSObject encodedValue = new JSObject();
187
392
 
188
393
  encodedValue.put("firstTimeStamp", value.getFirstTimeStamp());