Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
ab67ab1a
authored
Jan 30, 2019
by
olly
Committed by
Oliver Woodman
Jan 30, 2019
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Implement database CachedContentIndex.Storage
PiperOrigin-RevId: 231600104
parent
c9b848e5
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
250 additions
and
11 deletions
library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
View file @
ab67ab1a
...
...
@@ -15,16 +15,25 @@
*/
package
com
.
google
.
android
.
exoplayer2
.
upstream
.
cache
;
import
android.content.ContentValues
;
import
android.database.Cursor
;
import
android.database.sqlite.SQLiteDatabase
;
import
android.database.sqlite.SQLiteException
;
import
android.support.annotation.Nullable
;
import
android.support.annotation.VisibleForTesting
;
import
android.util.SparseArray
;
import
android.util.SparseBooleanArray
;
import
com.google.android.exoplayer2.database.DatabaseProvider
;
import
com.google.android.exoplayer2.database.ExoDatabaseProvider
;
import
com.google.android.exoplayer2.database.VersionTable
;
import
com.google.android.exoplayer2.upstream.cache.Cache.CacheException
;
import
com.google.android.exoplayer2.util.Assertions
;
import
com.google.android.exoplayer2.util.AtomicFile
;
import
com.google.android.exoplayer2.util.ReusableBufferedOutputStream
;
import
com.google.android.exoplayer2.util.Util
;
import
java.io.BufferedInputStream
;
import
java.io.ByteArrayInputStream
;
import
java.io.ByteArrayOutputStream
;
import
java.io.DataInputStream
;
import
java.io.DataOutputStream
;
import
java.io.File
;
...
...
@@ -51,7 +60,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** Maintains the index of cached content. */
/* package */
class
CachedContentIndex
{
public
static
final
String
FILE_NAME
=
"cached_content_index.exi"
;
/* package */
static
final
String
FILE_NAME_ATOMIC
=
"cached_content_index.exi"
;
private
static
final
String
FILE_NAME_DATABASE
=
"cached_content_index.db"
;
private
static
final
int
VERSION
=
2
;
private
static
final
int
VERSION_METADATA_INTRODUCED
=
2
;
...
...
@@ -87,6 +97,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private
final
Storage
storage
;
/**
* Returns whether the file is an index file, or an auxiliary file associated with an index file
* (e.g. an atomic file backup or auxiliary database file).
*/
public
static
final
boolean
isIndexFile
(
String
fileName
)
{
// Atomic file backups and auxiliary database files add additional suffixes to the file name.
return
fileName
.
startsWith
(
FILE_NAME_ATOMIC
)
||
fileName
.
startsWith
(
FILE_NAME_DATABASE
);
}
/**
* Creates a CachedContentIndex which works on the index file in the given cacheDir.
*
* @param cacheDir Directory where the index file is kept.
...
...
@@ -130,7 +149,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
keyToContent
=
new
HashMap
<>();
idToKey
=
new
SparseArray
<>();
removedIds
=
new
SparseBooleanArray
();
storage
=
new
AtomicFileStorage
(
new
File
(
cacheDir
,
FILE_NAME
),
encrypt
,
cipher
,
secretKeySpec
);
Random
random
=
new
Random
();
storage
=
new
AtomicFileStorage
(
new
File
(
cacheDir
,
FILE_NAME_ATOMIC
),
random
,
encrypt
,
cipher
,
secretKeySpec
);
// storage =
// new SQLiteStorage(
// new File(cacheDir, FILE_NAME_DATABASE),
// random,
// encrypt,
// cipher,
// secretKeySpec);
}
/** Loads the index file. */
...
...
@@ -369,25 +398,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/** {@link Storage} implementation that uses an {@link AtomicFile}. */
private
static
class
AtomicFileStorage
implements
Storage
{
private
final
Random
random
;
private
final
boolean
encrypt
;
@Nullable
private
final
Cipher
cipher
;
@Nullable
private
final
SecretKeySpec
secretKeySpec
;
private
final
AtomicFile
atomicFile
;
private
final
Random
random
;
private
boolean
changed
;
@Nullable
private
ReusableBufferedOutputStream
bufferedOutputStream
;
public
AtomicFileStorage
(
File
fileName
,
File
file
,
Random
random
,
boolean
encrypt
,
@Nullable
Cipher
cipher
,
@Nullable
SecretKeySpec
secretKeySpec
)
{
this
.
random
=
random
;
this
.
encrypt
=
encrypt
;
this
.
cipher
=
cipher
;
this
.
secretKeySpec
=
secretKeySpec
;
atomicFile
=
new
AtomicFile
(
fileName
);
random
=
new
Random
();
atomicFile
=
new
AtomicFile
(
file
);
}
@Override
...
...
@@ -570,4 +600,211 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
writeContentMetadata
(
cachedContent
.
getMetadata
(),
output
);
}
}
/** {@link Storage} implementation that uses an SQL database. */
// TODO:
// 1. Implement upgrade/downgrade paths from/to AtomicFileStorage.
// 2. If encryption is enabled having previously written data, decide whether we need to rewrite
// the entire table. Currently this implementation only encrypts new and updated entries.
private
static
final
class
SQLiteStorage
implements
Storage
{
private
static
final
String
TABLE_NAME
=
DatabaseProvider
.
TABLE_PREFIX
+
"Cache"
;
private
static
final
int
TABLE_VERSION
=
1
;
private
static
final
String
COLUMN_ID
=
"id"
;
private
static
final
String
COLUMN_FLAGS
=
"flags"
;
private
static
final
String
COLUMN_DATA
=
"data"
;
private
static
final
int
COLUMN_INDEX_ID
=
0
;
private
static
final
int
COLUMN_INDEX_FLAGS
=
1
;
private
static
final
int
COLUMN_INDEX_DATA
=
2
;
private
static
final
String
COLUMN_SELECTION_ID
=
COLUMN_ID
+
" = ?"
;
private
static
final
String
[]
COLUMNS
=
new
String
[]
{
COLUMN_ID
,
COLUMN_FLAGS
,
COLUMN_DATA
};
private
static
final
String
SQL_DROP_TABLE_IF_EXISTS
=
"DROP TABLE IF EXISTS "
+
TABLE_NAME
;
private
static
final
String
SQL_CREATE_TABLE
=
"CREATE TABLE "
+
TABLE_NAME
+
" ("
+
COLUMN_ID
+
" INTEGER PRIMARY KEY NOT NULL,"
+
COLUMN_FLAGS
+
" INTEGER NOT NULL,"
+
COLUMN_DATA
+
" BLOB NOT NULL)"
;
private
static
final
int
FLAG_ENCRYPTED
=
1
;
private
final
Random
random
;
private
final
boolean
encrypt
;
@Nullable
private
final
Cipher
cipher
;
@Nullable
private
final
SecretKeySpec
secretKeySpec
;
private
final
DatabaseProvider
databaseProvider
;
private
final
SparseArray
<
CachedContent
>
pendingUpdates
;
@Nullable
private
ReusableBufferedOutputStream
bufferedOutputStream
;
public
SQLiteStorage
(
File
file
,
Random
random
,
boolean
encrypt
,
@Nullable
Cipher
cipher
,
@Nullable
SecretKeySpec
secretKeySpec
)
{
this
.
random
=
random
;
this
.
encrypt
=
encrypt
;
this
.
cipher
=
cipher
;
this
.
secretKeySpec
=
secretKeySpec
;
databaseProvider
=
new
ExoDatabaseProvider
(
file
);
pendingUpdates
=
new
SparseArray
<>();
}
@Override
public
boolean
load
(
HashMap
<
String
,
CachedContent
>
content
,
SparseArray
<
@NullableType
String
>
idToKey
)
{
try
{
int
version
=
VersionTable
.
getVersion
(
databaseProvider
.
getReadableDatabase
(),
VersionTable
.
FEATURE_CACHE
);
if
(
version
==
VersionTable
.
VERSION_UNSET
||
version
>
TABLE_VERSION
)
{
SQLiteDatabase
writableDatabase
=
databaseProvider
.
getWritableDatabase
();
writableDatabase
.
beginTransaction
();
try
{
writableDatabase
.
execSQL
(
SQL_DROP_TABLE_IF_EXISTS
);
writableDatabase
.
execSQL
(
SQL_CREATE_TABLE
);
VersionTable
.
setVersion
(
writableDatabase
,
VersionTable
.
FEATURE_CACHE
,
TABLE_VERSION
);
writableDatabase
.
setTransactionSuccessful
();
}
finally
{
writableDatabase
.
endTransaction
();
}
}
else
if
(
version
<
TABLE_VERSION
)
{
// There is no previous version currently.
throw
new
IllegalStateException
();
}
try
(
Cursor
cursor
=
getCursor
())
{
while
(
cursor
.
moveToNext
())
{
int
id
=
cursor
.
getInt
(
COLUMN_INDEX_ID
);
boolean
encrypted
=
(
cursor
.
getInt
(
COLUMN_INDEX_FLAGS
)
&
FLAG_ENCRYPTED
)
!=
0
;
byte
[]
data
=
cursor
.
getBlob
(
COLUMN_INDEX_DATA
);
ByteArrayInputStream
inputStream
=
new
ByteArrayInputStream
(
data
);
DataInputStream
input
=
new
DataInputStream
(
inputStream
);
if
(
encrypted
)
{
byte
[]
initializationVector
=
new
byte
[
16
];
input
.
readFully
(
initializationVector
);
IvParameterSpec
ivParameterSpec
=
new
IvParameterSpec
(
initializationVector
);
try
{
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
secretKeySpec
,
ivParameterSpec
);
}
catch
(
InvalidKeyException
|
InvalidAlgorithmParameterException
e
)
{
throw
new
IllegalStateException
(
e
);
}
input
=
new
DataInputStream
(
new
CipherInputStream
(
inputStream
,
cipher
));
}
String
key
=
input
.
readUTF
();
DefaultContentMetadata
metadata
=
readContentMetadata
(
input
);
CachedContent
cachedContent
=
new
CachedContent
(
id
,
key
,
metadata
);
content
.
put
(
cachedContent
.
key
,
cachedContent
);
idToKey
.
put
(
cachedContent
.
id
,
cachedContent
.
key
);
}
}
return
true
;
}
catch
(
IOException
|
SQLiteException
e
)
{
return
false
;
}
}
@Override
public
void
store
(
HashMap
<
String
,
CachedContent
>
content
)
throws
CacheException
{
if
(
pendingUpdates
.
size
()
==
0
)
{
return
;
}
SQLiteDatabase
writableDatabase
=
databaseProvider
.
getWritableDatabase
();
writableDatabase
.
beginTransaction
();
try
{
for
(
int
i
=
0
;
i
<
pendingUpdates
.
size
();
i
++)
{
CachedContent
cachedContent
=
pendingUpdates
.
valueAt
(
i
);
if
(
cachedContent
==
null
)
{
deleteRow
(
writableDatabase
,
pendingUpdates
.
keyAt
(
i
));
}
else
{
addOrUpdateRow
(
writableDatabase
,
cachedContent
);
}
}
writableDatabase
.
setTransactionSuccessful
();
pendingUpdates
.
clear
();
}
catch
(
IOException
|
SQLiteException
e
)
{
throw
new
CacheException
(
e
);
}
finally
{
writableDatabase
.
endTransaction
();
}
}
@Override
public
void
onUpdate
(
CachedContent
cachedContent
)
{
pendingUpdates
.
put
(
cachedContent
.
id
,
cachedContent
);
}
@Override
public
void
onRemove
(
CachedContent
cachedContent
)
{
pendingUpdates
.
put
(
cachedContent
.
id
,
null
);
}
private
Cursor
getCursor
()
{
return
databaseProvider
.
getReadableDatabase
()
.
query
(
TABLE_NAME
,
COLUMNS
,
/* selection= */
null
,
/* selectionArgs= */
null
,
/* groupBy= */
null
,
/* having= */
null
,
/* orderBy= */
null
);
}
private
void
deleteRow
(
SQLiteDatabase
writableDatabase
,
int
key
)
{
String
[]
selectionArgs
=
{
Integer
.
toString
(
key
)};
writableDatabase
.
delete
(
TABLE_NAME
,
COLUMN_SELECTION_ID
,
selectionArgs
);
}
private
void
addOrUpdateRow
(
SQLiteDatabase
writableDatabase
,
CachedContent
cachedContent
)
throws
IOException
{
ByteArrayOutputStream
outputStream
=
new
ByteArrayOutputStream
();
if
(
bufferedOutputStream
==
null
)
{
bufferedOutputStream
=
new
ReusableBufferedOutputStream
(
outputStream
);
}
else
{
bufferedOutputStream
.
reset
(
outputStream
);
}
DataOutputStream
output
=
new
DataOutputStream
(
bufferedOutputStream
);
try
{
if
(
encrypt
)
{
byte
[]
initializationVector
=
new
byte
[
16
];
random
.
nextBytes
(
initializationVector
);
output
.
write
(
initializationVector
);
IvParameterSpec
ivParameterSpec
=
new
IvParameterSpec
(
initializationVector
);
try
{
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
secretKeySpec
,
ivParameterSpec
);
}
catch
(
InvalidKeyException
|
InvalidAlgorithmParameterException
e
)
{
throw
new
IllegalStateException
(
e
);
// Should never happen.
}
output
.
flush
();
output
=
new
DataOutputStream
(
new
CipherOutputStream
(
bufferedOutputStream
,
cipher
));
}
output
.
writeUTF
(
cachedContent
.
key
);
writeContentMetadata
(
cachedContent
.
getMetadata
(),
output
);
}
finally
{
// Necessary to finalize the cipher.
Util
.
closeQuietly
(
output
);
}
byte
[]
data
=
outputStream
.
toByteArray
();
ContentValues
values
=
new
ContentValues
();
values
.
put
(
COLUMN_ID
,
cachedContent
.
id
);
values
.
put
(
COLUMN_FLAGS
,
encrypt
?
FLAG_ENCRYPTED
:
0
);
values
.
put
(
COLUMN_DATA
,
data
);
writableDatabase
.
replace
(
TABLE_NAME
,
/* nullColumnHack= */
null
,
values
);
}
}
}
library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
View file @
ab67ab1a
...
...
@@ -408,8 +408,8 @@ public final class SimpleCache implements Cache {
if
(
isRootDirectory
&&
fileName
.
indexOf
(
'.'
)
==
-
1
)
{
loadDirectory
(
file
,
/* isRootDirectory= */
false
);
}
else
{
if
(
isRootDirectory
&&
CachedContentIndex
.
FILE_NAME
.
equals
(
fileName
))
{
// Skip the (expected) index file in the root directory.
if
(
isRootDirectory
&&
CachedContentIndex
.
isIndexFile
(
fileName
))
{
// Skip the (expected) index file
s
in the root directory.
continue
;
}
long
fileLength
=
file
.
length
();
...
...
library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
View file @
ab67ab1a
...
...
@@ -151,7 +151,8 @@ public class CachedContentIndexTest {
@Test
public
void
testLoadV1
()
throws
Exception
{
FileOutputStream
fos
=
new
FileOutputStream
(
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME
));
FileOutputStream
fos
=
new
FileOutputStream
(
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME_ATOMIC
));
fos
.
write
(
testIndexV1File
);
fos
.
close
();
...
...
@@ -169,7 +170,8 @@ public class CachedContentIndexTest {
@Test
public
void
testLoadV2
()
throws
Exception
{
FileOutputStream
fos
=
new
FileOutputStream
(
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME
));
FileOutputStream
fos
=
new
FileOutputStream
(
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME_ATOMIC
));
fos
.
write
(
testIndexV2File
);
fos
.
close
();
...
...
@@ -220,7 +222,7 @@ public class CachedContentIndexTest {
new
CachedContentIndex
(
cacheDir
,
key
),
new
CachedContentIndex
(
cacheDir
,
key
));
// Rename the index file from the test above
File
file1
=
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME
);
File
file1
=
new
File
(
cacheDir
,
CachedContentIndex
.
FILE_NAME
_ATOMIC
);
File
file2
=
new
File
(
cacheDir
,
"file2compare"
);
assertThat
(
file1
.
renameTo
(
file2
)).
isTrue
();
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment