[Android][Lollipop]UsageStatsManagerを使ってみた



ながらく放置されていた(？)IUsageStatsがLollipop(API21)で進化していました

[【Android】getRunningTasksが使えなくなったLollipopでアプリ使用状況を取得する](http://qiita.com/kgmyshin/items/35b002e42df2027fb56e)

の記事をみて気付きました。
以前の使い方やこれからの使い方など自分用にメモしておきます。

# IUsageStatsって？

##設定アプリに画面があった

おそらく2.x系から実装されていた、アプリの使用履歴を取得するAPIです。
もともと設定アプリに[UsageStats](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android-apps/4.4.4_r1/com/android/settings/UsageStats.java#56)という画面が存在していて、その内部で使っていました。
ActivityManagerなど、OSの画面やアプリケーション管理の基盤部分に実装されていたので
結構重要で便利そうな仕組みでした。
しかし実際にアクセス可能な画面としては用意されておらず、普通のユーザは使えませんでした。

![UsageStats_image.png](https://qiita-image-store.s3.amazonaws.com/0/42594/b511a06c-ace0-ef9f-b896-f4bb94a37ccc.png "UsageStats_image.png")


こんなイメージです。


##実装はシンプル


結構実装はシンプルそうで、/data/system/usagestatsの中にxml形式？で
ActivityManager（[ActivityManagerService](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/com/android/server/am/ActivityManagerService.java#1982)）の通過した時のポイントなどを記録しているようです。
実態は[UsageStatsService](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.4_r1/com/android/server/am/UsageStatsService.java#UsageStatsService.0mDir)です。
ちゃんと追えていませんが、リブートしたり何かしらのタイミングで履歴が消えてしまいそうです。

##System権限がないと使えなかった

もっとも重要な点ですが、このAPIはSystem権限がないと使えませんでした！
一般開発者は、自分でAOSPをビルドしたりしてシステム権限をもったアプリを作れる場合のみ、利用できるAPIです。

`@hide`で隠蔽されていましたし、permissionチェックでsystemOrSignatureだったので、カスタムOSやメーカー、キャリアが使うための機能といえます。
しかも標準OSでは、設定アプリの画面から辿れなくなっていました。

なので、利用する機会も存在を意識する機会もほとんどありませんでした。

### 取得し方と取得の内容

メソッドの実行にはシステムアプリである必要がありますが、
一応リフレクション等でgetSystemService()を使えば参照を取得することはできました。
他のSystemServiceと違い、Managerクラスがなかったのでプロセス間通信用のstub？を直接参照する必要がありました。

リフレクションはServiceManagerから取得するのですが、こちらのブログを参考にしました
[システムサービスのローカルインターフェイスを取得する](http://d.hatena.ne.jp/esmasui/20091104/1257350421)

```java:Test.java
        // ActivityManagerServiceからはこんな風に呼ばれていました。
        mUsageStatsService = IUsageStats.Stub.asInterface(ServiceManager.getService("usagestats"));
        
        // リフレクションではこう呼んだり。。ServiceLocatorは上記ブログ参照。。
        mUsageStatsService = ServiceLocator.getServiceStub("usagestats",
                "com.android.internal.app.IUsageStats$Stub");
                
        // 関数のコールもリフレクション。。
        Class<?> clazz = Class.fromName("com.android.internal.app.IUsageStats$Stub");
        Method method = clazz.getDeclaredMethod("getAllPkgUsageStats");
        method.setAccessible(true);
        PkgUsageStats[] result = (PkgUsageStats[]) method.invoke(mUsageStatsService);
```

getAllPkgUsageStatsやgetPkgUsageStatsなどで下のようなクラスが取得できました。
アプリの起動回数と合計起動時間(スリープ時間は含まず)、Activityごとの起動時間？でした。

```java:android.internal.os.PkgUsageStats.java

/**
 * implementation of PkgUsageStats associated with an
 * application package.
 *  @hide
 */
public class PkgUsageStats implements Parcelable {
    public String packageName;
    public int launchCount;
    public long usageTime;
    public Map<String, Long> componentResumeTimes;
    
    public PkgUsageStats(String pkgName, int count, long time, Map<String, Long> lastResumeTimes) {
        packageName = pkgName;
        launchCount = count;
        usageTime = time;
        componentResumeTimes = new HashMap<String, Long>(lastResumeTimes);
    }
    
    // Parcelableの実装があります。。
}

```


# UsageStatsManagerが公開された

久々にIUsageStatsを漁ってみたところ、以前あった場所の`com.android.internal.app`以下に
Usage関連のクラスがなくなってしまっていました。
その代わりに新たに`android.app.usage`パッケージに移動されています。すごい格上げですね！

[UsageStatsManager | Android Developers](https://developer.android.com/reference/android/app/usage/UsageStatsManager.html)

ここからは冒頭の記事と同じですね。。
新たに`android.permission.PACKAGE_USAGE_STATS`が追加されているので、
AndroidManifest.xmlでこれを要求して、
かつ
設定アプリの`セキュリティ→使用履歴にアクセスできるアプリ`から許可すれば取得できるようになります。

## UsageStatsManager

詳しくはReferencesを見ていただければわかると思います。

以前のシンプルな実装ではなく何かしらのDBで管理してるようです。
すべての取得メソッドでstartとstopのタイムスタンプを指定します。
集計されるものはintervalTypeで日単位とか週単位など指定できます。

### UsageEvents.Event

[UsageEvents.Event | Android Developers](https://developer.android.com/reference/android/app/usage/UsageEvents.Event.html)

UsageEvents.Eventという単位で起動([MOVE_TO_FOREGROUND](https://developer.android.com/reference/android/app/usage/UsageEvents.Event.html#MOVE_TO_FOREGROUND))と終了([MOVE_TO_BACKGROUND](https://developer.android.com/reference/android/app/usage/UsageEvents.Event.html#MOVE_TO_BACKGROUND))を保存しているようです。
他にも[CONFIGURATION_CHANGE](https://developer.android.com/reference/android/app/usage/UsageEvents.Event.html#CONFIGURATION_CHANGE)もあります。
[UsageEvents](https://developer.android.com/reference/android/app/usage/UsageEvents.html)はEventのリストを保持しています。

```
// クラス名
getClassName()
// パッケージ名
getPackageName()
// イベントのタイムスタンプ
getTimeStamp()
// イベントのタイプ. MOVE_TO_FOREGROUND/MOVE_TO_BACKGROUND/CONFIGURATION_CHANGE/NONE
getEventType()
// Configuration. イベントタイプがCONFIGURATION_CHANGEのときのみ返す
getConfiguration()
```

ComponentName(クラス名とパッケージ名)とタイムスタンプが取得できるので、
細かいアプリ切替の動きなどは[UsageStatsManager.queryEvents](https://developer.android.com/reference/android/app/usage/UsageStatsManager.html#queryEvents(long,%20long))でEventリストを取得するのが良さそうです。


### ConfigurationStats

[ConfigurationStats | Android Developers](https://developer.android.com/reference/android/app/usage/ConfigurationStats.html)

ConfigurationStatsはおそらくEventのTypeがCONFIGURATION_CHANGEのものを集計したものだと思います。
いつイベントが発生してるのかとかはあまり調べてません。。

```
// 集計期間の開始時刻
getFirstTimeStamp()
// 集計期間の終了時刻
getLastTimeStamp()
// 最後に実行された時刻
getLastTimeActive()
// 期間内に実行された回数
getActivationCount()
// 期間内に実行された合計時間
getTotalTimeActive()
// Configuration
getConfiguration()
```

[UsageStatsManager.queryConfigurations](https://developer.android.com/reference/android/app/usage/UsageStatsManager.html#queryConfigurations(int,%20long,%20long))で指定の単位で集計されたリストが取得できます。

### UsageStats

[UsageStats | Android Developers](https://developer.android.com/reference/android/app/usage/UsageStats.html)
UsageStatsは以前のPkgUsageStatsに代わるもので、基本的にUsageEventsを集計したものと思います。

```
// 集計期間の開始時刻
getFirstTimeStamp()
// 集計期間の終了時刻
getLastTimeStamp()
// 最後に実行された時刻
getLastTimeUsed()
// 期間内に全面で実行された合計時間
getTotalTimeInForeground()
// パッケージ名
getPackageName()
```

[UsageStatsManager.queryUsageStats](https://developer.android.com/reference/android/app/usage/UsageStatsManager.html#queryUsageStats(int,%20long,%20long))で指定の単位で集計されたリストが取得できます。
また、[UsageStatsManager. queryAndAggregateUsageStats](https://developer.android.com/reference/android/app/usage/UsageStatsManager.html#queryAndAggregateUsageStats(long,%20long))ではINTERVAL_BESTという単位とアプリパッケージ名で集計されたマップが取得できます。

INTERVAL_BESTは取得する際に指定した期間によって自動で判別してくれるものと思います。
mapにするときに重複されないように一つのpackageが一度しか出ないようなintervalに動的に変更してくれるのでしょう。。

## 注意点

Permissionの取得が一番注意すべき点かもしれませんが、
自分が使ってみて遭遇した注意点をメモしておきます。

### UsageStats.getLastTimeStampが丸められることがある

自分が試したときにqueryUsageStatsでIntervalを指定した期間よりも長めにとってしまったときに、[UsageStats.getLastTimeStamp](https://developer.android.com/reference/android/app/usage/UsageStats.html#getLastTimeStamp())が丸められたような値を返したときがありました。

```java:

// 期間が1日なのに、intervalTypeを年単位にしてしまった
int intervalType = UsageStatsManager.INTERVAL_YEARLY;
long start = System.currentTimeMillis() - 24 * 60 * 60 * 1000;
long end = System.currentTimeMillis();

// ついてませんが、@Nullableです
List<UsageStats> usageStatsList = usageStatsManager.queryUsageStats(intervalType, start, end);

:
usageStatsList.get(0).getLastTimeStamp();
// 1430907218507
// firstTimeStampは大丈夫
usageStatsList.get(0).getFirstTimeStamp();
// 2295981492
// 変に丸められてる？
usageStatsList.get(1).getLastTimeStamp();
// 1430907218507
usageStatsList.get(1).getFirstTimeStamp();
// 999981492
// 変に丸められてる？
usageStatsList.get(2).getLastTimeStamp();
// 1430907218507
usageStatsList.get(2).getFirstTimeStamp();
// 1433218181823
// 大丈夫なときもある
:
                           
```

整数とか集計に詳しくないので、普通に起こり得ることなのかもしれませんが、注意したほうがいいかもしれませんね。

### UsageStats.getFirstTimeStampがJSTだと9:00基準。。
UsageStatsを取得したときに、指定期間より前からアプリのイベントがあるときは、カットされて集計されます。
そして集計単位は最低でもINTERVAL_DAILYです。
そのときの切りとり始めの時間が9:00 JSTになってしまいます。。
つまりGMT(もしくはUTC)で表示されていると思われます。

もしグラフや表で表示など、意図した単位が取得できない場合はUsageEvents.Eventを自分で取得して集計したほうが良さそうですね。。

### PkgUsageStatsにあったようなlaunchCountなどがない。

以前の非公開APIであるPkgUsageStatsとUsageStatsは対応しています。
しかしすべてではありません。。

```
// パッケージ名
PkgUsageStats.packageName -> UsageStats.getPackageName()
// 期間内の合計利用時間
PkgUsageStats.usageTime -> UsageStats.getTotalTimeInForeground()
// 期間内の起動回数
PkgUsageStats.launchCount -> なし
// 期間内のActivityごとの起動回数
PkgUsageStats.componentResumeTimes ->　なし
```

launchCountなどを使う必要がある場合はUsageEventsなどを集計する必要があります。

###最新履歴の取得は保証していない。

UsageStatsManager.queryEventsのリファレンスに注記があります。

```java:text
NOTE: The last few minutes of the event log will be truncated to prevent abuse by applications.
// 注：イベントログの最後の数分間は、アプリケーションによって濫用を防ぐために切り捨てられます。
```

とのことです。。
結構つらいですね。
始めに参照した記事のようにActivityManager.getRecentTasksの代わりに使うのはお勧めできないと思います。
ActivityManager.getAppTasksを使えと書いてありますね。。
（よく調べていません。）
大雑把な集計が欲しい場合ならUsageStatsManagerを使えるでしょう。

## まとめ

* 用意された集計APIは少し変なのでUsageStatsManager.queryEventsを使ったほうが良い場面もある
* 権限取得が必要
* アプリの最新履歴は取得できない可能性がある。(getRecentTaskの代替ではない)

主に集計方法について、いろいろな注意点があります。

起動中のタスクを取得できなくなると不便ですが、
今までは期間を指定できなかったり、そもそも使えなかったAPIだったので、
このようなAPIがあれば面白いアプリも作れそうですね。

アプリ起動履歴などの個人情報を送信しようとしてるアプリも判別しやすくなると思います。

あと、まだまだ未完成のAPIっぽいので今後も改善していくように思います。


# Mではアプリ設定からみれる？

[Android Mで「アプリ」セクションがリニューアル、より機能的に](http://juggly.cn/archives/149780.html)

こちらの記事を見るとデータ使用量の計測にアプリのステータスが表示されてるので、
もしかしたらこちらのAPIを[TrafficStats](http://developer.android.com/reference/android/net/TrafficStats.html)と併用して使っているのかもしれません。。