如何给安卓虚拟机设置虚拟GPS定位

进行POI数据采集的时候,我们往往需要进行虚拟定位,需要通过设置虚假的当前位置,让APP暴出该点周边的POI列表。比如一些外卖平台,默认会仅显示你当前位置周边若干千米范围内的店铺列表。通过不断切换位置,最终就能拿到一个相对完整的POI集合。

怎么来实现虚拟GPS定位呢?这里分两种情况:

1. 能够直接修改App的lat,lng参数。
(1)一类是我们能够分析出APP与网站的HTTP交互接口在这种情况下,我们可以直接修改HTTP请求中的lat,lng之类的位置参数,使其返回指定位置的数据。
(2)还有一类是APP列表Activity的启动extra数据(APP内通过getIntent()获取)中含有lat,lng之类参数,如下图所示是某APP列表Activity的部分源码,我们可以看到lattiude和longitude参数是从启动extra数据中获取的。这样,我们在用“Am命令”启动列表Activity的时候可以直接指定lat,lng参数,从而绕过系统的GSP定位。

这种情况不是本文所要介绍的重点。

2. 无法直接修改lat,lng参数。

现在越来越多的App加入了强有力反采集策略,例如通过ssl证书固定(例如 小红书)、签名机制(例如 京东到家)、数据加密(例如  大众点评)、APK加壳、代码混淆,使得我们越来越难分析出或构造成出有效的HTTP交互,从而没法直接通过修改lat,lng参数来作弊。

对于这种情况,我们通常会采用模拟操作App(例如 基于adb的UI操作),然后结合HTTP(S)抓包或者动态二进制插桩(DBI)来获取有效的数据,从而绕过反采集限制。

但是,此时App获取的是设备的”真实”GSP定位。那我们如何实现定位到我们想要的某个位置(lat,lng)呢?这就是本文所要介绍的重点。

还是要分两种情况介绍:

1. 使用”夜神模拟器”、”MuMu模拟器”之类成熟的安卓模拟器。

如果你使用的是”夜神模拟器”、”MuMu模拟器”之类成熟的安卓模拟器,他们都带有“虚拟定位”功能,通过UI即可设置需要定位到的点。

对于“夜神模拟器”还支持通过”adb shel”l命令方式修改虚拟定位,示例命令如下:

# 将夜神模拟器的GPS定位修改为(108.958425, 34.224318),西安大雁塔的GSP坐标

adb shell setprop persist.nox.gps.latitude 34.224318
adb shell setprop persist.nox.gps.longitude 108.958425
adb shell setprop persist.nox.gps.latitude 34.224318 adb shell setprop persist.nox.gps.longitude 108.958425
adb shell setprop persist.nox.gps.latitude 34.224318
adb shell setprop persist.nox.gps.longitude 108.958425

 

打开虚拟定位UI,如下图所示,显示位置正确,说明定位生效:

有了命令行支持,这样自动化操作就很方便了(无需人工介入),我们可以在脚本中动态设置GPS坐标,然后模拟操作APP抓取周边的数据。
“夜神模拟器”、”MuMu模拟器”这些成熟的安卓模拟器功能完善、兼容性好,缺点是仅支持Windows平台,不支持Linux,我们公司的爬虫服务器大多都是Linux的,无法使用它们。

2. 在Linux平台使用KVM+原生的Android X86系统。

