/*
** Copyright 2012, 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.commands.content;

import android.app.ActivityManager;
import android.app.ContentProviderHolder;
import android.app.IActivityManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.IContentProvider;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import libcore.io.IoUtils;

/**
 * This class is a command line utility for manipulating content. A client
 * can insert, update, and remove records in a content provider. For example,
 * some settings may be configured before running the CTS tests, etc.
 * <p>
 * Examples:
 * <ul>
 * <li>
 * # Add "new_setting" secure setting with value "new_value".</br>
 * adb shell content insert --uri content://settings/secure --bind name:s:new_setting
 *  --bind value:s:new_value
 * </li>
 * <li>
 * # Change "new_setting" secure setting to "newer_value" (You have to escape single quotes in
 * the where clause).</br>
 * adb shell content update --uri content://settings/secure --bind value:s:newer_value
 *  --where "name=\'new_setting\'"
 * </li>
 * <li>
 * # Remove "new_setting" secure setting.</br>
 * adb shell content delete --uri content://settings/secure --where "name=\'new_setting\'"
 * </li>
 * <li>
 * # Query \"name\" and \"value\" columns from secure settings where \"name\" is equal to"
 *    \"new_setting\" and sort the result by name in ascending order.\n"
 * adb shell content query --uri content://settings/secure --projection name:value
 *  --where "name=\'new_setting\'" --sort \"name ASC\"
 * </li>
 * </ul>
 * </p>
 */
public class Content {

    private static final String USAGE =
            "usage: adb shell content [subcommand] [options]\n"
                    + "\n"
                    + "usage: adb shell content insert --uri <URI> [--user <USER_ID>]"
                    + " --bind <BINDING> [--bind <BINDING>...]\n"
                    + "  <URI> a content provider URI.\n"
                    + "  <BINDING> binds a typed value to a column and is formatted:\n"
                    + "  <COLUMN_NAME>:<TYPE>:<COLUMN_VALUE> where:\n"
                    + "  <TYPE> specifies data type such as:\n"
                    + "  b - boolean, s - string, i - integer, l - long, f - float, d - double\n"
                    + "  Note: Omit the value for passing an empty string, e.g column:s:\n"
                    + "  Example:\n"
                    + "  # Add \"new_setting\" secure setting with value \"new_value\".\n"
                    + "  adb shell content insert --uri content://settings/secure --bind name:s:new_setting"
                    + " --bind value:s:new_value\n"
                    + "\n"
                    + "usage: adb shell content update --uri <URI> [--user <USER_ID>] [--where <WHERE>]\n"
                    + "  <WHERE> is a SQL style where clause in quotes (You have to escape single quotes"
                    + " - see example below).\n"
                    + "  Example:\n"
                    + "  # Change \"new_setting\" secure setting to \"newer_value\".\n"
                    + "  adb shell content update --uri content://settings/secure --bind"
                    + " value:s:newer_value --where \"name=\'new_setting\'\"\n"
                    + "\n"
                    + "usage: adb shell content delete --uri <URI> [--user <USER_ID>] --bind <BINDING>"
                    + " [--bind <BINDING>...] [--where <WHERE>]\n"
                    + "  Example:\n"
                    + "  # Remove \"new_setting\" secure setting.\n"
                    + "  adb shell content delete --uri content://settings/secure "
                    + "--where \"name=\'new_setting\'\"\n"
                    + "\n"
                    + "usage: adb shell content query --uri <URI> [--user <USER_ID>]"
                    + " [--projection <PROJECTION>] [--where <WHERE>] [--sort <SORT_ORDER>]\n"
                    + "  <PROJECTION> is a list of colon separated column names and is formatted:\n"
                    + "  <COLUMN_NAME>[:<COLUMN_NAME>...]\n"
                    + "  <SORT_ORDER> is the order in which rows in the result should be sorted.\n"
                    + "  Example:\n"
                    + "  # Select \"name\" and \"value\" columns from secure settings where \"name\" is "
                    + "equal to \"new_setting\" and sort the result by name in ascending order.\n"
                    + "  adb shell content query --uri content://settings/secure --projection name:value"
                    + " --where \"name=\'new_setting\'\" --sort \"name ASC\"\n"
                    + "\n"
                    + "usage: adb shell content call --uri <URI> --method <METHOD> [--arg <ARG>]\n"
                    + "       [--extra <BINDING> ...]\n"
                    + "  <METHOD> is the name of a provider-defined method\n"
                    + "  <ARG> is an optional string argument\n"
                    + "  <BINDING> is like --bind above, typed data of the form <KEY>:{b,s,i,l,f,d}:<VAL>\n"
                    + "\n"
                    + "usage: adb shell content read --uri <URI> [--user <USER_ID>]\n"
                    + "  Example:\n"
                    + "  # cat default ringtone to a file, then pull to host\n"
                    + "  adb shell 'content read --uri content://settings/system/ringtone >"
                    + " /mnt/sdcard/tmp.ogg' && adb pull /mnt/sdcard/tmp.ogg\n"
                    + "\n"
                    + "usage: adb shell content gettype --uri <URI> [--user <USER_ID>]\n"
                    + "  Example:\n"
                    + "  # Show the mime-type of the URI\n"
                    + "  adb shell content gettype --uri content://media/internal/audio/media/\n"
                    + "\n";

