Don't require permissions for isLocationEnabled

Attempt to avoid requiring clients from holding location permissions on
most Android devices. This requires some reflection, and the fallback
code path may still require permissions.

Test: added new LocationManagerCompatTest
Change-Id: I4930fa2ddf37cbeed2a23a35795a3ae33598b8c3
diff --git a/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java b/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java
new file mode 100644
index 0000000..0fc6225
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/location/LocationManagerCompatTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location;
+
+import static android.provider.Settings.Secure.LOCATION_MODE;
+import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.location.LocationManager;
+import android.os.Build;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link androidx.core.location.LocationManagerCompat}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LocationManagerCompatTest {
+
+    private Context mContext;
+    private LocationManager mLocationManager;
+
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mLocationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+    }
+
+    @Test
+    public void testIsLocationEnabled() {
+        boolean isLocationEnabled;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            isLocationEnabled = mLocationManager.isLocationEnabled();
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            isLocationEnabled = Settings.Secure.getInt(mContext.getContentResolver(), LOCATION_MODE,
+                    LOCATION_MODE_OFF) != LOCATION_MODE_OFF;
+        } else {
+            isLocationEnabled = !TextUtils.isEmpty(
+                    Settings.Secure.getString(mContext.getContentResolver(),
+                            Settings.Secure.LOCATION_PROVIDERS_ALLOWED));
+        }
+
+        assertEquals(isLocationEnabled, LocationManagerCompat.isLocationEnabled(mLocationManager));
+    }
+}
diff --git a/core/core/src/androidTest/java/androidx/core/location/OWNERS b/core/core/src/androidTest/java/androidx/core/location/OWNERS
new file mode 100644
index 0000000..a420c39
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/location/OWNERS
@@ -0,0 +1,3 @@
[email protected]
[email protected]
[email protected]
diff --git a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
index 87e62fe..ccf6365 100644
--- a/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/location/LocationManagerCompat.java
@@ -17,10 +17,13 @@
 package androidx.core.location;
 
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.provider.Settings.Secure.LOCATION_MODE;
+import static android.provider.Settings.Secure.LOCATION_MODE_OFF;
 
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import android.content.Context;
 import android.location.GnssStatus;
 import android.location.GpsStatus;
 import android.location.LocationManager;
@@ -28,6 +31,9 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Handler;
 import android.os.Looper;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.text.TextUtils;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -38,6 +44,7 @@
 import androidx.core.os.HandlerExecutor;
 import androidx.core.util.Preconditions;
 
+import java.lang.reflect.Field;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
@@ -52,6 +59,8 @@
 
     private static final long PRE_N_LOOPER_TIMEOUT_S = 4;
 
+    private static Field sContextField;
+
     /**
      * Returns the current enabled/disabled state of location.
      *
@@ -60,16 +69,36 @@
     public static boolean isLocationEnabled(@NonNull LocationManager locationManager) {
         if (VERSION.SDK_INT >= VERSION_CODES.P) {
             return locationManager.isLocationEnabled();
-        } else {
-            // NOTE: for KitKat and above, it's preferable to use the proper API at the time to get
-            // the location mode, Secure.getInt(context, LOCATION_MODE, LOCATION_MODE_OFF). however,
-            // this requires a context we don't have directly (we could either ask the client to
-            // pass one in, or use reflection to get it from the location manager), and since KitKat
-            // and above remained backwards compatible, we can fallback to pre-kitkat behavior.
-
-            return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
-                || locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
         }
+
+        if (VERSION.SDK_INT <= VERSION_CODES.KITKAT) {
+            // kitkat and below have pointless location permission requirements when using
+            // isProviderEnabled(). instead we attempt to reflect a context so that we can query
+            // the underlying setting. if this fails, we fallback to isProviderEnabled() which may
+            // require the caller to hold location permissions
+            try {
+                if (sContextField == null) {
+                    sContextField = LocationManager.class.getDeclaredField("mContext");
+                }
+                sContextField.setAccessible(true);
+                Context context = (Context) sContextField.get(locationManager);
+
+                if (VERSION.SDK_INT == VERSION_CODES.KITKAT) {
+                    return Secure.getInt(context.getContentResolver(), LOCATION_MODE,
+                            LOCATION_MODE_OFF) != LOCATION_MODE_OFF;
+                } else {
+                    return !TextUtils.isEmpty(
+                            Settings.Secure.getString(context.getContentResolver(),
+                                    Settings.Secure.LOCATION_PROVIDERS_ALLOWED));
+                }
+            } catch (ClassCastException | SecurityException | NoSuchFieldException
+                    | IllegalAccessException e) {
+                // oh well, fallback to isProviderEnabled()
+            }
+        }
+
+        return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+            || locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
     }
 
     @GuardedBy("sGnssStatusListeners")