summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java
blob: cd69336ff4bcf75f75f0b70f05786fba3ab68bb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.WorkerThread;
import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException;
import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider;
import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/** Maintains an index of cache file metadata. */
/* package */ final class CacheFileMetadataIndex {

  private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata";
  private static final int TABLE_VERSION = 1;

  private static final String COLUMN_NAME = "name";
  private static final String COLUMN_LENGTH = "length";
  private static final String COLUMN_LAST_TOUCH_TIMESTAMP = "last_touch_timestamp";

  private static final int COLUMN_INDEX_NAME = 0;
  private static final int COLUMN_INDEX_LENGTH = 1;
  private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2;

  private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?";

  private static final String[] COLUMNS =
      new String[] {
        COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_TOUCH_TIMESTAMP,
      };
  private static final String TABLE_SCHEMA =
      "("
          + COLUMN_NAME
          + " TEXT PRIMARY KEY NOT NULL,"
          + COLUMN_LENGTH
          + " INTEGER NOT NULL,"
          + COLUMN_LAST_TOUCH_TIMESTAMP
          + " INTEGER NOT NULL)";

  private final DatabaseProvider databaseProvider;

  private @MonotonicNonNull String tableName;

  /**
   * Deletes index data for the specified cache.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @param databaseProvider Provides the database in which the index is stored.
   * @param uid The cache UID.
   * @throws DatabaseIOException If an error occurs deleting the index data.
   */
  @WorkerThread
  public static void delete(DatabaseProvider databaseProvider, long uid)
      throws DatabaseIOException {
    String hexUid = Long.toHexString(uid);
    try {
      String tableName = getTableName(hexUid);
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.beginTransactionNonExclusive();
      try {
        VersionTable.removeVersion(
            writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
        dropTable(writableDatabase, tableName);
        writableDatabase.setTransactionSuccessful();
      } finally {
        writableDatabase.endTransaction();
      }
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  /** @param databaseProvider Provides the database in which the index is stored. */
  public CacheFileMetadataIndex(DatabaseProvider databaseProvider) {
    this.databaseProvider = databaseProvider;
  }

  /**
   * Initializes the index for the given cache UID.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @param uid The cache UID.
   * @throws DatabaseIOException If an error occurs initializing the index.
   */
  @WorkerThread
  public void initialize(long uid) throws DatabaseIOException {
    try {
      String hexUid = Long.toHexString(uid);
      tableName = getTableName(hexUid);
      SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase();
      int version =
          VersionTable.getVersion(
              readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid);
      if (version != TABLE_VERSION) {
        SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
        writableDatabase.beginTransactionNonExclusive();
        try {
          VersionTable.setVersion(
              writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION);
          dropTable(writableDatabase, tableName);
          writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA);
          writableDatabase.setTransactionSuccessful();
        } finally {
          writableDatabase.endTransaction();
        }
      }
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  /**
   * Returns all file metadata keyed by file name. The returned map is mutable and may be modified
   * by the caller.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @return The file metadata keyed by file name.
   * @throws DatabaseIOException If an error occurs loading the metadata.
   */
  @WorkerThread
  public Map<String, CacheFileMetadata> getAll() throws DatabaseIOException {
    try (Cursor cursor = getCursor()) {
      Map<String, CacheFileMetadata> fileMetadata = new HashMap<>(cursor.getCount());
      while (cursor.moveToNext()) {
        String name = cursor.getString(COLUMN_INDEX_NAME);
        long length = cursor.getLong(COLUMN_INDEX_LENGTH);
        long lastTouchTimestamp = cursor.getLong(COLUMN_INDEX_LAST_TOUCH_TIMESTAMP);
        fileMetadata.put(name, new CacheFileMetadata(length, lastTouchTimestamp));
      }
      return fileMetadata;
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  /**
   * Sets metadata for a given file.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @param name The name of the file.
   * @param length The file length.
   * @param lastTouchTimestamp The file last touch timestamp.
   * @throws DatabaseIOException If an error occurs setting the metadata.
   */
  @WorkerThread
  public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException {
    Assertions.checkNotNull(tableName);
    try {
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      ContentValues values = new ContentValues();
      values.put(COLUMN_NAME, name);
      values.put(COLUMN_LENGTH, length);
      values.put(COLUMN_LAST_TOUCH_TIMESTAMP, lastTouchTimestamp);
      writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values);
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  /**
   * Removes metadata.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @param name The name of the file whose metadata is to be removed.
   * @throws DatabaseIOException If an error occurs removing the metadata.
   */
  @WorkerThread
  public void remove(String name) throws DatabaseIOException {
    Assertions.checkNotNull(tableName);
    try {
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  /**
   * Removes metadata.
   *
   * <p>This method may be slow and shouldn't normally be called on the main thread.
   *
   * @param names The names of the files whose metadata is to be removed.
   * @throws DatabaseIOException If an error occurs removing the metadata.
   */
  @WorkerThread
  public void removeAll(Set<String> names) throws DatabaseIOException {
    Assertions.checkNotNull(tableName);
    try {
      SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
      writableDatabase.beginTransactionNonExclusive();
      try {
        for (String name : names) {
          writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name});
        }
        writableDatabase.setTransactionSuccessful();
      } finally {
        writableDatabase.endTransaction();
      }
    } catch (SQLException e) {
      throw new DatabaseIOException(e);
    }
  }

  private Cursor getCursor() {
    Assertions.checkNotNull(tableName);
    return databaseProvider
        .getReadableDatabase()
        .query(
            tableName,
            COLUMNS,
            /* selection */ null,
            /* selectionArgs= */ null,
            /* groupBy= */ null,
            /* having= */ null,
            /* orderBy= */ null);
  }

  private static void dropTable(SQLiteDatabase writableDatabase, String tableName) {
    writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName);
  }

  private static String getTableName(String hexUid) {
    return TABLE_PREFIX + hexUid;
  }
}