Androidで現在地情報を偽装する

# はじめに
位置情報を扱うアプリを実装していると現在地情報を特定の場所に変更したいことがあったりします。これまではPlayストアにある現在地偽装アプリをインストールして利用していましたが、異なる開発機で毎回Googleログインをしてインストールするのが手間だったのでアプリ内で実装しました。

## 注意
今回紹介する現在地情報偽装をした状態でポケGOやDQウォークをすると**BAN対象**になる可能性があります。当方では責任を負いませんのでご了承ください。

# 現在地情報偽装機能の実装
## 権限
位置情報を偽装するためには専用のパーミッションが必要となります。

``` xml:AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> 
    ...
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> 
    ...
</manifest> 
```

この記述を通常のAndroidManifest.xmlに記載すると以下のエラーとなります。
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122574/4764bfbd-8f29-ee0e-8e4f-022511f55bb5.png)

今回はDebug用途での実装となったためこのパーミッションの記述のみ `src/debug/AndroidManifest.xml` に移しています。debugビルドであればエラーが出たままでもビルドすることはできましたがリリースビルドができるかまでは確認していません。

## 仮の現在地情報アプリ
### 設定方法
Android6.0以上で現在地情報を偽装するためには開発者オプションにある `仮の現在地情報アプリを選択` という項目にアプリケーションを指定する必要があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122574/f301b93a-5192-723d-406b-651f5c683145.png" width=50%/>

通常のアプリはこの項目の対象になりませんが前述の `ACCESS_MOCK_LOCATION` パーミッションを設定していると仮の現在地情報アプリとして選択できるようになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122574/b499787e-10a8-f774-0bae-1864927faa7e.png" width=50%/>

仮の現在地情報アプリとして選択するとこのような表示になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122574/77202fe7-5036-0325-c01f-be806b4f2cff.png" width=50%/>

### 設定されているかチェックする
以下のコードで対象のパッケージが仮の現在地情報アプリに設定されているかをチェックすることができます。

``` kotlin
private fun isMockAppEnabled(packageName: String): Boolean {
    try {
        val opsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
        return opsManager.checkOp(AppOpsManager.OPSTR_MOCK_LOCATION, android.os.Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
    } catch (e: SecurityException) {
        // MOCK_LOCATIONにアプリが設定されていないとcheckOpでSecurityExceptionが発生する
        return false
    }
}
```

仮の現在地情報アプリに設定されていない場合は開発者用オプションに遷移して設定してもらいましょう。

``` kotlin
if (!isMockAppEnabled(packageName)) {
    showToast("現在地情報を設定するには仮の現在地情報アプリに設定してください")
    startActivity(Intent(android.provider.Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS).apply {
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    })
    return
}
```
※開発者オプションを有効化していないとその前の画面までしか遷移できないようです

## 現在地情報を偽装する
以下のコードでは精度の高いGPSプロバイダ(`LocationManager.GPS_PROVIDER`)と精度の低いネットワークプロバイダ(`LocationManager.NETWORK_PROVIDER`)の情報をテスト用プロパイダとして上書きします。
実行後にGoogleMapなどの位置情報アプリを使うと現在地が設定した緯度経度になっていることが確認できます。

``` kotlin
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
val locationManager: LocationManager by lazy {
    getSystemService(Context.LOCATION_SERVICE) as LocationManager
}

private fun setMockLocation(latitude: Double, longitude: Double): Boolean {
    try {
        for (provider in providers) {
            locationManager.run {
                addTestProvider(
                    provider,
                    false,
                    false,
                    false,
                    false,
                    false,
                    false,
                    false,
                    Criteria.POWER_LOW,
                    Criteria.ACCURACY_FINE
                )
                setTestProviderEnabled(provider, true)
                setTestProviderLocation(provider, Location(provider).apply {
                    this.latitude = latitude
                    this.longitude = longitude
                    this.altitude = 0.0
                    this.accuracy = 100f
                    this.time = System.currentTimeMillis()
                    this.speed = 0f
                    this.elapsedRealtimeNanos = android.os.SystemClock.elapsedRealtimeNanos()
                })
            }
        }
        return true
    } catch (e: Exception) {
        return false
    }
}
```
### addTestProvider
テスト用プロバイダを作成します。
詳細は割愛しますが様々な設定項目があります。
### setTestProviderEnabled
テスト用プロバイダの有効/無効を設定します。
### setTestProviderLocation
テスト用プロバイダに対して任意のLocationを設定します。

## 現在地情報が偽装されたものであるかを確認する
Locationには偽装された位置情報であるかを判断することができるメソッドがあります。
※現在地情報の取得には`ACCESS_FINE_LOCATION`パーミッションが許可されている必要があります。

``` kotlin
private fun isLocationMocked(): Boolean {
    val locationManager = applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    val criteria = Criteria().apply { accuracy = Criteria.ACCURACY_FINE }
    val provider = locationManager.getBestProvider(criteria, true)
    val location = locationManager.getLastKnownLocation(provider)
    return location?.isFromMockProvider ?: false
}
```

## 現在地情報の偽装を解除する
設定したテスト用プロバイダは以下のコードで解除できます。

``` kotlin
private fun clearMockLocation() {
    try {
        for (provider in providers) {
            locationManager.run {
                removeTestProvider(provider)
            }
        }
    } catch (e: IllegalArgumentException) {
        // addTestProviderされていない状態でremoveTestProviderを呼ぶとIllegalArgumentExceptionが発生
    }
}
```

# 実用に当たって
紹介した手順にて位置情報の偽装は可能ですがマップのアプリやSDKによっては一度だけの偽装処理は無視される場合があります。恐らくですが位置情報は精度が完全ではないことから正しくない緯度経度を受け取ってしまうことを考慮し関連性のない位置情報を読み捨てするなどの処理が入っていると思われます。
そのため、Serviceなどのバックグラウンドで実行可能なコンポーネントを利用し一定間隔で継続して偽装処理を実行するなどの工夫が必要となりそうです。

位置情報偽装サービスをバックグラウンドで実行するサンプルを実装しました。

* [MockLocationService](https://github.com/misoca12/MockLocation/blob/master/mock_location/src/main/java/com/misoca/mock_location/MockLocationService.kt)