/*
 * Copyright (C) 2019 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.service.controls;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.service.controls.actions.ControlAction;
import android.service.controls.actions.ControlActionWrapper;
import android.service.controls.templates.ControlTemplate;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.util.Preconditions;

import java.util.List;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;

/**
 * Service implementation allowing applications to contribute controls to the
 * System UI.
 */
public abstract class ControlsProviderService extends Service {

    @SdkConstant(SdkConstantType.SERVICE_ACTION)
    public static final String SERVICE_CONTROLS =
            "android.service.controls.ControlsProviderService";

    /**
     * Manifest metadata to show a custom embedded activity as part of device controls.
     *
     * The value of this metadata must be the {@link ComponentName} as a string of an activity in
     * the same package that will be launched embedded in the device controls space.
     *
     * The activity must be exported, enabled and protected by
     * {@link Manifest.permission#BIND_CONTROLS}.
     *
     * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
     */
    public static final String META_DATA_PANEL_ACTIVITY =
            "android.service.controls.META_DATA_PANEL_ACTIVITY";

    /**
     * Boolean extra containing the value of the setting allowing actions on a locked device.
     *
     * This corresponds to the setting that indicates whether the user has
     * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as
     * {@code false} when the device is locked.
     *
     * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY}
     * is launched.
     */
    public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS =
            "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS";

    /**
     * @hide
     */
    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    public static final String ACTION_ADD_CONTROL =
            "android.service.controls.action.ADD_CONTROL";

    /**
     * @hide
     */
    public static final String EXTRA_CONTROL =
            "android.service.controls.extra.CONTROL";

    /**
     * @hide
     */
    public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";

    /**
     * @hide
     */
    public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";

    public static final @NonNull String TAG = "ControlsProviderService";

    private IBinder mToken;
    private RequestHandler mHandler;

    /**
     * Publisher for all available controls
     *
     * Retrieve all available controls. Use the stateless builder {@link Control.StatelessBuilder}
     * to build each Control. Call {@link Subscriber#onComplete} when done loading all unique
     * controls, or {@link Subscriber#onError} for error scenarios. Duplicate Controls will
     * replace the original.
     */
    @NonNull
    public abstract Publisher<Control> createPublisherForAllAvailable();

    /**
     * (Optional) Publisher for suggested controls
     *
     * The service may be asked to provide a small number of recommended controls, in
     * order to suggest some controls to the user for favoriting. The controls shall be built using
     * the stateless builder {@link Control.StatelessBuilder}. The total number of controls
     * requested through {@link Subscription#request} will be restricted to a maximum. Within this
     * larger limit, only 6 controls per structure will be loaded. Therefore, it is advisable to
     * seed multiple structures if they exist. Any control sent over this limit  will be discarded.
     * Call {@link Subscriber#onComplete} when done, or {@link Subscriber#onError} for error
     * scenarios.
     */
    @Nullable
    public Publisher<Control> createPublisherForSuggested() {
        return null;
    }

