/*
 * Copyright (C) 2022 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.app.ambientcontext;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.RemoteCallback;
import android.os.RemoteException;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Allows granted apps to register for event types defined in {@link AmbientContextEvent}.
 * After registration, the app receives a Consumer callback of the service status.
 * If it is {@link STATUS_SUCCESSFUL}, when the requested events are detected, the provided
 * {@link PendingIntent} callback will receive the list of detected {@link AmbientContextEvent}s.
 * If it is {@link STATUS_ACCESS_DENIED}, the app can call {@link #startConsentActivity}
 * to load the consent screen.
 *
 * @hide
 */
@SystemApi
@SystemService(Context.AMBIENT_CONTEXT_SERVICE)
public final class AmbientContextManager {
    /**
     * The bundle key for the service status query result, used in
     * {@code RemoteCallback#sendResult}.
     *
     * @hide
     */
    public static final String STATUS_RESPONSE_BUNDLE_KEY =
            "android.app.ambientcontext.AmbientContextStatusBundleKey";

    /**
     * The key of an intent extra indicating a list of detected {@link AmbientContextEvent}s.
     * The intent is sent to the app in the app's registered {@link PendingIntent}.
     */
    public static final String EXTRA_AMBIENT_CONTEXT_EVENTS =
            "android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENTS";

    /**
     * An unknown status.
     */
    public static final int STATUS_UNKNOWN = 0;

    /**
     * The value of the status code that indicates success.
     */
    public static final int STATUS_SUCCESS = 1;

    /**
     * The value of the status code that indicates one or more of the
     * requested events are not supported.
     */
    public static final int STATUS_NOT_SUPPORTED = 2;

    /**
     * The value of the status code that indicates service not available.
     */
    public static final int STATUS_SERVICE_UNAVAILABLE = 3;

    /**
     * The value of the status code that microphone is disabled.
     */
    public static final int STATUS_MICROPHONE_DISABLED = 4;

    /**
     * The value of the status code that the app is not granted access.
     */
    public static final int STATUS_ACCESS_DENIED = 5;

