/*
 * 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 android.location.provider;

import static android.location.Location.EXTRA_NO_GPS_LOCATION;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Base class for location providers outside the system server.
 *
 * Location providers should be wrapped in a non-exported service which returns the result of
 * {@link #getBinder()} from the service's {@link android.app.Service#onBind(Intent)} method. The
 * service should not be exported so that components other than the system server cannot bind to it.
 * Alternatively, the service may be guarded by a permission that only system server can obtain. The
 * service may specify metadata on its capabilities:
 *
 * <ul>
 *     <li>
 *         "serviceVersion": An integer version code to help tie break if multiple services are
 *         capable of implementing the same location provider. All else equal, the service with the
 *         highest version code will be chosen. Assumed to be 0 if not specified.
 *     </li>
 *     <li>
 *         "serviceIsMultiuser": A boolean property, indicating if the service wishes to take
 *         responsibility for handling changes to the current user on the device. If true, the
 *         service will always be bound from the system user. If false, the service will always be
 *         bound from the current user. If the current user changes, the old binding will be
 *         released, and a new binding established under the new user. Assumed to be false if not
 *         specified.
 *     </li>
 * </ul>
 *
 * <p>The service should have an intent filter in place for the location provider it wishes to
 * implements. Defaults for some providers are specified as constants in this class.
 *
 * <p>Location providers are identified by their UID / package name / attribution tag. Based on this
 * identity, location providers may be given some special privileges (such as making special
 * requests to other location providers).
 *
 * @hide
 */
@SystemApi
public abstract class LocationProviderBase {

    /**
     * Callback to be invoked when a flush operation is complete and all flushed locations have been
     * reported.
     */
    public interface OnFlushCompleteCallback {

        /**
         * Should be invoked once the flush is complete.
         */
        void onFlushComplete();
    }

    /**
     * The action the wrapping service should have in its intent filter to implement the
     * {@link android.location.LocationManager#NETWORK_PROVIDER}.
     */
    @SuppressLint("ActionValue")
    public static final String ACTION_NETWORK_PROVIDER =
            "com.android.location.service.v3.NetworkLocationProvider";

    /**
     * The action the wrapping service should have in its intent filter to implement the
     * {@link android.location.LocationManager#FUSED_PROVIDER}.
     */
    @SuppressLint("ActionValue")
    public static final String ACTION_FUSED_PROVIDER =
            "com.android.location.service.FusedLocationProvider";

    final String mTag;
    final @Nullable String mAttributionTag;
    final IBinder mBinder;

    // write locked on mBinder, read lock is optional depending on atomicity requirements
    volatile @Nullable ILocationProviderManager mManager;
    volatile ProviderProperties mProperties;
    volatile boolean mAllowed;

    public LocationProviderBase(@NonNull Context context, @NonNull String tag,
            @NonNull ProviderProperties properties) {
        mTag = tag;
        mAttributionTag = context.getAttributionTag();
        mBinder = new Service();

        mManager = null;
        mProperties = Objects.requireNonNull(properties);
        mAllowed = true;
    }

    /**
     * Returns the IBinder instance that should be returned from the
     * {@link android.app.Service#onBind(Intent)} method of the wrapping service.
     */
    public final @Nullable IBinder getBinder() {
        return mBinder;
    }

