在最近在抖音中看到很多app涉嫌违规使用和盗取用户定位等相关的个人隐私问题,那么下面我们来说下有关于:“在Android中要如何实现模拟定位?”这方面的相关内容吧!
在导航测试场景中经常需要定位模拟和路线回放,记录下通过LocationManager.setTestProviderLocation()方法实现模拟地位,如果要测试的应用不支持TestProviderLocation模拟位置输入,可以考虑从HAL层入手,hook系统默认的GPS实现。
一、Android模拟权限开启配置
在Android6.0以下的版本中,需要在设置中勾选模拟定位的开关,在6.0以上就改成了选择模拟定位的应用,对应的开启配置方式也不一样,相同的是在AndroidManifest.xml都需要配置以下两个权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
1)Android 6.0以下开启模拟定位开关
Settings.Secure.putInt(getContentResolver(), Settings.Secure.ALLOW_MOCK_LOCATION, 1);
通过这种方式去开启模拟定位需要在AndroidManifest.xml中配置以下系统权限,应用还需要经过系统签名,对于非系统应用不能通过这种实现方式。
android:sharedUserId="android.uid.system" <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
涉及到这种操作可以通过adb shell命令的方式配置来绕过系统权限配置:
adb shell settings put secure mock_location 1
2)Android 6.0以上代码配置选择模拟定位的应用
在6.0以上的Android版本就需要设置指定包名的mock_location权限为allow。
try { String mockLocationPkgName = getPackageName(); PackageManager mPackageManager = getPackageManager(); final ApplicationInfo ai = mPackageManager.getApplicationInfo( mockLocationPkgName, PackageManager.MATCH_DISABLED_COMPONENTS); AppOpsManager mAppsOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); mAppsOpsManager.setMode(AppOpsManager.OPSTR_MOCK_LOCATION, ai.uid, mockLocationPkgName, AppOpsManager.MODE_ALLOWED); } catch (PackageManager.NameNotFoundException e) { /* ignore */ }
同时在AndroidManifest.xml中还需要配置如下权限,可惜这种方式也是需要经过系统签名和源码一起编译,只有系统层级的应用才可以使用。
<uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" />
为了在设置界面模拟定位选项中显示模拟定位的应用,需要配置ACCESS_MOCK_LOCATION权限。
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>
以上配置也可以通过adb shell命令去实现,<package> 参数用自己应用的包名替换。
adb shell appops set <package> android:mock_location allow
关闭模拟定位权限,用如下命令:
adb shell appops set <package> android:mock_location deny
要查询有哪些应用开启了模拟定位权限,用如下命令:
adb shell appops query-op android:mock_location allow
执行后会输出应用的包名列表参数。
二、Android模拟定位实现
1)模拟定位开关检查
首先是代码中先判断模拟定位权限是否开启,6.0以上的只能通过添加定位监听是否有异常来判断。
boolean mockPermission = false; if (Build.VERSION.SDK_INT <= 22) {//6.0以下 mockPermission = Settings.Secure.getInt(getContentResolver(), Settings.Secure.ALLOW_MOCK_LOCATION, 0) == 1; } else { try { LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { mockPermission = false; return; } locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0, new LocationListener() { @Override public void onLocationChanged(Location location) { } @Override public void onStatusChanged(String s, int i, Bundle bundle) { } @Override public void onProviderEnabled(String s) { } @Override public void onProviderDisabled(String s) { } }); mockPermission = true; } catch (SecurityException e) { mockPermission = false; } }
可以看下添加定位监听的源码实现,以Android 7.0的源码实现做参考,LocationManager调用接口之后,最终是调用到LocationManagerService,在requestLocationUpdates 方法或 getLastLocation 方法中都有checkPackageName方法的调用
private void checkPackageName(String packageName) { if (packageName == null) { throw new SecurityException("invalid package name: " + packageName); } int uid = Binder.getCallingUid(); String[] packages = mPackageManager.getPackagesForUid(uid); if (packages == null) { throw new SecurityException("invalid UID " + uid); } for (String pkg : packages) { if (packageName.equals(pkg)) return; } throw new SecurityException("invalid package name: " + packageName); }
如果调用requestLocationUpdates方法的应用没有模拟定位的权限,就会报SecurityException异常,另外requestLocationUpdates需要在主线程中调用,如果在子线程中调用,还需要传一个looper参数,不然在实例化ListenerTransport的时候会报错。
看下ListenerTransport的构造函数,如果在子线程中添加监听,又没有传Loop,初始化mListenerHandler的时候就会报异常。
ListenerTransport(LocationListener listener, Looper looper) { mListener = listener; if (looper == null) { mListenerHandler = new Handler() { @Override public void handleMessage(Message msg) { _handleMessage(msg); } }; } else { mListenerHandler = new Handler(looper) { @Override public void handleMessage(Message msg) { _handleMessage(msg); } }; } }
接着添加对应的Provider,设置开关状态为true,配置状态为AVAILABLE。
LocationProvider provider = locationManager.getProvider(LocationManager.GPS_PROVIDER); if (provider != null) { locationManager.addTestProvider( provider.getName() , provider.requiresNetwork() , provider.requiresSatellite() , provider.requiresCell() , provider.hasMonetaryCost() , provider.supportsAltitude() , provider.supportsSpeed() , provider.supportsBearing() , provider.getPowerRequirement() , provider.getAccuracy()); } else { locationManager.addTestProvider(LocationManager.GPS_PROVIDER, true, true, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_FINE); } locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true); locationManager.setTestProviderStatus(LocationManager.GPS_PROVIDER, LocationProvider.AVAILABLE, null, System.currentTimeMillis());
2)setTestProviderLocation调用
调用代码示例如下,经纬度、车速、定位准确性、方位、海拔参数根据实际需求设置。
Location loc = new Location(LocationManager.GPS_PROVIDER); loc.setLongitude(24.522301); loc.setLatitude(118.115756); loc.setSpeed(60); loc.setAccuracy(Criteria.ACCURACY_HIGH); loc.setBearing(0); loc.setAltitude(0); loc.setTime(System.currentTimeMillis()); if (Build.VERSION.SDK_INT >= 17) { loc.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); } locationManager.setTestProviderLocation(LocationManager.GPS_PROVIDER,loc);
来看下为什么需要设置elapsedRealtimeNanos和time参数。在SDK版本17以下,Location(Android 4.1.1)是没有setElapsedRealtimeNanos这个方法的,在SDK版本17开始,Location(Android 4.2)加了这个方法,在调用setTestProviderLocation设置定位信息的时候,Android SDK版本17以上会做定位参数是否完整的校验,17以下的版本自动做补足,17开始的版本直接抛异常。
public void setTestProviderLocation(String provider, Location loc) { if (!loc.isComplete()) { IllegalArgumentException e = new IllegalArgumentException( "Incomplete location object, missing timestamp or accuracy? " + loc); if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN) { // just log on old platform (for backwards compatibility) Log.w(TAG, e); loc.makeComplete(); } else { // really throw it! throw e; } } try { mService.setTestProviderLocation(provider, loc, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
接着看下Location类中makeComplete和isComplete做的逻辑处理,isComplete里面有provider,Accuracy,mTime和mElapsedRealtimeNanos的判断。
@SystemApi public void makeComplete() { if (mProvider == null) mProvider = "?"; if (!hasAccuracy()) { mFieldsMask |= HAS_ACCURACY_MASK; mAccuracy = 100.0f; } if (mTime == 0) mTime = System.currentTimeMillis(); if (mElapsedRealtimeNanos == 0) mElapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(); } @SystemApi public boolean isComplete() { if (mProvider == null) return false; if (!hasAccuracy()) return false; if (mTime == 0) return false; if (mElapsedRealtimeNanos == 0) return false; return true; }
以上是就是单个点的模拟定位实现,如果要实现路线回放模拟,只要在后台请求到路线定位点数组数据之后,每隔1秒刷新调用setTestProviderLocation接口就可以了。
在模拟定位操作完毕之后,需要移除模拟定位对象,避免定位信息还是使用模拟定位接口的参数,如果没有移除下一次使用的时候又调用添加同名的Provider也会抛异常。
locationManager.removeTestProvider(LocationManager.GPS_PROVIDER);
就以上这些内容我们可以在实际的动作操作就可以看到实际的效果了,那么对于“在Android中要如何实现模拟定位?”这个问题也就迎刃而解,有感兴趣的小伙伴们也可以在W3Cschool学到更多的在Android使用的方法和相关基础内容!