/* //device/tools/ddms/src/com/android/ddms/DeviceCommandDialog.java
**
** Copyright 2007, 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.ddms;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.Log;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;


/**
 * Execute a command on an ADB-attached device and save the output.
 *
 * There are several ways to do this.  One is to run a single command
 * and show the output.  Another is to have several possible commands and
 * let the user click a button next to the one (or ones) they want.  This
 * currently uses the simple 1:1 form.
 */
public class DeviceCommandDialog extends Dialog {

    public static final int DEVICE_STATE = 0;
    public static final int APP_STATE = 1;
    public static final int RADIO_STATE = 2;
    public static final int LOGCAT = 3;

    private String mCommand;
    private String mFileName;

    private Label mStatusLabel;
    private Button mCancelDone;
    private Button mSave;
    private Text mText;
    private Font mFont = null;
    private boolean mCancel;
    private boolean mFinished;


    /**
     * Create with default style.
     */
    public DeviceCommandDialog(String command, String fileName, Shell parent) {
        // don't want a close button, but it seems hard to get rid of on GTK
        // keep it on all platforms for consistency
        this(command, fileName, parent,
            SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL | SWT.RESIZE);
    }

    /**
     * Create with app-defined style.
     */
    public DeviceCommandDialog(String command, String fileName, Shell parent,
        int style)
    {
        super(parent, style);
        mCommand = command;
        mFileName = fileName;
    }

    /**
     * Prepare and display the dialog.
     * @param currentDevice
     */
    public void open(IDevice currentDevice) {
        Shell parent = getParent();
        Shell shell = new Shell(parent, getStyle());
        shell.setText("Remote Command");

        mFinished = false;
        mFont = findFont(shell.getDisplay());
        createContents(shell);

        // Getting weird layout behavior under Linux when Text is added --
        // looks like text widget has min width of 400 when FILL_HORIZONTAL
        // is used, and layout gets tweaked to force this.  (Might be even
        // more with the scroll bars in place -- it wigged out when the
        // file save dialog was invoked.)
        shell.setMinimumSize(500, 200);
        shell.setSize(800, 600);
        shell.open();

        executeCommand(shell, currentDevice);

        Display display = parent.getDisplay();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }

        if (mFont != null)
            mFont.dispose();
    }

    /*
     * Create a text widget to show the output and some buttons to
     * manage things.
     */
    private void createContents(final Shell shell) {
        GridData data;

        shell.setLayout(new GridLayout(2, true));

        shell.addListener(SWT.Close, new Listener() {
            @Override
            public void handleEvent(Event event) {
                if (!mFinished) {
                    Log.d("ddms", "NOT closing - cancelling command");
                    event.doit = false;
                    mCancel = true;
                }
            }
        });

        mStatusLabel = new Label(shell, SWT.NONE);
        mStatusLabel.setText("Executing '" + shortCommandString() + "'");
        data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
        data.horizontalSpan = 2;
        mStatusLabel.setLayoutData(data);

        mText = new Text(shell, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
        mText.setEditable(false);
        mText.setFont(mFont);
        data = new GridData(GridData.FILL_BOTH);
        data.horizontalSpan = 2;
        mText.setLayoutData(data);

        // "save" button
        mSave = new Button(shell, SWT.PUSH);
        mSave.setText("Save");
        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
        data.widthHint = 80;
        mSave.setLayoutData(data);
        mSave.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                saveText(shell);
            }
        });
        mSave.setEnabled(false);

        // "cancel/done" button
        mCancelDone = new Button(shell, SWT.PUSH);
        mCancelDone.setText("Cancel");
        data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
        data.widthHint = 80;
        mCancelDone.setLayoutData(data);
        mCancelDone.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                if (!mFinished)
                    mCancel = true;
                else
                    shell.close();
            }
        });
    }

    /*
     * Figure out what font to use.
     *
     * Returns "null" if we can't figure it out, which SWT understands to
     * mean "use default system font".
     */
    private Font findFont(Display display) {
        String fontStr = PrefsDialog.getStore().getString("textOutputFont");
        if (fontStr != null) {
            FontData fdat = new FontData(fontStr);
            if (fdat != null)
                return new Font(display, fdat);
        }
        return null;
    }


    /*
     * Callback class for command execution.
     */
    class Gatherer extends Thread implements IShellOutputReceiver {
        public static final int RESULT_UNKNOWN = 0;
        public static final int RESULT_SUCCESS = 1;
        public static final int RESULT_FAILURE = 2;
        public static final int RESULT_CANCELLED = 3;

        private Shell mShell;
        private String mCommand;
        private Text mText;
        private int mResult;
        private IDevice mDevice;

        /**
         * Constructor; pass in the text widget that will receive the output.
         * @param device
         */
        public Gatherer(Shell shell, IDevice device, String command, Text text) {
            mShell = shell;
            mDevice = device;
            mCommand = command;
            mText = text;
            mResult = RESULT_UNKNOWN;

            // this is in outer class
            mCancel = false;
        }

        /**
         * Thread entry point.
         */
        @Override
        public void run() {

            if (mDevice == null) {
                Log.w("ddms", "Cannot execute command: no device selected.");
                mResult = RESULT_FAILURE;
            } else {
                try {
                    mDevice.executeShellCommand(mCommand, this);
                    if (mCancel)
                        mResult = RESULT_CANCELLED;
                    else
                        mResult = RESULT_SUCCESS;
                }
                catch (IOException ioe) {
                    Log.w("ddms", "Remote exec failed: " + ioe.getMessage());
                    mResult = RESULT_FAILURE;
                } catch (TimeoutException e) {
                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
                    mResult = RESULT_FAILURE;
                } catch (AdbCommandRejectedException e) {
                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
                    mResult = RESULT_FAILURE;
                } catch (ShellCommandUnresponsiveException e) {
                    Log.w("ddms", "Remote exec failed: " + e.getMessage());
                    mResult = RESULT_FAILURE;
                }
            }

            mShell.getDisplay().asyncExec(new Runnable() {
                @Override
                public void run() {
                    updateForResult(mResult);
                }
            });
        }

        /**
         * Called by executeRemoteCommand().
         */
        @Override
        public void addOutput(byte[] data, int offset, int length) {

            Log.v("ddms", "received " + length + " bytes");
            try {
                final String text;
                text = new String(data, offset, length, "ISO-8859-1");

                // add to text widget; must do in UI thread
                mText.getDisplay().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        mText.append(text);
                    }
                });
            }
            catch (UnsupportedEncodingException uee) {
                uee.printStackTrace();      // not expected
            }
        }

        @Override
        public void flush() {
            // nothing to flush.
        }

        /**
         * Called by executeRemoteCommand().
         */
        @Override
        public boolean isCancelled() {
            return mCancel;
        }
    };

    /*
     * Execute a remote command, add the output to the text widget, and
     * update controls.
     *
     * We have to run the command in a thread so that the UI continues
     * to work.
     */
    private void executeCommand(Shell shell, IDevice device) {
        Gatherer gath = new Gatherer(shell, device, commandString(), mText);
        gath.start();
    }

    /*
     * Update the controls after the remote operation completes.  This
     * must be called from the UI thread.
     */
    private void updateForResult(int result) {
        if (result == Gatherer.RESULT_SUCCESS) {
            mStatusLabel.setText("Successfully executed '"
                + shortCommandString() + "'");
            mSave.setEnabled(true);
        } else if (result == Gatherer.RESULT_CANCELLED) {
            mStatusLabel.setText("Execution cancelled; partial results below");
            mSave.setEnabled(true);     // save partial
        } else if (result == Gatherer.RESULT_FAILURE) {
            mStatusLabel.setText("Failed");
        }
        mStatusLabel.pack();
        mCancelDone.setText("Done");
        mFinished = true;
    }

    /*
     * Allow the user to save the contents of the text dialog.
     */
    private void saveText(Shell shell) {
        FileDialog dlg = new FileDialog(shell, SWT.SAVE);
        String fileName;

        dlg.setText("Save output...");
        dlg.setFileName(defaultFileName());
        dlg.setFilterPath(PrefsDialog.getStore().getString("lastTextSaveDir"));
        dlg.setFilterNames(new String[] {
            "Text Files (*.txt)"
        });
        dlg.setFilterExtensions(new String[] {
            "*.txt"
        });

        fileName = dlg.open();
        if (fileName != null) {
            PrefsDialog.getStore().setValue("lastTextSaveDir",
                                            dlg.getFilterPath());

            Log.d("ddms", "Saving output to " + fileName);

            /*
             * Convert to 8-bit characters.
             */
            String text = mText.getText();
            byte[] ascii;
            try {
                ascii = text.getBytes("ISO-8859-1");
            }
            catch (UnsupportedEncodingException uee) {
                uee.printStackTrace();
                ascii = new byte[0];
            }

            /*
             * Output data, converting CRLF to LF.
             */
            try {
                int length = ascii.length;

                FileOutputStream outFile = new FileOutputStream(fileName);
                BufferedOutputStream out = new BufferedOutputStream(outFile);
                for (int i = 0; i < length; i++) {
                    if (i < length-1 &&
                        ascii[i] == 0x0d && ascii[i+1] == 0x0a)
                    {
                        continue;
                    }
                    out.write(ascii[i]);
                }
                out.close();        // flush buffer, close file
            }
            catch (IOException ioe) {
                Log.w("ddms", "Unable to save " + fileName + ": " + ioe);
            }
        }
    }


    /*
     * Return the shell command we're going to use.
     */
    private String commandString() {
        return mCommand;

    }

    /*
     * Return a default filename for the "save" command.
     */
    private String defaultFileName() {
        return mFileName;
    }

    /*
     * Like commandString(), but length-limited.
     */
    private String shortCommandString() {
        String str = commandString();
        if (str.length() > 50)
            return str.substring(0, 50) + "...";
        else
            return str;
    }
}

