/*
 * Copyright (C) 2010 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.provider;

import android.annotation.Nullable;
import android.content.Context;
import android.location.ILocationManager;
import android.location.Location;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.WorkSource;
import android.util.Log;

import com.android.internal.location.ILocationProvider;
import com.android.internal.location.ILocationProviderManager;
import com.android.internal.location.ProviderProperties;
import com.android.internal.location.ProviderRequest;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Base class for location providers implemented as unbundled services.
 *
 * <p>The network location provider must export a service with action
 * "com.android.location.service.v2.NetworkLocationProvider"
 * and a valid minor version in a meta-data field on the service, and
 * then return the result of {@link #getBinder()} on service binding.
 *
 * <p>The fused location provider must export a service with action
 * "com.android.location.service.FusedLocationProvider"
 * and a valid minor version in a meta-data field on the service, and
 * then return the result of {@link #getBinder()} on service binding.
 *
 * <p>IMPORTANT: This class is effectively a public API for unbundled
 * applications, and must remain API stable. See README.txt in the root
 * of this package for more information.
 */
public abstract class LocationProviderBase {

    /**
     * Bundle key for a version of the location containing no GPS data.
     * Allows location providers to flag locations as being safe to
     * feed to LocationFudger.
     */
    public static final String EXTRA_NO_GPS_LOCATION = Location.EXTRA_NO_GPS_LOCATION;

    /**
     * Name of the Fused location provider.
     *
     * <p>This provider combines inputs for all possible location sources
     * to provide the best possible Location fix.
     */
    public static final String FUSED_PROVIDER = LocationManager.FUSED_PROVIDER;

    private final String mTag;
    private final IBinder mBinder;

    /**
     * This field may be removed in the future, do not rely on it.
     *
     * @deprecated Do not use this field! Use LocationManager APIs instead. If you use this field
     * you may be broken in the future.
     * @hide
     */
    @Deprecated
    protected final ILocationManager mLocationManager;

    // write locked on mBinder, read lock is optional depending on atomicity requirements
    @Nullable private volatile ILocationProviderManager mManager;
    private volatile ProviderProperties mProperties;
    private volatile boolean mEnabled;
    private final ArrayList<String> mAdditionalProviderPackages;

    public LocationProviderBase(String tag, ProviderPropertiesUnbundled properties) {
        mTag = tag;
        mBinder = new Service();

        mLocationManager = ILocationManager.Stub.asInterface(
                ServiceManager.getService(Context.LOCATION_SERVICE));

        mManager = null;
        mProperties = properties.getProviderProperties();
        mEnabled = true;
        mAdditionalProviderPackages = new ArrayList<>(0);
    }

    public IBinder getBinder() {
        return mBinder;
    }

