Help us understand the problem. What is going on with this article?

どうしてAndroid 6.0でBLEを使うのに位置情報のパーミッションが必要なワケ?

More than 1 year has passed since last update.

はじめに

Android6.0(Marshmallow)から、BLEを使うアプリにはandroid.permission.BLUETOOTHandroid.permission.BLUETOOTH_ADMINに加えて、android.permission.ACCESS_COARSE_LOCATIONまたはandroid.permission.ACCESS_FINE_LOCATIONのパーミッションが必要となりました。1,2

皆さん、ご存知でしたか?

私は知りませんでした。知らずにNexus5をバージョンアップしたら、自作アプリでBLEデバイスに接続できなくなって年始早々慌てました。

で、悔しいので、ちょっとソース眺めてみたよ、という記録です。

なお、ここに記載されている内容の正確性は保証できません。間違い等に気がつかれた場合は、コメントでご指摘いただけると助かります。

結論

長いので先に結論を書いておきます。


GattService.javastartScanに位置情報パーミッションの有無をチェックするコードが追加されており、位置情報のパーミッシヨンが与えられていないときはSecurityExceptionが投げられてスキャンに失敗する。


事象

Android6.0でBLEのスキャンを行う際に、ACCESS_COARSE_LOCATIONまたはACCESS_FINE_LOCAITONのいずれかのパーミッションがないと、デバイスが検出されません。

アプリが異常終了したり、端末に何かメッセージが表示されたりはしませんでした。

なので、非常にわかりづらい。ちょうどデバイス側のファームウェアを変えていたところだったので、てっきりBLEデバイスをダメにしたかと焦りまくりです。

logcat

とはいえ、logcatにはちゃんと以下のようなエラーが記録されます。というか、logcatにしか出ません。

W/Binder: Caught a RuntimeException from the binder stub implementation.
    java.lang.SecurityException: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to get scan results
    at android.os.Parcel.readException(Parcel.java:1620)
    at android.os.Parcel.readException(Parcel.java:1573)
    at android.bluetooth.IBluetoothGatt$Stub$Proxy.startScan(IBluetoothGatt.java:772)
    at android.bluetooth.le.BluetoothLeScanner$BleScanCallbackWrapper.onClientRegistered(BluetoothLeScanner.java:324)
    at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:56)
    at android.os.Binder.execTransact(Binder.java:453)

とりあえず、Bluetooth周りということで、BluetoothLeScanner.javaから当たってみます。

BluetoothLeScanner.java

エラーが発生しているところを見てみましょう。

private class BleScanCallbackWrapper extends BluetoothGattCallbackWrapper {
    private IBluetoothGatt mBluetoothGatt;

    (snip)

    /**
     * Application interface registered - app is ready to go
     */
    @Override
    public void onClientRegistered(int status, int clientIf) {
        Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf);
        synchronized (this) {
            if (mClientIf == -1) {
                if (DBG) Log.d(TAG, "onClientRegistered LE scan canceled");
            }

            if (status == BluetoothGatt.GATT_SUCCESS) {
                mClientIf = clientIf;
                try {
                    mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters,
                        mResultStorages, ActivityThread.currentOpPackageName());
                } catch (RemoteException e) {
                    Log.e(TAG, "fail to start le scan: " + e);
                    mClientIf = -1;
                }
            } else {
                // registration failed
                mClientIf = -1;
            }
            notifyAll();
        }
    }
    (snip)
}

実際にエラーになっているのは、mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters, mResultStorages, ActivityThread.currentOpPackageName());の部分ですが、ここでは特に位置情報パーミッションに限った記載はないですね。

ちなみに、ここを呼び出しているところには、結果を取得するためにACCESS_COARSE_LOCATIONか、ACCESS_FINE_LOCATIONが必要と記載されていますが、位置情報関連のパーミッションはチェックしていません。

/**
 * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
 * delivered through {@code callback}.
 * <p>
 * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
 * An app must hold
 * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
 * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
 * in order to get results.
 *
 * @param callback Callback used to deliver scan results.
 * @throws IllegalArgumentException If {@code callback} is null.
 */
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public void startScan(final ScanCallback callback) {
    (snip)
    startScan(null, new ScanSettings.Builder().build(), callback);
}