    private static class Parser {
        private static final String ARGUMENT_INSERT = "insert";
        private static final String ARGUMENT_DELETE = "delete";
        private static final String ARGUMENT_UPDATE = "update";
        private static final String ARGUMENT_QUERY = "query";
        private static final String ARGUMENT_CALL = "call";
        private static final String ARGUMENT_READ = "read";
        private static final String ARGUMENT_GET_TYPE = "gettype";
        private static final String ARGUMENT_WHERE = "--where";
        private static final String ARGUMENT_BIND = "--bind";
        private static final String ARGUMENT_URI = "--uri";
        private static final String ARGUMENT_USER = "--user";
        private static final String ARGUMENT_PROJECTION = "--projection";
        private static final String ARGUMENT_SORT = "--sort";
        private static final String ARGUMENT_METHOD = "--method";
        private static final String ARGUMENT_ARG = "--arg";
        private static final String ARGUMENT_EXTRA = "--extra";
        private static final String TYPE_BOOLEAN = "b";
        private static final String TYPE_STRING = "s";
        private static final String TYPE_INTEGER = "i";
        private static final String TYPE_LONG = "l";
        private static final String TYPE_FLOAT = "f";
        private static final String TYPE_DOUBLE = "d";
        private static final String COLON = ":";
        private static final String ARGUMENT_PREFIX = "--";

        private final Tokenizer mTokenizer;

        public Parser(String[] args) {
            mTokenizer = new Tokenizer(args);
        }

        public Command parseCommand() {
            try {
                String operation = mTokenizer.nextArg();
                if (ARGUMENT_INSERT.equals(operation)) {
                    return parseInsertCommand();
                } else if (ARGUMENT_DELETE.equals(operation)) {
                    return parseDeleteCommand();
                } else if (ARGUMENT_UPDATE.equals(operation)) {
                    return parseUpdateCommand();
                } else if (ARGUMENT_QUERY.equals(operation)) {
                    return parseQueryCommand();
                } else if (ARGUMENT_CALL.equals(operation)) {
                    return parseCallCommand();
                } else if (ARGUMENT_READ.equals(operation)) {
                    return parseReadCommand();
                } else if (ARGUMENT_GET_TYPE.equals(operation)) {
                    return parseGetTypeCommand();
                } else {
                    throw new IllegalArgumentException("Unsupported operation: " + operation);
                }
            } catch (IllegalArgumentException iae) {
                System.out.println(USAGE);
                System.out.println("[ERROR] " + iae.getMessage());
                return null;
            }
        }

