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