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