markzhang12345/MockLocation: 一个安卓 GPS 信号模拟工具

不想 Keep 打卡,所以尝试找出一个比较好的替代方案。原本的想法是写一个基于 adb 命令的简单脚本,然后对手机发送定位命令,如

adb shell geo fix 116.397428 39.908717

adb shell am startservice -a com.android.location.service.v3.NetworkLocationProvider --es network-location "{'latitude':39.908717,'longitude':116.397428,'accuracy':10.0}"

然而,第一种方式在Android 14中已经不再适用,第二种方式需要手机的 root 权限。手边并没有的设备给我折腾 root,所以考虑写一个安卓软件

为什么要写安卓

img

你说的对,但是现在的安卓版本虽然禁用了adb shell geo fix方法,在开发者选项中仍然存在模拟位置信息应用,所以我们直接在应用中调用ACCESS_MOCK_LOCATION权限,就可以为手机提供位置模拟服务

电脑上还留着大一时候下载的 Android Studio,倒是也不用新增环境,直接新建项目开始琢磨

这里想着直接用 Kotlin 一步到位,但是越写越发现自己根本没写过完整的面向对象的项目。之前写 C++ 的原因也只是 STL 好用,完全没有认真尝试过他的面向对象特性。连带着这两天看 C# 也非常头大

开始认真思考要不要看着 Java 好好学一下面向对象的写法

项目新建后会出现一堆乱七八糟的文件结构,在刚开始开发的时候确实让人无从下手。简单分析之后,其实需要关注的文件只有以下三个:

  • MainActivity.kt,入口文件
  • activity_main.xml,页面配置,类似前端的 HTML
  • AndroidManifest.xml,配置文件

AndroidManifest.xml

既然我们软件的核心功能是通过ACCESS_MOCK_LOCATION实现的,那么当然首先应该在配置文件中声明该权限

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
  • ACCESS_FINE_LOCATION允许应用获取精确位置信息,这里就是使用 GPS 获取高精位置
  • ACCESS_COARSE_LOCATION允许应用获取大致位置信息,例如基于网络的位置
  • ACCESS_MOCK_LOCATION允许应用创建模拟位置并将其提供给系统

其实理论上来讲,以上三个权限都是非必要的。我的软件只需要提供位置信息,并不用获取位置信息。而第三个权限在Android 6.0版本纸上已经被开发者模式的设置取代了……

img

写文章的时候意识到ACCESS_MOCK_LOCATION这个影响我软件打包的权限竟然是非必要的,删了之后果然能够正常打包了,并且不影响软件的正常运行

被自己气笑了

权限需要使用adb shell appops set icu.kpmark.mocklocation android:mock_location allow手动赋予,开发者选项中可能没有

activity_main.xml

然后就是简单设计软件页面,因为这个软件的功能实在是比较单一,暂时也没有什么扩展的接口,于是设置一个开始/结束键用来绑定stop()start()函数就好了

没了

以后考虑添加路径,速度选择功能,但是经纬度计算实在是有点难处理,所以暂时搁置

MainActivity.kt

这个是程序的入口文件,我们在这个文件中重写系统父类AppCompatActivity中的onCreate方法,启动视图模式,并使用根视图处理窗口插图

其实最主要的,处理初始化一堆必要的选项之外,只有将我主要逻辑的stop()start()函数绑定到界面按钮的监听事件上,然后将软件终止事件也绑定至stop(),确保主要逻辑的正确进入与退出

当然不会把主要逻辑写在里面,不然真就编程依托莫名其妙的代码了,这里我们新建MockLocation.kt来存放我们的EnhancedMockLocation类(为什么当时脑子一抽起了这个名字呢)

MockLocation.kt

为了骗过软件,我们必须将模拟信号提供者的名字替换为系统已有的 GPS 提供者,这里我们给出一个提供者数组

// 所有可能的位置提供者
private val providers = arrayOf(
	LocationManager.GPS_PROVIDER,
	LocationManager.NETWORK_PROVIDER,
	LocationManager.PASSIVE_PROVIDER
)

分别是 GPS 传感器,网络定位以及被动身份,毕竟不是所有软件都像高德地图一样,对 GPS 信号毫不怀疑(笑),所以这些假身份越多越好

由于要进行应用的后台运行,这里需要开个协程来运行模拟位置逻辑,然后按照类中预设的经纬度点,力求还原的提供位置信息

这里我们使用interpolationPosition函数来实现两个经纬度点之间的平滑过渡,类似以下逻辑

进度条 += (速度 × 时间差) / 两点距离
if (进度条 > 1.0) {
    切到下个点
    进度条 = 0.0
}

当手机移动速度超过光速时,可以直接触发瞬移特效(误

你说的对,但是我们还要计算方位角来模拟更加真是的方向朝向,这里我们使用以下代码

val R = 6371000.0 // 地球半径
val a = sin(Δ纬度/2)^2 + cos(纬度1)*cos(纬度2)*sin(Δ经度/2)^2

尝试计算当前的方位角,但是显然,效果不是很好

img

这里,我们做了三个比较特殊的算法来尝试骗过软件的自动识别

  • warmupLocationServices()会先发送三个抖动坐标,用来迷惑手机系统,效果类似:先让系统相信我在北京 → 上海 → 广州之间反复横跳,之后突然出现在纽约就不会被怀疑了
  • 方向角的计算,我使用 5 个历史位置来计算平均方向
  • 尝试速度和方向添加随机噪声,试图模拟手机在身上的随机扰动
val smoothedBearing = calculateSmoothedBearing()
val smoothedSpeed = if (currentSpeed > 0.1) currentSpeed else 0.0

val randomizedSpeed = smoothedSpeed * (0.95 + 0.1 * Random.nextDouble()) // ±5%波动
val randomizedBearing = (smoothedBearing + Random.nextDouble() * 3 - 1.5).toFloat()

for (provider in providers) {
    setEnhancedMockLocation(
        locationManager,
        provider,
        location.latitude,
        location.longitude,
        randomizedSpeed,    // 随机速度
        randomizedBearing   // 假装手抖的方向
    )
}

你说的对,但是在一些运动软件中,这个随机扰动的检测是通过陀螺仪和水平仪之类的其他传感器来检测的,使用 GPS 模拟的效果,只能说聊胜于无

img

本程序仅供学习交流使用,请勿用作不良用途

目前此软件仅经过不完全测试,请确保安装软件的设备上不存在任何重要信息

错误使用本工具可能导致账号封禁等风险使用者需自行承担一切后果