        private InsertCommand parseInsertCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;
            ContentValues values = new ContentValues();
            for (String argument; (argument = mTokenizer.nextArg()) != null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else if (ARGUMENT_BIND.equals(argument)) {
                    parseBindValue(values);
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            if (values.size() == 0) {
                throw new IllegalArgumentException("Bindings not specified."
                        + " Did you specify --bind argument(s)?");
            }
            return new InsertCommand(uri, userId, values);
        }

        private DeleteCommand parseDeleteCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;
            String where = null;
            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else if (ARGUMENT_WHERE.equals(argument)) {
                    where = argumentValueRequired(argument);
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            return new DeleteCommand(uri, userId, where);
        }

        private UpdateCommand parseUpdateCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;
            String where = null;
            ContentValues values = new ContentValues();
            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else if (ARGUMENT_WHERE.equals(argument)) {
                    where = argumentValueRequired(argument);
                } else if (ARGUMENT_BIND.equals(argument)) {
                    parseBindValue(values);
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            if (values.size() == 0) {
                throw new IllegalArgumentException("Bindings not specified."
                        + " Did you specify --bind argument(s)?");
            }
            return new UpdateCommand(uri, userId, values, where);
        }

        public CallCommand parseCallCommand() {
            String method = null;
            int userId = UserHandle.USER_SYSTEM;
            String arg = null;
            Uri uri = null;
            ContentValues values = new ContentValues();
            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else if (ARGUMENT_METHOD.equals(argument)) {
                    method = argumentValueRequired(argument);
                } else if (ARGUMENT_ARG.equals(argument)) {
                    arg = argumentValueRequired(argument);
                } else if (ARGUMENT_EXTRA.equals(argument)) {
                    parseBindValue(values);
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }

            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            if (method == null) {
                throw new IllegalArgumentException("Content provider method not specified.");
            }
            return new CallCommand(uri, userId, method, arg, values);
        }

        private GetTypeCommand parseGetTypeCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;

            for (String argument; (argument = mTokenizer.nextArg()) != null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            return new GetTypeCommand(uri, userId);
        }

        private ReadCommand parseReadCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;
            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            return new ReadCommand(uri, userId);
        }

        public QueryCommand parseQueryCommand() {
            Uri uri = null;
            int userId = UserHandle.USER_SYSTEM;
            String[] projection = null;
            String sort = null;
            String where = null;
            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
                if (ARGUMENT_URI.equals(argument)) {
                    uri = Uri.parse(argumentValueRequired(argument));
                } else if (ARGUMENT_USER.equals(argument)) {
                    userId = Integer.parseInt(argumentValueRequired(argument));
                } else if (ARGUMENT_WHERE.equals(argument)) {
                    where = argumentValueRequired(argument);
                } else if (ARGUMENT_SORT.equals(argument)) {
                    sort = argumentValueRequired(argument);
                } else if (ARGUMENT_PROJECTION.equals(argument)) {
                    projection = argumentValueRequired(argument).split("[\\s]*:[\\s]*");
                } else {
                    throw new IllegalArgumentException("Unsupported argument: " + argument);
                }
            }
            if (uri == null) {
                throw new IllegalArgumentException("Content provider URI not specified."
                        + " Did you specify --uri argument?");
            }
            return new QueryCommand(uri, userId, projection, where, sort);
        }

