方法一、引导用户修改

此方法使用系统提供的Action,跳转到系统界面,不涉及敏感权限,所有APP均可通用。方法如下:

  1. Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
  2. startActivityForResult(intent, 0);

方法二、代码修改

权限

这种方法需要修改SettingsProvider的值,需要使用的权限也很高:

  1. <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>

但是,这个权限不是什么APP都能申请的,它的限定如下:

Permission is only granted to system apps. Permissions with the protection level signature, privileged or signatureOrSystem are only granted to system apps. If an app is a regular non-system app, it will never be able to use these permissions.

意思就是说,要使用这个权限,必须是系统APP(可以没有 platform 签名)。如果是系统APP,或者有platform权限,就继续往下看;否则,就放弃吧,苦海无涯回头是岸……

SettingsProvider

一般 SettingsProvider 的值,都能通过 android.provider.Settings 的内部类 GlobalSecureSystem各种 put 方法进行修改,修改也是即时生效的。比如修改WiFi开关状态:

  1. Settings.Global.putInt(
  2. getContentResolver(),
  3. Settings.Global.WIFI_ON,
  4. isChecked ? 1 : 0 //1开,0关
  5. );

而位置信息的KEY值是:Settings.Secure.LOCATION_PROVIDERS_ALLOWED,具体说明如下:

  1. /**
  2. * Comma-separated list of location providers that activities may access. Do not rely on
  3. * this value being present in settings.db or on ContentObserver notifications on the
  4. * corresponding Uri.
  5. *
  6. * @deprecated use {@link #LOCATION_MODE} and
  7. * {@link LocationManager#MODE_CHANGED_ACTION} (or
  8. * {@link LocationManager#PROVIDERS_CHANGED_ACTION})
  9. */
  10. @Deprecated
  11. public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed";
  12. /**
  13. * The degree of location access enabled by the user.
  14. * <p>
  15. * When used with {@link #putInt(ContentResolver, String, int)}, must be one of {@link
  16. * #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, {@link
  17. * #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. When used with {@link
  18. * #getInt(ContentResolver, String)}, the caller must gracefully handle additional location
  19. * modes that might be added in the future.
  20. * <p>
  21. * Note: do not rely on this value being present in settings.db or on ContentObserver
  22. * notifications for the corresponding Uri. Use {@link LocationManager#MODE_CHANGED_ACTION}
  23. * to receive changes in this value.
  24. */
  25. public static final String LOCATION_MODE = "location_mode";

可以看到,LOCATION_PROVIDERS_ALLOWED 已经废弃(API19)了,取而代之的是 LOCATION_MODE,这个值的取值范围是如下这几个 int 值之一:

  1. /**
  2. * Location access disabled.
  3. */
  4. public static final int LOCATION_MODE_OFF = 0;
  5. /**
  6. * Network Location Provider disabled, but GPS and other sensors enabled.
  7. */
  8. public static final int LOCATION_MODE_SENSORS_ONLY = 1;
  9. /**
  10. * Reduced power usage, such as limiting the number of GPS updates per hour. Requests
  11. * with {@link android.location.Criteria#POWER_HIGH} may be downgraded to
  12. * {@link android.location.Criteria#POWER_MEDIUM}.
  13. */
  14. public static final int LOCATION_MODE_BATTERY_SAVING = 2;
  15. /**
  16. * Best-effort location computation allowed.
  17. */
  18. public static final int LOCATION_MODE_HIGH_ACCURACY = 3;

这些啥意思就不解释了,不懂的copy一下,出门右转百度翻译……

踩坑(M0,API22)

这里我的需求是,当检测到用户关闭位置信息开关时,自动将其重置为“网络定位”,即 BATTERY_SAVING 模式。

