/*
 * Copyright (C) 2013 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.media.remotedisplay;

import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.IRemoteDisplayCallback;
import android.media.IRemoteDisplayProvider;
import android.media.RemoteDisplayState;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.ArrayMap;

import java.util.Collection;

/**
 * Base class for remote display providers implemented as unbundled services.
 * <p>
 * To implement your remote display provider service, create a subclass of
 * {@link Service} and override the {@link Service#onBind Service.onBind()} method
 * to return the provider's binder when the {@link #SERVICE_INTERFACE} is requested.
 * </p>
 * <pre>
 *   public class SampleRemoteDisplayProviderService extends Service {
 *       private SampleProvider mProvider;
 *
 *       public IBinder onBind(Intent intent) {
 *           if (intent.getAction().equals(RemoteDisplayProvider.SERVICE_INTERFACE)) {
 *               if (mProvider == null) {
 *                   mProvider = new SampleProvider(this);
 *               }
 *               return mProvider.getBinder();
 *           }
 *           return null;
 *       }
 *
 *       class SampleProvider extends RemoteDisplayProvider {
 *           public SampleProvider() {
 *               super(SampleRemoteDisplayProviderService.this);
 *           }
 *
 *           // --- Implementation goes here ---
 *       }
 *   }
 * </pre>
 * <p>
 * Declare your remote display provider service in your application manifest
 * like this:
 * </p>
 * <pre>
 *   &lt;application>
 *       &lt;uses-library android:name="com.android.media.remotedisplay" />
 *
 *       &lt;service android:name=".SampleRemoteDisplayProviderService"
 *               android:label="@string/sample_remote_display_provider_service"
 *               android:exported="true"
 *               android:permission="android.permission.BIND_REMOTE_DISPLAY">
 *           &lt;intent-filter>
 *               &lt;action android:name="com.android.media.remotedisplay.RemoteDisplayProvider" />
 *           &lt;/intent-filter>
 *       &lt;/service>
 *   &lt;/application>
 * </pre>
 * <p>
 * This object is not thread safe.  It is only intended to be accessed on the
 * {@link Context#getMainLooper main looper thread} of an application.
 * </p><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.
 * </p>
 */
public abstract class RemoteDisplayProvider {
    private static final int MSG_SET_CALLBACK = 1;
    private static final int MSG_SET_DISCOVERY_MODE = 2;
    private static final int MSG_CONNECT = 3;
    private static final int MSG_DISCONNECT = 4;
    private static final int MSG_SET_VOLUME = 5;
    private static final int MSG_ADJUST_VOLUME = 6;

    private final Context mContext;
    private final ProviderStub mStub;
    private final ProviderHandler mHandler;
    private final ArrayMap<String, RemoteDisplay> mDisplays =
            new ArrayMap<String, RemoteDisplay>();
    private IRemoteDisplayCallback mCallback;
    private int mDiscoveryMode = DISCOVERY_MODE_NONE;

    private PendingIntent mSettingsPendingIntent;

    /**
     * The {@link Intent} that must be declared as handled by the service.
     * Put this in your manifest.
     */
    public static final String SERVICE_INTERFACE = RemoteDisplayState.SERVICE_INTERFACE;

    /**
     * Discovery mode: Do not perform any discovery.
     */
    public static final int DISCOVERY_MODE_NONE = RemoteDisplayState.DISCOVERY_MODE_NONE;

    /**
     * Discovery mode: Passive or low-power periodic discovery.
     * <p>
     * This mode indicates that an application is interested in knowing whether there
     * are any remote displays paired or available but doesn't need the latest or
     * most detailed information.  The provider may scan at a lower rate or rely on
     * knowledge of previously paired devices.
     * </p>
     */
    public static final int DISCOVERY_MODE_PASSIVE = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;

    /**
     * Discovery mode: Active discovery.
     * <p>
     * This mode indicates that the user is actively trying to connect to a route
     * and we should perform continuous scans.  This mode may use significantly more
     * power but is intended to be short-lived.
     * </p>
     */
    public static final int DISCOVERY_MODE_ACTIVE = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;

    /**
     * Creates a remote display provider.
     *
     * @param context The application context for the remote display provider.
     */
    public RemoteDisplayProvider(Context context) {
        mContext = context;
        mStub = new ProviderStub();
        mHandler = new ProviderHandler(context.getMainLooper());
    }

    /**
     * Gets the context of the remote display provider.
     */
    public final Context getContext() {
        return mContext;
    }

    /**
     * Gets the Binder associated with the provider.
     * <p>
     * This is intended to be used for the onBind() method of a service that implements
     * a remote display provider service.
     * </p>
     *
     * @return The IBinder instance associated with the provider.
     */
    public IBinder getBinder() {
        return mStub;
    }

    /**
     * Called when the current discovery mode changes.
     *
     * @param mode The new discovery mode.
     */
    public void onDiscoveryModeChanged(int mode) {
    }

    /**
     * Called when the system would like to connect to a display.
     *
     * @param display The remote display.
     */
    public void onConnect(RemoteDisplay display) {
    }

    /**
     * Called when the system would like to disconnect from a display.
     *
     * @param display The remote display.
     */
    public void onDisconnect(RemoteDisplay display) {
    }

    /**
     * Called when the system would like to set the volume of a display.
     *
     * @param display The remote display.
     * @param volume The desired volume.
     */
    public void onSetVolume(RemoteDisplay display, int volume) {
    }