    /**
     * Sets whether this provider is currently allowed or not. Note that this is specific to the
     * provider only, and is unrelated to global location settings. This is a hint to the location
     * manager that this provider will be unable to fulfill incoming requests. Setting a provider
     * as not allowed will result in the provider being disabled. Setting a provider as allowed
     * means that the provider may be in either the enabled or disabled state.
     *
     * <p>Some guidelines: providers should set their own allowed/disallowed 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 allowed or disallowed, 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 allowed/disallowed
     * state. If the provider proxies to another provider, the child provider's allowed/disallowed
     * state should be taken into account in the parent's allowed state. For most providers, it is
     * expected that they will be always allowed.
     */
    public void setAllowed(boolean allowed) {
        synchronized (mBinder) {
            if (mAllowed == allowed) {
                return;
            }

            mAllowed = allowed;
        }

        ILocationProviderManager manager = mManager;
        if (manager != null) {
            try {
                manager.onSetAllowed(mAllowed);
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            } catch (RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    /**
     * Returns true if this provider is allowed. Providers start as allowed on construction.
     */
    public boolean isAllowed() {
        return mAllowed;
    }

    /**
     * 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(@NonNull ProviderProperties properties) {
        synchronized (mBinder) {
            mProperties = Objects.requireNonNull(properties);
        }

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

    /**
     * Returns the currently set properties of the provider.
     */
    public @NonNull ProviderProperties getProperties() {
        return mProperties;
    }

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

    /**
     * Reports a new batch of locations from this provider. Locations must be ordered in the list
     * from earliest first to latest last.
     */
    public void reportLocations(@NonNull List<Location> locations) {
        ILocationProviderManager manager = mManager;
        if (manager != null) {


            try {
                manager.onReportLocations(stripExtras(locations));
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            } catch (RuntimeException e) {
                Log.w(mTag, e);
            }
        }
    }

    /**
     * Set the current {@link ProviderRequest} for this provider. Each call to this method overrides
     * any prior ProviderRequests. The provider should immediately attempt to provide locations (or
     * not provide locations) according to the parameters of the provider request.
     */
    public abstract void onSetRequest(@NonNull ProviderRequest request);

    /**
     * Requests a flush of any pending batched locations. The callback must always be invoked once
     * per invocation, and should be invoked after {@link #reportLocation(Location)} or
     * {@link #reportLocations(List)} has been invoked with any flushed locations. The callback may
     * be invoked immediately if no locations are flushed.
     */
    public abstract void onFlush(@NonNull OnFlushCompleteCallback callback);

    /**
     * Implements optional custom commands.
     */
    public abstract void onSendExtraCommand(@NonNull String command,
            @SuppressLint("NullableCollection")
            @Nullable Bundle extras);

    private static Location stripExtras(Location location) {
        Bundle extras = location.getExtras();
        if (extras != null && (extras.containsKey(EXTRA_NO_GPS_LOCATION)
                || extras.containsKey("indoorProbability")
                || extras.containsKey("coarseLocation"))) {
            location = new Location(location);
            extras = location.getExtras();
            extras.remove(EXTRA_NO_GPS_LOCATION);
            extras.remove("indoorProbability");
            extras.remove("coarseLocation");
            if (extras.isEmpty()) {
                location.setExtras(null);
            }
        }
        return location;
    }

    private static List<Location> stripExtras(List<Location> locations) {
        List<Location> mapped = locations;
        final int size = locations.size();
        int i = 0;
        for (Location location : locations) {
            Location newLocation = stripExtras(location);
            if (mapped != locations) {
                mapped.add(newLocation);
            } else if (newLocation != location) {
                mapped = new ArrayList<>(size);
                int j = 0;
                for (Location copiedLocation : locations) {
                    if (j >= i) {
                        break;
                    }
                    mapped.add(copiedLocation);
                    j++;
                }
                mapped.add(newLocation);
            }
            i++;
        }

        return mapped;
    }

    private final class Service extends ILocationProvider.Stub {

        Service() {}

        @Override
        public void setLocationProviderManager(ILocationProviderManager manager) {
            synchronized (mBinder) {
                try {
                    manager.onInitialize(mAllowed, mProperties, mAttributionTag);
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                } catch (RuntimeException e) {
                    Log.w(mTag, e);
                }

                mManager = manager;
            }
        }

        @Override
        public void setRequest(ProviderRequest request) {
            onSetRequest(request);
        }

        @Override
        public void flush() {
            onFlush(this::onFlushComplete);
        }

        private void onFlushComplete() {
            ILocationProviderManager manager = mManager;
            if (manager != null) {
                try {
                    manager.onFlushComplete();
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                } catch (RuntimeException e) {
                    Log.w(mTag, e);
                }
            }
        }

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