blob: 3faa8f4394f79d7ce43d2ddad37d2d422fff2db8 [file] [log] [blame]
Daichi Hirono259ce802015-11-20 17:51:53 +09001/*
2 * Copyright (C) 2015 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.mtp;
18
19import static com.android.mtp.MtpDatabaseConstants.*;
20
Daichi Hirono7a375c42015-12-14 17:14:29 +090021import android.annotation.Nullable;
Daichi Hirono259ce802015-11-20 17:51:53 +090022import android.content.ContentValues;
Daichi Hirono259ce802015-11-20 17:51:53 +090023import android.database.Cursor;
24import android.database.DatabaseUtils;
25import android.database.sqlite.SQLiteDatabase;
Daichi Hirono259ce802015-11-20 17:51:53 +090026import android.mtp.MtpObjectInfo;
27import android.provider.DocumentsContract.Document;
28import android.provider.DocumentsContract.Root;
29
30import com.android.internal.annotations.VisibleForTesting;
31import com.android.internal.util.Preconditions;
32
33import java.util.HashMap;
34import java.util.Map;
35
36import static com.android.mtp.MtpDatabase.strings;
37
Daichi Hirono259ce802015-11-20 17:51:53 +090038/**
39 * Mapping operations for MtpDatabase.
40 * Also see the comments of {@link MtpDatabase}.
41 */
42class Mapper {
Daichi Hironob3fe72b2015-12-15 07:45:06 +000043 private static final String[] EMPTY_ARGS = new String[0];
Daichi Hirono259ce802015-11-20 17:51:53 +090044 private final MtpDatabase mDatabase;
45
46 /**
Daichi Hirono7a375c42015-12-14 17:14:29 +090047 * Mapping mode for a parent. The key is document ID of parent, or null for root documents.
Daichi Hirono259ce802015-11-20 17:51:53 +090048 * Methods operate the state needs to be synchronized.
Daichi Hirono7a375c42015-12-14 17:14:29 +090049 * TODO: Replace this with unboxing int map.
Daichi Hirono259ce802015-11-20 17:51:53 +090050 */
51 private final Map<String, Integer> mMappingMode = new HashMap<>();
52
53 Mapper(MtpDatabase database) {
54 mDatabase = database;
55 }
56
Daichi Hirono20754c52015-12-15 18:52:26 +090057 /**
58 * Puts device information to database.
59 * @return If device is added to the database.
60 */
61 synchronized boolean putDeviceDocument(MtpDeviceRecord device) {
Daichi Hironob3fe72b2015-12-15 07:45:06 +000062 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
63 Preconditions.checkState(mMappingMode.containsKey(/* no parent for root */ null));
64 database.beginTransaction();
65 try {
66 final ContentValues[] valuesList = new ContentValues[1];
Daichi Hirono81d48532015-12-16 15:03:19 +090067 final ContentValues[] extraValuesList = new ContentValues[1];
Daichi Hironob3fe72b2015-12-15 07:45:06 +000068 valuesList[0] = new ContentValues();
Daichi Hirono81d48532015-12-16 15:03:19 +090069 extraValuesList[0] = new ContentValues();
70 MtpDatabase.getDeviceDocumentValues(valuesList[0], extraValuesList[0], device);
Daichi Hirono20754c52015-12-15 18:52:26 +090071 final boolean changed = putDocuments(
Daichi Hironob3fe72b2015-12-15 07:45:06 +000072 valuesList,
Daichi Hirono81d48532015-12-16 15:03:19 +090073 extraValuesList,
Daichi Hironob3fe72b2015-12-15 07:45:06 +000074 COLUMN_PARENT_DOCUMENT_ID + " IS NULL",
75 EMPTY_ARGS,
76 /* heuristic */ false,
77 COLUMN_DEVICE_ID);
78 database.setTransactionSuccessful();
Daichi Hirono20754c52015-12-15 18:52:26 +090079 return changed;
Daichi Hironob3fe72b2015-12-15 07:45:06 +000080 } finally {
81 database.endTransaction();
82 }
83 }
84
Daichi Hirono259ce802015-11-20 17:51:53 +090085 /**
Daichi Hirono259ce802015-11-20 17:51:53 +090086 * Puts root information to database.
Daichi Hironob3fe72b2015-12-15 07:45:06 +000087 * @param parentDocumentId Document ID of device document.
Daichi Hirono259ce802015-11-20 17:51:53 +090088 * @param roots List of root information.
89 * @return If roots are added or removed from the database.
90 */
Daichi Hironof83ccbd2016-02-04 16:58:55 +090091 synchronized boolean putStorageDocuments(String parentDocumentId, MtpRoot[] roots) {
Daichi Hirono259ce802015-11-20 17:51:53 +090092 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
93 database.beginTransaction();
94 try {
95 final boolean heuristic;
96 final String mapColumn;
Daichi Hironob3fe72b2015-12-15 07:45:06 +000097 Preconditions.checkState(mMappingMode.containsKey(parentDocumentId));
98 switch (mMappingMode.get(parentDocumentId)) {
Daichi Hirono259ce802015-11-20 17:51:53 +090099 case MAP_BY_MTP_IDENTIFIER:
100 heuristic = false;
101 mapColumn = COLUMN_STORAGE_ID;
102 break;
103 case MAP_BY_NAME:
104 heuristic = true;
105 mapColumn = Document.COLUMN_DISPLAY_NAME;
106 break;
107 default:
108 throw new Error("Unexpected map mode.");
109 }
110 final ContentValues[] valuesList = new ContentValues[roots.length];
Daichi Hirono81d48532015-12-16 15:03:19 +0900111 final ContentValues[] extraValuesList = new ContentValues[roots.length];
Daichi Hirono259ce802015-11-20 17:51:53 +0900112 for (int i = 0; i < roots.length; i++) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900113 valuesList[i] = new ContentValues();
Daichi Hirono81d48532015-12-16 15:03:19 +0900114 extraValuesList[i] = new ContentValues();
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000115 MtpDatabase.getStorageDocumentValues(
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900116 valuesList[i], extraValuesList[i], parentDocumentId, roots[i]);
Daichi Hirono259ce802015-11-20 17:51:53 +0900117 }
118 final boolean changed = putDocuments(
119 valuesList,
Daichi Hirono81d48532015-12-16 15:03:19 +0900120 extraValuesList,
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000121 COLUMN_PARENT_DOCUMENT_ID + "=?",
122 strings(parentDocumentId),
Daichi Hirono259ce802015-11-20 17:51:53 +0900123 heuristic,
124 mapColumn);
Daichi Hirono81d48532015-12-16 15:03:19 +0900125
Daichi Hirono259ce802015-11-20 17:51:53 +0900126 database.setTransactionSuccessful();
127 return changed;
128 } finally {
129 database.endTransaction();
130 }
131 }
132
133 /**
134 * Puts document information to database.
135 * @param deviceId Device ID
136 * @param parentId Parent document ID.
137 * @param documents List of document information.
138 */
139 synchronized void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
140 final boolean heuristic;
141 final String mapColumn;
Daichi Hirono7a375c42015-12-14 17:14:29 +0900142 Preconditions.checkState(mMappingMode.containsKey(parentId));
143 switch (mMappingMode.get(parentId)) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900144 case MAP_BY_MTP_IDENTIFIER:
145 heuristic = false;
146 mapColumn = COLUMN_OBJECT_HANDLE;
147 break;
148 case MAP_BY_NAME:
149 heuristic = true;
150 mapColumn = Document.COLUMN_DISPLAY_NAME;
151 break;
152 default:
153 throw new Error("Unexpected map mode.");
154 }
155 final ContentValues[] valuesList = new ContentValues[documents.length];
156 for (int i = 0; i < documents.length; i++) {
157 valuesList[i] = new ContentValues();
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000158 MtpDatabase.getObjectDocumentValues(
Daichi Hirono259ce802015-11-20 17:51:53 +0900159 valuesList[i], deviceId, parentId, documents[i]);
160 }
161 putDocuments(
Daichi Hirono7a375c42015-12-14 17:14:29 +0900162 valuesList,
Daichi Hirono81d48532015-12-16 15:03:19 +0900163 null,
Daichi Hirono7a375c42015-12-14 17:14:29 +0900164 COLUMN_PARENT_DOCUMENT_ID + "=?",
165 strings(parentId),
166 heuristic,
167 mapColumn);
Daichi Hirono259ce802015-11-20 17:51:53 +0900168 }
169
170 @VisibleForTesting
171 void clearMapping() {
172 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
173 database.beginTransaction();
174 try {
175 mDatabase.deleteDocumentsAndRootsRecursively(
176 COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
177 final ContentValues values = new ContentValues();
178 values.putNull(COLUMN_OBJECT_HANDLE);
179 values.putNull(COLUMN_STORAGE_ID);
180 values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
181 database.update(TABLE_DOCUMENTS, values, null, null);
182 database.setTransactionSuccessful();
183 mMappingMode.clear();
184 } finally {
185 database.endTransaction();
186 }
187 }
188
189 /**
190 * Starts adding new documents.
191 * The methods decides mapping mode depends on if all documents under the given parent have MTP
192 * identifier or not. If all the documents have MTP identifier, it uses the identifier to find
193 * a corresponding existing row. Otherwise it does heuristic.
194 *
Daichi Hirono7a375c42015-12-14 17:14:29 +0900195 * @param parentDocumentId Parent document ID or NULL for root documents.
Daichi Hirono259ce802015-11-20 17:51:53 +0900196 */
Daichi Hirono7a375c42015-12-14 17:14:29 +0900197 void startAddingDocuments(@Nullable String parentDocumentId) {
198 Preconditions.checkState(!mMappingMode.containsKey(parentDocumentId));
199 final String selection;
200 final String[] args;
201 if (parentDocumentId != null) {
202 selection = COLUMN_PARENT_DOCUMENT_ID + " = ?";
203 args = strings(parentDocumentId);
204 } else {
205 selection = COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000206 args = EMPTY_ARGS;
Daichi Hirono7a375c42015-12-14 17:14:29 +0900207 }
208
Daichi Hirono259ce802015-11-20 17:51:53 +0900209 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
210 database.beginTransaction();
211 try {
212 // Delete all pending rows.
213 mDatabase.deleteDocumentsAndRootsRecursively(
Daichi Hirono7a375c42015-12-14 17:14:29 +0900214 selection + " AND " + COLUMN_ROW_STATE + "=?",
215 DatabaseUtils.appendSelectionArgs(args, strings(ROW_STATE_PENDING)));
Daichi Hirono259ce802015-11-20 17:51:53 +0900216
217 // Set all documents as invalidated.
218 final ContentValues values = new ContentValues();
219 values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
Daichi Hirono7a375c42015-12-14 17:14:29 +0900220 database.update(TABLE_DOCUMENTS, values, selection, args);
Daichi Hirono259ce802015-11-20 17:51:53 +0900221
222 // If we have rows that does not have MTP identifier, do heuristic mapping by name.
223 final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
224 database,
225 TABLE_DOCUMENTS,
226 selection + " AND " + COLUMN_STORAGE_ID + " IS NULL",
Daichi Hirono7a375c42015-12-14 17:14:29 +0900227 args) > 0;
Daichi Hirono259ce802015-11-20 17:51:53 +0900228 database.setTransactionSuccessful();
Daichi Hirono7a375c42015-12-14 17:14:29 +0900229 mMappingMode.put(
230 parentDocumentId, useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER);
Daichi Hirono259ce802015-11-20 17:51:53 +0900231 } finally {
232 database.endTransaction();
233 }
234 }
235
236 /**
237 * Puts the documents into the database.
238 * If the mapping mode is not heuristic, it just adds the rows to the database or updates the
239 * existing rows with the new values. If the mapping mode is heuristic, it adds some new rows as
240 * 'pending' state when that rows may be corresponding to existing 'invalidated' rows. Then
Daichi Hirono20754c52015-12-15 18:52:26 +0900241 * {@link #stopAddingDocuments(String)} turns the pending rows into 'valid'
Daichi Hirono259ce802015-11-20 17:51:53 +0900242 * rows. If the methods adds rows to database, it updates valueList with correct document ID.
243 *
244 * @param valuesList Values for documents to be stored in the database.
Daichi Hirono81d48532015-12-16 15:03:19 +0900245 * @param rootExtraValuesList Values for root extra to be stored in the database.
Daichi Hirono259ce802015-11-20 17:51:53 +0900246 * @param selection SQL where closure to select rows that shares the same parent.
Daichi Hirono7a375c42015-12-14 17:14:29 +0900247 * @param args Argument for selection SQL.
Daichi Hirono259ce802015-11-20 17:51:53 +0900248 * @param heuristic Whether the mapping mode is heuristic.
249 * @return Whether the method adds new rows.
250 */
251 private boolean putDocuments(
252 ContentValues[] valuesList,
Daichi Hirono81d48532015-12-16 15:03:19 +0900253 @Nullable ContentValues[] rootExtraValuesList,
Daichi Hirono259ce802015-11-20 17:51:53 +0900254 String selection,
Daichi Hirono7a375c42015-12-14 17:14:29 +0900255 String[] args,
Daichi Hirono259ce802015-11-20 17:51:53 +0900256 boolean heuristic,
257 String mappingKey) {
258 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
259 boolean added = false;
260 database.beginTransaction();
261 try {
Daichi Hirono81d48532015-12-16 15:03:19 +0900262 for (int i = 0; i < valuesList.length; i++) {
263 final ContentValues values = valuesList[i];
264 final ContentValues rootExtraValues;
265 if (rootExtraValuesList != null) {
266 rootExtraValues = rootExtraValuesList[i];
267 } else {
268 rootExtraValues = null;
269 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900270 final Cursor candidateCursor = database.query(
271 TABLE_DOCUMENTS,
272 strings(Document.COLUMN_DOCUMENT_ID),
273 selection + " AND " +
274 COLUMN_ROW_STATE + "=? AND " +
275 mappingKey + "=?",
Daichi Hirono7a375c42015-12-14 17:14:29 +0900276 DatabaseUtils.appendSelectionArgs(
277 args,
278 strings(ROW_STATE_INVALIDATED, values.getAsString(mappingKey))),
Daichi Hirono259ce802015-11-20 17:51:53 +0900279 null,
280 null,
281 null,
282 "1");
283 try {
284 final long rowId;
285 if (candidateCursor.getCount() == 0) {
286 rowId = database.insert(TABLE_DOCUMENTS, null, values);
Daichi Hirono259ce802015-11-20 17:51:53 +0900287 added = true;
288 } else if (!heuristic) {
289 candidateCursor.moveToNext();
Daichi Hirono81d48532015-12-16 15:03:19 +0900290 rowId = candidateCursor.getLong(0);
291 database.update(
Daichi Hirono259ce802015-11-20 17:51:53 +0900292 TABLE_DOCUMENTS,
293 values,
294 SELECTION_DOCUMENT_ID,
Daichi Hirono81d48532015-12-16 15:03:19 +0900295 strings(rowId));
Daichi Hirono259ce802015-11-20 17:51:53 +0900296 } else {
297 values.put(COLUMN_ROW_STATE, ROW_STATE_PENDING);
Daichi Hirono81d48532015-12-16 15:03:19 +0900298 rowId = database.insertOrThrow(TABLE_DOCUMENTS, null, values);
Daichi Hirono259ce802015-11-20 17:51:53 +0900299 }
300 // Document ID is a primary integer key of the table. So the returned row
301 // IDs should be same with the document ID.
302 values.put(Document.COLUMN_DOCUMENT_ID, rowId);
Daichi Hirono81d48532015-12-16 15:03:19 +0900303 if (rootExtraValues != null) {
304 rootExtraValues.put(Root.COLUMN_ROOT_ID, rowId);
305 database.replace(TABLE_ROOT_EXTRA, null, rootExtraValues);
306 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900307 } finally {
308 candidateCursor.close();
309 }
310 }
311
312 database.setTransactionSuccessful();
313 return added;
314 } finally {
315 database.endTransaction();
316 }
317 }
318
319 /**
320 * Maps 'pending' document and 'invalidated' document that shares the same column of groupKey.
321 * If the database does not find corresponding 'invalidated' document, it just removes
322 * 'invalidated' document from the database.
Daichi Hirono7a375c42015-12-14 17:14:29 +0900323 * @param parentId Parent document ID or null for root documents.
Daichi Hirono259ce802015-11-20 17:51:53 +0900324 * @return Whether the methods adds or removed visible rows.
325 */
Daichi Hirono7a375c42015-12-14 17:14:29 +0900326 boolean stopAddingDocuments(@Nullable String parentId) {
327 Preconditions.checkState(mMappingMode.containsKey(parentId));
328 final String selection;
329 final String[] args;
330 if (parentId != null) {
331 selection = COLUMN_PARENT_DOCUMENT_ID + "=?";
332 args = strings(parentId);
333 } else {
334 selection = COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
Daichi Hironob3fe72b2015-12-15 07:45:06 +0000335 args = EMPTY_ARGS;
Daichi Hirono7a375c42015-12-14 17:14:29 +0900336 }
337 final String groupKey;
338 switch (mMappingMode.get(parentId)) {
339 case MAP_BY_MTP_IDENTIFIER:
340 groupKey = parentId != null ? COLUMN_OBJECT_HANDLE : COLUMN_STORAGE_ID;
341 break;
342 case MAP_BY_NAME:
343 groupKey = Document.COLUMN_DISPLAY_NAME;
344 break;
345 default:
346 throw new Error("Unexpected mapping state.");
347 }
348 mMappingMode.remove(parentId);
Daichi Hirono259ce802015-11-20 17:51:53 +0900349 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
350 database.beginTransaction();
351 try {
352 // Get 1-to-1 mapping of invalidated document and pending document.
353 final String invalidatedIdQuery = createStateFilter(
354 ROW_STATE_INVALIDATED, Document.COLUMN_DOCUMENT_ID);
355 final String pendingIdQuery = createStateFilter(
356 ROW_STATE_PENDING, Document.COLUMN_DOCUMENT_ID);
357 // SQL should be like:
358 // SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
359 // group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
360 // WHERE device_id = ? AND parent_document_id IS NULL
361 // GROUP BY display_name
362 // HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
363 // count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
364 final Cursor mergingCursor = database.query(
365 TABLE_DOCUMENTS,
366 new String[] {
367 "group_concat(" + invalidatedIdQuery + ")",
368 "group_concat(" + pendingIdQuery + ")"
369 },
370 selection,
Daichi Hirono7a375c42015-12-14 17:14:29 +0900371 args,
Daichi Hirono259ce802015-11-20 17:51:53 +0900372 groupKey,
373 "count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
374 null);
375
376 final ContentValues values = new ContentValues();
377 while (mergingCursor.moveToNext()) {
378 final String invalidatedId = mergingCursor.getString(0);
379 final String pendingId = mergingCursor.getString(1);
380
381 // Obtain the new values including the latest object handle from mapping row.
382 getFirstRow(
383 TABLE_DOCUMENTS,
384 SELECTION_DOCUMENT_ID,
385 new String[] { pendingId },
386 values);
387 values.remove(Document.COLUMN_DOCUMENT_ID);
388 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
389 database.update(
390 TABLE_DOCUMENTS,
391 values,
392 SELECTION_DOCUMENT_ID,
393 new String[] { invalidatedId });
394
395 getFirstRow(
396 TABLE_ROOT_EXTRA,
397 SELECTION_ROOT_ID,
398 new String[] { pendingId },
399 values);
400 if (values.size() > 0) {
401 values.remove(Root.COLUMN_ROOT_ID);
402 database.update(
403 TABLE_ROOT_EXTRA,
404 values,
405 SELECTION_ROOT_ID,
406 new String[] { invalidatedId });
407 }
408
409 // Delete 'pending' row.
410 mDatabase.deleteDocumentsAndRootsRecursively(
411 SELECTION_DOCUMENT_ID, new String[] { pendingId });
412 }
413 mergingCursor.close();
414
415 boolean changed = false;
416
417 // Delete all invalidated rows that cannot be mapped.
418 if (mDatabase.deleteDocumentsAndRootsRecursively(
419 COLUMN_ROW_STATE + " = ? AND " + selection,
Daichi Hirono7a375c42015-12-14 17:14:29 +0900420 DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900421 changed = true;
422 }
423
424 // The database cannot find old document ID for the pending rows.
425 // Turn the all pending rows into valid state, which means the rows become to be
426 // valid with new document ID.
427 values.clear();
428 values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
429 if (database.update(
430 TABLE_DOCUMENTS,
431 values,
432 COLUMN_ROW_STATE + " = ? AND " + selection,
Daichi Hirono7a375c42015-12-14 17:14:29 +0900433 DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_PENDING), args)) != 0) {
Daichi Hirono259ce802015-11-20 17:51:53 +0900434 changed = true;
435 }
436 database.setTransactionSuccessful();
437 return changed;
438 } finally {
439 database.endTransaction();
440 }
441 }
442
443 /**
444 * Obtains values of the first row for the query.
445 * @param values ContentValues that the values are stored to.
446 * @param table Target table.
447 * @param selection Query to select rows.
448 * @param args Argument for query.
449 */
450 private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
451 final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
452 values.clear();
453 final Cursor cursor = database.query(table, null, selection, args, null, null, null, "1");
Daichi Hirono20754c52015-12-15 18:52:26 +0900454 try {
455 if (cursor.getCount() == 0) {
456 return;
457 }
458 cursor.moveToNext();
459 DatabaseUtils.cursorRowToContentValues(cursor, values);
460 } finally {
461 cursor.close();
Daichi Hirono259ce802015-11-20 17:51:53 +0900462 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900463 }
464
465 /**
466 * Gets SQL expression that represents the given value or NULL depends on the row state.
467 * You must pass static constants to this methods otherwise you may be suffered from SQL
468 * injections.
469 * @param state Expected row state.
470 * @param a SQL value.
471 * @return Expression that represents a if the row state is expected one, and represents NULL
472 * otherwise.
473 */
474 private static String createStateFilter(int state, String a) {
475 return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
476 " THEN " + a + " ELSE NULL END";
477 }
Daichi Hirono259ce802015-11-20 17:51:53 +0900478}