    /**
     * Called when the system would like to adjust the volume of a display.
     *
     * @param display The remote display.
     * @param delta An increment to add to the current volume, such as +1 or -1.
     */
    public void onAdjustVolume(RemoteDisplay display, int delta) {
    }

    /**
     * Gets the current discovery mode.
     *
     * @return The current discovery mode.
     */
    public int getDiscoveryMode() {
        return mDiscoveryMode;
    }

    /**
     * Gets the current collection of displays.
     *
     * @return The current collection of displays, which must not be modified.
     */
    public Collection<RemoteDisplay> getDisplays() {
        return mDisplays.values();
    }

    /**
     * Adds the specified remote display and notifies the system.
     *
     * @param display The remote display that was added.
     * @throws IllegalStateException if there is already a display with the same id.
     */
    public void addDisplay(RemoteDisplay display) {
        if (display == null || mDisplays.containsKey(display.getId())) {
            throw new IllegalArgumentException("display");
        }
        mDisplays.put(display.getId(), display);
        publishState();
    }

    /**
     * Updates information about the specified remote display and notifies the system.
     *
     * @param display The remote display that was added.
     * @throws IllegalStateException if the display was n
     */
    public void updateDisplay(RemoteDisplay display) {
        if (display == null || mDisplays.get(display.getId()) != display) {
            throw new IllegalArgumentException("display");
        }
        publishState();
    }

    /**
     * Removes the specified remote display and tells the system about it.
     *
     * @param display The remote display that was removed.
     */
    public void removeDisplay(RemoteDisplay display) {
        if (display == null || mDisplays.get(display.getId()) != display) {
            throw new IllegalArgumentException("display");
        }
        mDisplays.remove(display.getId());
        publishState();
    }

    /**
     * Finds the remote display with the specified id, returns null if not found.
     *
     * @param id Id of the remote display.
     * @return The display, or null if none.
     */
    public RemoteDisplay findRemoteDisplay(String id) {
        return mDisplays.get(id);
    }

    /**
     * Gets a pending intent to launch the remote display settings activity.
     *
     * @return A pending intent to launch the settings activity.
     */
    public PendingIntent getSettingsPendingIntent() {
        if (mSettingsPendingIntent == null) {
            Intent settingsIntent = new Intent(Settings.ACTION_CAST_SETTINGS);
            settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                    | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
                    | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            mSettingsPendingIntent = PendingIntent.getActivity(
                    mContext, 0, settingsIntent, 0, null);
        }
        return mSettingsPendingIntent;
    }

    void setCallback(IRemoteDisplayCallback callback) {
        mCallback = callback;
        publishState();
    }

    void setDiscoveryMode(int mode) {
        if (mDiscoveryMode != mode) {
            mDiscoveryMode = mode;
            onDiscoveryModeChanged(mode);
        }
    }

    void publishState() {
        if (mCallback != null) {
            RemoteDisplayState state = new RemoteDisplayState();
            final int count = mDisplays.size();
            for (int i = 0; i < count; i++) {
                final RemoteDisplay display = mDisplays.valueAt(i);
                state.displays.add(display.getInfo());
            }
            try {
                mCallback.onStateChanged(state);
            } catch (RemoteException ex) {
                // system server died?
            }
        }
    }

    final class ProviderStub extends IRemoteDisplayProvider.Stub {
        @Override
        public void setCallback(IRemoteDisplayCallback callback) {
            mHandler.obtainMessage(MSG_SET_CALLBACK, callback).sendToTarget();
        }

        @Override
        public void setDiscoveryMode(int mode) {
            mHandler.obtainMessage(MSG_SET_DISCOVERY_MODE, mode, 0).sendToTarget();
        }

        @Override
        public void connect(String id) {
            mHandler.obtainMessage(MSG_CONNECT, id).sendToTarget();
        }

        @Override
        public void disconnect(String id) {
            mHandler.obtainMessage(MSG_DISCONNECT, id).sendToTarget();
        }

        @Override
        public void setVolume(String id, int volume) {
            mHandler.obtainMessage(MSG_SET_VOLUME, volume, 0, id).sendToTarget();
        }

        @Override
        public void adjustVolume(String id, int delta) {
            mHandler.obtainMessage(MSG_ADJUST_VOLUME, delta, 0, id).sendToTarget();
        }
    }

    final class ProviderHandler extends Handler {
        public ProviderHandler(Looper looper) {
            super(looper, null, true);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SET_CALLBACK: {
                    setCallback((IRemoteDisplayCallback)msg.obj);
                    break;
                }
                case MSG_SET_DISCOVERY_MODE: {
                    setDiscoveryMode(msg.arg1);
                    break;
                }
                case MSG_CONNECT: {
                    RemoteDisplay display = findRemoteDisplay((String)msg.obj);
                    if (display != null) {
                        onConnect(display);
                    }
                    break;
                }
                case MSG_DISCONNECT: {
                    RemoteDisplay display = findRemoteDisplay((String)msg.obj);
                    if (display != null) {
                        onDisconnect(display);
                    }
                    break;
                }
                case MSG_SET_VOLUME: {
                    RemoteDisplay display = findRemoteDisplay((String)msg.obj);
                    if (display != null) {
                        onSetVolume(display, msg.arg1);
                    }
                    break;
                }
                case MSG_ADJUST_VOLUME: {
                    RemoteDisplay display = findRemoteDisplay((String)msg.obj);
                    if (display != null) {
                        onAdjustVolume(display, msg.arg1);
                    }
                    break;
                }
            }
        }
    }
}