前面说了,由于公司的服务器是Linux系统,无法直接使用”夜神模拟器”、”MuMu模拟器”这些Windows平台的方案。在Linux下,我们可以使用KVM来直接运行Android X86系统(https://www.android-x86.org/)来实现一个安卓模拟器。

我在《Linux server(headless模式)下Android模拟器的实现》一文中曾详细介绍了这种模式的配置方法,详见 配置过程这里不再赘述。

当然,在Windows系统下,你也可以使用VMware或者VirtualBox来运行Android X86系统。实际上,”夜神模拟器”就是基于”VirtualBox + Android X86″开发的,但功能比原生的Android X86更加强大,例如加入了”虚拟定位”功能。而且也更稳定了。例如,一些App在原生Android X86下无法正常运行(例如,启动后闪退),但在”夜神模拟器”中就可以正常的工作。

原生的Android X86并没有提供类似”夜神模拟器”的虚拟定位功能,那如何实现虚拟GPS定位呢?

好在Android系统本身是支持”模拟位置”(Mock location)功能的,配过一系列配置,再借助第三方App即可实现”虚拟定位”功能。

具体配置方法如下:

(1)先安装一个位置模拟App,例如 “位置修改器”(https://www.wandoujia.com/apps/5773183)。PS:这类应用有很多,不过大多是从国外开发的,使用的谷歌地图,因此国内无法正常工作。

(2)开启开发人员选项。

(3)激活“位置模拟”功能。

Android 6.0及以下版本,激活“允许模拟位置”即可;

Android 6.0及以上版本,需要点击“选择模拟位置信息系应用”(英文版里叫做Select mock location app),然后选择安装的位置模拟App,如下图所示。

(4)然后就可以打开”位置修改器”App,来设置一个想要的GPS点。

 

我们测试一下效果:

(1)先不启动”位置修改器”或者先不设置开发人员选项中的虚拟定位。用安卓浏览器访问一下饿了么的首页(这里能显示出当前的定位),如下图所示,可以看出网站无法识别出当前的位置。

(2)启动”位置修改器”,并将位置切换到长安大学(34.237006,108.961188)附近。然后再访问一下饿了么首页,如下图所示,网站成功获取到了当前的定位。

功能是实现了,但是缺点也是显而易见的。需要UI操作才能实现位置切换,这样就必须人工介入,无法在自动化采集方案中使用了。

何不自己实现一个虚拟定位的App,并且支持命令来切换位置呢?

经过反编译一个别人写好的虚拟定位APK,发现实现起来并不难。再通过参考Stackoverflow上分享的一些代码(例如 https://stackoverflow.com/questions/2531317/android-mock-location-on-device),我实现了一个叫做MockLocation的虚拟定位App,核心代码如下:

package cn.webscraping.qi.mocklocation;
import android.location.Criteria;
import android.location.LocationProvider;
import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.SystemClock;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private String TAG = "MockLocation";
// 纬度和经度
public double lat = 34.227868d;
public double lng = 108.954195d;
// 信息提示标签
private TextView tip_label;
private LocationManager lm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取传递的lat, lng参数
Bundle bundle = this.getIntent().getExtras();
if(bundle != null) {
lat = Double.valueOf(bundle.getString("lat", "34.227868"));
lng = Double.valueOf(bundle.getString("lng", "108.954195"));
}
// 信息提示标签
tip_label = (TextView)findViewById(R.id.tip_label);
String label_text = "Set \"lat = " + lat + ", lng = " + lng + "\"";
tip_label.setText(label_text);
Log.i(TAG, label_text);
lm = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
LocationProvider provider = lm.getProvider(LocationManager.GPS_PROVIDER);
if(provider != null){
lm.addTestProvider(provider.getName()
, provider.requiresNetwork()
, provider.requiresSatellite()
, provider.requiresCell()
, provider.hasMonetaryCost()
, provider.supportsAltitude()
, provider.supportsSpeed()
, provider.supportsBearing()
, provider.getPowerRequirement()
, provider.getAccuracy());
}else{
lm.addTestProvider(LocationManager.GPS_PROVIDER, true, true, false, false, true, true, true, Criteria.POWER_HIGH, Criteria.ACCURACY_FINE);
}
lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true);
lm.setTestProviderStatus(LocationManager.GPS_PROVIDER, LocationProvider.AVAILABLE, null, System.currentTimeMillis());
new Thread(new Runnable() {
@Override
public void run() {
Location mockLocation = new Location(LocationManager.GPS_PROVIDER);
while (true) {
try {
mockLocation.setLatitude(lat);
mockLocation.setLongitude(lng);
mockLocation.setAltitude(30);
mockLocation.setBearing(180);
mockLocation.setSpeed(0.2f);
mockLocation.setAccuracy(0.1f);
mockLocation.setTime(System.currentTimeMillis());
Log.i(TAG, "Set mocklocation as: \"lat = " + lat + ", lng = " + lng + "\"");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
mockLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
}
lm.setTestProviderLocation(LocationManager.GPS_PROVIDER, mockLocation);
Thread.sleep(1000);
} catch (Exception e){
Log.e(TAG, e.toString());
}
}
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
lm.removeTestProvider(LocationManager.GPS_PROVIDER);
Log.i(TAG, "Quit");
System.exit(0);
}
}
package cn.webscraping.qi.mocklocation; import android.location.Criteria; import android.location.LocationProvider; import android.os.Build; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.content.Context; import android.location.Location; import android.location.LocationManager; import android.os.SystemClock; import android.util.Log; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private String TAG = "MockLocation"; // 纬度和经度 public double lat = 34.227868d; public double lng = 108.954195d; // 信息提示标签 private TextView tip_label; private LocationManager lm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取传递的lat, lng参数 Bundle bundle = this.getIntent().getExtras(); if(bundle != null) { lat = Double.valueOf(bundle.getString("lat", "34.227868")); lng = Double.valueOf(bundle.getString("lng", "108.954195")); } // 信息提示标签 tip_label = (TextView)findViewById(R.id.tip_label); String label_text = "Set \"lat = " + lat + ", lng = " + lng + "\""; tip_label.setText(label_text); Log.i(TAG, label_text); lm = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); LocationProvider provider = lm.getProvider(LocationManager.GPS_PROVIDER); if(provider != null){ lm.addTestProvider(provider.getName() , provider.requiresNetwork() , provider.requiresSatellite() , provider.requiresCell() , provider.hasMonetaryCost() , provider.supportsAltitude() , provider.supportsSpeed() , provider.supportsBearing() , provider.getPowerRequirement() , provider.getAccuracy()); }else{ lm.addTestProvider(LocationManager.GPS_PROVIDER, true, true, false, false, true, true, true, Criteria.POWER_HIGH, Criteria.ACCURACY_FINE); } lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true); lm.setTestProviderStatus(LocationManager.GPS_PROVIDER, LocationProvider.AVAILABLE, null, System.currentTimeMillis()); new Thread(new Runnable() { @Override public void run() { Location mockLocation = new Location(LocationManager.GPS_PROVIDER); while (true) { try { mockLocation.setLatitude(lat); mockLocation.setLongitude(lng); mockLocation.setAltitude(30); mockLocation.setBearing(180); mockLocation.setSpeed(0.2f); mockLocation.setAccuracy(0.1f); mockLocation.setTime(System.currentTimeMillis()); Log.i(TAG, "Set mocklocation as: \"lat = " + lat + ", lng = " + lng + "\""); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { mockLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); } lm.setTestProviderLocation(LocationManager.GPS_PROVIDER, mockLocation); Thread.sleep(1000); } catch (Exception e){ Log.e(TAG, e.toString()); } } } }).start(); } @Override protected void onDestroy() { super.onDestroy(); lm.removeTestProvider(LocationManager.GPS_PROVIDER); Log.i(TAG, "Quit"); System.exit(0); } }
package cn.webscraping.qi.mocklocation;


import android.location.Criteria;
import android.location.LocationProvider;
import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.SystemClock;
import android.util.Log;
import android.widget.TextView;




public class MainActivity extends AppCompatActivity {


    private String TAG = "MockLocation";
    // 纬度和经度
    public double lat = 34.227868d;
    public double lng = 108.954195d;
    // 信息提示标签
    private TextView tip_label;
    private LocationManager lm;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        // 获取传递的lat, lng参数
        Bundle bundle = this.getIntent().getExtras();
        if(bundle != null) {
            lat = Double.valueOf(bundle.getString("lat", "34.227868"));
            lng = Double.valueOf(bundle.getString("lng", "108.954195"));
        }


        // 信息提示标签
        tip_label = (TextView)findViewById(R.id.tip_label);
        String label_text = "Set \"lat = " + lat + ", lng = " + lng + "\"";
        tip_label.setText(label_text);
        Log.i(TAG, label_text);


        lm = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
        LocationProvider provider = lm.getProvider(LocationManager.GPS_PROVIDER);
        if(provider != null){
            lm.addTestProvider(provider.getName()
                    , provider.requiresNetwork()
                    , provider.requiresSatellite()
                    , provider.requiresCell()
                    , provider.hasMonetaryCost()
                    , provider.supportsAltitude()
                    , provider.supportsSpeed()
                    , provider.supportsBearing()
                    , provider.getPowerRequirement()
                    , provider.getAccuracy());
        }else{
            lm.addTestProvider(LocationManager.GPS_PROVIDER, true, true, false, false, true, true, true, Criteria.POWER_HIGH, Criteria.ACCURACY_FINE);
        }
        lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true);
        lm.setTestProviderStatus(LocationManager.GPS_PROVIDER, LocationProvider.AVAILABLE, null, System.currentTimeMillis());


        new Thread(new Runnable() {
            @Override
            public void run() {
                Location mockLocation = new Location(LocationManager.GPS_PROVIDER);
                while (true) {
                    try {
                        mockLocation.setLatitude(lat);
                        mockLocation.setLongitude(lng);
                        mockLocation.setAltitude(30);
                        mockLocation.setBearing(180);
                        mockLocation.setSpeed(0.2f);
                        mockLocation.setAccuracy(0.1f);
                        mockLocation.setTime(System.currentTimeMillis());
                        Log.i(TAG, "Set mocklocation as: \"lat = " + lat + ", lng = " + lng + "\"");
                        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) {
                            mockLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
                        }
                        lm.setTestProviderLocation(LocationManager.GPS_PROVIDER, mockLocation);
                        Thread.sleep(1000);
                    } catch (Exception e){
                        Log.e(TAG, e.toString());
                    }
                }
            }
        }).start();
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        lm.removeTestProvider(LocationManager.GPS_PROVIDER);
        Log.i(TAG, "Quit");
        System.exit(0);
    }
}

 

Manifest中必须要有如下三个权限声明,否则在”选择模拟位置信息应用”列表中将找不到我们的App:

<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"/>
<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"/>
<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"/>

 

另外附上一个编译好的MockLocation APK文件下载:MockLocation.apk

用法如下所示,通过”Am命令”来启动APP并设置经纬度。无需UI操作,能够完全在命令行下通过自动化脚本来调用:

停止正在运行的MockLocation,准备重新启动并修改到一个新的位置
adb shell am force-stop cn.webscraping.qi.mocklocation
# 启动新的MockLocation示例,并将lat设置为34.229875,lng设置为108.954689
adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.229875 --es lng 108.954689
停止正在运行的MockLocation,准备重新启动并修改到一个新的位置 adb shell am force-stop cn.webscraping.qi.mocklocation # 启动新的MockLocation示例,并将lat设置为34.229875,lng设置为108.954689 adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.229875 --es lng 108.954689
停止正在运行的MockLocation,准备重新启动并修改到一个新的位置
adb shell am force-stop cn.webscraping.qi.mocklocation


# 启动新的MockLocation示例,并将lat设置为34.229875,lng设置为108.954689
adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.229875 --es lng 108.954689

 

我们来测试一下效果,测试环境是Ubuntu 16.04 + KVM + android-x86-6.0-r3,Android版本是6.0。