        private void parseBindValue(ContentValues values) {
            String argument = mTokenizer.nextArg();
            if (TextUtils.isEmpty(argument)) {
                throw new IllegalArgumentException("Binding not well formed: " + argument);
            }
            final int firstColonIndex = argument.indexOf(COLON);
            if (firstColonIndex < 0) {
                throw new IllegalArgumentException("Binding not well formed: " + argument);
            }
            final int secondColonIndex = argument.indexOf(COLON, firstColonIndex + 1);
            if (secondColonIndex < 0) {
                throw new IllegalArgumentException("Binding not well formed: " + argument);
            }
            String column = argument.substring(0, firstColonIndex);
            String type = argument.substring(firstColonIndex + 1, secondColonIndex);
            String value = argument.substring(secondColonIndex + 1);
            if (TYPE_STRING.equals(type)) {
                values.put(column, value);
            } else if (TYPE_BOOLEAN.equalsIgnoreCase(type)) {
                values.put(column, Boolean.parseBoolean(value));
            } else if (TYPE_INTEGER.equalsIgnoreCase(type) || TYPE_LONG.equalsIgnoreCase(type)) {
                values.put(column, Long.parseLong(value));
            } else if (TYPE_FLOAT.equalsIgnoreCase(type) || TYPE_DOUBLE.equalsIgnoreCase(type)) {
                values.put(column, Double.parseDouble(value));
            } else {
                throw new IllegalArgumentException("Unsupported type: " + type);
            }
        }

