51
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-01-04

はじめに

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. 公式サイト

51
45
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?