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