    /** @hide */
    @IntDef(prefix = { "STATUS_" }, value = {
            STATUS_UNKNOWN,
            STATUS_SUCCESS,
            STATUS_NOT_SUPPORTED,
            STATUS_SERVICE_UNAVAILABLE,
            STATUS_MICROPHONE_DISABLED,
            STATUS_ACCESS_DENIED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface StatusCode {}

    /**
     * Allows clients to retrieve the list of {@link AmbientContextEvent}s from the intent.
     *
     * @param intent received from the PendingIntent callback
     *
     * @return the list of events, or an empty list if the intent doesn't have such events.
     */
    @NonNull public static List<AmbientContextEvent> getEventsFromIntent(@NonNull Intent intent) {
        if (intent.hasExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENTS)) {
            return intent.getParcelableArrayListExtra(EXTRA_AMBIENT_CONTEXT_EVENTS,
                    android.app.ambientcontext.AmbientContextEvent.class);
        } else {
            return new ArrayList<>();
        }
    }

    private final Context mContext;
    private final IAmbientContextManager mService;

    /**
     * {@hide}
     */
    public AmbientContextManager(Context context, IAmbientContextManager service) {
        mContext = context;
        mService = service;
    }

    /**
     * Queries the {@link AmbientContextEvent} service status for the calling package, and
     * sends the result to the {@link Consumer} right after the call. This is used by foreground
     * apps to check whether the requested events are enabled for detection on the device.
     * If all events are enabled for detection, the response has
     * {@link AmbientContextManager#STATUS_SUCCESS}.
     * If any of the events are not consented by user, the response has
     * {@link AmbientContextManager#STATUS_ACCESS_DENIED}, and the app can
     * call {@link #startConsentActivity} to redirect the user to the consent screen.
     * If the AmbientContextRequest contains a mixed set of events containing values both greater
     * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
     * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
     * <p />
     *
     * Example:
     *
     * <pre><code>
     *   Set<Integer> eventTypes = new HashSet<>();
     *   eventTypes.add(AmbientContextEvent.EVENT_COUGH);
     *   eventTypes.add(AmbientContextEvent.EVENT_SNORE);
     *
     *   // Create Consumer
     *   Consumer<Integer> statusConsumer = status -> {
     *     int status = status.getStatusCode();
     *     if (status == AmbientContextManager.STATUS_SUCCESS) {
     *       // Show user it's enabled
     *     } else if (status == AmbientContextManager.STATUS_ACCESS_DENIED) {
     *       // Send user to grant access
     *       startConsentActivity(eventTypes);
     *     }
     *   };
     *
     *   // Query status
     *   AmbientContextManager ambientContextManager =
     *       context.getSystemService(AmbientContextManager.class);
     *   ambientContextManager.queryAmbientContextStatus(eventTypes, executor, statusConsumer);
     * </code></pre>
     *
     * @param eventTypes The set of event codes to check status on.
     * @param executor Executor on which to run the consumer callback.
     * @param consumer The consumer that handles the status code.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void queryAmbientContextServiceStatus(
            @NonNull @AmbientContextEvent.EventCode Set<Integer> eventTypes,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> consumer) {
        try {
            RemoteCallback callback = new RemoteCallback(result -> {
                int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> consumer.accept(status));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            });
            mService.queryServiceStatus(integerSetToIntArray(eventTypes),
                    mContext.getOpPackageName(), callback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Requests the consent data host to open an activity that allows users to modify consent.
     * If the eventTypes contains a mixed set of events containing values both greater than and less
     * than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request will be rejected
     * with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
     *
     * @param eventTypes The set of event codes to be consented.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void startConsentActivity(
            @NonNull @AmbientContextEvent.EventCode Set<Integer> eventTypes) {
        try {
            mService.startConsentActivity(
                    integerSetToIntArray(eventTypes), mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    private static int[] integerSetToIntArray(@NonNull Set<Integer> integerSet) {
        int[] intArray = new int[integerSet.size()];
        int i = 0;
        for (Integer type : integerSet) {
            intArray[i++] = type;
        }
        return intArray;
    }

    /**
     * Allows app to register as a {@link AmbientContextEvent} observer. The
     * observer receives a callback on the provided {@link PendingIntent} when the requested
     * event is detected. Registering another observer from the same package that has already been
     * registered will override the previous observer.
     * If the AmbientContextRequest contains a mixed set of events containing values both greater
     * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
     * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
     * <p />
     *
     * Example:
     *
     * <pre><code>
     *   // Create request
     *   AmbientContextEventRequest request = new AmbientContextEventRequest.Builder()
     *       .addEventType(AmbientContextEvent.EVENT_COUGH)
     *       .addEventType(AmbientContextEvent.EVENT_SNORE)
     *       .build();
     *
     *   // Create PendingIntent for delivering detection results to my receiver
     *   Intent intent = new Intent(actionString, null, context, MyBroadcastReceiver.class)
     *       .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
     *   PendingIntent pendingIntent =
     *       PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     *
     *   // Create Consumer of service status
     *   Consumer<Integer> statusConsumer = status -> {
     *       if (status == AmbientContextManager.STATUS_ACCESS_DENIED) {
     *         // User did not consent event detection. See #queryAmbientContextServiceStatus and
     *         // #startConsentActivity
     *       }
     *   };
     *
     *   // Register as observer
     *   AmbientContextManager ambientContextManager =
     *       context.getSystemService(AmbientContextManager.class);
     *   ambientContextManager.registerObserver(request, pendingIntent, executor, statusConsumer);
     *
     *   // Handle the list of {@link AmbientContextEvent}s in your receiver
     *   {@literal @}Override
     *   protected void onReceive(Context context, Intent intent) {
     *     List<AmbientContextEvent> events = AmbientContextManager.getEventsFromIntent(intent);
     *     if (!events.isEmpty()) {
     *       // Do something useful with the events.
     *     }
     *   }
     * </code></pre>
     *
     * @param request The request with events to observe.
     * @param resultPendingIntent A mutable {@link PendingIntent} that will be dispatched after the
     *                            requested events are detected.
     * @param executor Executor on which to run the consumer callback.
     * @param statusConsumer A consumer that handles the status code, which is returned
     *                      right after the call.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void registerObserver(
            @NonNull AmbientContextEventRequest request,
            @NonNull PendingIntent resultPendingIntent,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> statusConsumer) {
        Preconditions.checkArgument(!resultPendingIntent.isImmutable());
        try {
            RemoteCallback callback = new RemoteCallback(result -> {
                int statusCode = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> statusConsumer.accept(statusCode));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            });
            mService.registerObserver(request, resultPendingIntent, callback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Allows app to register as a {@link AmbientContextEvent} observer. Same as {@link
     * #registerObserver(AmbientContextEventRequest, PendingIntent, Executor, Consumer)},
     * but use {@link AmbientContextCallback} instead of {@link PendingIntent} as a callback on
     * detected events.
     * Registering another observer from the same package that has already been
     * registered will override the previous observer. If the same app previously calls
     * {@link #registerObserver(AmbientContextEventRequest, AmbientContextCallback, Executor)},
     * and now calls
     * {@link #registerObserver(AmbientContextEventRequest, PendingIntent, Executor, Consumer)},
     * the previous observer will be replaced with the new observer with the PendingIntent callback.
     * Or vice versa.
     * If the AmbientContextRequest contains a mixed set of events containing values both greater
     * than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
     * will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
     *
     * When the registration completes, a status will be returned to client through
     * {@link AmbientContextCallback#onRegistrationComplete(int)}.
     * If the AmbientContextManager service is not enabled yet, or the underlying detection service
     * is not running yet, {@link AmbientContextManager#STATUS_SERVICE_UNAVAILABLE} will be
     * returned, and the detection won't be really started.
     * If the underlying detection service feature is not enabled, or the requested event type is
     * not enabled yet, {@link AmbientContextManager#STATUS_NOT_SUPPORTED} will be returned, and the
     * detection won't be really started.
     * If there is no user consent,  {@link AmbientContextManager#STATUS_ACCESS_DENIED} will be
     * returned, and the detection won't be really started.
     * Otherwise, it will try to start the detection. And if it starts successfully, it will return
     * {@link AmbientContextManager#STATUS_SUCCESS}. If it fails to start the detection, then
     * it will return {@link AmbientContextManager#STATUS_SERVICE_UNAVAILABLE}
     * After registerObserver succeeds and when the service detects an event, the service will
     * trigger {@link AmbientContextCallback#onEvents(List)}.
     *
     * @hide
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void registerObserver(
            @NonNull AmbientContextEventRequest request,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull AmbientContextCallback ambientContextCallback) {
        try {
            IAmbientContextObserver observer = new IAmbientContextObserver.Stub() {
                @Override
                public void onEvents(List<AmbientContextEvent> events) throws RemoteException {
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        executor.execute(() -> ambientContextCallback.onEvents(events));
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                }

                @Override
                public void onRegistrationComplete(int statusCode) throws RemoteException {
                    final long identity = Binder.clearCallingIdentity();
                    try {
                        executor.execute(
                                () -> ambientContextCallback.onRegistrationComplete(statusCode));
                    } finally {
                        Binder.restoreCallingIdentity(identity);
                    }
                }
            };

            mService.registerObserverWithCallback(request, mContext.getPackageName(), observer);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Unregisters the requesting app as an {@code AmbientContextEvent} observer. Unregistering an
     * observer that was already unregistered or never registered will have no effect.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void unregisterObserver() {
        try {
            mService.unregisterObserver(mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}
