/*
 * Copyright (C) 2008 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.stubs;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.test.PerformanceTestCase;
import android.util.Log;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class MyBadParcelable implements Parcelable {
    public MyBadParcelable() {
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeString("I am bad");
    }

    public int describeContents() {
        return 0;
    }

    public static final Parcelable.Creator<MyBadParcelable> CREATOR =
        new Parcelable.Creator<MyBadParcelable>() {
        public MyBadParcelable createFromParcel(Parcel in) {
            return new MyBadParcelable(in);
        }

        public MyBadParcelable[] newArray(int size) {
            return new MyBadParcelable[size];
        }
    };

    public MyBadParcelable(Parcel in) {
        in.readString();
    }
}

public class LaunchpadActivity extends Activity {
    public interface CallingTest extends PerformanceTestCase.Intermediates {
        public void startTiming(boolean realTime);

        public void addIntermediate(String name);

        public void addIntermediate(String name, long timeInNS);

        public void finishTiming(boolean realTime);

        public void activityFinished(int resultCode, Intent data, RuntimeException where);
    }

    // Also used as the Binder interface descriptor string in these tests
    public static final String LAUNCH = "android.app.cts.activity.LAUNCH";

    public static final String FORWARD_RESULT = "android.app.cts.activity.FORWARD_RESULT";
    public static final String RETURNED_RESULT = "android.app.cts.activity.RETURNED_RESULT";

    public static final String BAD_PARCELABLE = "android.app.cts.activity.BAD_PARCELABLE";

    public static final int LAUNCHED_RESULT = 1;
    public static final int FORWARDED_RESULT = 2;

    public static final String LIFECYCLE_BASIC = "android.app.cts.activity.LIFECYCLE_BASIC";
    public static final String LIFECYCLE_SCREEN = "android.app.cts.activity.LIFECYCLE_SCREEN";
    public static final String LIFECYCLE_DIALOG = "android.app.cts.activity.LIFECYCLE_DIALOG";

    public static final String BROADCAST_REGISTERED = "android.app.cts.activity.BROADCAST_REGISTERED";
    public static final String BROADCAST_LOCAL = "android.app.cts.activity.BROADCAST_LOCAL";
    public static final String BROADCAST_REMOTE = "android.app.cts.activity.BROADCAST_REMOTE";
    public static final String BROADCAST_ALL = "android.app.cts.activity.BROADCAST_ALL";
    public static final String BROADCAST_REPEAT = "android.app.cts.activity.BROADCAST_REPEAT";
    public static final String BROADCAST_MULTI = "android.app.cts.activity.BROADCAST_MULTI";
    public static final String BROADCAST_ABORT = "android.app.cts.activity.BROADCAST_ABORT";

    public static final String EXPANDLIST_SELECT = "EXPANDLIST_SELECT";
    public static final String EXPANDLIST_VIEW = "EXPANDLIST_VIEW";
    public static final String EXPANDLIST_CALLBACK = "EXPANDLIST_CALLBACK";

    public static final String BROADCAST_STICKY1 = "android.app.cts.activity.BROADCAST_STICKY1";
    public static final String BROADCAST_STICKY2 = "android.app.cts.activity.BROADCAST_STICKY2";

    public static final String ALIAS_ACTIVITY = "android.app.cts.activity.ALIAS_ACTIVITY";

    public static final String RECEIVER_REG = "receiver-reg";
    public static final String RECEIVER_LOCAL = "receiver-local";
    public static final String RECEIVER_REMOTE = "receiver-remote";
    public static final String RECEIVER_ABORT = "receiver-abort";

    public static final String DATA_1 = "one";
    public static final String DATA_2 = "two";

    public static final String ON_START = "onStart";
    public static final String ON_RESTART = "onRestart";
    public static final String ON_RESUME = "onResume";
    public static final String ON_FREEZE = "onSaveInstanceState";
    public static final String ON_PAUSE = "onPause";

    // ON_STOP and ON_DESTROY are not tested because they may not be called.

    public static final String DO_FINISH = "finish";
    public static final String DO_LOCAL_SCREEN = "local-screen";
    public static final String DO_LOCAL_DIALOG = "local-dialog";

    private static final String TAG = "LaunchpadActivity";

    private boolean mBadParcelable = false;

    private boolean mStarted = false;

    private int mResultCode = RESULT_CANCELED;
    private Intent mData = new Intent().setAction("No result received");
    private RuntimeException mResultStack = null;

    /** Index into the {@link #mNextLifecycle} array. */
    private int mNextLifecycle;

    /** Current lifecycle expected to be followed. */
    private String[] mExpectedLifecycle;

    /** Other possible lifecycles. Never includes the current {@link #mExpectedLifecycle}. */
    private List<String[]> mOtherPossibleLifecycles = new ArrayList<String[]>(2);

    /** Map from lifecycle arrays to debugging log names. */
    private Map<String[], String> mLifecycleNames = new HashMap<String[], String>(2);

    private String[] mExpectedReceivers = null;
    private int mNextReceiver;

    private String[] mExpectedData = null;
    private boolean[] mReceivedData = null;

    boolean mReceiverRegistered = false;

    private static CallingTest sCallingTest = null;

    public static void setCallingTest(CallingTest ct) {
        sCallingTest = ct;
    }

    public LaunchpadActivity() {
    }

    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        resetLifecycles();

        // ON_STOP and ON_DESTROY are not tested because they may not be called.

        final String action = getIntent().getAction();
        if (LIFECYCLE_BASIC.equals(action)) {
            addPossibleLifecycle(LIFECYCLE_BASIC, new String[] {
                    ON_START, ON_RESUME, DO_FINISH, ON_PAUSE
            });
        } else if (LIFECYCLE_SCREEN.equals(action)) {
            addPossibleLifecycle(LIFECYCLE_SCREEN + "_RESTART", new String[] {
                    ON_START, ON_RESUME, DO_LOCAL_SCREEN, ON_PAUSE,
                    ON_RESTART, ON_START, ON_RESUME, DO_FINISH, ON_PAUSE
            });
            addPossibleLifecycle(LIFECYCLE_SCREEN + "_RESUME", new String[] {
                    ON_START, ON_RESUME, DO_LOCAL_SCREEN, ON_PAUSE,
                    ON_RESUME, DO_FINISH, ON_PAUSE
            });
        } else if (LIFECYCLE_DIALOG.equals(action)) {
            addPossibleLifecycle(LIFECYCLE_DIALOG + "_RESTART", new String[] {
                    ON_START, ON_RESUME, DO_LOCAL_DIALOG, ON_PAUSE,
                    ON_RESTART, ON_START, ON_RESUME, DO_FINISH, ON_PAUSE
            });
            addPossibleLifecycle(LIFECYCLE_DIALOG + "_RESUME", new String[] {
                    ON_START, ON_RESUME, DO_LOCAL_DIALOG, ON_PAUSE,
                    ON_RESUME, DO_FINISH, ON_PAUSE
            });
        }
    }

    private void resetLifecycles() {
        mNextLifecycle = 0;
        mExpectedLifecycle = null;
        mOtherPossibleLifecycles.clear();
        mLifecycleNames.clear();
    }

    /**
     * Add a potential lifecycle that this activity may follow, since there
     * are usually multiple valid lifecycles. For instance, sometimes onPause
     * will lead to onResume rather than onStop when another activity is
     * raised over the current one.
     *
     * @param debugName for the lifecycle shown in the logs
     * @param lifecycle array containing tokens indicating the expected lifecycle
     */
    private void addPossibleLifecycle(String debugName, String[] lifecycle) {
        mLifecycleNames.put(lifecycle, debugName);
        if (mExpectedLifecycle == null) {
            mExpectedLifecycle = lifecycle;
        } else {
            mOtherPossibleLifecycles.add(lifecycle);
        }
    }

    /**
     * Switch to the next possible lifecycle and return if switching was
     * successful. Call this method when mExpectedLifecycle doesn't match
     * the current lifecycle and you need to check another possible lifecycle.
     *
     * @return whether on not there was a lifecycle to switch to
     */
    private boolean switchToNextPossibleLifecycle() {
        if (!mOtherPossibleLifecycles.isEmpty()) {
            String[] newLifecycle = mOtherPossibleLifecycles.remove(0);
            Log.w(TAG, "Switching expected lifecycles from "
                    + mLifecycleNames.get(mExpectedLifecycle) + " to "
                    + mLifecycleNames.get(newLifecycle));
            mExpectedLifecycle = newLifecycle;
            return true;
        } else {
            Log.w(TAG, "No more lifecycles after "
                    + mLifecycleNames.get(mExpectedLifecycle));
            mExpectedLifecycle = null;
            return false;
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        checkLifecycle(ON_START);
    }

    @Override
    protected void onRestart() {
        super.onStart();
        checkLifecycle(ON_RESTART);
    }

    @Override
    protected void onResume() {
        super.onResume();

        checkLifecycle(ON_RESUME);

        if (!mStarted) {
            mStarted = true;

            mHandler.postDelayed(mTimeout, 10 * 1000);

            final String action = getIntent().getAction();

            sCallingTest.startTiming(true);

            if (LAUNCH.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setComponent((ComponentName) intent.getParcelableExtra("component"));
                startActivityForResult(intent, LAUNCHED_RESULT);

            } else if (FORWARD_RESULT.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setClass(this, LocalScreen.class);
                startActivityForResult(intent, FORWARDED_RESULT);
            } else if (BAD_PARCELABLE.equals(action)) {
                mBadParcelable = true;
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setClass(this, LocalScreen.class);
                startActivityForResult(intent, LAUNCHED_RESULT);
            } else if (BROADCAST_REGISTERED.equals(action)) {
                setExpectedReceivers(new String[] {
                    RECEIVER_REG
                });
                registerMyReceiver(new IntentFilter(BROADCAST_REGISTERED));
                sCallingTest.addIntermediate("after-register");
                sendBroadcast(makeBroadcastIntent(BROADCAST_REGISTERED));
            } else if (BROADCAST_LOCAL.equals(action)) {
                setExpectedReceivers(new String[] {
                    RECEIVER_LOCAL
                });
                sendBroadcast(makeBroadcastIntent(BROADCAST_LOCAL));
            } else if (BROADCAST_REMOTE.equals(action)) {
                setExpectedReceivers(new String[] {
                    RECEIVER_REMOTE
                });
                sendBroadcast(makeBroadcastIntent(BROADCAST_REMOTE));
            } else if (BROADCAST_ALL.equals(action)) {
                setExpectedReceivers(new String[] {
                        RECEIVER_REMOTE, RECEIVER_REG, RECEIVER_LOCAL
                });
                registerMyReceiver(new IntentFilter(BROADCAST_ALL));
                sCallingTest.addIntermediate("after-register");
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
            } else if (BROADCAST_MULTI.equals(action)) {
                setExpectedReceivers(new String[] {
                        RECEIVER_REMOTE, RECEIVER_REG, RECEIVER_LOCAL, RECEIVER_REMOTE,
                        RECEIVER_REG, RECEIVER_LOCAL, RECEIVER_REMOTE, RECEIVER_REG,
                        RECEIVER_LOCAL, RECEIVER_LOCAL, RECEIVER_REMOTE, RECEIVER_LOCAL,
                        RECEIVER_REMOTE, RECEIVER_REMOTE, RECEIVER_REG, RECEIVER_LOCAL,
                        RECEIVER_REMOTE, RECEIVER_REG, RECEIVER_LOCAL, RECEIVER_REMOTE,
                        RECEIVER_REG, RECEIVER_LOCAL, RECEIVER_REMOTE, RECEIVER_LOCAL,
                        RECEIVER_REMOTE, RECEIVER_LOCAL
                });
                registerMyReceiver(new IntentFilter(BROADCAST_ALL));
                sCallingTest.addIntermediate("after-register");
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_LOCAL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_REMOTE), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_LOCAL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_REMOTE), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ALL), null);
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_REPEAT), null);
            } else if (BROADCAST_ABORT.equals(action)) {
                setExpectedReceivers(new String[] {
                        RECEIVER_REMOTE, RECEIVER_ABORT
                });
                registerMyReceiver(new IntentFilter(BROADCAST_ABORT));
                sCallingTest.addIntermediate("after-register");
                sendOrderedBroadcast(makeBroadcastIntent(BROADCAST_ABORT), null);
            } else if (BROADCAST_STICKY1.equals(action)) {
                setExpectedReceivers(new String[] {
                    RECEIVER_REG
                });
                setExpectedData(new String[] {
                    DATA_1
                });
                registerMyReceiver(new IntentFilter(BROADCAST_STICKY1));
                sCallingTest.addIntermediate("after-register");
            } else if (BROADCAST_STICKY2.equals(action)) {
                setExpectedReceivers(new String[] {
                        RECEIVER_REG, RECEIVER_REG
                });
                setExpectedData(new String[] {
                        DATA_1, DATA_2
                });
                final IntentFilter filter = new IntentFilter(BROADCAST_STICKY1);
                filter.addAction(BROADCAST_STICKY2);
                registerMyReceiver(filter);
                sCallingTest.addIntermediate("after-register");
            } else if (ALIAS_ACTIVITY.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setClass(this, AliasActivityStub.class);
                startActivityForResult(intent, LAUNCHED_RESULT);
            } else if (EXPANDLIST_SELECT.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setAction(action);
                intent.setComponent((ComponentName) intent.getParcelableExtra("component"));
                startActivityForResult(intent, LAUNCHED_RESULT);
            } else if (EXPANDLIST_VIEW.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setAction(action);
                intent.setComponent((ComponentName) intent.getParcelableExtra("component"));
                startActivityForResult(intent, LAUNCHED_RESULT);
            } else if (EXPANDLIST_CALLBACK.equals(action)) {
                final Intent intent = getIntent();
                intent.setFlags(0);
                intent.setAction(action);
                intent.setComponent((ComponentName) intent.getParcelableExtra("component"));
                startActivityForResult(intent, LAUNCHED_RESULT);
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle icicle) {
        super.onSaveInstanceState(icicle);
        if (mBadParcelable) {
            icicle.putParcelable("baddy", new MyBadParcelable());
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        checkLifecycle(ON_PAUSE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case LAUNCHED_RESULT:
                sCallingTest.finishTiming(true);
                finishWithResult(resultCode, data);
                break;
            case FORWARDED_RESULT:
                sCallingTest.finishTiming(true);
                if (RETURNED_RESULT.equals(data.getAction())) {
                    finishWithResult(resultCode, data);
                } else {
                    finishWithResult(RESULT_CANCELED, new Intent().setAction("Bad data returned: "
                            + data));
                }
                break;
            default:
                sCallingTest.finishTiming(true);
                finishWithResult(RESULT_CANCELED, new Intent()
                        .setAction("Unexpected request code: " + requestCode));
                break;
        }
    }

    private void checkLifecycle(String where) {
        String action = getIntent().getAction();

        if (mExpectedLifecycle == null) {
            return;
        }

        if (mNextLifecycle >= mExpectedLifecycle.length) {
            finishBad("Activity lifecycle for " + action + " incorrect: received " + where
                    + " but don't expect any more calls");
            mExpectedLifecycle = null;
            return;
        }

        do {
            if (mExpectedLifecycle[mNextLifecycle].equals(where)) {
                Log.w(TAG, "Matched: " + where);
                break;
            } else {
                Log.w(TAG, "Expected " + mExpectedLifecycle[mNextLifecycle] + " but got " + where);
            }
        } while (switchToNextPossibleLifecycle());

        if (mExpectedLifecycle == null) {
            finishBad("Activity lifecycle for " + action + " incorrect: received " + where
                    + " at " + mNextLifecycle);
            return;
        }

        mNextLifecycle++;

        if (mNextLifecycle >= mExpectedLifecycle.length) {
            finishGood();
            return;
        }

        final String next = mExpectedLifecycle[mNextLifecycle];
        if (next.equals(DO_FINISH)) {
            mNextLifecycle++;
            if (mNextLifecycle >= mExpectedLifecycle.length) {
                setTestResult(RESULT_OK, null);
            }
            if (!isFinishing()) {
                finish();
            }
        } else if (next.equals(DO_LOCAL_SCREEN)) {
            mNextLifecycle++;
            final Intent intent = new Intent(TestedScreen.WAIT_BEFORE_FINISH);
            intent.setClass(this, LocalScreen.class);
            startActivity(intent);
        } else if (next.equals(DO_LOCAL_DIALOG)) {
            mNextLifecycle++;
            final Intent intent = new Intent(TestedScreen.WAIT_BEFORE_FINISH);
            intent.setClass(this, LocalDialog.class);
            startActivity(intent);
        }
    }

    private void setExpectedReceivers(String[] receivers) {
        mExpectedReceivers = receivers;
        mNextReceiver = 0;
    }

    private void setExpectedData(String[] data) {
        mExpectedData = data;
        mReceivedData = new boolean[data.length];
    }

    @SuppressWarnings("deprecation")
    private Intent makeBroadcastIntent(String action) {
        final Intent intent = new Intent(action, null);
        intent.putExtra("caller", mCallTarget);
        return intent;
    }

    private void finishGood() {
        finishWithResult(RESULT_OK, null);
    }

    private void finishBad(String error) {
        finishWithResult(RESULT_CANCELED, new Intent().setAction(error));
    }

    private void finishWithResult(int resultCode, Intent data) {
        setTestResult(resultCode, data);
        finish();

        // Member fields set by calling setTestResult above...
        sCallingTest.activityFinished(mResultCode, mData, mResultStack);
    }

    private void setTestResult(int resultCode, Intent data) {
        mHandler.removeCallbacks(mTimeout);
        unregisterMyReceiver();
        mResultCode = resultCode;
        mData = data;
        mResultStack = new RuntimeException("Original error was here");
        mResultStack.fillInStackTrace();
    }

    private void registerMyReceiver(IntentFilter filter) {
        mReceiverRegistered = true;
        registerReceiver(mReceiver, filter);
    }

    private void unregisterMyReceiver() {
        if (mReceiverRegistered) {
            mReceiverRegistered = false;
            unregisterReceiver(mReceiver);
        }
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
        }
    };

    static final int GOT_RECEIVE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
    static final int ERROR_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1;

    private final Binder mCallTarget = new Binder() {
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
            data.setDataPosition(0);
            data.enforceInterface(LaunchpadActivity.LAUNCH);
            if (code == GOT_RECEIVE_TRANSACTION) {
                final String name = data.readString();
                gotReceive(name, null);
                return true;
            } else if (code == ERROR_TRANSACTION) {
                finishBad(data.readString());
                return true;
            }
            return false;
        }
    };

    private final void gotReceive(String name, Intent intent) {
        synchronized (this) {

            sCallingTest.addIntermediate(mNextReceiver + "-" + name);

            if (mExpectedData != null) {
                final int n = mExpectedData.length;
                int i;
                boolean prev = false;
                for (i = 0; i < n; i++) {
                    if (mExpectedData[i].equals(intent.getStringExtra("test"))) {
                        if (mReceivedData[i]) {
                            prev = true;
                            continue;
                        }
                        mReceivedData[i] = true;
                        break;
                    }
                }
                if (i >= n) {
                    if (prev) {
                        finishBad("Receive got data too many times: "
                                + intent.getStringExtra("test"));
                    } else {
                        finishBad("Receive got unexpected data: " + intent.getStringExtra("test"));
                    }
                    return;
                }
            }

            if (mNextReceiver >= mExpectedReceivers.length) {
                finishBad("Got too many onReceiveIntent() calls!");
            } else if (!mExpectedReceivers[mNextReceiver].equals(name)) {
                finishBad("Receive out of order: got " + name + " but expected "
                        + mExpectedReceivers[mNextReceiver] + " at " + mNextReceiver);
            } else {
                mNextReceiver++;
                if (mNextReceiver == mExpectedReceivers.length) {
                    mHandler.post(mUnregister);
                }
            }

        }
    }

    private final Runnable mUnregister = new Runnable() {
        public void run() {
            if (mReceiverRegistered) {
                sCallingTest.addIntermediate("before-unregister");
                unregisterMyReceiver();
            }
            sCallingTest.finishTiming(true);
            finishGood();
        }
    };

    private final Runnable mTimeout = new Runnable() {
        public void run() {
            Log.i(TAG, "timeout");
            String msg = "Timeout";
            if (mExpectedReceivers != null && mNextReceiver < mExpectedReceivers.length) {
                msg = msg + " waiting for " + mExpectedReceivers[mNextReceiver];
            }
            finishBad(msg);
        }
    };

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            gotReceive(RECEIVER_REG, intent);
        }
    };
}