private void startScan(List<ScanFilter> filters, ScanSettings settings,
        final ScanCallback callback, List<List<ResultStorageDescriptor>> resultStorages) {
    BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
        if (settings == null || callback == null) {
            throw new IllegalArgumentException("settings or callback is null");
        }
        synchronized (mLeScanClients) {
            if (mLeScanClients.containsKey(callback)) {
                postCallbackError(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED);
                return;
            }
            IBluetoothGatt gatt;
            try {
                gatt = mBluetoothManager.getBluetoothGatt();
            } catch (RemoteException e) {
                gatt = null;
            }
            if (gatt == null) {
                postCallbackError(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
                return;
            }
            if (!isSettingsConfigAllowedForScan(settings)) {
                postCallbackError(callback,
                        ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
                return;
            }
            if (!isHardwareResourcesAvailableForScan(settings)) {
                postCallbackError(callback,
                        ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
                return;
            }
            if (!isSettingsAndFilterComboAllowed(settings, filters)) {
                postCallbackError(callback,
                        ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
                return;
            }
            BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
                    settings, callback, resultStorages);
            wrapper.startRegisteration();
        }
    }

ということで、次はIBluetoothGattを調べてみます。

IBluetoothGatt.aidl / GattService.java

I〜となっていることから予想できるように、IBluetoothGattはインターフェースで、IBluetoothGatt.aidlで定義されています。

IBluetoothGatt.aidlを実装しているのは?ということで、AndroidManifest.xmlを見ると、下記の記載が見つかります。

<service
    android:process="@string/process"
    android:name = ".gatt.GattService"
    android:enabled="@bool/profile_supported_gatt">
    <intent-filter>
        <action android:name="android.bluetooth.IBluetoothGatt" />
    </intent-filter>
</service>

と言うことで、早速startScan()を見てみます。

private static class BluetoothGattBinder extends IBluetoothGatt.Stub implements IProfileServiceBinder {
    private GattService mService;

    (snip)

    @Override
    public void startScan(int appIf, boolean isServer, ScanSettings settings,
            List<ScanFilter> filters, List storages, String callingPackage) {
        GattService service = getService();
        if (service == null) return;
        service.startScan(appIf, isServer, settings, filters, storages, callingPackage);
    }
}

serviceはGattService自体なので、その中のstartScanは?というと、、、

void startScan(int appIf, boolean isServer, ScanSettings settings,
    List<ScanFilter> filters, List<List<ResultStorageDescriptor>> storages,
    String callingPackage) {
    if (DBG) Log.d(TAG, "start scan with filters");
    enforceAdminPermission();
    if (needsPrivilegedPermissionForScan(settings)) {
        enforcePrivilegedPermission();
    }
    boolean hasLocationPermission = Utils.checkCallerHasLocationPermission(this,
        mAppOps, callingPackage);
    final ScanClient scanClient = new ScanClient(appIf, isServer, settings, filters, storages);
    scanClient.hasLocationPermission = hasLocationPermission;
    scanClient.hasPeersMacAddressPermission = Utils.checkCallerHasPeersMacAddressPermission(
    this);
    mScanManager.startScan(scanClient);
}

はい、ここに居ました。Utils.checkCallerHasLocationPermission

Utils.java

Utils.checkCallerHasLocationPermissionでは、Android M以上の場合は位置情報のパーミッションがないとSecurityExceptionが投げられます。

/**
 * Checks that calling process has android.Manifest.permission.ACCESS_COARSE_LOCATION or
 * android.Manifest.permission.ACCESS_FINE_LOCATION and a corresponding app op is allowed
 */
public static boolean checkCallerHasLocationPermission(Context context, AppOpsManager appOps,
            String callingPackage) {
        if (context.checkCallingOrSelfPermission(android.Manifest.permission.
                ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                && isAppOppAllowed(appOps, AppOpsManager.OP_FINE_LOCATION, callingPackage)) {
            return true;
        }
        if (context.checkCallingOrSelfPermission(android.Manifest.permission.
                ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
                && isAppOppAllowed(appOps, AppOpsManager.OP_COARSE_LOCATION, callingPackage)) {
            return true;
        }
        // Enforce location permission for apps targeting M and later versions
        if (isMApp(context, callingPackage)) {
            throw new SecurityException("Need ACCESS_COARSE_LOCATION or "
                    + "ACCESS_FINE_LOCATION permission to get scan results");
        } else {
            // Pre-M apps running in the foreground should continue getting scan results
            if (isForegroundApp(context, callingPackage)) {
                return true;
            }
            Log.e(TAG, "Permission denial: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION "
                    + "permission to get scan results");
        }
        return false;
    }

これでスキャンが失敗する、ということのようです。

雑感

Android 6.0(多分)から[設定] > [位置情報]のメニュー内にある[スキャン]に「Bleutoothのスキャン」というのが追加されています。

ACCESS_COARSE_LOCAITON / ACCESS_FINE_LOCATIONというと、GPS/Wi-Fi/モバイルネットワークで使うイメージですが、Bluetoothでも位置情報が取得できるようになったのでパーミッションを追加しろ、ということなのかもしれないですね。

ただ、Bluetoothに加えて、GPS(をイメージさせるようなパーミッション)が付いているとなると、電池を喰いそうなイメージが強くなるのが気にはなります。また、権限を気にするユーザーへの説明も面倒そうで、正直あまり嬉しくはないような気がします。。。

追記(2016/01/06)

コメントでご指摘いただいた「ペアリング済みのデバイス」の場合の動作を確認してみました。

上で確認したとの違いは、

  • 位置情報のパーミッションを削除する。
  • BluetoothLeScanner.startScan()でデバイスをスキャンする代わりに、BluetoothAdapter.getBondedDevices()でペアリング済みのデバイスを取得する。

の2点で、それ以外の接続やCharacteristicの書き込みに違いはありません。

結果的には、位置情報のパーミッションがなくても、接続や書き込みには問題がありませんでした。

なので、本記事のタイトルは、正確には、「どうしてAndroid 6.0でBLEの "スキャンをする" のに位置情報のパーミッションが必要なワケ?」でした。

追記(2016/01/12)

上記以外にも位置情報パーミッションと位置情報モードの設定が影響することが確認できましたので、フォロー記事を書きました。

Android 6.0でBLEデバイスのスキャン結果を受け取るには位置情報モードをオンにする必要がある


  1. targetSDKを23以上にした場合 

  2. 公式サイト 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away