chromium/chrome/android/java/src/org/chromium/chrome/browser/browserservices/permissiondelegation/InstalledWebappGeolocationBridge.java

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.browserservices.permissiondelegation;

import android.os.Bundle;
import android.text.TextUtils;

import androidx.annotation.Nullable;
import androidx.browser.trusted.TrustedWebActivityCallback;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.ChromeApplicationImpl;
import org.chromium.chrome.browser.browserservices.TrustedWebActivityClient;
import org.chromium.url.GURL;

/**
 * Provides Trusted Web Activity Client App location for native. The C++ counterpart is the
 * {@code installed_webapp_geolocation_bridge.h}. The InstalledWebappGeolocationBridge is
 * responsible for converting start and stop location command from the native to the corresponding
 * TWA client app via {@link TrustedWebActivityClient}, and notify the native part when there is
 * a location result.
 * Lifecycle: The native part is responsible for controlling its lifecycle. A new instance will be
 * created for each new geolocation request. This class should not be used after "stopAndDestroy" is
 * called.
 */
public class InstalledWebappGeolocationBridge {
    static final String EXTRA_NEW_LOCATION_AVAILABLE_CALLBACK = "onNewLocationAvailable";
    public static final String EXTRA_NEW_LOCATION_ERROR_CALLBACK = "onNewLocationError";

    private long mNativePointer;
    private final GURL mUrl;

    private final TrustedWebActivityClient mTwaClient;

    private final TrustedWebActivityCallback mLocationUpdateCallback =
            new TrustedWebActivityCallback() {
                @Override
                public void onExtraCallback(String callbackName, @Nullable Bundle bundle) {
                    // Hop back over to the UI thread to deal with the result.
                    PostTask.postTask(
                            TaskTraits.UI_USER_VISIBLE,
                            () -> {
                                if (TextUtils.equals(
                                        callbackName, EXTRA_NEW_LOCATION_AVAILABLE_CALLBACK)) {
                                    notifyNewGeoposition(bundle);
                                } else if (TextUtils.equals(
                                        callbackName, EXTRA_NEW_LOCATION_ERROR_CALLBACK)) {
                                    String message =
                                            bundle != null ? bundle.getString("message", "") : "";
                                    notifyNewGeopositionError(message);
                                }
                            });
                }
            };

    InstalledWebappGeolocationBridge(long nativePtr, GURL url, TrustedWebActivityClient client) {
        mNativePointer = nativePtr;
        mUrl = url;
        mTwaClient = client;
    }

    @CalledByNative
    public static @Nullable InstalledWebappGeolocationBridge create(long nativePtr, GURL url) {
        if (url == null) return null;

        return new InstalledWebappGeolocationBridge(
                nativePtr,
                url,
                ChromeApplicationImpl.getComponent().resolveTrustedWebActivityClient());
    }

    @CalledByNative
    public void start(boolean highAccuracy) {
        mTwaClient.startListeningLocationUpdates(
                mUrl.getSpec(), highAccuracy, mLocationUpdateCallback);
    }

    @CalledByNative
    public void stopAndDestroy() {
        mNativePointer = 0;
        mTwaClient.stopLocationUpdates(mUrl.getSpec());
    }

    private void notifyNewGeoposition(@Nullable Bundle bundle) {
        if (bundle == null || mNativePointer == 0) return;

        double latitude = bundle.getDouble("latitude");
        double longitude = bundle.getDouble("longitude");
        // Android Location.getTime() is in milliseconds, convert to seconds before passing the
        // value to native.
        double timeStamp = bundle.getLong("timeStamp") / 1000.0;
        boolean hasAltitude = bundle.containsKey("altitude");
        double altitude = bundle.getDouble("altitude");
        boolean hasAccuracy = bundle.containsKey("accuracy");
        double accuracy = bundle.getDouble("accuracy");
        boolean hasBearing = bundle.containsKey("bearing");
        double bearing = bundle.getDouble("bearing");
        boolean hasSpeed = bundle.containsKey("speed");
        double speed = bundle.getDouble("speed");
        InstalledWebappGeolocationBridgeJni.get()
                .onNewLocationAvailable(
                        mNativePointer,
                        latitude,
                        longitude,
                        timeStamp,
                        hasAltitude,
                        altitude,
                        hasAccuracy,
                        accuracy,
                        hasBearing,
                        bearing,
                        hasSpeed,
                        speed);
    }

    private void notifyNewGeopositionError(String message) {
        if (mNativePointer == 0) return;
        InstalledWebappGeolocationBridgeJni.get().onNewErrorAvailable(mNativePointer, message);
    }

    @NativeMethods
    interface Natives {
        void onNewLocationAvailable(
                long nativeInstalledWebappGeolocationBridge,
                double latitude,
                double longitude,
                double timeStamp,
                boolean hasAltitude,
                double altitude,
                boolean hasAccuracy,
                double accuracy,
                boolean hasHeading,
                double heading,
                boolean hasSpeed,
                double speed);

        void onNewErrorAvailable(
                long nativeInstalledWebappGeolocationBridge,
                @JniType("std::string") String message);
    }
}