はじめに
位置情報を扱うアプリを実装していると現在地情報を特定の場所に変更したいことがあったりします。これまではPlayストアにある現在地偽装アプリをインストールして利用していましたが、異なる開発機で毎回Googleログインをしてインストールするのが手間だったのでアプリ内で実装しました。
注意
今回紹介する現在地情報偽装をした状態でポケGOやDQウォークをするとBAN対象になる可能性があります。当方では責任を負いませんのでご了承ください。
現在地情報偽装機能の実装
権限
位置情報を偽装するためには専用のパーミッションが必要となります。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
...
</manifest>
この記述を通常のAndroidManifest.xmlに記載すると以下のエラーとなります。
今回はDebug用途での実装となったためこのパーミッションの記述のみ src/debug/AndroidManifest.xml
に移しています。debugビルドであればエラーが出たままでもビルドすることはできましたがリリースビルドができるかまでは確認していません。
仮の現在地情報アプリ
設定方法
Android6.0以上で現在地情報を偽装するためには開発者オプションにある 仮の現在地情報アプリを選択
という項目にアプリケーションを指定する必要があります。
通常のアプリはこの項目の対象になりませんが前述の ACCESS_MOCK_LOCATION
パーミッションを設定していると仮の現在地情報アプリとして選択できるようになります。
仮の現在地情報アプリとして選択するとこのような表示になります。
設定されているかチェックする
以下のコードで対象のパッケージが仮の現在地情報アプリに設定されているかをチェックすることができます。
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
}
}
仮の現在地情報アプリに設定されていない場合は開発者用オプションに遷移して設定してもらいましょう。
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などの位置情報アプリを使うと現在地が設定した緯度経度になっていることが確認できます。
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
パーミッションが許可されている必要があります。
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
}
現在地情報の偽装を解除する
設定したテスト用プロバイダは以下のコードで解除できます。
private fun clearMockLocation() {
try {
for (provider in providers) {
locationManager.run {
removeTestProvider(provider)
}
}
} catch (e: IllegalArgumentException) {
// addTestProviderされていない状態でremoveTestProviderを呼ぶとIllegalArgumentExceptionが発生
}
}
実用に当たって
紹介した手順にて位置情報の偽装は可能ですがマップのアプリやSDKによっては一度だけの偽装処理は無視される場合があります。恐らくですが位置情報は精度が完全ではないことから正しくない緯度経度を受け取ってしまうことを考慮し関連性のない位置情報を読み捨てするなどの処理が入っていると思われます。
そのため、Serviceなどのバックグラウンドで実行可能なコンポーネントを利用し一定間隔で継続して偽装処理を実行するなどの工夫が必要となりそうです。
位置情報偽装サービスをバックグラウンドで実行するサンプルを実装しました。