Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e6244c15 authored by David Christie's avatar David Christie Committed by Android (Google) Code Review
Browse files

Merge "Fix issue where location provider overlay may not bind" into main

parents b5eabdec 51057b0f
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -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>
+134 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+52 −0
Original line number Diff line number Diff line
/*
 * 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) {
    }
}
+202 −0
Original line number Diff line number Diff line
/*
 * 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()));
        }
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -463,11 +463,13 @@ public class LocationManagerService extends ILocationManager.Stub implements
                    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) {
Loading