1 /* 2 * Copyright (C) 2023 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 17 package com.android.ondevicepersonalization.services.data.vendor; 18 19 import android.content.ComponentName; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.SQLException; 24 import android.database.sqlite.SQLiteDatabase; 25 import android.database.sqlite.SQLiteException; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.odp.module.common.PackageUtils; 29 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 30 import com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication; 31 import com.android.ondevicepersonalization.services.data.DbUtils; 32 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper; 33 import com.android.ondevicepersonalization.services.data.events.EventsDao; 34 35 import java.io.File; 36 import java.io.IOException; 37 import java.nio.file.Files; 38 import java.util.AbstractMap; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.concurrent.ConcurrentHashMap; 46 import java.util.stream.Collectors; 47 48 /** 49 * Dao used to manage access to vendor data tables 50 */ 51 public class OnDevicePersonalizationVendorDataDao { 52 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 53 private static final String TAG = "OnDevicePersonalizationVendorDataDao"; 54 private static final String VENDOR_DATA_TABLE_NAME_PREFIX = "vendordata"; 55 56 private static final long BLOB_SIZE_LIMIT = 100000; 57 58 private static final Map<String, OnDevicePersonalizationVendorDataDao> sVendorDataDaos = 59 new ConcurrentHashMap<>(); 60 private final OnDevicePersonalizationDbHelper mDbHelper; 61 private final ComponentName mOwner; 62 private final String mCertDigest; 63 private final String mTableName; 64 65 private final String mFileDir; 66 private final OnDevicePersonalizationLocalDataDao mLocalDao; 67 OnDevicePersonalizationVendorDataDao(OnDevicePersonalizationDbHelper dbHelper, ComponentName owner, String certDigest, String fileDir, OnDevicePersonalizationLocalDataDao localDataDao)68 private OnDevicePersonalizationVendorDataDao(OnDevicePersonalizationDbHelper dbHelper, 69 ComponentName owner, String certDigest, String fileDir, 70 OnDevicePersonalizationLocalDataDao localDataDao) { 71 this.mDbHelper = dbHelper; 72 this.mOwner = owner; 73 this.mCertDigest = certDigest; 74 this.mTableName = getTableName(owner, certDigest); 75 this.mFileDir = fileDir; 76 this.mLocalDao = localDataDao; 77 } 78 79 /** 80 * Returns an instance of the OnDevicePersonalizationVendorDataDao given a context. 81 * 82 * @param context The context of the application 83 * @param owner Name of package that owns the table 84 * @param certDigest Hash of the certificate used to sign the package 85 * @return Instance of OnDevicePersonalizationVendorDataDao for accessing the requested 86 * package's table 87 */ getInstance(Context context, ComponentName owner, String certDigest)88 public static OnDevicePersonalizationVendorDataDao getInstance(Context context, 89 ComponentName owner, String certDigest) { 90 // TODO: Validate the owner and certDigest 91 String tableName = getTableName(owner, certDigest); 92 String fileDir = getFileDir(tableName, context.getFilesDir()); 93 OnDevicePersonalizationVendorDataDao instance = sVendorDataDaos.get(tableName); 94 if (instance == null) { 95 synchronized (sVendorDataDaos) { 96 instance = sVendorDataDaos.get(tableName); 97 if (instance == null) { 98 OnDevicePersonalizationDbHelper dbHelper = 99 OnDevicePersonalizationDbHelper.getInstance(context); 100 instance = new OnDevicePersonalizationVendorDataDao( 101 dbHelper, owner, certDigest, fileDir, 102 OnDevicePersonalizationLocalDataDao.getInstance(context, owner, 103 certDigest)); 104 sVendorDataDaos.put(tableName, instance); 105 } 106 } 107 } 108 return instance; 109 } 110 111 /** 112 * Returns an instance of the OnDevicePersonalizationVendorDataDao given a context. This is used 113 * for testing only 114 */ 115 @VisibleForTesting getInstanceForTest(Context context, ComponentName owner, String certDigest)116 public static OnDevicePersonalizationVendorDataDao getInstanceForTest(Context context, 117 ComponentName owner, String certDigest) { 118 synchronized (OnDevicePersonalizationVendorDataDao.class) { 119 String tableName = getTableName(owner, certDigest); 120 String fileDir = getFileDir(tableName, context.getFilesDir()); 121 OnDevicePersonalizationVendorDataDao instance = sVendorDataDaos.get(tableName); 122 if (instance == null) { 123 OnDevicePersonalizationDbHelper dbHelper = 124 OnDevicePersonalizationDbHelper.getInstanceForTest(context); 125 instance = new OnDevicePersonalizationVendorDataDao( 126 dbHelper, owner, certDigest, fileDir, 127 OnDevicePersonalizationLocalDataDao.getInstanceForTest(context, owner, 128 certDigest)); 129 sVendorDataDaos.put(tableName, instance); 130 } 131 return instance; 132 } 133 } 134 135 /** 136 * Creates table name based on owner and certDigest 137 */ getTableName(ComponentName owner, String certDigest)138 public static String getTableName(ComponentName owner, String certDigest) { 139 return DbUtils.getTableName(VENDOR_DATA_TABLE_NAME_PREFIX, owner, certDigest); 140 } 141 142 /** 143 * Creates file directory name based on table name and base directory 144 */ getFileDir(String tableName, File baseDir)145 public static String getFileDir(String tableName, File baseDir) { 146 return baseDir + "/VendorData/" + tableName; 147 } 148 149 /** 150 * Gets the name and cert of all vendors with VendorData & VendorSettings 151 */ getVendors(Context context)152 public static List<Map.Entry<String, String>> getVendors(Context context) { 153 OnDevicePersonalizationDbHelper dbHelper = 154 OnDevicePersonalizationDbHelper.getInstance(context); 155 SQLiteDatabase db = dbHelper.getReadableDatabase(); 156 String[] projection = {VendorSettingsContract.VendorSettingsEntry.OWNER, 157 VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST}; 158 Cursor cursor = db.query( 159 /* distinct= */ true, 160 VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, 161 projection, 162 /* selection= */ null, 163 /* selectionArgs= */ null, 164 /* groupBy= */ null, 165 /* having= */ null, 166 /* orderBy= */ null, 167 /* limit= */ null 168 ); 169 170 List<Map.Entry<String, String>> result = new ArrayList<>(); 171 try { 172 while (cursor.moveToNext()) { 173 String owner = cursor.getString(cursor.getColumnIndexOrThrow( 174 VendorSettingsContract.VendorSettingsEntry.OWNER)); 175 String cert = cursor.getString(cursor.getColumnIndexOrThrow( 176 VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST)); 177 result.add(new AbstractMap.SimpleImmutableEntry<>(owner, cert)); 178 } 179 } catch (Exception e) { 180 sLogger.e(TAG + ": Failed to get Vendors", e); 181 } finally { 182 cursor.close(); 183 } 184 return result; 185 } 186 187 /** 188 * Performs a transaction to delete the vendorData table and vendorSettings for a given package. 189 */ deleteVendorData( Context context, ComponentName owner, String certDigest)190 public static boolean deleteVendorData( 191 Context context, ComponentName owner, String certDigest) { 192 OnDevicePersonalizationDbHelper dbHelper = 193 OnDevicePersonalizationDbHelper.getInstance(context); 194 SQLiteDatabase db = dbHelper.getWritableDatabase(); 195 String vendorDataTableName = getTableName(owner, certDigest); 196 try { 197 db.beginTransactionNonExclusive(); 198 // Delete rows from VendorSettings 199 String selection = VendorSettingsContract.VendorSettingsEntry.OWNER + " = ? AND " 200 + VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST + " = ?"; 201 String[] selectionArgs = {DbUtils.toTableValue(owner), certDigest}; 202 db.delete(VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, selection, 203 selectionArgs); 204 205 // Delete the vendorData and localData table 206 db.execSQL("DROP TABLE IF EXISTS " + vendorDataTableName); 207 OnDevicePersonalizationLocalDataDao.deleteTable(context, owner, certDigest); 208 209 db.setTransactionSuccessful(); 210 } catch (Exception e) { 211 sLogger.e(TAG + ": Failed to delete vendorData for: " + owner, e); 212 return false; 213 } finally { 214 db.endTransaction(); 215 } 216 FileUtils.deleteDirectory(new File(getFileDir(vendorDataTableName, context.getFilesDir()))); 217 FileUtils.deleteDirectory(new File(OnDevicePersonalizationLocalDataDao.getFileDir( 218 OnDevicePersonalizationLocalDataDao.getTableName(owner, certDigest), 219 context.getFilesDir()))); 220 return true; 221 } 222 createTableIfNotExists(String tableName)223 private boolean createTableIfNotExists(String tableName) { 224 try { 225 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 226 db.execSQL(VendorDataContract.VendorDataEntry.getCreateTableIfNotExistsStatement( 227 tableName)); 228 } catch (SQLException e) { 229 sLogger.e(TAG + ": Failed to create table: " + tableName, e); 230 return false; 231 } 232 // Create directory for large files 233 File dir = new File(mFileDir); 234 if (!dir.isDirectory()) { 235 return dir.mkdirs(); 236 } 237 return true; 238 } 239 240 /** 241 * Reads all rows in the vendor data table 242 * 243 * @return Cursor of all rows in table 244 */ readAllVendorData()245 public Cursor readAllVendorData() { 246 try { 247 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 248 return db.query( 249 mTableName, 250 /* columns= */ null, 251 /* selection= */ null, 252 /* selectionArgs= */ null, 253 /* groupBy= */ null, 254 /* having= */ null, 255 /* orderBy= */ null 256 ); 257 } catch (SQLiteException e) { 258 sLogger.e(TAG + ": Failed to read vendor data rows", e); 259 } 260 return null; 261 } 262 263 /** 264 * Reads single row in the vendor data table 265 * 266 * @return Vendor data for the single row requested 267 */ readSingleVendorDataRow(String key)268 public byte[] readSingleVendorDataRow(String key) { 269 try { 270 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 271 String[] projection = { 272 VendorDataContract.VendorDataEntry.TYPE, 273 VendorDataContract.VendorDataEntry.DATA 274 }; 275 String selection = VendorDataContract.VendorDataEntry.KEY + " = ?"; 276 String[] selectionArgs = {key}; 277 try (Cursor cursor = db.query( 278 mTableName, 279 projection, 280 selection, 281 selectionArgs, 282 /* groupBy= */ null, 283 /* having= */ null, 284 /* orderBy= */ null 285 )) { 286 if (cursor.getCount() < 1) { 287 sLogger.d(TAG + ": Failed to find requested key: " + key); 288 return null; 289 } 290 cursor.moveToNext(); 291 byte[] blob = cursor.getBlob( 292 cursor.getColumnIndexOrThrow(VendorDataContract.VendorDataEntry.DATA)); 293 int type = cursor.getInt( 294 cursor.getColumnIndexOrThrow(VendorDataContract.VendorDataEntry.TYPE)); 295 if (type == VendorDataContract.DATA_TYPE_FILE) { 296 File file = new File(mFileDir, new String(blob)); 297 return Files.readAllBytes(file.toPath()); 298 } 299 return blob; 300 } 301 } catch (SQLiteException | IOException e) { 302 sLogger.e(TAG + ": Failed to read vendor data row", e); 303 } 304 return null; 305 } 306 307 /** 308 * Reads all keys in the vendor data table 309 * 310 * @return Set of keys in the vendor data table. 311 */ readAllVendorDataKeys()312 public Set<String> readAllVendorDataKeys() { 313 Set<String> keyset = new HashSet<>(); 314 try { 315 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 316 String[] projection = {VendorDataContract.VendorDataEntry.KEY}; 317 try (Cursor cursor = db.query( 318 mTableName, 319 projection, 320 /* selection= */ null, 321 /* selectionArgs= */ null, 322 /* groupBy= */ null, 323 /* having= */ null, 324 /* orderBy= */ null 325 )) { 326 while (cursor.moveToNext()) { 327 String key = cursor.getString( 328 cursor.getColumnIndexOrThrow(VendorDataContract.VendorDataEntry.KEY)); 329 keyset.add(key); 330 } 331 cursor.close(); 332 return keyset; 333 } 334 } catch (SQLiteException e) { 335 sLogger.e(TAG + ": Failed to read all vendor data keys", e); 336 } 337 return keyset; 338 } 339 340 /** 341 * Batch updates and/or inserts a list of vendor data and a corresponding syncToken and 342 * deletes unretained keys. 343 * 344 * @return true if the transaction is successful. False otherwise. 345 */ batchUpdateOrInsertVendorDataTransaction(List<VendorData> vendorDataList, List<String> retainedKeys, long syncToken)346 public boolean batchUpdateOrInsertVendorDataTransaction(List<VendorData> vendorDataList, 347 List<String> retainedKeys, long syncToken) { 348 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 349 try { 350 db.beginTransactionNonExclusive(); 351 if (!createTableIfNotExists(mTableName)) { 352 return false; 353 } 354 if (!mLocalDao.createTableIfNotExists()) { 355 return false; 356 } 357 if (!deleteUnretainedRows(retainedKeys)) { 358 return false; 359 } 360 for (VendorData vendorData : vendorDataList) { 361 if (!updateOrInsertVendorData(vendorData, syncToken)) { 362 // The query failed. Return and don't finalize the transaction. 363 return false; 364 } 365 } 366 if (!updateOrInsertSyncToken(syncToken)) { 367 return false; 368 } 369 db.setTransactionSuccessful(); 370 } finally { 371 db.endTransaction(); 372 } 373 return true; 374 } 375 deleteUnretainedRows(List<String> retainedKeys)376 private boolean deleteUnretainedRows(List<String> retainedKeys) { 377 try { 378 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 379 String retainedKeysString = retainedKeys.stream().map(s -> "'" + s + "'").collect( 380 Collectors.joining(",", "(", ")")); 381 String whereClause = VendorDataContract.VendorDataEntry.KEY + " NOT IN " 382 + retainedKeysString; 383 return db.delete(mTableName, whereClause, 384 null) != -1; 385 } catch (SQLiteException e) { 386 sLogger.e(TAG + ": Failed to delete unretained rows", e); 387 } 388 return false; 389 } 390 391 /** 392 * Updates the given vendor data row, adds it if it doesn't already exist. 393 * 394 * @return true if the update/insert succeeded, false otherwise 395 */ updateOrInsertVendorData(VendorData vendorData, long syncToken)396 private boolean updateOrInsertVendorData(VendorData vendorData, long syncToken) { 397 try { 398 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 399 ContentValues values = new ContentValues(); 400 values.put(VendorDataContract.VendorDataEntry.KEY, vendorData.getKey()); 401 if (vendorData.getData().length > BLOB_SIZE_LIMIT) { 402 String filename = vendorData.getKey() + "_" + syncToken; 403 File file = new File(mFileDir, filename); 404 Files.write(file.toPath(), vendorData.getData()); 405 values.put(VendorDataContract.VendorDataEntry.TYPE, 406 VendorDataContract.DATA_TYPE_FILE); 407 values.put(VendorDataContract.VendorDataEntry.DATA, filename.getBytes()); 408 } else { 409 values.put(VendorDataContract.VendorDataEntry.DATA, vendorData.getData()); 410 } 411 return db.insertWithOnConflict(mTableName, null, 412 values, SQLiteDatabase.CONFLICT_REPLACE) != -1; 413 } catch (SQLiteException | IOException e) { 414 sLogger.e(TAG + ": Failed to update or insert buyer data", e); 415 // Attempt to delete file if something failed 416 String filename = vendorData.getKey() + "_" + syncToken; 417 File file = new File(mFileDir, filename); 418 file.delete(); 419 } 420 return false; 421 } 422 423 /** 424 * Updates the syncToken, adds it if it doesn't already exist. 425 * 426 * @return true if the update/insert succeeded, false otherwise 427 */ updateOrInsertSyncToken(long syncToken)428 private boolean updateOrInsertSyncToken(long syncToken) { 429 try { 430 SQLiteDatabase db = mDbHelper.getWritableDatabase(); 431 ContentValues values = new ContentValues(); 432 values.put(VendorSettingsContract.VendorSettingsEntry.OWNER, 433 DbUtils.toTableValue(mOwner)); 434 values.put(VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST, mCertDigest); 435 values.put(VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN, syncToken); 436 return db.insertWithOnConflict(VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, 437 null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1; 438 } catch (SQLiteException e) { 439 sLogger.e(TAG + ": Failed to update or insert syncToken", e); 440 } 441 return false; 442 } 443 444 /** Deletes data for all isolated services except the ones listed. */ deleteVendorTables( Context context, List<ComponentName> excludedServices)445 public static void deleteVendorTables( 446 Context context, List<ComponentName> excludedServices) throws Exception { 447 EventsDao eventsDao = EventsDao.getInstance(context); 448 // Set of packageName and cert 449 Set<Map.Entry<String, String>> vendors = new HashSet<>(getVendors(context)); 450 451 // Set of valid packageName and cert 452 Set<Map.Entry<String, String>> validVendors = new HashSet<>(); 453 Set<String> validTables = new HashSet<>(); 454 455 456 // Remove all valid packages from the set 457 for (ComponentName service : excludedServices) { 458 String certDigest = 459 PackageUtils.getCertDigest( 460 OnDevicePersonalizationApplication.getAppContext(), 461 service.getPackageName()); 462 // Remove valid packages from set 463 vendors.remove(new AbstractMap.SimpleImmutableEntry<>( 464 DbUtils.toTableValue(service), certDigest)); 465 466 // Add valid package to new set 467 validVendors.add(new AbstractMap.SimpleImmutableEntry<>( 468 DbUtils.toTableValue(service), certDigest)); 469 validTables.add(getTableName(service, certDigest)); 470 validTables.add(OnDevicePersonalizationLocalDataDao.getTableName(service, certDigest)); 471 } 472 sLogger.d(TAG + ": Retaining tables: " + validTables); 473 sLogger.d(TAG + ": Deleting vendors: " + vendors); 474 // Delete the remaining tables for packages not found onboarded 475 for (Map.Entry<String, String> entry : vendors) { 476 String serviceNameStr = entry.getKey(); 477 ComponentName service = DbUtils.fromTableValue(serviceNameStr); 478 String certDigest = entry.getValue(); 479 deleteVendorData(context, service, certDigest); 480 eventsDao.deleteEventState(service); 481 } 482 483 // Cleanup files from internal storage for valid packages. 484 for (Map.Entry<String, String> entry : validVendors) { 485 String serviceNameStr = entry.getKey(); 486 ComponentName service = DbUtils.fromTableValue(serviceNameStr); 487 String certDigest = entry.getValue(); 488 // VendorDao 489 OnDevicePersonalizationVendorDataDao vendorDao = 490 OnDevicePersonalizationVendorDataDao.getInstance(context, service, 491 certDigest); 492 File vendorDir = new File(OnDevicePersonalizationVendorDataDao.getFileDir( 493 OnDevicePersonalizationVendorDataDao.getTableName(service, certDigest), 494 context.getFilesDir())); 495 FileUtils.cleanUpFilesDir(vendorDao.readAllVendorDataKeys(), vendorDir); 496 497 // LocalDao 498 OnDevicePersonalizationLocalDataDao localDao = 499 OnDevicePersonalizationLocalDataDao.getInstance(context, service, 500 certDigest); 501 File localDir = new File(OnDevicePersonalizationLocalDataDao.getFileDir( 502 OnDevicePersonalizationLocalDataDao.getTableName(service, certDigest), 503 context.getFilesDir())); 504 FileUtils.cleanUpFilesDir(localDao.readAllLocalDataKeys(), localDir); 505 } 506 507 // Cleanup any loose data directories. Tables deleted, but directory still exists. 508 List<File> filesToDelete = new ArrayList<>(); 509 File vendorDir = new File(context.getFilesDir(), "VendorData"); 510 if (vendorDir.isDirectory()) { 511 for (File f : vendorDir.listFiles()) { 512 if (f.isDirectory()) { 513 // Delete files for non-existent tables 514 if (!validTables.contains(f.getName())) { 515 filesToDelete.add(f); 516 } 517 } else { 518 // There should not be regular files. 519 filesToDelete.add(f); 520 } 521 } 522 } 523 File localDir = new File(context.getFilesDir(), "LocalData"); 524 if (localDir.isDirectory()) { 525 for (File f : localDir.listFiles()) { 526 if (f.isDirectory()) { 527 // Delete files for non-existent tables 528 if (!validTables.contains(f.getName())) { 529 filesToDelete.add(f); 530 } 531 } else { 532 // There should not be regular files. 533 filesToDelete.add(f); 534 } 535 } 536 } 537 sLogger.d(TAG + ": deleting " 538 + Arrays.asList(filesToDelete.stream().map(v -> v.getName()).toArray())); 539 filesToDelete.forEach(FileUtils::deleteDirectory); 540 } 541 542 /** 543 * Inserts the syncToken, ignoring on conflict. 544 * 545 * @return true if the insert succeeded with no error, false otherwise 546 */ insertNewSyncToken(SQLiteDatabase db, ComponentName owner, String certDigest, long syncToken)547 protected static boolean insertNewSyncToken(SQLiteDatabase db, 548 ComponentName owner, String certDigest, long syncToken) { 549 try { 550 ContentValues values = new ContentValues(); 551 values.put(VendorSettingsContract.VendorSettingsEntry.OWNER, 552 DbUtils.toTableValue(owner)); 553 values.put(VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST, certDigest); 554 values.put(VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN, syncToken); 555 return db.insertWithOnConflict(VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, 556 null, values, SQLiteDatabase.CONFLICT_IGNORE) != -1; 557 } catch (SQLiteException e) { 558 sLogger.e(TAG + ": Failed to insert syncToken", e); 559 } 560 return false; 561 } 562 563 /** 564 * Gets the syncToken owned by {@link #mOwner} with cert {@link #mCertDigest} 565 * 566 * @return syncToken if found, -1 otherwise 567 */ getSyncToken()568 public long getSyncToken() { 569 SQLiteDatabase db = mDbHelper.getReadableDatabase(); 570 String selection = VendorSettingsContract.VendorSettingsEntry.OWNER + " = ? AND " 571 + VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST + " = ?"; 572 String[] selectionArgs = {DbUtils.toTableValue(mOwner), mCertDigest}; 573 String[] projection = {VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN}; 574 Cursor cursor = db.query( 575 VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, 576 projection, 577 selection, 578 selectionArgs, 579 /* groupBy= */ null, 580 /* having= */ null, 581 /* orderBy= */ null 582 ); 583 try { 584 if (cursor.moveToFirst()) { 585 return cursor.getLong(cursor.getColumnIndexOrThrow( 586 VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN)); 587 } 588 } catch (SQLiteException e) { 589 sLogger.e(TAG + ": Failed to update or insert syncToken", e); 590 } finally { 591 cursor.close(); 592 } 593 return -1; 594 } 595 } 596