自分のアプリが前面にいるのか知りたい
Androidでは、アプリの状態がいろいろとあります。
UIとしては基本的にActivityを単位として状態が遷移しますが、ホームボタンだけでなく戻るボタンがあったり、Taskという概念で同一タスクでも自分のアプリの画面でないこともあります。
通常は呼ばれるCallback関数を定義しておけば、想定されるライフサイクルに沿ってアプリの動きをコントロールできます。
しかし、ServiceやBroadcastReceiver等を用いて、Activityをともなわないロジック上で、自分のアプリがユーザに見える形で前面で表示されている(=フォアグラウンドで起動している)か、判別したい場合もあります。
今まであまりよくない方法で実装していたのを修正したのでメモしておきます。
前提
この方法は「自分のアプリが表示中か」の判別であって、「どのアプリが起動中か」ではありません。
本来は設計でUIとロジックを分離していたり、UIの状態はUIだけが知っていればいいようにすべきだと思います。
しかし、バックグラウンドのロジックからダイアログや画面を表示したいときなど、現在ユーザが見ている画面を知りたいと思います。
Androidの標準APIでは、自分のアプリケーションが現在、前面でユーザに見えている状態か、わかる方法がありません。
また、よく紹介されているActivityManager.getRunningTasksなどを用いた方法は、UIスレッドでは処理が重く、Lollipop以降では機能が制限されているため、推奨されません。
元ネタ
StackOverflowのこの回答が元ネタです。
Checking if an Android application is running in the background
正しい方法
自分のアプリの状態を判別するのであれば、Applicationクラスを実装して、そこで保持しておくのが一番いいようです。
ActivityLifecycleCallback(API14以降)
API14(ICS)以降ではActivityLifecycleCallbackとApplication.registerActivityLifecycleCallbackが追加されています。
これを実装することで、それぞれのActivityに記録する処理をしなくてもまとめて状態を記録しておくことができます。
// Applicationクラスを実装します。もちろんManifestに登録する必要があります。
public class MyApplication extends Application {
@Override
public void onCreate() {
// API14から追加されたinterfaceを登録することで、Activityに特別な記述は不要です。
registerActivityLifecycleCallbacks(new MyLifecycleHandler());
}
}
Lifecycleのリスナーを実装します。
Activityのライフサイクルの変化によって呼ばれるので、ここでカウントしておけば、実際のActivityの状態が確認できます。
それぞれのActivityに特に記述は不要ですが、superクラスのライフサイクルメソッドを呼んでおく必要があるかもしれません(未確認)。
// 実際にActivityのLifecycleが変わった時に呼ばれるinterfaceです。
public class MyLifecycleHandler implements ActivityLifecycleCallbacks {
// どんな方法でもいいですが、例としてintでcountしています。
private int resumed;
private int paused;
private int started;
private int stopped;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
++resumed;
}
@Override
public void onActivityPaused(Activity activity) {
++paused;
android.util.Log.w("test", "application is in foreground: " + (resumed > paused));
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityStarted(Activity activity) {
++started;
}
@Override
public void onActivityStopped(Activity activity) {
++stopped;
android.util.Log.w("test", "application is visible: " + (started > stopped));
}
// もし必要ならstaticにして参照できるようにすれば、Serviceなどから状態の確認ができます。
/*
private static int resumed;
private static int paused;
private static int started;
private static int stopped;
public static boolean isApplicationVisible() {
return started > stopped;
}
public static boolean isApplicationInForeground() {
return resumed > paused;
}
*/
}
こんなAPIが追加されていたなんて知りませんでした。
activityが返されるので、activityによってカウントするかを切り替えたり、他にも色々なことができそうですね。
API13以前に対応が必要な場合
(追記)
こちらの方がバックポートされているので、このライブラリを使えば、API14以降と同様に使えると思います。
ActivityLifecycleCallbacksをバックポートしました
https://github.com/esmasui/underdevelopment/tree/master/android-lifecyclecallbacks-support
Activityをライブラリのものを継承する必要があります。
自分でApplicationクラスに記録する(API13以前)
API13以前に対応する必要がある場合で、サポートライブラリを使わない場合は、以下のようにできます。
Applicationクラスに記録用の関数を追加します。
// もちろんManifestに登録してください。
public class MyApplication extends Application {
public static boolean isActivityVisible() {
return activityVisible;
}
public static void activityResumed() {
activityVisible = true;
}
public static void activityPaused() {
activityVisible = false;
}
private static boolean activityVisible;
}
また、すべてのActivityから記録用の関数を呼びます。
例としてBaseActivityで記述しておけば、ちゃんと継承していけば問題ないですね。
// BaseActivityじゃなくてもいいですが、Applicationクラスのメソッドを呼んでカウントします。
@Override
protected void onResume() {
super.onResume();
MyApplication.activityResumed();
}
@Override
protected void onPause() {
super.onPause();
MyApplication.activityPaused();
}
こちらの方法の方が若干煩雑ですが、やっていることは変わらないと思います。
注意点
この方法の注意点として、プロセスが複数にわたる場合はApplicationクラスもそれぞれのプロセスにインスタンスを持つことになるため、異なるプロセス間では確認ができないと思われます。
ServiceとActivityでプロセスを分割している場合は、それぞれをbindするとか他の方法を考える必要があると思います。
非推奨な方法
時間がかかる処理のため、UIスレッドで行うには非推奨とされている方法です。
Lollipopでは制約が大きくなったため、ますますなるべく使わないほうがよくなったと言えるでしょう。
ActivityManager.getRunningTasksなどを使う(API20以前)
Lollipop以前では、ActivityManager.getRunningTasksなどで、他のアプリのTaskも取得することができたため、よくこの方法が見られました。
この方法を避けたい理由は以下です。
- ファイルI/Oを伴うクエリのため、UIスレッドではなるべく避けたい
- android.permission.GET_TASKSというpermissionが必要
public static boolean isMe(@NonNull Context con) {
ActivityManager am = (ActivityManager) con.getSystemService(Context.ACTIVITY_SERVICE);
// getRunningTasksやgetRecentTasksを使って直前の状態を取得します。
List<ActivityManager.RunningTaskInfo> list = am.getRunningTasks(1);
ActivityManager.RunningTaskInfo topTask = list.get(0);
ComponentName topActivity = topTask.topActivity;
return con.getPackageName().equals(topActivity.getPackageName());
}
例外処理は特にやっていませんが、割と簡単だったので結構使っていたと思います
UsageStatsManager.queryEventsを使う(API21以降/要Permission)
Lollipopでは上の方法は、android.permission.GET_TASKS自体がDeprecatedになり、内部的にandroid.permission.REAL_GET_TASKSというsystemOrSignatureな権限に変わりました。
そしてAcitivityManager.getRunningTasks自体もDeprecatedになり、自アプリ(とランチャーアプリ)以外のタスクは取得できなくなりました。
代替としてはUsageStatsManagerを使うことになっています
【Android】getRunningTasksが使えなくなったLollipopでアプリ使用状況を取得する
しかし、こちらも権限の制約が大きく、
- android.permission.PACKAGE_USAGE_STATS(systemOrSignature)をManifestに追加
- ユーザに設定アプリ->セキュリティから権限を許可してもらう
必要があります。
詳細はリンク先を参考にしてください。
まとめ
- 自分のアプリが前面かどうかを判別するなら、Applicationクラスでカウントするのが良い。
- 複数プロセスを使っている場合は注意が必要。
- 他アプリを含めた前面アプリを判別するのはLollipopからは難しい。
以上です。
他に問題やもっといい方法があったら教えてください。