1/*
2** Copyright 2012, The Android Open Source Project
3**
4** Licensed under the Apache License, Version 2.0 (the "License");
5** you may not use this file except in compliance with the License.
6** You may obtain a copy of the License at
7**
8**     http://www.apache.org/licenses/LICENSE-2.0
9**
10** Unless required by applicable law or agreed to in writing, software
11** distributed under the License is distributed on an "AS IS" BASIS,
12** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13** See the License for the specific language governing permissions and
14** limitations under the License.
15*/
16
17package com.android.commands.content;
18
19import android.app.ActivityManagerNative;
20import android.app.IActivityManager;
21import android.app.IActivityManager.ContentProviderHolder;
22import android.content.ContentValues;
23import android.content.IContentProvider;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Binder;
27import android.os.Bundle;
28import android.os.IBinder;
29import android.os.ParcelFileDescriptor;
30import android.os.UserHandle;
31import android.text.TextUtils;
32
33import java.io.FileInputStream;
34import java.io.IOException;
35import java.io.InputStream;
36import java.io.OutputStream;
37
38import libcore.io.IoUtils;
39
40/**
41 * This class is a command line utility for manipulating content. A client
42 * can insert, update, and remove records in a content provider. For example,
43 * some settings may be configured before running the CTS tests, etc.
44 * <p>
45 * Examples:
46 * <ul>
47 * <li>
48 * # Add "new_setting" secure setting with value "new_value".</br>
49 * adb shell content insert --uri content://settings/secure --bind name:s:new_setting
50 *  --bind value:s:new_value
51 * </li>
52 * <li>
53 * # Change "new_setting" secure setting to "newer_value" (You have to escape single quotes in
54 * the where clause).</br>
55 * adb shell content update --uri content://settings/secure --bind value:s:newer_value
56 *  --where "name=\'new_setting\'"
57 * </li>
58 * <li>
59 * # Remove "new_setting" secure setting.</br>
60 * adb shell content delete --uri content://settings/secure --where "name=\'new_setting\'"
61 * </li>
62 * <li>
63 * # Query \"name\" and \"value\" columns from secure settings where \"name\" is equal to"
64 *    \"new_setting\" and sort the result by name in ascending order.\n"
65 * adb shell content query --uri content://settings/secure --projection name:value
66 *  --where "name=\'new_setting\'" --sort \"name ASC\"
67 * </li>
68 * </ul>
69 * </p>
70 */
71public class Content {
72
73    private static final String USAGE =
74        "usage: adb shell content [subcommand] [options]\n"
75        + "\n"
76        + "usage: adb shell content insert --uri <URI> [--user <USER_ID>]"
77                + " --bind <BINDING> [--bind <BINDING>...]\n"
78        + "  <URI> a content provider URI.\n"
79        + "  <BINDING> binds a typed value to a column and is formatted:\n"
80        + "  <COLUMN_NAME>:<TYPE>:<COLUMN_VALUE> where:\n"
81        + "  <TYPE> specifies data type such as:\n"
82        + "  b - boolean, s - string, i - integer, l - long, f - float, d - double\n"
83        + "  Note: Omit the value for passing an empty string, e.g column:s:\n"
84        + "  Example:\n"
85        + "  # Add \"new_setting\" secure setting with value \"new_value\".\n"
86        + "  adb shell content insert --uri content://settings/secure --bind name:s:new_setting"
87                + " --bind value:s:new_value\n"
88        + "\n"
89        + "usage: adb shell content update --uri <URI> [--user <USER_ID>] [--where <WHERE>]\n"
90        + "  <WHERE> is a SQL style where clause in quotes (You have to escape single quotes"
91                + " - see example below).\n"
92        + "  Example:\n"
93        + "  # Change \"new_setting\" secure setting to \"newer_value\".\n"
94        + "  adb shell content update --uri content://settings/secure --bind"
95                + " value:s:newer_value --where \"name=\'new_setting\'\"\n"
96        + "\n"
97        + "usage: adb shell content delete --uri <URI> [--user <USER_ID>] --bind <BINDING>"
98                + " [--bind <BINDING>...] [--where <WHERE>]\n"
99        + "  Example:\n"
100        + "  # Remove \"new_setting\" secure setting.\n"
101        + "  adb shell content delete --uri content://settings/secure "
102                + "--where \"name=\'new_setting\'\"\n"
103        + "\n"
104        + "usage: adb shell content query --uri <URI> [--user <USER_ID>]"
105                + " [--projection <PROJECTION>] [--where <WHERE>] [--sort <SORT_ORDER>]\n"
106        + "  <PROJECTION> is a list of colon separated column names and is formatted:\n"
107        + "  <COLUMN_NAME>[:<COLUMN_NAME>...]\n"
108        + "  <SORT_ORDER> is the order in which rows in the result should be sorted.\n"
109        + "  Example:\n"
110        + "  # Select \"name\" and \"value\" columns from secure settings where \"name\" is "
111                + "equal to \"new_setting\" and sort the result by name in ascending order.\n"
112        + "  adb shell content query --uri content://settings/secure --projection name:value"
113                + " --where \"name=\'new_setting\'\" --sort \"name ASC\"\n"
114        + "\n"
115        + "usage: adb shell content call --uri <URI> --method <METHOD> [--arg <ARG>]\n"
116        + "       [--extra <BINDING> ...]\n"
117        + "  <METHOD> is the name of a provider-defined method\n"
118        + "  <ARG> is an optional string argument\n"
119        + "  <BINDING> is like --bind above, typed data of the form <KEY>:{b,s,i,l,f,d}:<VAL>\n"
120        + "\n"
121        + "usage: adb shell content read --uri <URI> [--user <USER_ID>]\n"
122        + "  Example:\n"
123        + "  # cat default ringtone to a file, then pull to host\n"
124        + "  adb shell 'content read --uri content://settings/system/ringtone >"
125                + " /mnt/sdcard/tmp.ogg' && adb pull /mnt/sdcard/tmp.ogg\n"
126        + "\n";
127
128    private static class Parser {
129        private static final String ARGUMENT_INSERT = "insert";
130        private static final String ARGUMENT_DELETE = "delete";
131        private static final String ARGUMENT_UPDATE = "update";
132        private static final String ARGUMENT_QUERY = "query";
133        private static final String ARGUMENT_CALL = "call";
134        private static final String ARGUMENT_READ = "read";
135        private static final String ARGUMENT_WHERE = "--where";
136        private static final String ARGUMENT_BIND = "--bind";
137        private static final String ARGUMENT_URI = "--uri";
138        private static final String ARGUMENT_USER = "--user";
139        private static final String ARGUMENT_PROJECTION = "--projection";
140        private static final String ARGUMENT_SORT = "--sort";
141        private static final String ARGUMENT_METHOD = "--method";
142        private static final String ARGUMENT_ARG = "--arg";
143        private static final String ARGUMENT_EXTRA = "--extra";
144        private static final String TYPE_BOOLEAN = "b";
145        private static final String TYPE_STRING = "s";
146        private static final String TYPE_INTEGER = "i";
147        private static final String TYPE_LONG = "l";
148        private static final String TYPE_FLOAT = "f";
149        private static final String TYPE_DOUBLE = "d";
150        private static final String COLON = ":";
151        private static final String ARGUMENT_PREFIX = "--";
152
153        private final Tokenizer mTokenizer;
154
155        public Parser(String[] args) {
156            mTokenizer = new Tokenizer(args);
157        }
158
159        public Command parseCommand() {
160            try {
161                String operation = mTokenizer.nextArg();
162                if (ARGUMENT_INSERT.equals(operation)) {
163                    return parseInsertCommand();
164                } else if (ARGUMENT_DELETE.equals(operation)) {
165                    return parseDeleteCommand();
166                } else if (ARGUMENT_UPDATE.equals(operation)) {
167                    return parseUpdateCommand();
168                } else if (ARGUMENT_QUERY.equals(operation)) {
169                    return parseQueryCommand();
170                } else if (ARGUMENT_CALL.equals(operation)) {
171                    return parseCallCommand();
172                } else if (ARGUMENT_READ.equals(operation)) {
173                    return parseReadCommand();
174                } else {
175                    throw new IllegalArgumentException("Unsupported operation: " + operation);
176                }
177            } catch (IllegalArgumentException iae) {
178                System.out.println(USAGE);
179                System.out.println("[ERROR] " + iae.getMessage());
180                return null;
181            }
182        }
183
184        private InsertCommand parseInsertCommand() {
185            Uri uri = null;
186            int userId = UserHandle.USER_OWNER;
187            ContentValues values = new ContentValues();
188            for (String argument; (argument = mTokenizer.nextArg()) != null;) {
189                if (ARGUMENT_URI.equals(argument)) {
190                    uri = Uri.parse(argumentValueRequired(argument));
191                } else if (ARGUMENT_USER.equals(argument)) {
192                    userId = Integer.parseInt(argumentValueRequired(argument));
193                } else if (ARGUMENT_BIND.equals(argument)) {
194                    parseBindValue(values);
195                } else {
196                    throw new IllegalArgumentException("Unsupported argument: " + argument);
197                }
198            }
199            if (uri == null) {
200                throw new IllegalArgumentException("Content provider URI not specified."
201                        + " Did you specify --uri argument?");
202            }
203            if (values.size() == 0) {
204                throw new IllegalArgumentException("Bindings not specified."
205                        + " Did you specify --bind argument(s)?");
206            }
207            return new InsertCommand(uri, userId, values);
208        }
209
210        private DeleteCommand parseDeleteCommand() {
211            Uri uri = null;
212            int userId = UserHandle.USER_OWNER;
213            String where = null;
214            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
215                if (ARGUMENT_URI.equals(argument)) {
216                    uri = Uri.parse(argumentValueRequired(argument));
217                } else if (ARGUMENT_USER.equals(argument)) {
218                    userId = Integer.parseInt(argumentValueRequired(argument));
219                } else if (ARGUMENT_WHERE.equals(argument)) {
220                    where = argumentValueRequired(argument);
221                } else {
222                    throw new IllegalArgumentException("Unsupported argument: " + argument);
223                }
224            }
225            if (uri == null) {
226                throw new IllegalArgumentException("Content provider URI not specified."
227                        + " Did you specify --uri argument?");
228            }
229            return new DeleteCommand(uri, userId, where);
230        }
231
232        private UpdateCommand parseUpdateCommand() {
233            Uri uri = null;
234            int userId = UserHandle.USER_OWNER;
235            String where = null;
236            ContentValues values = new ContentValues();
237            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
238                if (ARGUMENT_URI.equals(argument)) {
239                    uri = Uri.parse(argumentValueRequired(argument));
240                } else if (ARGUMENT_USER.equals(argument)) {
241                    userId = Integer.parseInt(argumentValueRequired(argument));
242                } else if (ARGUMENT_WHERE.equals(argument)) {
243                    where = argumentValueRequired(argument);
244                } else if (ARGUMENT_BIND.equals(argument)) {
245                    parseBindValue(values);
246                } else {
247                    throw new IllegalArgumentException("Unsupported argument: " + argument);
248                }
249            }
250            if (uri == null) {
251                throw new IllegalArgumentException("Content provider URI not specified."
252                        + " Did you specify --uri argument?");
253            }
254            if (values.size() == 0) {
255                throw new IllegalArgumentException("Bindings not specified."
256                        + " Did you specify --bind argument(s)?");
257            }
258            return new UpdateCommand(uri, userId, values, where);
259        }
260
261        public CallCommand parseCallCommand() {
262            String method = null;
263            int userId = UserHandle.USER_OWNER;
264            String arg = null;
265            Uri uri = null;
266            ContentValues values = new ContentValues();
267            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
268                if (ARGUMENT_URI.equals(argument)) {
269                    uri = Uri.parse(argumentValueRequired(argument));
270                } else if (ARGUMENT_USER.equals(argument)) {
271                    userId = Integer.parseInt(argumentValueRequired(argument));
272                } else if (ARGUMENT_METHOD.equals(argument)) {
273                    method = argumentValueRequired(argument);
274                } else if (ARGUMENT_ARG.equals(argument)) {
275                    arg = argumentValueRequired(argument);
276                } else if (ARGUMENT_EXTRA.equals(argument)) {
277                    parseBindValue(values);
278                } else {
279                    throw new IllegalArgumentException("Unsupported argument: " + argument);
280                }
281
282            }
283            if (uri == null) {
284                throw new IllegalArgumentException("Content provider URI not specified."
285                        + " Did you specify --uri argument?");
286            }
287            if (method == null) {
288                throw new IllegalArgumentException("Content provider method not specified.");
289            }
290            return new CallCommand(uri, userId, method, arg, values);
291        }
292
293        private ReadCommand parseReadCommand() {
294            Uri uri = null;
295            int userId = UserHandle.USER_OWNER;
296            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
297                if (ARGUMENT_URI.equals(argument)) {
298                    uri = Uri.parse(argumentValueRequired(argument));
299                } else if (ARGUMENT_USER.equals(argument)) {
300                    userId = Integer.parseInt(argumentValueRequired(argument));
301                } else {
302                    throw new IllegalArgumentException("Unsupported argument: " + argument);
303                }
304            }
305            if (uri == null) {
306                throw new IllegalArgumentException("Content provider URI not specified."
307                        + " Did you specify --uri argument?");
308            }
309            return new ReadCommand(uri, userId);
310        }
311
312        public QueryCommand parseQueryCommand() {
313            Uri uri = null;
314            int userId = UserHandle.USER_OWNER;
315            String[] projection = null;
316            String sort = null;
317            String where = null;
318            for (String argument; (argument = mTokenizer.nextArg())!= null;) {
319                if (ARGUMENT_URI.equals(argument)) {
320                    uri = Uri.parse(argumentValueRequired(argument));
321                } else if (ARGUMENT_USER.equals(argument)) {
322                    userId = Integer.parseInt(argumentValueRequired(argument));
323                } else if (ARGUMENT_WHERE.equals(argument)) {
324                    where = argumentValueRequired(argument);
325                } else if (ARGUMENT_SORT.equals(argument)) {
326                    sort = argumentValueRequired(argument);
327                } else if (ARGUMENT_PROJECTION.equals(argument)) {
328                    projection = argumentValueRequired(argument).split("[\\s]*:[\\s]*");
329                } else {
330                    throw new IllegalArgumentException("Unsupported argument: " + argument);
331                }
332            }
333            if (uri == null) {
334                throw new IllegalArgumentException("Content provider URI not specified."
335                        + " Did you specify --uri argument?");
336            }
337            return new QueryCommand(uri, userId, projection, where, sort);
338        }
339
340        private void parseBindValue(ContentValues values) {
341            String argument = mTokenizer.nextArg();
342            if (TextUtils.isEmpty(argument)) {
343                throw new IllegalArgumentException("Binding not well formed: " + argument);
344            }
345            final int firstColonIndex = argument.indexOf(COLON);
346            if (firstColonIndex < 0) {
347                throw new IllegalArgumentException("Binding not well formed: " + argument);
348            }
349            final int secondColonIndex = argument.indexOf(COLON, firstColonIndex + 1);
350            if (secondColonIndex < 0) {
351                throw new IllegalArgumentException("Binding not well formed: " + argument);
352            }
353            String column = argument.substring(0, firstColonIndex);
354            String type = argument.substring(firstColonIndex + 1, secondColonIndex);
355            String value = argument.substring(secondColonIndex + 1);
356            if (TYPE_STRING.equals(type)) {
357                values.put(column, value);
358            } else if (TYPE_BOOLEAN.equalsIgnoreCase(type)) {
359                values.put(column, Boolean.parseBoolean(value));
360            } else if (TYPE_INTEGER.equalsIgnoreCase(type) || TYPE_LONG.equalsIgnoreCase(type)) {
361                values.put(column, Long.parseLong(value));
362            } else if (TYPE_FLOAT.equalsIgnoreCase(type) || TYPE_DOUBLE.equalsIgnoreCase(type)) {
363                values.put(column, Double.parseDouble(value));
364            } else {
365                throw new IllegalArgumentException("Unsupported type: " + type);
366            }
367        }
368
369        private String argumentValueRequired(String argument) {
370            String value = mTokenizer.nextArg();
371            if (TextUtils.isEmpty(value) || value.startsWith(ARGUMENT_PREFIX)) {
372                throw new IllegalArgumentException("No value for argument: " + argument);
373            }
374            return value;
375        }
376    }
377
378    private static class Tokenizer {
379        private final String[] mArgs;
380        private int mNextArg;
381
382        public Tokenizer(String[] args) {
383            mArgs = args;
384        }
385
386        private String nextArg() {
387            if (mNextArg < mArgs.length) {
388                return mArgs[mNextArg++];
389            } else {
390                return null;
391            }
392        }
393    }
394
395    private static abstract class Command {
396        final Uri mUri;
397        final int mUserId;
398
399        public Command(Uri uri, int userId) {
400            mUri = uri;
401            mUserId = userId;
402        }
403
404        public final void execute() {
405            String providerName = mUri.getAuthority();
406            try {
407                IActivityManager activityManager = ActivityManagerNative.getDefault();
408                IContentProvider provider = null;
409                IBinder token = new Binder();
410                try {
411                    ContentProviderHolder holder = activityManager.getContentProviderExternal(
412                            providerName, mUserId, token);
413                    if (holder == null) {
414                        throw new IllegalStateException("Could not find provider: " + providerName);
415                    }
416                    provider = holder.provider;
417                    onExecute(provider);
418                } finally {
419                    if (provider != null) {
420                        activityManager.removeContentProviderExternal(providerName, token);
421                    }
422                }
423            } catch (Exception e) {
424                System.err.println("Error while accessing provider:" + providerName);
425                e.printStackTrace();
426            }
427        }
428
429        protected abstract void onExecute(IContentProvider provider) throws Exception;
430    }
431
432    private static class InsertCommand extends Command {
433        final ContentValues mContentValues;
434
435        public InsertCommand(Uri uri, int userId, ContentValues contentValues) {
436            super(uri, userId);
437            mContentValues = contentValues;
438        }
439
440        @Override
441        public void onExecute(IContentProvider provider) throws Exception {
442            provider.insert(null, mUri, mContentValues);
443        }
444    }
445
446    private static class DeleteCommand extends Command {
447        final String mWhere;
448
449        public DeleteCommand(Uri uri, int userId, String where) {
450            super(uri, userId);
451            mWhere = where;
452        }
453
454        @Override
455        public void onExecute(IContentProvider provider) throws Exception {
456            provider.delete(null, mUri, mWhere, null);
457        }
458    }
459
460    private static class CallCommand extends Command {
461        final String mMethod, mArg;
462        Bundle mExtras = null;
463
464        public CallCommand(Uri uri, int userId, String method, String arg, ContentValues values) {
465            super(uri, userId);
466            mMethod = method;
467            mArg = arg;
468            if (values != null) {
469                mExtras = new Bundle();
470                for (String key : values.keySet()) {
471                    final Object val = values.get(key);
472                    if (val instanceof String) {
473                        mExtras.putString(key, (String) val);
474                    } else if (val instanceof Float) {
475                        mExtras.putFloat(key, (Float) val);
476                    } else if (val instanceof Double) {
477                        mExtras.putDouble(key, (Double) val);
478                    } else if (val instanceof Boolean) {
479                        mExtras.putBoolean(key, (Boolean) val);
480                    } else if (val instanceof Integer) {
481                        mExtras.putInt(key, (Integer) val);
482                    } else if (val instanceof Long) {
483                        mExtras.putLong(key, (Long) val);
484                    }
485                }
486            }
487        }
488
489        @Override
490        public void onExecute(IContentProvider provider) throws Exception {
491            Bundle result = provider.call(null, mMethod, mArg, mExtras);
492            final int size = result.size(); // unpack
493            System.out.println("Result: " + result);
494        }
495    }
496
497    private static class ReadCommand extends Command {
498        public ReadCommand(Uri uri, int userId) {
499            super(uri, userId);
500        }
501
502        @Override
503        public void onExecute(IContentProvider provider) throws Exception {
504            final ParcelFileDescriptor fd = provider.openFile(null, mUri, "r", null, null);
505            copy(new FileInputStream(fd.getFileDescriptor()), System.out);
506        }
507
508        private static void copy(InputStream is, OutputStream os) throws IOException {
509            final byte[] buffer = new byte[8 * 1024];
510            int read;
511            try {
512                while ((read = is.read(buffer)) > -1) {
513                    os.write(buffer, 0, read);
514                }
515            } finally {
516                IoUtils.closeQuietly(is);
517                IoUtils.closeQuietly(os);
518            }
519        }
520    }
521
522    private static class QueryCommand extends DeleteCommand {
523        final String[] mProjection;
524        final String mSortOrder;
525
526        public QueryCommand(
527                Uri uri, int userId, String[] projection, String where, String sortOrder) {
528            super(uri, userId, where);
529            mProjection = projection;
530            mSortOrder = sortOrder;
531        }
532
533        @Override
534        public void onExecute(IContentProvider provider) throws Exception {
535            Cursor cursor = provider.query(null, mUri, mProjection, mWhere, null, mSortOrder, null);
536            if (cursor == null) {
537                System.out.println("No result found.");
538                return;
539            }
540            try {
541                if (cursor.moveToFirst()) {
542                    int rowIndex = 0;
543                    StringBuilder builder = new StringBuilder();
544                    do {
545                        builder.setLength(0);
546                        builder.append("Row: ").append(rowIndex).append(" ");
547                        rowIndex++;
548                        final int columnCount = cursor.getColumnCount();
549                        for (int i = 0; i < columnCount; i++) {
550                            if (i > 0) {
551                                builder.append(", ");
552                            }
553                            String columnName = cursor.getColumnName(i);
554                            String columnValue = null;
555                            final int columnIndex = cursor.getColumnIndex(columnName);
556                            final int type = cursor.getType(columnIndex);
557                            switch (type) {
558                                case Cursor.FIELD_TYPE_FLOAT:
559                                    columnValue = String.valueOf(cursor.getFloat(columnIndex));
560                                    break;
561                                case Cursor.FIELD_TYPE_INTEGER:
562                                    columnValue = String.valueOf(cursor.getLong(columnIndex));
563                                    break;
564                                case Cursor.FIELD_TYPE_STRING:
565                                    columnValue = cursor.getString(columnIndex);
566                                    break;
567                                case Cursor.FIELD_TYPE_BLOB:
568                                    columnValue = "BLOB";
569                                    break;
570                                case Cursor.FIELD_TYPE_NULL:
571                                    columnValue = "NULL";
572                                    break;
573                            }
574                            builder.append(columnName).append("=").append(columnValue);
575                        }
576                        System.out.println(builder);
577                    } while (cursor.moveToNext());
578                } else {
579                    System.out.println("No result found.");
580                }
581            } finally {
582                cursor.close();
583            }
584        }
585    }
586
587    private static class UpdateCommand extends InsertCommand {
588        final String mWhere;
589
590        public UpdateCommand(Uri uri, int userId, ContentValues contentValues, String where) {
591            super(uri, userId, contentValues);
592            mWhere = where;
593        }
594
595        @Override
596        public void onExecute(IContentProvider provider) throws Exception {
597            provider.update(null, mUri, mContentValues, mWhere, null);
598        }
599    }
600
601    public static void main(String[] args) {
602        Parser parser = new Parser(args);
603        Command command = parser.parseCommand();
604        if (command != null) {
605            command.execute();
606        }
607    }
608}
609