1 package org.robolectric.shadows; 2 3 import android.annotation.NonNull; 4 import android.app.PendingIntent; 5 import android.app.PendingIntent.CanceledException; 6 import android.app.usage.UsageEvents; 7 import android.app.usage.UsageEvents.Event; 8 import android.app.usage.UsageStats; 9 import android.app.usage.UsageStatsManager; 10 import android.app.usage.UsageStatsManager.StandbyBuckets; 11 import android.content.Intent; 12 import android.content.res.Configuration; 13 import android.os.Build; 14 import android.os.Parcel; 15 import android.util.ArraySet; 16 import com.google.common.collect.HashMultimap; 17 import com.google.common.collect.ImmutableList; 18 import com.google.common.collect.ImmutableMap; 19 import com.google.common.collect.Range; 20 import com.google.common.collect.SetMultimap; 21 import java.util.ArrayList; 22 import java.util.Arrays; 23 import java.util.Collection; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.TreeMap; 28 import java.util.concurrent.TimeUnit; 29 import org.robolectric.RuntimeEnvironment; 30 import org.robolectric.annotation.HiddenApi; 31 import org.robolectric.annotation.Implementation; 32 import org.robolectric.annotation.Implements; 33 import org.robolectric.annotation.Resetter; 34 35 /** Shadow of {@link UsageStatsManager}. */ 36 @Implements(value = UsageStatsManager.class, minSdk = Build.VERSION_CODES.LOLLIPOP) 37 public class ShadowUsageStatsManager { 38 private static @StandbyBuckets int currentAppStandbyBucket = 39 UsageStatsManager.STANDBY_BUCKET_ACTIVE; 40 private static final TreeMap<Long, Event> eventsByTimeStamp = new TreeMap<>(); 41 42 /** 43 * Keys {@link UsageStats} objects by intervalType (e.g. {@link 44 * UsageStatsManager#INTERVAL_WEEKLY}). 45 */ 46 private SetMultimap<Integer, UsageStats> usageStatsByIntervalType = HashMultimap.create(); 47 48 private static final Map<String, Integer> appStandbyBuckets = new HashMap<>(); 49 50 /** 51 * App usage observer registered via {@link UsageStatsManager#registerAppUsageObserver(int, 52 * String[], long, TimeUnit, PendingIntent)}. 53 */ 54 public static final class AppUsageObserver { 55 private final int observerId; 56 private final Collection<String> packageNames; 57 private final long timeLimit; 58 private final TimeUnit timeUnit; 59 private final PendingIntent callbackIntent; 60 61 public AppUsageObserver( 62 int observerId, 63 @NonNull Collection<String> packageNames, 64 long timeLimit, 65 @NonNull TimeUnit timeUnit, 66 @NonNull PendingIntent callbackIntent) { 67 this.observerId = observerId; 68 this.packageNames = packageNames; 69 this.timeLimit = timeLimit; 70 this.timeUnit = timeUnit; 71 this.callbackIntent = callbackIntent; 72 } 73 74 public int getObserverId() { 75 return observerId; 76 } 77 78 @NonNull 79 public Collection<String> getPackageNames() { 80 return packageNames; 81 } 82 83 public long getTimeLimit() { 84 return timeLimit; 85 } 86 87 @NonNull 88 public TimeUnit getTimeUnit() { 89 return timeUnit; 90 } 91 92 @NonNull 93 public PendingIntent getCallbackIntent() { 94 return callbackIntent; 95 } 96 97 @Override 98 public boolean equals(Object o) { 99 if (this == o) { 100 return true; 101 } 102 if (o == null || getClass() != o.getClass()) { 103 return false; 104 } 105 AppUsageObserver that = (AppUsageObserver) o; 106 return observerId == that.observerId 107 && timeLimit == that.timeLimit 108 && packageNames.equals(that.packageNames) 109 && timeUnit == that.timeUnit 110 && callbackIntent.equals(that.callbackIntent); 111 } 112 113 @Override 114 public int hashCode() { 115 int result = observerId; 116 result = 31 * result + packageNames.hashCode(); 117 result = 31 * result + (int) (timeLimit ^ (timeLimit >>> 32)); 118 result = 31 * result + timeUnit.hashCode(); 119 result = 31 * result + callbackIntent.hashCode(); 120 return result; 121 } 122 } 123 124 private static final Map<Integer, AppUsageObserver> appUsageObserversById = new HashMap<>(); 125 126 @Implementation 127 protected UsageEvents queryEvents(long beginTime, long endTime) { 128 List<Event> results = 129 ImmutableList.copyOf(eventsByTimeStamp.subMap(beginTime, endTime).values()); 130 131 ArraySet<String> names = new ArraySet<>(); 132 for (Event result : results) { 133 names.add(result.mPackage); 134 if (result.mClass != null) { 135 names.add(result.mClass); 136 } 137 } 138 139 String[] table = names.toArray(new String[0]); 140 Arrays.sort(table); 141 142 // We can't directly construct usable UsageEvents, so we replicate what the framework does: 143 // First the system marshalls the usage events into a Parcel. 144 UsageEvents usageEvents = new UsageEvents(results, table); 145 Parcel parcel = Parcel.obtain(); 146 usageEvents.writeToParcel(parcel, 0); 147 // Then the app unmarshalls the usage events from the Parcel. 148 parcel.setDataPosition(0); 149 return new UsageEvents(parcel); 150 } 151 152 /** 153 * Adds an event to be returned by {@link UsageStatsManager#queryEvents}. 154 * 155 * This method won't affect the results of {@link #queryUsageStats} method. 156 * 157 * @deprecated Use {@link #addEvent(Event)} and {@link EventBuilder} instead. 158 */ 159 @Deprecated 160 public void addEvent(String packageName, long timeStamp, int eventType) { 161 EventBuilder eventBuilder = 162 EventBuilder.buildEvent() 163 .setPackage(packageName) 164 .setTimeStamp(timeStamp) 165 .setEventType(eventType); 166 if (eventType == Event.CONFIGURATION_CHANGE) { 167 eventBuilder.setConfiguration(new Configuration()); 168 } 169 addEvent(eventBuilder.build()); 170 } 171 172 /** 173 * Adds an event to be returned by {@link UsageStatsManager#queryEvents}. 174 * 175 * This method won't affect the results of {@link #queryUsageStats} method. 176 * 177 * The {@link Event} can be built by {@link EventBuilder}. 178 */ 179 public void addEvent(Event event) { 180 eventsByTimeStamp.put(event.getTimeStamp(), event); 181 } 182 183 /** 184 * Simulates the operations done by the framework when there is a time change. If the time is 185 * changed, the timestamps of all existing usage events will be shifted by the same offset as the 186 * time change, in order to make sure they remain stable relative to the new time. 187 * 188 * This method won't affect the results of {@link #queryUsageStats} method. 189 * 190 * @param offsetToAddInMillis the offset to be applied to all events. For example, if {@code 191 * offsetInMillis} is 60,000, then all {@link Event}s will be shifted forward by 1 minute 192 * (into the future). Likewise, if {@code offsetInMillis} is -60,000, then all {@link Event}s 193 * will be shifted backward by 1 minute (into the past). 194 */ 195 public void simulateTimeChange(long offsetToAddInMillis) { 196 ImmutableMap.Builder<Long, Event> eventMapBuilder = ImmutableMap.builder(); 197 for (Event event : eventsByTimeStamp.values()) { 198 long newTimestamp = event.getTimeStamp() + offsetToAddInMillis; 199 eventMapBuilder.put( 200 newTimestamp, EventBuilder.fromEvent(event).setTimeStamp(newTimestamp).build()); 201 } 202 eventsByTimeStamp.putAll(eventMapBuilder.build()); 203 } 204 205 /** 206 * Returns aggregated UsageStats added by calling {@link #addUsageStats}. 207 * 208 * The real implementation creates these aggregated objects from individual {@link Event}. This 209 * aggregation logic is nontrivial, so the shadow implementation just returns the aggregate data 210 * added using {@link #addUsageStats}. 211 */ 212 @Implementation 213 protected List<UsageStats> queryUsageStats(int intervalType, long beginTime, long endTime) { 214 List<UsageStats> results = new ArrayList<>(); 215 Range<Long> queryRange = Range.closed(beginTime, endTime); 216 for (UsageStats stats : usageStatsByIntervalType.get(intervalType)) { 217 Range<Long> statsRange = Range.closed(stats.getFirstTimeStamp(), stats.getLastTimeStamp()); 218 if (queryRange.isConnected(statsRange)) { 219 results.add(stats); 220 } 221 } 222 return results; 223 } 224 225 /** 226 * Adds an aggregated {@code UsageStats} object, to be returned by {@link #queryUsageStats}. 227 * Construct these objects with {@link UsageStatsBuilder}, and set the firstTimestamp and 228 * lastTimestamp fields to make time filtering work in {@link #queryUsageStats}. 229 * 230 * @param intervalType An interval type constant, e.g. {@link UsageStatsManager#INTERVAL_WEEKLY}. 231 */ 232 public void addUsageStats(int intervalType, UsageStats stats) { 233 usageStatsByIntervalType.put(intervalType, stats); 234 } 235 236 /** 237 * Returns the current standby bucket of the specified app that is set by {@code 238 * setAppStandbyBucket}. If the standby bucket value has never been set, return {@link 239 * UsageStatsManager.STANDBY_BUCKET_ACTIVE}. 240 */ 241 @Implementation(minSdk = Build.VERSION_CODES.P) 242 @HiddenApi 243 public @StandbyBuckets int getAppStandbyBucket(String packageName) { 244 Integer bucket = appStandbyBuckets.get(packageName); 245 return (bucket == null) ? UsageStatsManager.STANDBY_BUCKET_ACTIVE : bucket; 246 } 247 248 @Implementation(minSdk = Build.VERSION_CODES.P) 249 @HiddenApi 250 public Map<String, Integer> getAppStandbyBuckets() { 251 return new HashMap<>(appStandbyBuckets); 252 } 253 254 /** Sets the standby bucket of the specified app. */ 255 @Implementation(minSdk = Build.VERSION_CODES.P) 256 @HiddenApi 257 public void setAppStandbyBucket(String packageName, @StandbyBuckets int bucket) { 258 appStandbyBuckets.put(packageName, bucket); 259 } 260 261 @Implementation(minSdk = Build.VERSION_CODES.P) 262 @HiddenApi 263 public void setAppStandbyBuckets(Map<String, Integer> appBuckets) { 264 appStandbyBuckets.putAll(appBuckets); 265 } 266 267 @Implementation(minSdk = Build.VERSION_CODES.P) 268 @HiddenApi 269 protected void registerAppUsageObserver( 270 int observerId, 271 String[] packages, 272 long timeLimit, 273 TimeUnit timeUnit, 274 PendingIntent callbackIntent) { 275 appUsageObserversById.put( 276 observerId, 277 new AppUsageObserver( 278 observerId, ImmutableList.copyOf(packages), timeLimit, timeUnit, callbackIntent)); 279 } 280 281 @Implementation(minSdk = Build.VERSION_CODES.P) 282 @HiddenApi 283 protected void unregisterAppUsageObserver(int observerId) { 284 appUsageObserversById.remove(observerId); 285 } 286 287 /** Returns the {@link AppUsageObserver}s currently registered in {@link UsageStatsManager}. */ 288 public Collection<AppUsageObserver> getRegisteredAppUsageObservers() { 289 return ImmutableList.copyOf(appUsageObserversById.values()); 290 } 291 292 /** 293 * Triggers a currently registered {@link AppUsageObserver} with {@code observerId}. 294 * 295 * The observer will be no longer registered afterwards. 296 */ 297 public void triggerRegisteredAppUsageObserver(int observerId, long timeUsedInMillis) { 298 AppUsageObserver observer = appUsageObserversById.remove(observerId); 299 long timeLimitInMillis = observer.timeUnit.toMillis(observer.timeLimit); 300 Intent intent = 301 new Intent() 302 .putExtra(UsageStatsManager.EXTRA_OBSERVER_ID, observerId) 303 .putExtra(UsageStatsManager.EXTRA_TIME_LIMIT, timeLimitInMillis) 304 .putExtra(UsageStatsManager.EXTRA_TIME_USED, timeUsedInMillis); 305 try { 306 observer.callbackIntent.send(RuntimeEnvironment.application, 0, intent); 307 } catch (CanceledException e) { 308 throw new RuntimeException(e); 309 } 310 } 311 312 /** 313 * Returns the current app's standby bucket that is set by {@code setCurrentAppStandbyBucket}. If 314 * the standby bucket value has never been set, return {@link 315 * UsageStatsManager.STANDBY_BUCKET_ACTIVE}. 316 */ 317 @Implementation(minSdk = Build.VERSION_CODES.P) 318 @StandbyBuckets 319 protected int getAppStandbyBucket() { 320 return currentAppStandbyBucket; 321 } 322 323 /** Sets the current app's standby bucket */ 324 public void setCurrentAppStandbyBucket(@StandbyBuckets int bucket) { 325 currentAppStandbyBucket = bucket; 326 } 327 328 @Resetter 329 public static void reset() { 330 currentAppStandbyBucket = UsageStatsManager.STANDBY_BUCKET_ACTIVE; 331 eventsByTimeStamp.clear(); 332 333 appStandbyBuckets.clear(); 334 appUsageObserversById.clear(); 335 } 336 337 /** 338 * Builder for constructing {@link UsageStats} objects. The constructor of UsageStats is not part 339 * of the Android API. 340 */ 341 public static class UsageStatsBuilder { 342 private UsageStats usageStats = new UsageStats(); 343 344 // Use {@link #newBuilder} to construct builders. 345 private UsageStatsBuilder() {} 346 347 public static UsageStatsBuilder newBuilder() { 348 return new UsageStatsBuilder(); 349 } 350 351 public UsageStats build() { 352 return usageStats; 353 } 354 355 public UsageStatsBuilder setPackageName(String packageName) { 356 usageStats.mPackageName = packageName; 357 return this; 358 } 359 360 public UsageStatsBuilder setFirstTimeStamp(long firstTimeStamp) { 361 usageStats.mBeginTimeStamp = firstTimeStamp; 362 return this; 363 } 364 365 public UsageStatsBuilder setLastTimeStamp(long lastTimeStamp) { 366 usageStats.mEndTimeStamp = lastTimeStamp; 367 return this; 368 } 369 370 public UsageStatsBuilder setTotalTimeInForeground(long totalTimeInForeground) { 371 usageStats.mTotalTimeInForeground = totalTimeInForeground; 372 return this; 373 } 374 375 public UsageStatsBuilder setLastTimeUsed(long lastTimeUsed) { 376 usageStats.mLastTimeUsed = lastTimeUsed; 377 return this; 378 } 379 } 380 381 /** 382 * Builder for constructing {@link Event} objects. The fields of Event are not part of the Android 383 * API. 384 */ 385 public static class EventBuilder { 386 private Event event = new Event(); 387 388 private EventBuilder() {} 389 390 public static EventBuilder fromEvent(Event event) { 391 EventBuilder eventBuilder = 392 new EventBuilder() 393 .setPackage(event.mPackage) 394 .setClass(event.mClass) 395 .setTimeStamp(event.mTimeStamp) 396 .setEventType(event.mEventType) 397 .setConfiguration(event.mConfiguration); 398 if (event.mEventType == Event.CONFIGURATION_CHANGE) { 399 eventBuilder.setConfiguration(new Configuration()); 400 } 401 return eventBuilder; 402 } 403 404 public static EventBuilder buildEvent() { 405 return new EventBuilder(); 406 } 407 408 public Event build() { 409 return event; 410 } 411 412 public EventBuilder setPackage(String packageName) { 413 event.mPackage = packageName; 414 return this; 415 } 416 417 public EventBuilder setClass(String className) { 418 event.mClass = className; 419 return this; 420 } 421 422 public EventBuilder setTimeStamp(long timeStamp) { 423 event.mTimeStamp = timeStamp; 424 return this; 425 } 426 427 public EventBuilder setEventType(int eventType) { 428 event.mEventType = eventType; 429 return this; 430 } 431 432 public EventBuilder setConfiguration(Configuration configuration) { 433 event.mConfiguration = configuration; 434 return this; 435 } 436 437 public EventBuilder setShortcutId(String shortcutId) { 438 event.mShortcutId = shortcutId; 439 return this; 440 } 441 } 442 } 443