検索
連載

ACTION_OUTSIDEが切り開くAndroidアプリ間連携の可能性実業務でちゃんと使えるAndroidアプリ開発入門(1)(2/4 ページ)

本連載では、バージョンの違いに左右されないスタンダードなアーキテクチャで、セキュリティやパーミッション、テストのしやすさ、開発効率の向上などを考慮した、実業務で使えるAndroidアプリ開発のノウハウを提供していきます。初回は、連載の今後を紹介し、アプリ間連携でさまざまなことができるACTION_OUTSIDEイベントの使い方を解説します。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

Android 5/6で変わるパーミッションの設定

 今回のアプリは以下3つのパーミッションを付与しています。

<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
    tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

Android 6以降のSYSTEM_ALERT_WINDOWとオーバーレイView

 SYSTEM_ALERT_WINDOWは、いつでもタッチイベントを取得できるように非表示のViewをWindowManagerに追加するために必要なパーミッションです。このパーミッションはAndroid 6未満とAndroid 6以降で挙動が異なります。

 Android 6未満の場合は、このパーミッションだけで問題なく動作しますが、Android 6以降はセキュリティが強化され、ユーザーの許可なしで常駐するオーバーレイViewを表示できなくなり、許可されていない状態で「WindowManager#addView(View, ViewGroup.LayoutParams)」を呼び出すと、「WindowManager$BadTokenException」が発生します。

 例外を回避するための実装については、コード説明部分で後述します。

Android 5以降のタスク一覧取得

 GET_TASKSはタスク一覧を取得するために必要なパーミッションです。この機能を利用して、フォアグラウンドのActivityが何のアプリであるかを判別します。ただし、Android 5以降では個人情報保護の観点から、サードパーティーアプリからタスク一覧取得メソッドである、「ActivityManager#getRunningTasks(int)」を呼び出しても期待通りの結果が得られません(自分自身のタスクであるか、それ以外のタスクであるかは判別可能です)。そのため、Android 5以降ではPACKAGE_USAGE_STATSパーミッションを用います。

使用履歴を取得するために必要なパーミッション

 PACKAGE_USAGE_STATSは使用履歴を取得するために必要なパーミッションです。使用履歴はタスク一覧取得よりも、さらに幅広く強力なクエリ検索機能を提供するため、ユーザーの同意を得てからでなければ、例外は発生しないものの履歴は取得できません。

 なお、このパーミッションだけ「tools:ignore」という属性が付与されているのは、付与しない場合、下図のようにAndroid Studioが警告を出すためです。

 警告の内容は「Permission is only granted to system apps」で、要約すると「システムアプリにのみ認められたパーミッション」ということです。

コラム「“パーミッション”の概念自体が、今後変わる可能性も」

 ここまでのパーミッションの説明で分かる通り、パーミッションを必要とするメソッドの振る舞いは、Androidのバージョンによって変わり得ます。特に個人情報やそれに準ずる情報を取得可能なメソッドにその傾向が多いようです。またパーミッションの定義自体がAndroidのバージョンアップでなくなってしまうことすらあります。

 今回のサンプルアプリはフォアグラウンドActivityがどのアプリであるかを特定したいという理由で警告まで出る「システムアプリにのみ認められた」パーミッションを使用していますが、このパーミッションを必要とするメソッドが将来にわたり同じ振る舞いをするかどうかは不明であり、強力なパーミッションを主軸とするアプリはそうした意味でリスクがあると認識しておいてください。


ActivityでAndroid 5/6の違いごとの処理を実装

 本稿のサンプルアプリのActivityは「MainActivity.java」に実装があります。処理の流れを追う形で実装を説明していきます。

 まずはstartService()です。

@TargetApi(Build.VERSION_CODES.M)
private void startService() {
    Intent intent = null;
    if (!canGetUsageStats()) {  // 【1】
        intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
    } else if (!canDrawOverlays()) {  // 【2】
        Uri uri = Uri.parse("package:" + getPackageName());
        intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, uri);
    }
    if (intent != null) {  // 【3】
        startActivityForResult(intent, REQUEST_SETTINGS);
        Toast.makeText(getApplicationContext(), "Please turn ON", Toast.LENGTH_SHORT).show();
    } else {
        startService(new Intent(MainActivity.this, MainService.class));
    }
}
startService()

 このアプリはユーザーに最大2つの設定をONにしてもらう必要があり、それら設定がONになっていればServiceを起動、そうでなければ設定をONにしてもらうために該当の画面を表示するIntentを発行します。

 まずは【1】で、Android 5以降で使用履歴にアクセス可能かどうかを判定する「canGetUsageStats()」という独自メソッドを呼び出し、アクセス不可なら、設定画面を開くIntentを作成します。次に【2】で、Android 6以降でオーバーレイViewが許可されているかどうかを判定する「canDrawOverlays()」という独自メソッドを呼び出し、不許可なら設定画面を開くIntentを作成しています。

 【3】で設定のどちらかが許可されていないなら設定画面を開き、どちらも許可されているならオーバーレイViewを表示し、タッチイベントをハンドリングするServiceを起動します。

 なお、ACTION_USAGE_ACCESS_SETTINGSで以下の画面を表示します。

 ACTION_MANAGE_OVERLAY_PERMISSIONで以下の画面を表示します。

 これら画面はstartActivityForResult(Intent, int)で開き、アプリに戻ってきた際に、以下のようにstartService()の呼び出しをリトライしています。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_SETTINGS) {
        startService();
    }
}

使用履歴にアクセス可能かどうかを判定する「canGetUsageStats()」メソッド

 canGetUsageStats()を見てみます。

public boolean canGetUsageStats() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {  // 【1】
        return true;
    }
    AppOpsManager aom = (AppOpsManager) getSystemService(APP_OPS_SERVICE);
    int uid = android.os.Process.myUid();
    int mode = aom.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, uid, getPackageName());
    return mode == AppOpsManager.MODE_ALLOWED;
}
「canGetUsageStats()」メソッド

 Android 5未満であれば使用履歴を利用する必要はなく、また使用履歴機能自体も提供されていないので、【1】で判定してtrueを返します。この判定文がない場合、Android Studio上で警告が表示され、Android 5未満で実行すると例外が発生します。Android 5以降ならAppOpsManagerを取得し、自身のアプリが使用履歴にアクセス許可があるかどうかを上記のように判定します。

 なお、「AppOpsManager#checkOpNoThrow(String, int, String)」は状態に応じて以下の値を返します。

戻り値 意味
MODE_ALLOWED 指定された操作が許可されている
MODE_IGNORED 指定された操作は許可されておらず、かつ何事も無く失敗する(アプリはクラッシュしない)
MODE_ERRORED 指定された操作は許可されておらず、SecurityExceptionにより失敗する
MODE_DEFAULT デフォルトのセキュリティチェックが行われる(このモードは通常は利用されない)

オーバーレイViewが許可されているかどうかを判定する「canDrawOverlays()」メソッド

 canDrawOverlays()を見てみます。

private boolean canDrawOverlays() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {  // 【1】
        return true;
    }
    return Settings.canDrawOverlays(getApplicationContext());
}
「canDrawOverlays()」メソッド

 Android 6未満であればAndroidManifest.xmlのパーミッションだけでオーバーレイViewは実現でき、また「Settings.canDrawOverlays(Context)」メソッドやその設定画面も提供されていないため、【1】で判定してtrueを返します。canGetUsageStats()と同様、この判定文がない場合、Android Studio上で警告が表示され、Android 6未満で実行すると例外が発生します。

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る