(1)先安装MockLocation.apk,然后开启开发人员选项,并在”选择模拟位置信息应用”列表中选择MockLocation。注意:安装的时候要给予”定位”权限(如下图所示),否则后面启动APP的时候会由于权限不足出错而闪退。

(2)然后执行如下代码,切换定位到”秦始皇陵”(34.384225, 109.254423):

# 切换定位到"秦始皇陵"(34.384225, 109.254423)
adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.384225 --es lng 109.254423
# 切换定位到"秦始皇陵"(34.384225, 109.254423) adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.384225 --es lng 109.254423
# 切换定位到"秦始皇陵"(34.384225, 109.254423)
adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity --es lat 34.384225 --es lng 109.254423

 

(3)然后打开安卓浏览器,访问https://h5.ele.me 如下图所示,可以看到定位生效了:

(4)然后再执行,切换定位到”清华大学”(40.009645, 116.333374):

  1. # 停止正在运行的MockLocation
  2. adb shell am force-stop cn.webscraping.qi.mocklocation
  3. # 切换定位到”清华大学”(40.009645, 116.333374)
  4. adb shell am start -n cn.webscraping.qi.mocklocation/cn.webscraping.qi.mocklocation.MainActivity –es lat 40.009645 –es lng 116.333374

(5)然后打开安卓浏览器,访问https://h5.ele.me 如下图所示,可以看到定位生效了:

我们再换一个测试环境,这次采用VMware + PhoenixOSv3.6.1,凤凰系统(http://www.phoenixos.com/),也是基于Android X86开发的,Android版本是7.1,凤凰系统默认也没有提供虚拟定位功能。这次我们使用MockLocation将坐标设置到洛阳龙门石窟(34.564649,112.484008),查看饿了么首页的定位,如下图所示,定位成功。

 

赞 (1)