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

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ProviderInfo;
import android.os.AsyncTask;
import android.os.Debug;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;

import com.android.emailcommon.provider.EmailContent;
import com.android.mail.utils.LogUtils;

/**
 * ServiceProxy is a superclass for proxy objects which make a single call to a service. It handles
 * connecting to the service, running a task supplied by the subclass when the connection is ready,
 * and disconnecting from the service afterwards. ServiceProxy objects cannot be reused (trying to
 * do so generates an {@link IllegalStateException}).
 *
 * Subclasses must override {@link #onConnected} to store the binder. Then, when the subclass wants
 * to make a service call, it should call {@link #setTask}, supplying the {@link ProxyTask} that
 * should run when the connection is ready. {@link ProxyTask#run} should implement the necessary
 * logic to make the call on the service.
 */

public abstract class ServiceProxy {
    public static final String EXTRA_FORCE_SHUTDOWN = "ServiceProxy.FORCE_SHUTDOWN";

    private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
    private final String mTag;

    private final Context mContext;
    protected final Intent mIntent;
    private ProxyTask mTask;
    private String mName = " unnamed";
    private final ServiceConnection mConnection = new ProxyConnection();
    // Service call timeout (in seconds)
    private int mTimeout = 45;
    private long mStartTime;
    private boolean mTaskSet = false;
    private boolean mTaskCompleted = false;

    public static Intent getIntentForEmailPackage(Context context, String actionName) {
        /**
         * We want to scope the intent so that only the Email app will handle it. Unfortunately
         * we found that there are many instances where the package name of the Email app is
         * not what we expect. The easiest way to find the package of the correct app is to
         * see who is the EmailContent.AUTHORITY as there is only one app that can implement
         * the content provider for this authority and this is the right app to handle this intent.
         */
        final Intent intent = new Intent(EmailContent.EMAIL_PACKAGE_NAME + "." + actionName);
        final ProviderInfo info = context.getPackageManager().resolveContentProvider(
                EmailContent.AUTHORITY, 0);
        if (info != null) {
            final String packageName = info.packageName;
            intent.setPackage(packageName);
        } else {
            LogUtils.e(LogUtils.TAG, "Could not find the Email Content Provider");
        }
        return intent;
    }

    /**
     * This function is called after the proxy connects to the service but before it runs its task.
     * Subclasses must override this to store the binder correctly.
     * @param binder The service IBinder.
     */
    public abstract void onConnected(IBinder binder);

    public ServiceProxy(Context _context, Intent _intent) {
        mContext = _context;
        mIntent = _intent;
        mTag = getClass().getSimpleName();
        if (Debug.isDebuggerConnected()) {
            mTimeout <<= 2;
        }
    }

    private class ProxyConnection implements ServiceConnection {
        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            if (DEBUG_PROXY) {
                LogUtils.v(mTag, "Connected: " + name.getShortClassName() + " at " +
                        (System.currentTimeMillis() - mStartTime) + "ms");
            }

            // Let subclasses handle the binder.
            onConnected(binder);

            // Do our work in another thread.
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    try {
                        mTask.run();
                    } catch (RemoteException e) {
                        LogUtils.e(mTag, e, "RemoteException thrown running mTask!");
                    } finally {
                        // Make sure that we unbind the mConnection even on exceptions in the
                        // task provided by the subclass.
                        try {
                            // Each ServiceProxy handles just one task, so we unbind after we're
                            // done with our work.
                            mContext.unbindService(mConnection);
                        } catch (RuntimeException e) {
                            // The exceptions that are thrown here look like IllegalStateException,
                            // IllegalArgumentException and RuntimeException. Catching
                            // RuntimeException which get them all. Reasons for these exceptions
                            // include services that have already been stopped or unbound. This can
                            // happen if the user ended the activity that was using the service.
                            // This is harmless, but we've got to catch it.
                            LogUtils.e(mTag, e,
                                    "RuntimeException when trying to unbind from service");
                        }
                    }
                    mTaskCompleted = true;
                    synchronized(mConnection) {
                        if (DEBUG_PROXY) {
                            LogUtils.v(mTag, "Task " + mName + " completed; disconnecting");
                        }
                        mConnection.notify();
                    }
                    return null;
                }
            }.execute();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            if (DEBUG_PROXY) {
                LogUtils.v(mTag, "Disconnected: " + name.getShortClassName() + " at " +
                        (System.currentTimeMillis() - mStartTime) + "ms");
            }
        }
    }

    protected interface ProxyTask {
        public void run() throws RemoteException;
    }

    public ServiceProxy setTimeout(int secs) {
        mTimeout = secs;
        return this;
    }

    public int getTimeout() {
        return mTimeout;
    }

    protected boolean setTask(ProxyTask task, String name) throws IllegalStateException {
        if (mTaskSet) {
            throw new IllegalStateException("Cannot call setTask twice on the same ServiceProxy.");
        }
        mTaskSet = true;
        mName = name;
        mTask = task;
        mStartTime = System.currentTimeMillis();
        if (DEBUG_PROXY) {
            LogUtils.v(mTag, "Bind requested for task " + mName);
        }
        return mContext.bindService(mIntent, mConnection, Context.BIND_AUTO_CREATE);
    }

    /**
     * Callers that want to wait on the {@link ProxyTask} should call this immediately after calling
     * {@link #setTask}. This will wait until the task completes, up to the timeout (which can be
     * set with {@link #setTimeout}).
     */
    protected void waitForCompletion() {
        /*
         * onServiceConnected() is always called on the main thread, and we block the current thread
         * for up to 10 seconds as a timeout. If we're currently on the main thread,
         * onServiceConnected() is not called until our timeout elapses (and the UI is frozen for
         * the duration).
         */
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new IllegalStateException("This cannot be called on the main thread.");
        }

        synchronized (mConnection) {
            long time = System.currentTimeMillis();
            try {
                if (DEBUG_PROXY) {
                    LogUtils.v(mTag, "Waiting for task " + mName + " to complete...");
                }
                mConnection.wait(mTimeout * 1000L);
            } catch (InterruptedException e) {
                // Can be ignored safely
            }
            if (DEBUG_PROXY) {
                LogUtils.v(mTag, "Wait for " + mName +
                        (mTaskCompleted ? " finished in " : " timed out in ") +
                        (System.currentTimeMillis() - time) + "ms");
            }
        }
    }

    /**
     * Connection test; return indicates whether the remote service can be connected to
     * @return the result of trying to connect to the remote service
     */
    public boolean test() {
        try {
            return setTask(new ProxyTask() {
                @Override
                public void run() throws RemoteException {
                    if (DEBUG_PROXY) {
                        LogUtils.v(mTag, "Connection test succeeded in " +
                                (System.currentTimeMillis() - mStartTime) + "ms");
                    }
                }
            }, "test");
        } catch (Exception e) {
            // For any failure, return false.
            return false;
        }
    }
}