    /**
     * Return a valid Publisher for the given controlIds. This publisher will be asked to provide
     * updates for the given list of controlIds as long as the {@link Subscription} is valid.
     * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from
     * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected
     * that controls provided by this publisher were created using {@link Control.StatefulBuilder}.
     *
     * By default, all controls require the device to be unlocked in order for the user to interact
     * with it. This can be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}.
     */
    @NonNull
    public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds);

    /**
     * The user has interacted with a Control. The action is dictated by the type of
     * {@link ControlAction} that was sent. A response can be sent via
     * {@link Consumer#accept}, with the Integer argument being one of the provided
     * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
     * was received successfully, or if additional prompts should be presented to
     * the user. Any visual control updates should be sent via the Publisher.

     * By default, all invocations of this method will require the device be unlocked. This can
     * be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}.
     */
    public abstract void performControlAction(@NonNull String controlId,
            @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);

    @Override
    @NonNull
    public final IBinder onBind(@NonNull Intent intent) {
        mHandler = new RequestHandler(Looper.getMainLooper());

        Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
        mToken = bundle.getBinder(CALLBACK_TOKEN);

        return new IControlsProvider.Stub() {
            public void load(IControlsSubscriber subscriber) {
                mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
            }

            public void loadSuggested(IControlsSubscriber subscriber) {
                mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
                        .sendToTarget();
            }

            public void subscribe(List<String> controlIds,
                    IControlsSubscriber subscriber) {
                SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
                mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
            }

            public void action(String controlId, ControlActionWrapper action,
                               IControlsActionCallback cb) {
                ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb);
                mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget();
            }
        };
    }

    @Override
    public final boolean onUnbind(@NonNull Intent intent) {
        mHandler = null;
        return true;
    }

    private class RequestHandler extends Handler {
        private static final int MSG_LOAD = 1;
        private static final int MSG_SUBSCRIBE = 2;
        private static final int MSG_ACTION = 3;
        private static final int MSG_LOAD_SUGGESTED = 4;

        RequestHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_LOAD: {
                    final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);

                    ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy);
                    break;
                }

                case MSG_LOAD_SUGGESTED: {
                    final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);

                    Publisher<Control> publisher =
                            ControlsProviderService.this.createPublisherForSuggested();
                    if (publisher == null) {
                        Log.i(TAG, "No publisher provided for suggested controls");
                        proxy.onComplete();
                    } else {
                        publisher.subscribe(proxy);
                    }
                    break;
                }

                case MSG_SUBSCRIBE: {
                    final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
                    final SubscriberProxy proxy = new SubscriberProxy(
                            ControlsProviderService.this, false, mToken, sMsg.mSubscriber);

                    ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
                            .subscribe(proxy);
                    break;
                }

                case MSG_ACTION: {
                    final ActionMessage aMsg = (ActionMessage) msg.obj;
                    ControlsProviderService.this.performControlAction(aMsg.mControlId,
                            aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
                    break;
                }
            }
        }

        private Consumer<Integer> consumerFor(final String controlId,
                final IControlsActionCallback cb) {
            return (@NonNull Integer response) -> {
                Preconditions.checkNotNull(response);
                if (!ControlAction.isValidResponse(response)) {
                    Log.e(TAG, "Not valid response result: " + response);
                    response = ControlAction.RESPONSE_UNKNOWN;
                }
                try {
                    cb.accept(mToken, controlId, response);
                } catch (RemoteException ex) {
                    ex.rethrowAsRuntimeException();
                }
            };
        }
    }

    private static boolean isStatelessControl(Control control) {
        return (control.getStatus() == Control.STATUS_UNKNOWN
                && control.getControlTemplate().getTemplateType()
                == ControlTemplate.TYPE_NO_TEMPLATE
                && TextUtils.isEmpty(control.getStatusText()));
    }

    private static class SubscriberProxy implements Subscriber<Control> {
        private IBinder mToken;
        private IControlsSubscriber mCs;
        private boolean mEnforceStateless;
        private Context mContext;

        SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) {
            mEnforceStateless = enforceStateless;
            mToken = token;
            mCs = cs;
        }

        SubscriberProxy(Context context, boolean enforceStateless, IBinder token,
                IControlsSubscriber cs) {
            this(enforceStateless, token, cs);
            mContext = context;
        }

        public void onSubscribe(Subscription subscription) {
            try {
                mCs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
            } catch (RemoteException ex) {
                ex.rethrowAsRuntimeException();
            }
        }
        public void onNext(@NonNull Control control) {
            Preconditions.checkNotNull(control);
            try {
                if (mEnforceStateless && !isStatelessControl(control)) {
                    Log.w(TAG, "onNext(): control is not stateless. Use the "
                            + "Control.StatelessBuilder() to build the control.");
                    control = new Control.StatelessBuilder(control).build();
                }
                if (mContext != null) {
                    control.getControlTemplate().prepareTemplateForBinder(mContext);
                }
                mCs.onNext(mToken, control);
            } catch (RemoteException ex) {
                ex.rethrowAsRuntimeException();
            }
        }
        public void onError(Throwable t) {
            try {
                mCs.onError(mToken, t.toString());
            } catch (RemoteException ex) {
                ex.rethrowAsRuntimeException();
            }
        }
        public void onComplete() {
            try {
                mCs.onComplete(mToken);
            } catch (RemoteException ex) {
                ex.rethrowAsRuntimeException();
            }
        }
    }

    /**
     * Request SystemUI to prompt the user to add a control to favorites.
     * <br>
     * SystemUI may not honor this request in some cases, for example if the requested
     * {@link Control} is already a favorite, or the requesting package is not currently in the
     * foreground.
     *
     * @param context A context
     * @param componentName Component name of the {@link ControlsProviderService}
     * @param control A stateless control to show to the user
     */
    public static void requestAddControl(@NonNull Context context,
            @NonNull ComponentName componentName,
            @NonNull Control control) {
        Preconditions.checkNotNull(context);
        Preconditions.checkNotNull(componentName);
        Preconditions.checkNotNull(control);
        final String controlsPackage = context.getString(
                com.android.internal.R.string.config_controlsPackage);
        Intent intent = new Intent(ACTION_ADD_CONTROL);
        intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName);
        intent.setPackage(controlsPackage);
        if (isStatelessControl(control)) {
            intent.putExtra(EXTRA_CONTROL, control);
        } else {
            intent.putExtra(EXTRA_CONTROL, new Control.StatelessBuilder(control).build());
        }
        context.sendBroadcast(intent, Manifest.permission.BIND_CONTROLS);
    }

    private static class SubscriptionAdapter extends IControlsSubscription.Stub {
        final Subscription mSubscription;

        SubscriptionAdapter(Subscription s) {
            this.mSubscription = s;
        }

        public void request(long n) {
            mSubscription.request(n);
        }

        public void cancel() {
            mSubscription.cancel();
        }
    }

    private static class ActionMessage {
        final String mControlId;
        final ControlAction mAction;
        final IControlsActionCallback mCb;

        ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) {
            this.mControlId = controlId;
            this.mAction = action;
            this.mCb = cb;
        }
    }

    private static class SubscribeMessage {
        final List<String> mControlIds;
        final IControlsSubscriber mSubscriber;

        SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) {
            this.mControlIds = controlIds;
            this.mSubscriber = subscriber;
        }
    }
}
