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