        private String argumentValueRequired(String argument) {
            String value = mTokenizer.nextArg();
            if (TextUtils.isEmpty(value) || value.startsWith(ARGUMENT_PREFIX)) {
                throw new IllegalArgumentException("No value for argument: " + argument);
            }
            return value;
        }
    }

    private static class Tokenizer {
        private final String[] mArgs;
        private int mNextArg;

        public Tokenizer(String[] args) {
            mArgs = args;
        }

        private String nextArg() {
            if (mNextArg < mArgs.length) {
                return mArgs[mNextArg++];
            } else {
                return null;
            }
        }
    }

    private static abstract class Command {
        final Uri mUri;
        final int mUserId;

        public Command(Uri uri, int userId) {
            mUri = uri;
            mUserId = userId;
        }

        public final void execute() {
            String providerName = mUri.getAuthority();
            try {
                IActivityManager activityManager = ActivityManager.getService();
                IContentProvider provider = null;
                IBinder token = new Binder();
                try {
                    ContentProviderHolder holder = activityManager.getContentProviderExternal(
                            providerName, mUserId, token);
                    if (holder == null) {
                        throw new IllegalStateException("Could not find provider: " + providerName);
                    }
                    provider = holder.provider;
                    onExecute(provider);
                } finally {
                    if (provider != null) {
                        activityManager.removeContentProviderExternal(providerName, token);
                    }
                }
            } catch (Exception e) {
                System.err.println("Error while accessing provider:" + providerName);
                e.printStackTrace();
            }
        }

        public static String resolveCallingPackage() {
            switch (Process.myUid()) {
                case Process.ROOT_UID: {
                    return "root";
                }

                case Process.SHELL_UID: {
                    return "com.android.shell";
                }

                default: {
                    return null;
                }
            }
        }

        protected abstract void onExecute(IContentProvider provider) throws Exception;
    }

    private static class InsertCommand extends Command {
        final ContentValues mContentValues;

        public InsertCommand(Uri uri, int userId, ContentValues contentValues) {
            super(uri, userId);
            mContentValues = contentValues;
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            provider.insert(resolveCallingPackage(), mUri, mContentValues);
        }
    }

    private static class DeleteCommand extends Command {
        final String mWhere;

        public DeleteCommand(Uri uri, int userId, String where) {
            super(uri, userId);
            mWhere = where;
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            provider.delete(resolveCallingPackage(), mUri, mWhere, null);
        }
    }

    private static class CallCommand extends Command {
        final String mMethod, mArg;
        Bundle mExtras = null;

        public CallCommand(Uri uri, int userId, String method, String arg, ContentValues values) {
            super(uri, userId);
            mMethod = method;
            mArg = arg;
            if (values != null) {
                mExtras = new Bundle();
                for (String key : values.keySet()) {
                    final Object val = values.get(key);
                    if (val instanceof String) {
                        mExtras.putString(key, (String) val);
                    } else if (val instanceof Float) {
                        mExtras.putFloat(key, (Float) val);
                    } else if (val instanceof Double) {
                        mExtras.putDouble(key, (Double) val);
                    } else if (val instanceof Boolean) {
                        mExtras.putBoolean(key, (Boolean) val);
                    } else if (val instanceof Integer) {
                        mExtras.putInt(key, (Integer) val);
                    } else if (val instanceof Long) {
                        mExtras.putLong(key, (Long) val);
                    }
                }
            }
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            Bundle result = provider.call(null, mMethod, mArg, mExtras);
            final int size = result.size(); // unpack
            System.out.println("Result: " + result);
        }
    }

    private static class GetTypeCommand extends Command {
        public GetTypeCommand(Uri uri, int userId) {
            super(uri, userId);
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            String type = provider.getType(mUri);
            System.out.println("Result: " + type);
        }
    }

    private static class ReadCommand extends Command {
        public ReadCommand(Uri uri, int userId) {
            super(uri, userId);
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            final ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null);
            copy(new FileInputStream(fd.getFileDescriptor()), System.out);
        }

        private static void copy(InputStream is, OutputStream os) throws IOException {
            final byte[] buffer = new byte[8 * 1024];
            int read;
            try {
                while ((read = is.read(buffer)) > -1) {
                    os.write(buffer, 0, read);
                }
            } finally {
                IoUtils.closeQuietly(is);
                IoUtils.closeQuietly(os);
            }
        }
    }

    private static class QueryCommand extends DeleteCommand {
        final String[] mProjection;
        final String mSortOrder;

        public QueryCommand(
                Uri uri, int userId, String[] projection, String where, String sortOrder) {
            super(uri, userId, where);
            mProjection = projection;
            mSortOrder = sortOrder;
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            Cursor cursor = provider.query(resolveCallingPackage(), mUri, mProjection,
                    ContentResolver.createSqlQueryBundle(mWhere, null, mSortOrder), null);
            if (cursor == null) {
                System.out.println("No result found.");
                return;
            }
            try {
                if (cursor.moveToFirst()) {
                    int rowIndex = 0;
                    StringBuilder builder = new StringBuilder();
                    do {
                        builder.setLength(0);
                        builder.append("Row: ").append(rowIndex).append(" ");
                        rowIndex++;
                        final int columnCount = cursor.getColumnCount();
                        for (int i = 0; i < columnCount; i++) {
                            if (i > 0) {
                                builder.append(", ");
                            }
                            String columnName = cursor.getColumnName(i);
                            String columnValue = null;
                            final int columnIndex = cursor.getColumnIndex(columnName);
                            final int type = cursor.getType(columnIndex);
                            switch (type) {
                                case Cursor.FIELD_TYPE_FLOAT:
                                    columnValue = String.valueOf(cursor.getFloat(columnIndex));
                                    break;
                                case Cursor.FIELD_TYPE_INTEGER:
                                    columnValue = String.valueOf(cursor.getLong(columnIndex));
                                    break;
                                case Cursor.FIELD_TYPE_STRING:
                                    columnValue = cursor.getString(columnIndex);
                                    break;
                                case Cursor.FIELD_TYPE_BLOB:
                                    columnValue = "BLOB";
                                    break;
                                case Cursor.FIELD_TYPE_NULL:
                                    columnValue = "NULL";
                                    break;
                            }
                            builder.append(columnName).append("=").append(columnValue);
                        }
                        System.out.println(builder);
                    } while (cursor.moveToNext());
                } else {
                    System.out.println("No result found.");
                }
            } finally {
                cursor.close();
            }
        }
    }

    private static class UpdateCommand extends InsertCommand {
        final String mWhere;

        public UpdateCommand(Uri uri, int userId, ContentValues contentValues, String where) {
            super(uri, userId, contentValues);
            mWhere = where;
        }

        @Override
        public void onExecute(IContentProvider provider) throws Exception {
            provider.update(resolveCallingPackage(), mUri, mContentValues, mWhere, null);
        }
    }

    public static void main(String[] args) {
        Parser parser = new Parser(args);
        Command command = parser.parseCommand();
        if (command != null) {
            command.execute();
        }
    }
}
