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


# はじめに

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

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

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

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

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


[^1]: targetSDKを23以上にした場合
[^2]: [公式サイト](http://developer.android.com/intl/ja/reference/android/bluetooth/le/BluetoothLeScanner.html#startScan(android.bluetooth.le.ScanCallback))

# 結論

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

---

**`GattService.java`の`startScan`に位置情報パーミッションの有無をチェックするコードが追加されており、位置情報のパーミッシヨンが与えられていないときは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

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

```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`が必要と記載されていますが、位置情報関連のパーミッションはチェックしていません。

```java

/**
　* 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を見ると、下記の記載が見つかります。

```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()`を見てみます。

```java
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は?というと、、、

```java
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が投げられます。

```java
/**
 * 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デバイスのスキャン結果を受け取るには位置情報モードをオンにする必要がある](http://qiita.com/hisanaka/items/518db08d4a700247ec69)