1/*
2 * Copyright (C) 2006 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 android.database.sqlite;
18
19import android.database.Cursor;
20import android.database.DatabaseUtils;
21import android.os.CancellationSignal;
22import android.os.OperationCanceledException;
23import android.provider.BaseColumns;
24import android.text.TextUtils;
25import android.util.Log;
26
27import java.util.Iterator;
28import java.util.Map;
29import java.util.Map.Entry;
30import java.util.Set;
31import java.util.regex.Pattern;
32
33/**
34 * This is a convience class that helps build SQL queries to be sent to
35 * {@link SQLiteDatabase} objects.
36 */
37public class SQLiteQueryBuilder
38{
39    private static final String TAG = "SQLiteQueryBuilder";
40    private static final Pattern sLimitPattern =
41            Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
42
43    private Map<String, String> mProjectionMap = null;
44    private String mTables = "";
45    private StringBuilder mWhereClause = null;  // lazily created
46    private boolean mDistinct;
47    private SQLiteDatabase.CursorFactory mFactory;
48    private boolean mStrict;
49
50    public SQLiteQueryBuilder() {
51        mDistinct = false;
52        mFactory = null;
53    }
54
55    /**
56     * Mark the query as DISTINCT.
57     *
58     * @param distinct if true the query is DISTINCT, otherwise it isn't
59     */
60    public void setDistinct(boolean distinct) {
61        mDistinct = distinct;
62    }
63
64    /**
65     * Returns the list of tables being queried
66     *
67     * @return the list of tables being queried
68     */
69    public String getTables() {
70        return mTables;
71    }
72
73    /**
74     * Sets the list of tables to query. Multiple tables can be specified to perform a join.
75     * For example:
76     *   setTables("foo, bar")
77     *   setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
78     *
79     * @param inTables the list of tables to query on
80     */
81    public void setTables(String inTables) {
82        mTables = inTables;
83    }
84
85    /**
86     * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
87     * by parenthesis and ANDed with the selection passed to {@link #query}. The final
88     * WHERE clause looks like:
89     *
90     * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
91     *
92     * @param inWhere the chunk of text to append to the WHERE clause.
93     */
94    public void appendWhere(CharSequence inWhere) {
95        if (mWhereClause == null) {
96            mWhereClause = new StringBuilder(inWhere.length() + 16);
97        }
98        if (mWhereClause.length() == 0) {
99            mWhereClause.append('(');
100        }
101        mWhereClause.append(inWhere);
102    }
103
104    /**
105     * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded
106     * by parenthesis and ANDed with the selection passed to {@link #query}. The final
107     * WHERE clause looks like:
108     *
109     * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
110     *
111     * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
112     * to avoid SQL injection attacks
113     */
114    public void appendWhereEscapeString(String inWhere) {
115        if (mWhereClause == null) {
116            mWhereClause = new StringBuilder(inWhere.length() + 16);
117        }
118        if (mWhereClause.length() == 0) {
119            mWhereClause.append('(');
120        }
121        DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
122    }
123
124    /**
125     * Sets the projection map for the query.  The projection map maps
126     * from column names that the caller passes into query to database
127     * column names. This is useful for renaming columns as well as
128     * disambiguating column names when doing joins. For example you
129     * could map "name" to "people.name".  If a projection map is set
130     * it must contain all column names the user may request, even if
131     * the key and value are the same.
132     *
133     * @param columnMap maps from the user column names to the database column names
134     */
135    public void setProjectionMap(Map<String, String> columnMap) {
136        mProjectionMap = columnMap;
137    }
138
139    /**
140     * Sets the cursor factory to be used for the query.  You can use
141     * one factory for all queries on a database but it is normally
142     * easier to specify the factory when doing this query.
143     *
144     * @param factory the factory to use.
145     */
146    public void setCursorFactory(SQLiteDatabase.CursorFactory factory) {
147        mFactory = factory;
148    }
149
150    /**
151     * When set, the selection is verified against malicious arguments.
152     * When using this class to create a statement using
153     * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
154     * non-numeric limits will raise an exception. If a projection map is specified, fields
155     * not in that map will be ignored.
156     * If this class is used to execute the statement directly using
157     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
158     * or
159     * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
160     * additionally also parenthesis escaping selection are caught.
161     *
162     * To summarize: To get maximum protection against malicious third party apps (for example
163     * content provider consumers), make sure to do the following:
164     * <ul>
165     * <li>Set this value to true</li>
166     * <li>Use a projection map</li>
167     * <li>Use one of the query overloads instead of getting the statement as a sql string</li>
168     * </ul>
169     * By default, this value is false.
170     */
171    public void setStrict(boolean flag) {
172        mStrict = flag;
173    }
174
175    /**
176     * Build an SQL query string from the given clauses.
177     *
178     * @param distinct true if you want each row to be unique, false otherwise.
179     * @param tables The table names to compile the query against.
180     * @param columns A list of which columns to return. Passing null will
181     *            return all columns, which is discouraged to prevent reading
182     *            data from storage that isn't going to be used.
183     * @param where A filter declaring which rows to return, formatted as an SQL
184     *            WHERE clause (excluding the WHERE itself). Passing null will
185     *            return all rows for the given URL.
186     * @param groupBy A filter declaring how to group rows, formatted as an SQL
187     *            GROUP BY clause (excluding the GROUP BY itself). Passing null
188     *            will cause the rows to not be grouped.
189     * @param having A filter declare which row groups to include in the cursor,
190     *            if row grouping is being used, formatted as an SQL HAVING
191     *            clause (excluding the HAVING itself). Passing null will cause
192     *            all row groups to be included, and is required when row
193     *            grouping is not being used.
194     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
195     *            (excluding the ORDER BY itself). Passing null will use the
196     *            default sort order, which may be unordered.
197     * @param limit Limits the number of rows returned by the query,
198     *            formatted as LIMIT clause. Passing null denotes no LIMIT clause.
199     * @return the SQL query string
200     */
201    public static String buildQueryString(
202            boolean distinct, String tables, String[] columns, String where,
203            String groupBy, String having, String orderBy, String limit) {
204        if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
205            throw new IllegalArgumentException(
206                    "HAVING clauses are only permitted when using a groupBy clause");
207        }
208        if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
209            throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
210        }
211
212        StringBuilder query = new StringBuilder(120);
213
214        query.append("SELECT ");
215        if (distinct) {
216            query.append("DISTINCT ");
217        }
218        if (columns != null && columns.length != 0) {
219            appendColumns(query, columns);
220        } else {
221            query.append("* ");
222        }
223        query.append("FROM ");
224        query.append(tables);
225        appendClause(query, " WHERE ", where);
226        appendClause(query, " GROUP BY ", groupBy);
227        appendClause(query, " HAVING ", having);
228        appendClause(query, " ORDER BY ", orderBy);
229        appendClause(query, " LIMIT ", limit);
230
231        return query.toString();
232    }
233
234    private static void appendClause(StringBuilder s, String name, String clause) {
235        if (!TextUtils.isEmpty(clause)) {
236            s.append(name);
237            s.append(clause);
238        }
239    }
240
241    /**
242     * Add the names that are non-null in columns to s, separating
243     * them with commas.
244     */
245    public static void appendColumns(StringBuilder s, String[] columns) {
246        int n = columns.length;
247
248        for (int i = 0; i < n; i++) {
249            String column = columns[i];
250
251            if (column != null) {
252                if (i > 0) {
253                    s.append(", ");
254                }
255                s.append(column);
256            }
257        }
258        s.append(' ');
259    }
260
261    /**
262     * Perform a query by combining all current settings and the
263     * information passed into this method.
264     *
265     * @param db the database to query on
266     * @param projectionIn A list of which columns to return. Passing
267     *   null will return all columns, which is discouraged to prevent
268     *   reading data from storage that isn't going to be used.
269     * @param selection A filter declaring which rows to return,
270     *   formatted as an SQL WHERE clause (excluding the WHERE
271     *   itself). Passing null will return all rows for the given URL.
272     * @param selectionArgs You may include ?s in selection, which
273     *   will be replaced by the values from selectionArgs, in order
274     *   that they appear in the selection. The values will be bound
275     *   as Strings.
276     * @param groupBy A filter declaring how to group rows, formatted
277     *   as an SQL GROUP BY clause (excluding the GROUP BY
278     *   itself). Passing null will cause the rows to not be grouped.
279     * @param having A filter declare which row groups to include in
280     *   the cursor, if row grouping is being used, formatted as an
281     *   SQL HAVING clause (excluding the HAVING itself).  Passing
282     *   null will cause all row groups to be included, and is
283     *   required when row grouping is not being used.
284     * @param sortOrder How to order the rows, formatted as an SQL
285     *   ORDER BY clause (excluding the ORDER BY itself). Passing null
286     *   will use the default sort order, which may be unordered.
287     * @return a cursor over the result set
288     * @see android.content.ContentResolver#query(android.net.Uri, String[],
289     *      String, String[], String)
290     */
291    public Cursor query(SQLiteDatabase db, String[] projectionIn,
292            String selection, String[] selectionArgs, String groupBy,
293            String having, String sortOrder) {
294        return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
295                null /* limit */, null /* cancellationSignal */);
296    }
297
298    /**
299     * Perform a query by combining all current settings and the
300     * information passed into this method.
301     *
302     * @param db the database to query on
303     * @param projectionIn A list of which columns to return. Passing
304     *   null will return all columns, which is discouraged to prevent
305     *   reading data from storage that isn't going to be used.
306     * @param selection A filter declaring which rows to return,
307     *   formatted as an SQL WHERE clause (excluding the WHERE
308     *   itself). Passing null will return all rows for the given URL.
309     * @param selectionArgs You may include ?s in selection, which
310     *   will be replaced by the values from selectionArgs, in order
311     *   that they appear in the selection. The values will be bound
312     *   as Strings.
313     * @param groupBy A filter declaring how to group rows, formatted
314     *   as an SQL GROUP BY clause (excluding the GROUP BY
315     *   itself). Passing null will cause the rows to not be grouped.
316     * @param having A filter declare which row groups to include in
317     *   the cursor, if row grouping is being used, formatted as an
318     *   SQL HAVING clause (excluding the HAVING itself).  Passing
319     *   null will cause all row groups to be included, and is
320     *   required when row grouping is not being used.
321     * @param sortOrder How to order the rows, formatted as an SQL
322     *   ORDER BY clause (excluding the ORDER BY itself). Passing null
323     *   will use the default sort order, which may be unordered.
324     * @param limit Limits the number of rows returned by the query,
325     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
326     * @return a cursor over the result set
327     * @see android.content.ContentResolver#query(android.net.Uri, String[],
328     *      String, String[], String)
329     */
330    public Cursor query(SQLiteDatabase db, String[] projectionIn,
331            String selection, String[] selectionArgs, String groupBy,
332            String having, String sortOrder, String limit) {
333        return query(db, projectionIn, selection, selectionArgs,
334                groupBy, having, sortOrder, limit, null);
335    }
336
337    /**
338     * Perform a query by combining all current settings and the
339     * information passed into this method.
340     *
341     * @param db the database to query on
342     * @param projectionIn A list of which columns to return. Passing
343     *   null will return all columns, which is discouraged to prevent
344     *   reading data from storage that isn't going to be used.
345     * @param selection A filter declaring which rows to return,
346     *   formatted as an SQL WHERE clause (excluding the WHERE
347     *   itself). Passing null will return all rows for the given URL.
348     * @param selectionArgs You may include ?s in selection, which
349     *   will be replaced by the values from selectionArgs, in order
350     *   that they appear in the selection. The values will be bound
351     *   as Strings.
352     * @param groupBy A filter declaring how to group rows, formatted
353     *   as an SQL GROUP BY clause (excluding the GROUP BY
354     *   itself). Passing null will cause the rows to not be grouped.
355     * @param having A filter declare which row groups to include in
356     *   the cursor, if row grouping is being used, formatted as an
357     *   SQL HAVING clause (excluding the HAVING itself).  Passing
358     *   null will cause all row groups to be included, and is
359     *   required when row grouping is not being used.
360     * @param sortOrder How to order the rows, formatted as an SQL
361     *   ORDER BY clause (excluding the ORDER BY itself). Passing null
362     *   will use the default sort order, which may be unordered.
363     * @param limit Limits the number of rows returned by the query,
364     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
365     * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
366     * If the operation is canceled, then {@link OperationCanceledException} will be thrown
367     * when the query is executed.
368     * @return a cursor over the result set
369     * @see android.content.ContentResolver#query(android.net.Uri, String[],
370     *      String, String[], String)
371     */
372    public Cursor query(SQLiteDatabase db, String[] projectionIn,
373            String selection, String[] selectionArgs, String groupBy,
374            String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
375        if (mTables == null) {
376            return null;
377        }
378
379        if (mStrict && selection != null && selection.length() > 0) {
380            // Validate the user-supplied selection to detect syntactic anomalies
381            // in the selection string that could indicate a SQL injection attempt.
382            // The idea is to ensure that the selection clause is a valid SQL expression
383            // by compiling it twice: once wrapped in parentheses and once as
384            // originally specified. An attacker cannot create an expression that
385            // would escape the SQL expression while maintaining balanced parentheses
386            // in both the wrapped and original forms.
387            String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy,
388                    having, sortOrder, limit);
389            validateQuerySql(db, sqlForValidation,
390                    cancellationSignal); // will throw if query is invalid
391        }
392
393        String sql = buildQuery(
394                projectionIn, selection, groupBy, having,
395                sortOrder, limit);
396
397        if (Log.isLoggable(TAG, Log.DEBUG)) {
398            Log.d(TAG, "Performing query: " + sql);
399        }
400        return db.rawQueryWithFactory(
401                mFactory, sql, selectionArgs,
402                SQLiteDatabase.findEditTable(mTables),
403                cancellationSignal); // will throw if query is invalid
404    }
405
406    /**
407     * Verifies that a SQL SELECT statement is valid by compiling it.
408     * If the SQL statement is not valid, this method will throw a {@link SQLiteException}.
409     */
410    private void validateQuerySql(SQLiteDatabase db, String sql,
411            CancellationSignal cancellationSignal) {
412        db.getThreadSession().prepare(sql,
413                db.getThreadDefaultConnectionFlags(true /*readOnly*/), cancellationSignal, null);
414    }
415
416    /**
417     * Construct a SELECT statement suitable for use in a group of
418     * SELECT statements that will be joined through UNION operators
419     * in buildUnionQuery.
420     *
421     * @param projectionIn A list of which columns to return. Passing
422     *    null will return all columns, which is discouraged to
423     *    prevent reading data from storage that isn't going to be
424     *    used.
425     * @param selection A filter declaring which rows to return,
426     *   formatted as an SQL WHERE clause (excluding the WHERE
427     *   itself).  Passing null will return all rows for the given
428     *   URL.
429     * @param groupBy A filter declaring how to group rows, formatted
430     *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
431     *   Passing null will cause the rows to not be grouped.
432     * @param having A filter declare which row groups to include in
433     *   the cursor, if row grouping is being used, formatted as an
434     *   SQL HAVING clause (excluding the HAVING itself).  Passing
435     *   null will cause all row groups to be included, and is
436     *   required when row grouping is not being used.
437     * @param sortOrder How to order the rows, formatted as an SQL
438     *   ORDER BY clause (excluding the ORDER BY itself). Passing null
439     *   will use the default sort order, which may be unordered.
440     * @param limit Limits the number of rows returned by the query,
441     *   formatted as LIMIT clause. Passing null denotes no LIMIT clause.
442     * @return the resulting SQL SELECT statement
443     */
444    public String buildQuery(
445            String[] projectionIn, String selection, String groupBy,
446            String having, String sortOrder, String limit) {
447        String[] projection = computeProjection(projectionIn);
448
449        StringBuilder where = new StringBuilder();
450        boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0;
451
452        if (hasBaseWhereClause) {
453            where.append(mWhereClause.toString());
454            where.append(')');
455        }
456
457        // Tack on the user's selection, if present.
458        if (selection != null && selection.length() > 0) {
459            if (hasBaseWhereClause) {
460                where.append(" AND ");
461            }
462
463            where.append('(');
464            where.append(selection);
465            where.append(')');
466        }
467
468        return buildQueryString(
469                mDistinct, mTables, projection, where.toString(),
470                groupBy, having, sortOrder, limit);
471    }
472
473    /**
474     * @deprecated This method's signature is misleading since no SQL parameter
475     * substitution is carried out.  The selection arguments parameter does not get
476     * used at all.  To avoid confusion, call
477     * {@link #buildQuery(String[], String, String, String, String, String)} instead.
478     */
479    @Deprecated
480    public String buildQuery(
481            String[] projectionIn, String selection, String[] selectionArgs,
482            String groupBy, String having, String sortOrder, String limit) {
483        return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit);
484    }
485
486    /**
487     * Construct a SELECT statement suitable for use in a group of
488     * SELECT statements that will be joined through UNION operators
489     * in buildUnionQuery.
490     *
491     * @param typeDiscriminatorColumn the name of the result column
492     *   whose cells will contain the name of the table from which
493     *   each row was drawn.
494     * @param unionColumns the names of the columns to appear in the
495     *   result.  This may include columns that do not appear in the
496     *   table this SELECT is querying (i.e. mTables), but that do
497     *   appear in one of the other tables in the UNION query that we
498     *   are constructing.
499     * @param columnsPresentInTable a Set of the names of the columns
500     *   that appear in this table (i.e. in the table whose name is
501     *   mTables).  Since columns in unionColumns include columns that
502     *   appear only in other tables, we use this array to distinguish
503     *   which ones actually are present.  Other columns will have
504     *   NULL values for results from this subquery.
505     * @param computedColumnsOffset all columns in unionColumns before
506     *   this index are included under the assumption that they're
507     *   computed and therefore won't appear in columnsPresentInTable,
508     *   e.g. "date * 1000 as normalized_date"
509     * @param typeDiscriminatorValue the value used for the
510     *   type-discriminator column in this subquery
511     * @param selection A filter declaring which rows to return,
512     *   formatted as an SQL WHERE clause (excluding the WHERE
513     *   itself).  Passing null will return all rows for the given
514     *   URL.
515     * @param groupBy A filter declaring how to group rows, formatted
516     *   as an SQL GROUP BY clause (excluding the GROUP BY itself).
517     *   Passing null will cause the rows to not be grouped.
518     * @param having A filter declare which row groups to include in
519     *   the cursor, if row grouping is being used, formatted as an
520     *   SQL HAVING clause (excluding the HAVING itself).  Passing
521     *   null will cause all row groups to be included, and is
522     *   required when row grouping is not being used.
523     * @return the resulting SQL SELECT statement
524     */
525    public String buildUnionSubQuery(
526            String typeDiscriminatorColumn,
527            String[] unionColumns,
528            Set<String> columnsPresentInTable,
529            int computedColumnsOffset,
530            String typeDiscriminatorValue,
531            String selection,
532            String groupBy,
533            String having) {
534        int unionColumnsCount = unionColumns.length;
535        String[] projectionIn = new String[unionColumnsCount];
536
537        for (int i = 0; i < unionColumnsCount; i++) {
538            String unionColumn = unionColumns[i];
539
540            if (unionColumn.equals(typeDiscriminatorColumn)) {
541                projectionIn[i] = "'" + typeDiscriminatorValue + "' AS "
542                        + typeDiscriminatorColumn;
543            } else if (i <= computedColumnsOffset
544                       || columnsPresentInTable.contains(unionColumn)) {
545                projectionIn[i] = unionColumn;
546            } else {
547                projectionIn[i] = "NULL AS " + unionColumn;
548            }
549        }
550        return buildQuery(
551                projectionIn, selection, groupBy, having,
552                null /* sortOrder */,
553                null /* limit */);
554    }
555
556    /**
557     * @deprecated This method's signature is misleading since no SQL parameter
558     * substitution is carried out.  The selection arguments parameter does not get
559     * used at all.  To avoid confusion, call
560     * {@link #buildUnionSubQuery}
561     * instead.
562     */
563    @Deprecated
564    public String buildUnionSubQuery(
565            String typeDiscriminatorColumn,
566            String[] unionColumns,
567            Set<String> columnsPresentInTable,
568            int computedColumnsOffset,
569            String typeDiscriminatorValue,
570            String selection,
571            String[] selectionArgs,
572            String groupBy,
573            String having) {
574        return buildUnionSubQuery(
575                typeDiscriminatorColumn, unionColumns, columnsPresentInTable,
576                computedColumnsOffset, typeDiscriminatorValue, selection,
577                groupBy, having);
578    }
579
580    /**
581     * Given a set of subqueries, all of which are SELECT statements,
582     * construct a query that returns the union of what those
583     * subqueries return.
584     * @param subQueries an array of SQL SELECT statements, all of
585     *   which must have the same columns as the same positions in
586     *   their results
587     * @param sortOrder How to order the rows, formatted as an SQL
588     *   ORDER BY clause (excluding the ORDER BY itself).  Passing
589     *   null will use the default sort order, which may be unordered.
590     * @param limit The limit clause, which applies to the entire union result set
591     *
592     * @return the resulting SQL SELECT statement
593     */
594    public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) {
595        StringBuilder query = new StringBuilder(128);
596        int subQueryCount = subQueries.length;
597        String unionOperator = mDistinct ? " UNION " : " UNION ALL ";
598
599        for (int i = 0; i < subQueryCount; i++) {
600            if (i > 0) {
601                query.append(unionOperator);
602            }
603            query.append(subQueries[i]);
604        }
605        appendClause(query, " ORDER BY ", sortOrder);
606        appendClause(query, " LIMIT ", limit);
607        return query.toString();
608    }
609
610    private String[] computeProjection(String[] projectionIn) {
611        if (projectionIn != null && projectionIn.length > 0) {
612            if (mProjectionMap != null) {
613                String[] projection = new String[projectionIn.length];
614                int length = projectionIn.length;
615
616                for (int i = 0; i < length; i++) {
617                    String userColumn = projectionIn[i];
618                    String column = mProjectionMap.get(userColumn);
619
620                    if (column != null) {
621                        projection[i] = column;
622                        continue;
623                    }
624
625                    if (!mStrict &&
626                            ( userColumn.contains(" AS ") || userColumn.contains(" as "))) {
627                        /* A column alias already exist */
628                        projection[i] = userColumn;
629                        continue;
630                    }
631
632                    throw new IllegalArgumentException("Invalid column "
633                            + projectionIn[i]);
634                }
635                return projection;
636            } else {
637                return projectionIn;
638            }
639        } else if (mProjectionMap != null) {
640            // Return all columns in projection map.
641            Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
642            String[] projection = new String[entrySet.size()];
643            Iterator<Entry<String, String>> entryIter = entrySet.iterator();
644            int i = 0;
645
646            while (entryIter.hasNext()) {
647                Entry<String, String> entry = entryIter.next();
648
649                // Don't include the _count column when people ask for no projection.
650                if (entry.getKey().equals(BaseColumns._COUNT)) {
651                    continue;
652                }
653                projection[i++] = entry.getValue();
654            }
655            return projection;
656        }
657        return null;
658    }
659}
660