    /**
     * Sets whether this provider is currently enabled or not. Note that this is specific to the
     * provider only, and is not related to global location settings. This is a hint to the Location
     * Manager that this provider will generally be unable to fulfill incoming requests. This
     * provider may still receive callbacks to onSetRequest while not enabled, and must decide
     * whether to attempt to satisfy those requests or not.
     *
     * Some guidelines: providers should set their own enabled/disabled status based only on state
     * "owned" by that provider. For instance, providers should not take into account the state of
     * the location master setting when setting themselves enabled or disabled, as this state is not
     * owned by a particular provider. If a provider requires some additional user consent that is
     * particular to the provider, this should be use to set the enabled/disabled state. If the
     * provider proxies to another provider, the child provider's enabled/disabled state should be
     * taken into account in the parent's enabled/disabled state. For most providers, it is expected
     * that they will be always enabled.
     */
    public void setEnabled(boolean enabled) {
        synchronized (mBinder) {
            if (mEnabled == enabled) {
                return;
            }

            mEnabled = enabled;
        }

        ILocationProviderManager manager = mManager;
        if (manager != null) {
            try {
                manager.onSetEnabled(mEnabled);
            } catch (RemoteException | RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    /**
     * Sets the provider properties that may be queried by clients. Generally speaking, providers
     * should try to avoid changing their properties after construction.
     */
    public void setProperties(ProviderPropertiesUnbundled properties) {
        synchronized (mBinder) {
            mProperties = properties.getProviderProperties();
        }

        ILocationProviderManager manager = mManager;
        if (manager != null) {
            try {
                manager.onSetProperties(mProperties);
            } catch (RemoteException | RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    /**
     * Sets a list of additional packages that should be considered as part of this location
     * provider for the purposes of generating locations. This should generally only be used when
     * another package may issue location requests on behalf of this package in the course of
     * providing location. This will inform location services to treat the other packages as
     * location providers as well.
     */
    public void setAdditionalProviderPackages(List<String> packageNames) {
        synchronized (mBinder) {
            mAdditionalProviderPackages.clear();
            mAdditionalProviderPackages.addAll(packageNames);
        }

        ILocationProviderManager manager = mManager;
        if (manager != null) {
            try {
                manager.onSetAdditionalProviderPackages(mAdditionalProviderPackages);
            } catch (RemoteException | RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    /**
     * Returns true if this provider has been set as enabled. This will be true unless explicitly
     * set otherwise.
     */
    public boolean isEnabled() {
        return mEnabled;
    }

    /**
     * Reports a new location from this provider.
     */
    public void reportLocation(Location location) {
        ILocationProviderManager manager = mManager;
        if (manager != null) {
            try {
                manager.onReportLocation(location);
            } catch (RemoteException | RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    protected void onInit() {
        // call once so that providers designed for APIs pre-Q are not broken
        onEnable();
    }

    /**
     * @deprecated This callback will be invoked once when the provider is created to maintain
     * backwards compatibility with providers not designed for Android Q and above. This method
     * should only be implemented in location providers that need to support SDKs below Android Q.
     * Even in this case, it is usually unnecessary to implement this callback with the correct
     * design. This method may be removed in the future.
     */
    @Deprecated
    protected void onEnable() {}

    /**
     * @deprecated This callback will be never be invoked on Android Q and above. This method should
     * only be implemented in location providers that need to support SDKs below Android Q. Even in
     * this case, it is usually unnecessary to implement this callback with the correct design. This
     * method may be removed in the future.
     */
    @Deprecated
    protected void onDisable() {}

    /**
     * Set the {@link ProviderRequest} requirements for this provider. Each call to this method
     * overrides all previous requests. This method might trigger the provider to start returning
     * locations, or to stop returning locations, depending on the parameters in the request.
     */
    protected abstract void onSetRequest(ProviderRequestUnbundled request, WorkSource source);

    /**
     * @deprecated This callback will never be invoked on Android Q and above. This method may be
     * removed in the future. Prefer to dump provider state via the containing service instead.
     */
    @Deprecated
    protected void onDump(FileDescriptor fd, PrintWriter pw, String[] args) {}

    /**
     * This method will no longer be invoked.
     *
     * Returns a information on the status of this provider.
     * <p>{@link android.location.LocationProvider#OUT_OF_SERVICE} is returned if the provider is
     * out of service, and this is not expected to change in the near
     * future; {@link android.location.LocationProvider#TEMPORARILY_UNAVAILABLE} is returned if
     * the provider is temporarily unavailable but is expected to be
     * available shortly; and {@link android.location.LocationProvider#AVAILABLE} is returned
     * if the provider is currently available.
     *
     * <p>If extras is non-null, additional status information may be
     * added to it in the form of provider-specific key/value pairs.
     *
     * @deprecated This callback will be never be invoked on Android Q and above. This method should
     * only be implemented in location providers that need to support SDKs below Android Q. This
     * method may be removed in the future.
     */
    @Deprecated
    protected int onGetStatus(Bundle extras) {
        return LocationProvider.AVAILABLE;
    }

    /**
     * This method will no longer be invoked.
     *
     * Returns the time at which the status was last updated. It is the
     * responsibility of the provider to appropriately set this value using
     * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}.
     * there is a status update that it wishes to broadcast to all its
     * listeners. The provider should be careful not to broadcast
     * the same status again.
     *
     * @return time of last status update in millis since last reboot
     *
     * @deprecated This callback will be never be invoked on Android Q and above. This method should
     * only be implemented in location providers that need to support SDKs below Android Q. This
     * method may be removed in the future.
     */
    @Deprecated
    protected long onGetStatusUpdateTime() {
        return 0;
    }

    /**
     * Implements location provider specific custom commands. The return value will be ignored on
     * Android Q and above.
     */
    protected boolean onSendExtraCommand(@Nullable String command, @Nullable Bundle extras) {
        return false;
    }

    private final class Service extends ILocationProvider.Stub {

        @Override
        public void setLocationProviderManager(ILocationProviderManager manager) {
            synchronized (mBinder) {
                try {
                    if (!mAdditionalProviderPackages.isEmpty()) {
                        manager.onSetAdditionalProviderPackages(mAdditionalProviderPackages);
                    }
                    manager.onSetProperties(mProperties);
                    manager.onSetEnabled(mEnabled);
                } catch (RemoteException e) {
                    Log.w(mTag, e);
                }

                mManager = manager;
            }

            onInit();
        }

        @Override
        public void setRequest(ProviderRequest request, WorkSource ws) {
            onSetRequest(new ProviderRequestUnbundled(request), ws);
        }

        @Override
        public int getStatus(Bundle extras) {
            return onGetStatus(extras);
        }

        @Override
        public long getStatusUpdateTime() {
            return onGetStatusUpdateTime();
        }

        @Override
        public void sendExtraCommand(String command, Bundle extras) {
            onSendExtraCommand(command, extras);
        }
    }
}
