Fix issue where location provider overlay may not bind
Using a default backup provider build into the system that will be
used as last priority. Guards against many cases of user location
not being available when the overlay app is misbehaving.
Bug: 325028454
Test: atest GnssOverlayLocationServiceTest, manual
Change-Id: I6f357c9b0fcc19d56f80119a4fd358260f89b057
diff --git a/packages/FusedLocation/AndroidManifest.xml b/packages/FusedLocation/AndroidManifest.xml
index 05561d7..158c33a 100644
--- a/packages/FusedLocation/AndroidManifest.xml
+++ b/packages/FusedLocation/AndroidManifest.xml
@@ -28,6 +28,7 @@
<uses-permission android:name="android.permission.INSTALL_LOCATION_PROVIDER" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+ <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<application
android:label="@string/app_label"
@@ -49,5 +50,17 @@
<meta-data android:name="serviceVersion" android:value="0" />
<meta-data android:name="serviceIsMultiuser" android:value="true" />
</service>
+
+ <!-- GNSS overlay Service that LocationManagerService binds to.
+ LocationManagerService will bind to the service with the highest
+ version. -->
+ <service android:name="com.android.location.gnss.GnssOverlayLocationService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.location.provider.action.GNSS_PROVIDER" />
+ </intent-filter>
+ <meta-data android:name="serviceVersion" android:value="0" />
+ <meta-data android:name="serviceIsMultiuser" android:value="true" />
+ </service>
</application>
</manifest>
diff --git a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java
new file mode 100644
index 0000000..c6576e3
--- /dev/null
+++ b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationProvider.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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 com.android.location.gnss;
+
+import static android.location.provider.ProviderProperties.ACCURACY_FINE;
+import static android.location.provider.ProviderProperties.POWER_USAGE_HIGH;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.location.provider.LocationProviderBase;
+import android.location.provider.ProviderProperties;
+import android.location.provider.ProviderRequest;
+import android.os.Bundle;
+import android.util.SparseArray;
+
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ConcurrentUtils;
+
+import java.util.List;
+
+/** Basic pass-through GNSS location provider implementation. */
+public class GnssOverlayLocationProvider extends LocationProviderBase {
+
+ private static final String TAG = "GnssOverlay";
+
+ private static final ProviderProperties PROPERTIES = new ProviderProperties.Builder()
+ .setHasAltitudeSupport(true)
+ .setHasSpeedSupport(true)
+ .setHasBearingSupport(true)
+ .setPowerUsage(POWER_USAGE_HIGH)
+ .setAccuracy(ACCURACY_FINE)
+ .build();
+
+ @GuardedBy("mPendingFlushes")
+ private final SparseArray<OnFlushCompleteCallback> mPendingFlushes = new SparseArray<>();
+
+ private final LocationManager mLocationManager;
+
+ private final GnssLocationListener mGnssLocationListener = new GnssLocationListener();
+
+ @GuardedBy("mPendingFlushes")
+ private int mFlushCode = 0;
+
+ /** Location listener for receiving locations from LocationManager. */
+ private class GnssLocationListener implements LocationListener {
+ @Override
+ public void onLocationChanged(Location location) {
+ reportLocation(location);
+ }
+
+ @Override
+ public void onLocationChanged(List<Location> locations) {
+ reportLocations(locations);
+ }
+
+ @Override
+ public void onFlushComplete(int requestCode) {
+ OnFlushCompleteCallback flushCompleteCallback;
+ synchronized (mPendingFlushes) {
+ flushCompleteCallback = mPendingFlushes.get(requestCode);
+ mPendingFlushes.remove(requestCode);
+ }
+ if (flushCompleteCallback != null) {
+ flushCompleteCallback.onFlushComplete();
+ }
+ }
+ }
+
+ public GnssOverlayLocationProvider(Context context) {
+ super(context, TAG, PROPERTIES);
+ mLocationManager = context.getSystemService(LocationManager.class);
+ }
+
+ void start() {
+ }
+
+ void stop() {
+ mLocationManager.removeUpdates(mGnssLocationListener);
+ }
+
+ @Override
+ public void onSendExtraCommand(String command, @Nullable Bundle extras) {
+ mLocationManager.sendExtraCommand(LocationManager.GPS_HARDWARE_PROVIDER, command, extras);
+ }
+
+ @Override
+ public void onFlush(OnFlushCompleteCallback callback) {
+ int flushCodeCopy;
+ synchronized (mPendingFlushes) {
+ flushCodeCopy = mFlushCode++;
+ mPendingFlushes.put(flushCodeCopy, callback);
+ }
+ mLocationManager.requestFlush(
+ LocationManager.GPS_HARDWARE_PROVIDER, mGnssLocationListener, flushCodeCopy);
+ }
+
+ @Override
+ public void onSetRequest(ProviderRequest request) {
+ if (request.isActive()) {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_HARDWARE_PROVIDER,
+ new LocationRequest.Builder(request.getIntervalMillis())
+ .setMaxUpdateDelayMillis(request.getMaxUpdateDelayMillis())
+ .setLowPower(request.isLowPower())
+ .setLocationSettingsIgnored(request.isLocationSettingsIgnored())
+ .setWorkSource(request.getWorkSource())
+ .setQuality(request.getQuality())
+ .build(),
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ mGnssLocationListener);
+ } else {
+ mLocationManager.removeUpdates(mGnssLocationListener);
+ }
+ }
+}
diff --git a/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java
new file mode 100644
index 0000000..dd034fe
--- /dev/null
+++ b/packages/FusedLocation/src/com/android/location/gnss/GnssOverlayLocationService.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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 com.android.location.gnss;
+
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class GnssOverlayLocationService extends Service {
+
+ @Nullable private GnssOverlayLocationProvider mProvider;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (mProvider == null) {
+ mProvider = new GnssOverlayLocationProvider(this);
+ mProvider.start();
+ }
+
+ return mProvider.getBinder();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProvider != null) {
+ mProvider.stop();
+ mProvider = null;
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ }
+}
diff --git a/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java b/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java
new file mode 100644
index 0000000..5b33deb
--- /dev/null
+++ b/packages/FusedLocation/test/src/com/android/location/gnss/tests/GnssOverlayLocationServiceTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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 com.android.location.gnss.tests;
+
+import static android.location.LocationManager.GPS_HARDWARE_PROVIDER;
+
+import static androidx.test.ext.truth.location.LocationSubject.assertThat;
+
+import android.content.Context;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationRequest;
+import android.location.provider.ILocationProvider;
+import android.location.provider.ILocationProviderManager;
+import android.location.provider.ProviderProperties;
+import android.location.provider.ProviderRequest;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.location.gnss.GnssOverlayLocationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class GnssOverlayLocationServiceTest {
+
+ private static final String TAG = "GnssOverlayLocationServiceTest";
+
+ private static final long TIMEOUT_MS = 5000;
+
+ private Random mRandom;
+ private LocationManager mLocationManager;
+
+ private ILocationProvider mProvider;
+ private LocationProviderManagerCapture mManager;
+
+ @Before
+ public void setUp() throws Exception {
+ long seed = System.currentTimeMillis();
+ Log.i(TAG, "location seed: " + seed);
+
+ Context context = ApplicationProvider.getApplicationContext();
+ mRandom = new Random(seed);
+ mLocationManager = context.getSystemService(LocationManager.class);
+
+ setMockLocation(true);
+
+ mManager = new LocationProviderManagerCapture();
+ mProvider = ILocationProvider.Stub.asInterface(
+ new GnssOverlayLocationProvider(context).getBinder());
+ mProvider.setLocationProviderManager(mManager);
+
+ mLocationManager.addTestProvider(GPS_HARDWARE_PROVIDER,
+ true,
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ Criteria.POWER_MEDIUM,
+ Criteria.ACCURACY_FINE);
+ mLocationManager.setTestProviderEnabled(GPS_HARDWARE_PROVIDER, true);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (String provider : mLocationManager.getAllProviders()) {
+ mLocationManager.removeTestProvider(provider);
+ }
+
+ setMockLocation(false);
+ }
+
+ @Test
+ public void testGpsRequest() throws Exception {
+ mProvider.setRequest(
+ new ProviderRequest.Builder()
+ .setQuality(LocationRequest.QUALITY_HIGH_ACCURACY)
+ .setIntervalMillis(1000)
+ .build());
+
+ Location location = createLocation(GPS_HARDWARE_PROVIDER, mRandom);
+ mLocationManager.setTestProviderLocation(GPS_HARDWARE_PROVIDER, location);
+
+ assertThat(mManager.getNextLocation(TIMEOUT_MS)).isEqualTo(location);
+ }
+
+ private static class LocationProviderManagerCapture extends ILocationProviderManager.Stub {
+
+ private final LinkedBlockingQueue<Location> mLocations;
+
+ private LocationProviderManagerCapture() {
+ mLocations = new LinkedBlockingQueue<>();
+ }
+
+ @Override
+ public void onInitialize(boolean allowed, ProviderProperties properties,
+ String attributionTag) {}
+
+ @Override
+ public void onSetAllowed(boolean allowed) {}
+
+ @Override
+ public void onSetProperties(ProviderProperties properties) {}
+
+ @Override
+ public void onReportLocation(Location location) {
+ mLocations.add(location);
+ }
+
+ @Override
+ public void onReportLocations(List<Location> locations) {
+ mLocations.addAll(locations);
+ }
+
+ @Override
+ public void onFlushComplete() {}
+
+ public Location getNextLocation(long timeoutMs) throws InterruptedException {
+ return mLocations.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private static final double MIN_LATITUDE = -90D;
+ private static final double MAX_LATITUDE = 90D;
+ private static final double MIN_LONGITUDE = -180D;
+ private static final double MAX_LONGITUDE = 180D;
+
+ private static final float MIN_ACCURACY = 1;
+ private static final float MAX_ACCURACY = 100;
+
+ private static Location createLocation(String provider, Random random) {
+ return createLocation(provider,
+ MIN_LATITUDE + random.nextDouble() * (MAX_LATITUDE - MIN_LATITUDE),
+ MIN_LONGITUDE + random.nextDouble() * (MAX_LONGITUDE - MIN_LONGITUDE),
+ MIN_ACCURACY + random.nextFloat() * (MAX_ACCURACY - MIN_ACCURACY));
+ }
+
+ private static Location createLocation(String provider, double latitude, double longitude,
+ float accuracy) {
+ Location location = new Location(provider);
+ location.setLatitude(latitude);
+ location.setLongitude(longitude);
+ location.setAccuracy(accuracy);
+ location.setTime(System.currentTimeMillis());
+ location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
+ return location;
+ }
+
+ private static void setMockLocation(boolean allowed) throws IOException {
+ ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .executeShellCommand("appops set "
+ + InstrumentationRegistry.getTargetContext().getPackageName()
+ + " android:mock_location " + (allowed ? "allow" : "deny"));
+ try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] buffer = new byte[32768];
+ int count;
+ try {
+ while ((count = fis.read(buffer)) != -1) {
+ os.write(buffer, 0, count);
+ }
+ fis.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ Log.e(TAG, new String(os.toByteArray()));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index 75be068..a608049 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -463,11 +463,13 @@
com.android.internal.R.bool.config_useGnssHardwareProvider);
AbstractLocationProvider gnssProvider = null;
if (!useGnssHardwareProvider) {
+ // TODO: Create a separate config_enableGnssLocationOverlay config resource
+ // if we want to selectively enable a GNSS overlay but disable a fused overlay.
gnssProvider = ProxyLocationProvider.create(
mContext,
GPS_PROVIDER,
ACTION_GNSS_PROVIDER,
- com.android.internal.R.bool.config_useGnssHardwareProvider,
+ com.android.internal.R.bool.config_enableFusedLocationOverlay,
com.android.internal.R.string.config_gnssLocationProviderPackageName);
}
if (gnssProvider == null) {
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index ecb4fcc..40e538b 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -1618,6 +1618,17 @@
*/
@SuppressLint("AndroidFrameworkRequiresPermission")
public boolean isVisibleToCaller() {
+ // Anything sharing the system's UID can view all providers
+ if (Binder.getCallingUid() == Process.SYSTEM_UID) {
+ return true;
+ }
+
+ // If an app mocked this provider, anybody can access it (the goal is
+ // to behave as if this provider didn't naturally exist).
+ if (mProvider.isMock()) {
+ return true;
+ }
+
for (String permission : mRequiredPermissions) {
if (mContext.checkCallingOrSelfPermission(permission) != PERMISSION_GRANTED) {
return false;