于是基于上面的分析,还有自己的经验积累,就有了一个方案:
LOCATION_MODE 的值进行监听,检测到变化后,判断是不是关闭操作;如果是,则重新设置为 BATTERY_SAVING 模式。样例代码如下:

  1. getContentResolver().registerContentObserver(
  2. Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
  3. false,
  4. new ContentObserver(new Handler(Looper.getMainLooper())) {
  5. @Override
  6. public void onChange(boolean selfChange) {
  7. super.onChange(selfChange);
  8. int mode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
  9. if (Settings.Secure.LOCATION_MODE_OFF == mode) {
  10. Settings.Secure.putInt(getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
  11. }
  12. }
  13. }
  14. );

看起来没毛病,结果却发现怎么也监听不到 LOCATION_MODE 的变化,WTF???
因为时间关系,就没有研究为什么监听不到了,后面有时间再细看一下。

因为我本职工作是做ROM定制开发的,在位置信息开关这个点上,经常有客需要默认开或者关,每次我们改的都是 xml 里面的字符串值,而这个修改方法到 O1 都是可行的。

字符串值说明: “”,关闭 “gps”,仅GPS传感器定位 “network”,仅网络定位,即省电模式 “gps,network”,高精度模式

这写字符串值是存储在LOCATION_PROVIDERS_ALLOWED里的,于是稍微调整了一下上面的代码:

  1. getContentResolver().registerContentObserver(
  2. Settings.Secure.getUriFor(Settings.Secure.LOCATION_PROVIDERS_ALLOWED),
  3. false,
  4. new ContentObserver(new Handler(Looper.getMainLooper())) {
  5. @Override
  6. public void onChange(boolean selfChange) {
  7. super.onChange(selfChange);
  8. String mode = Settings.Secure.getString(getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
  9. if (null == mode || "".equals(mode)) {
  10. Settings.Secure.putString(getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "network");
  11. }
  12. }
  13. }
  14. );

本以为这样就可以了,结果却发现能监听到 LOCATION_PROVIDERS_ALLOWED 的变化,但是重置为“network”却不能成功……

不对呀,string 值就是这么 put 的呀!就在一脸懵逼的时候,仔细看了下 Settings.Secure 的方法列表,发现这么两个方法:
[app]Android 修改位置信息(GPS)开关状态 - 图1
好家伙,在这儿等着呢!

  1. @Deprecated
  2. public static final void setLocationProviderEnabled(ContentResolver cr,
  3. String provider, boolean enabled) {
  4. setLocationProviderEnabledForUser(cr, provider, enabled, UserHandle.myUserId());
  5. }
  6. @Deprecated
  7. public static final boolean setLocationProviderEnabledForUser(ContentResolver cr,
  8. String provider, boolean enabled, int userId) {
  9. synchronized (mLocationSettingsLock) {
  10. // to ensure thread safety, we write the provider name with a '+' or '-'
  11. // and let the SettingsProvider handle it rather than reading and modifying
  12. // the list of enabled providers.
  13. if (enabled) {
  14. provider = "+" + provider;
  15. } else {
  16. provider = "-" + provider;
  17. }
  18. return putStringForUser(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider,
  19. userId);
  20. }
  21. }

看到这就明白了,这货虽然看起来存的是"gps,network"的值,实际上是两个字段拼接的,每次通过 "+/- provider"去改变的。

因此,要想在用户关闭位置信息开关后,能重置为"network",只需要将前面的代码稍作修改即可:

  1. String mode = Settings.Secure.getString(getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
  2. if (null == mode || "".equals(mode)) {
  3. Settings.Secure.setLocationProviderEnabled(getContentResolver(), LocationManager.NETWORK_PROVIDER, true);
  4. //或者用下面的方式
  5. //Settings.Secure.putString(getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED, "+network");
  6. }

补充:三方APP如何监听位置开关变化?

上面介绍的接听方法是系统APP适用的,三方APP没有权限这么做,但并不是没有办法,只不过实现不一样而已。
而方法在前面介绍源码时就已在注释信息中出现:

  1. /**
  2. * The degree of location access enabled by the user.
  3. * <p>
  4. * When used with {@link #putInt(ContentResolver, String, int)}, must be one of {@link
  5. * #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY}, {@link
  6. * #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}. When used with {@link
  7. * #getInt(ContentResolver, String)}, the caller must gracefully handle additional location
  8. * modes that might be added in the future.
  9. * <p>
  10. * Note: do not rely on this value being present in settings.db or on ContentObserver
  11. * notifications for the corresponding Uri. Use {@link LocationManager#MODE_CHANGED_ACTION}
  12. * to receive changes in this value.
  13. */
  14. public static final String LOCATION_MODE = "location_mode";

三方APP可以通过监听如下广播来监听开关变化:

  1. /**
  2. * Broadcast intent action when {@link android.provider.Settings.Secure#LOCATION_MODE} changes.
  3. * For use with the {@link android.provider.Settings.Secure#LOCATION_MODE} API.
  4. * If you're interacting with {@link #isProviderEnabled(String)}, use
  5. * {@link #PROVIDERS_CHANGED_ACTION} instead.
  6. *
  7. * In the future, there may be mode changes that do not result in
  8. * {@link #PROVIDERS_CHANGED_ACTION} broadcasts.
  9. */
  10. public static final String MODE_CHANGED_ACTION = "android.location.MODE_CHANGED";

示例如下:

  1. registerReceiver(
  2. new BroadcastReceiver() {
  3. @Override
  4. public void onReceive(Context context, Intent intent) {
  5. LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE);
  6. boolean isEnabled = manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);//可以换成其它provider
  7. if (isEnabled) {
  8. //已开启
  9. }
  10. else {
  11. //已关闭,可引导用户打开
  12. }
  13. }
  14. },
  15. new IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
  16. );