Home | History | Annotate | Download | only in shadows
      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