4

Android模拟定位实现详解

 3 years ago
source link: https://www.chenwenguan.com/android-mock-location/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android模拟定位实现详解

2021年5月5日 | 最近更新于 下午8:50

在导航测试场景中经常需要定位模拟和路线回放,记录下通过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 性能监控之CPU监控

Android 性能监控之内存监控

转载请注明出处:陈文管的博客 – Android模拟定位实现详解

扫码或搜索:文呓

微信公众号 扫一扫关注

Filed Under: Android


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK