1 /* 2 * Copyright (C) 2018 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 package com.google.android.exoplayer2.database; 17 18 import android.content.ContentValues; 19 import android.database.Cursor; 20 import android.database.DatabaseUtils; 21 import android.database.SQLException; 22 import android.database.sqlite.SQLiteDatabase; 23 import androidx.annotation.IntDef; 24 import androidx.annotation.VisibleForTesting; 25 import java.lang.annotation.Documented; 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 29 /** 30 * Utility methods for accessing versions of ExoPlayer database components. This allows them to be 31 * versioned independently to the version of the containing database. 32 */ 33 public final class VersionTable { 34 35 /** Returned by {@link #getVersion(SQLiteDatabase, int, String)} if the version is unset. */ 36 public static final int VERSION_UNSET = -1; 37 /** Version of tables used for offline functionality. */ 38 public static final int FEATURE_OFFLINE = 0; 39 /** Version of tables used for cache content metadata. */ 40 public static final int FEATURE_CACHE_CONTENT_METADATA = 1; 41 /** Version of tables used for cache file metadata. */ 42 public static final int FEATURE_CACHE_FILE_METADATA = 2; 43 /** Version of tables used from external features. */ 44 public static final int FEATURE_EXTERNAL = 1000; 45 46 private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; 47 48 private static final String COLUMN_FEATURE = "feature"; 49 private static final String COLUMN_INSTANCE_UID = "instance_uid"; 50 private static final String COLUMN_VERSION = "version"; 51 52 private static final String WHERE_FEATURE_AND_INSTANCE_UID_EQUALS = 53 COLUMN_FEATURE + " = ? AND " + COLUMN_INSTANCE_UID + " = ?"; 54 55 private static final String PRIMARY_KEY = 56 "PRIMARY KEY (" + COLUMN_FEATURE + ", " + COLUMN_INSTANCE_UID + ")"; 57 private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS = 58 "CREATE TABLE IF NOT EXISTS " 59 + TABLE_NAME 60 + " (" 61 + COLUMN_FEATURE 62 + " INTEGER NOT NULL," 63 + COLUMN_INSTANCE_UID 64 + " TEXT NOT NULL," 65 + COLUMN_VERSION 66 + " INTEGER NOT NULL," 67 + PRIMARY_KEY 68 + ")"; 69 70 @Documented 71 @Retention(RetentionPolicy.SOURCE) 72 @IntDef({ 73 FEATURE_OFFLINE, 74 FEATURE_CACHE_CONTENT_METADATA, 75 FEATURE_CACHE_FILE_METADATA, 76 FEATURE_EXTERNAL 77 }) 78 private @interface Feature {} 79 VersionTable()80 private VersionTable() {} 81 82 /** 83 * Sets the version of a specified instance of a specified feature. 84 * 85 * @param writableDatabase The database to update. 86 * @param feature The feature. 87 * @param instanceUid The unique identifier of the instance of the feature. 88 * @param version The version. 89 * @throws DatabaseIOException If an error occurs executing the SQL. 90 */ setVersion( SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version)91 public static void setVersion( 92 SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid, int version) 93 throws DatabaseIOException { 94 try { 95 writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); 96 ContentValues values = new ContentValues(); 97 values.put(COLUMN_FEATURE, feature); 98 values.put(COLUMN_INSTANCE_UID, instanceUid); 99 values.put(COLUMN_VERSION, version); 100 writableDatabase.replaceOrThrow(TABLE_NAME, /* nullColumnHack= */ null, values); 101 } catch (SQLException e) { 102 throw new DatabaseIOException(e); 103 } 104 } 105 106 /** 107 * Removes the version of a specified instance of a feature. 108 * 109 * @param writableDatabase The database to update. 110 * @param feature The feature. 111 * @param instanceUid The unique identifier of the instance of the feature. 112 * @throws DatabaseIOException If an error occurs executing the SQL. 113 */ removeVersion( SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid)114 public static void removeVersion( 115 SQLiteDatabase writableDatabase, @Feature int feature, String instanceUid) 116 throws DatabaseIOException { 117 try { 118 if (!tableExists(writableDatabase, TABLE_NAME)) { 119 return; 120 } 121 writableDatabase.delete( 122 TABLE_NAME, 123 WHERE_FEATURE_AND_INSTANCE_UID_EQUALS, 124 featureAndInstanceUidArguments(feature, instanceUid)); 125 } catch (SQLException e) { 126 throw new DatabaseIOException(e); 127 } 128 } 129 130 /** 131 * Returns the version of a specified instance of a feature, or {@link #VERSION_UNSET} if no 132 * version is set. 133 * 134 * @param database The database to query. 135 * @param feature The feature. 136 * @param instanceUid The unique identifier of the instance of the feature. 137 * @return The version, or {@link #VERSION_UNSET} if no version is set. 138 * @throws DatabaseIOException If an error occurs executing the SQL. 139 */ getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid)140 public static int getVersion(SQLiteDatabase database, @Feature int feature, String instanceUid) 141 throws DatabaseIOException { 142 try { 143 if (!tableExists(database, TABLE_NAME)) { 144 return VERSION_UNSET; 145 } 146 try (Cursor cursor = 147 database.query( 148 TABLE_NAME, 149 new String[] {COLUMN_VERSION}, 150 WHERE_FEATURE_AND_INSTANCE_UID_EQUALS, 151 featureAndInstanceUidArguments(feature, instanceUid), 152 /* groupBy= */ null, 153 /* having= */ null, 154 /* orderBy= */ null)) { 155 if (cursor.getCount() == 0) { 156 return VERSION_UNSET; 157 } 158 cursor.moveToNext(); 159 return cursor.getInt(/* COLUMN_VERSION index */ 0); 160 } 161 } catch (SQLException e) { 162 throw new DatabaseIOException(e); 163 } 164 } 165 166 @VisibleForTesting tableExists(SQLiteDatabase readableDatabase, String tableName)167 /* package */ static boolean tableExists(SQLiteDatabase readableDatabase, String tableName) { 168 long count = 169 DatabaseUtils.queryNumEntries( 170 readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); 171 return count > 0; 172 } 173 featureAndInstanceUidArguments(int feature, String instance)174 private static String[] featureAndInstanceUidArguments(int feature, String instance) { 175 return new String[] {Integer.toString(feature), instance}; 176 } 177 } 178