mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-04-26 22:35:13 +08:00
@@ -2,6 +2,7 @@ package com.danikula.videocache;
|
||||
|
||||
import com.danikula.videocache.file.DiskUsage;
|
||||
import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -15,11 +16,13 @@ class Config {
|
||||
public final File cacheRoot;
|
||||
public final FileNameGenerator fileNameGenerator;
|
||||
public final DiskUsage diskUsage;
|
||||
public final SourceInfoStorage sourceInfoStorage;
|
||||
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage) {
|
||||
Config(File cacheRoot, FileNameGenerator fileNameGenerator, DiskUsage diskUsage, SourceInfoStorage sourceInfoStorage) {
|
||||
this.cacheRoot = cacheRoot;
|
||||
this.fileNameGenerator = fileNameGenerator;
|
||||
this.diskUsage = diskUsage;
|
||||
this.sourceInfoStorage = sourceInfoStorage;
|
||||
}
|
||||
|
||||
File generateCacheFile(String url) {
|
||||
|
||||
@@ -101,7 +101,7 @@ class HttpProxyCache extends ProxyCache {
|
||||
@Override
|
||||
protected void onCachePercentsAvailableChanged(int percents) {
|
||||
if (listener != null) {
|
||||
listener.onCacheAvailable(cache.file, source.url, percents);
|
||||
listener.onCacheAvailable(cache.file, source.getUrl(), percents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.danikula.videocache.file.FileNameGenerator;
|
||||
import com.danikula.videocache.file.Md5FileNameGenerator;
|
||||
import com.danikula.videocache.file.TotalCountLruDiskUsage;
|
||||
import com.danikula.videocache.file.TotalSizeLruDiskUsage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -192,6 +194,8 @@ public class HttpProxyCacheServer {
|
||||
|
||||
shutdownClients();
|
||||
|
||||
config.sourceInfoStorage.release();
|
||||
|
||||
waitConnectionThread.interrupt();
|
||||
try {
|
||||
if (!serverSocket.isClosed()) {
|
||||
@@ -364,8 +368,10 @@ public class HttpProxyCacheServer {
|
||||
private File cacheRoot;
|
||||
private FileNameGenerator fileNameGenerator;
|
||||
private DiskUsage diskUsage;
|
||||
private SourceInfoStorage sourceInfoStorage;
|
||||
|
||||
public Builder(Context context) {
|
||||
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
|
||||
this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
|
||||
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
|
||||
this.fileNameGenerator = new Md5FileNameGenerator();
|
||||
@@ -439,7 +445,7 @@ public class HttpProxyCacheServer {
|
||||
}
|
||||
|
||||
private Config buildConfig() {
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage);
|
||||
return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ final class HttpProxyCacheServerClients {
|
||||
}
|
||||
|
||||
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
|
||||
HttpUrlSource source = new HttpUrlSource(url);
|
||||
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage);
|
||||
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
|
||||
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
|
||||
httpProxyCache.registerCacheListener(uiCacheListener);
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.danikula.videocache;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
|
||||
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -10,6 +13,7 @@ import java.io.InterruptedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
|
||||
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
|
||||
@@ -26,51 +30,53 @@ import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
|
||||
public class HttpUrlSource implements Source {
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
public final String url;
|
||||
private final SourceInfoStorage sourceInfoStorage;
|
||||
private SourceInfo sourceInfo;
|
||||
private HttpURLConnection connection;
|
||||
private InputStream inputStream;
|
||||
private volatile int length = Integer.MIN_VALUE;
|
||||
private volatile String mime;
|
||||
|
||||
public HttpUrlSource(String url) {
|
||||
this(url, ProxyCacheUtils.getSupposablyMime(url));
|
||||
this(url, SourceInfoStorageFactory.newEmptySourceInfoStorage());
|
||||
}
|
||||
|
||||
public HttpUrlSource(String url, String mime) {
|
||||
this.url = Preconditions.checkNotNull(url);
|
||||
this.mime = mime;
|
||||
public HttpUrlSource(String url, SourceInfoStorage sourceInfoStorage) {
|
||||
this.sourceInfoStorage = checkNotNull(sourceInfoStorage);
|
||||
SourceInfo sourceInfo = sourceInfoStorage.get(url);
|
||||
this.sourceInfo = sourceInfo != null ? sourceInfo :
|
||||
new SourceInfo(url, Integer.MIN_VALUE, ProxyCacheUtils.getSupposablyMime(url));
|
||||
}
|
||||
|
||||
public HttpUrlSource(HttpUrlSource source) {
|
||||
this.url = source.url;
|
||||
this.mime = source.mime;
|
||||
this.length = source.length;
|
||||
this.sourceInfo = source.sourceInfo;
|
||||
this.sourceInfoStorage = source.sourceInfoStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int length() throws ProxyCacheException {
|
||||
if (length == Integer.MIN_VALUE) {
|
||||
if (sourceInfo.length == Integer.MIN_VALUE) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return length;
|
||||
return sourceInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(int offset) throws ProxyCacheException {
|
||||
try {
|
||||
connection = openConnection(offset, -1);
|
||||
mime = connection.getContentType();
|
||||
String mime = connection.getContentType();
|
||||
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
|
||||
length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||
int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
|
||||
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
||||
throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
|
||||
}
|
||||
}
|
||||
|
||||
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
|
||||
int contentLength = connection.getContentLength();
|
||||
return responseCode == HTTP_OK ? contentLength
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
|
||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : sourceInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,29 +96,31 @@ public class HttpUrlSource implements Source {
|
||||
@Override
|
||||
public int read(byte[] buffer) throws ProxyCacheException {
|
||||
if (inputStream == null) {
|
||||
throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!");
|
||||
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
|
||||
}
|
||||
try {
|
||||
return inputStream.read(buffer, 0, buffer.length);
|
||||
} catch (InterruptedIOException e) {
|
||||
throw new InterruptedProxyCacheException("Reading source " + url + " is interrupted", e);
|
||||
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
|
||||
} catch (IOException e) {
|
||||
throw new ProxyCacheException("Error reading data from " + url, e);
|
||||
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchContentInfo() throws ProxyCacheException {
|
||||
Log.d(LOG_TAG, "Read content info from " + url);
|
||||
Log.d(LOG_TAG, "Read content info from " + sourceInfo.url);
|
||||
HttpURLConnection urlConnection = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
urlConnection = openConnection(0, 10000);
|
||||
length = urlConnection.getContentLength();
|
||||
mime = urlConnection.getContentType();
|
||||
int length = urlConnection.getContentLength();
|
||||
String mime = urlConnection.getContentType();
|
||||
inputStream = urlConnection.getInputStream();
|
||||
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length);
|
||||
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
|
||||
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
|
||||
Log.i(LOG_TAG, "Source info fetched: " + sourceInfo);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error fetching info from " + url, e);
|
||||
Log.e(LOG_TAG, "Error fetching info from " + sourceInfo.url, e);
|
||||
} finally {
|
||||
ProxyCacheUtils.close(inputStream);
|
||||
if (urlConnection != null) {
|
||||
@@ -125,7 +133,7 @@ public class HttpUrlSource implements Source {
|
||||
HttpURLConnection connection;
|
||||
boolean redirected;
|
||||
int redirectCount = 0;
|
||||
String url = this.url;
|
||||
String url = this.sourceInfo.url;
|
||||
do {
|
||||
Log.d(LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
@@ -151,18 +159,18 @@ public class HttpUrlSource implements Source {
|
||||
}
|
||||
|
||||
public synchronized String getMime() throws ProxyCacheException {
|
||||
if (TextUtils.isEmpty(mime)) {
|
||||
if (TextUtils.isEmpty(sourceInfo.mime)) {
|
||||
fetchContentInfo();
|
||||
}
|
||||
return mime;
|
||||
return sourceInfo.mime;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
return sourceInfo.url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpUrlSource{url='" + url + "}";
|
||||
return "HttpUrlSource{sourceInfo='" + sourceInfo + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
final class Preconditions {
|
||||
public final class Preconditions {
|
||||
|
||||
static <T> T checkNotNull(T reference) {
|
||||
public static <T> T checkNotNull(T reference) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
static void checkAllNotNull(Object... references) {
|
||||
public static void checkAllNotNull(Object... references) {
|
||||
for (Object reference : references) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException();
|
||||
@@ -17,7 +17,7 @@ final class Preconditions {
|
||||
}
|
||||
}
|
||||
|
||||
static <T> T checkNotNull(T reference, String errorMessage) {
|
||||
public static <T> T checkNotNull(T reference, String errorMessage) {
|
||||
if (reference == null) {
|
||||
throw new NullPointerException(errorMessage);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.danikula.videocache;
|
||||
|
||||
/**
|
||||
* Stores source's info.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class SourceInfo {
|
||||
|
||||
public final String url;
|
||||
public final int length;
|
||||
public final String mime;
|
||||
|
||||
public SourceInfo(String url, int length, String mime) {
|
||||
this.url = url;
|
||||
this.length = length;
|
||||
this.mime = mime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SourceInfo{" +
|
||||
"url='" + url + '\'' +
|
||||
", length=" + length +
|
||||
", mime='" + mime + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
import static com.danikula.videocache.Preconditions.checkAllNotNull;
|
||||
import static com.danikula.videocache.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Database based {@link SourceInfoStorage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {
|
||||
|
||||
private static final String TABLE = "SourceInfo";
|
||||
private static final String COLUMN_ID = "_id";
|
||||
private static final String COLUMN_URL = "url";
|
||||
private static final String COLUMN_LENGTH = "length";
|
||||
private static final String COLUMN_MIME = "mime";
|
||||
private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};
|
||||
private static final String CREATE_SQL =
|
||||
"CREATE TABLE " + TABLE + " (" +
|
||||
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
COLUMN_URL + " TEXT NOT NULL," +
|
||||
COLUMN_MIME + " TEXT," +
|
||||
COLUMN_LENGTH + " INTEGER" +
|
||||
");";
|
||||
|
||||
DatabaseSourceInfoStorage(Context context) {
|
||||
super(context, "AndroidVideoCache.db", null, 1);
|
||||
checkNotNull(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
checkNotNull(db);
|
||||
db.execSQL(CREATE_SQL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
throw new IllegalStateException("Should not be called. There is no any migration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceInfo get(String url) {
|
||||
checkNotNull(url);
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);
|
||||
return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String url, SourceInfo sourceInfo) {
|
||||
checkAllNotNull(url, sourceInfo);
|
||||
SourceInfo sourceInfoFromDb = get(url);
|
||||
boolean exist = sourceInfoFromDb != null;
|
||||
ContentValues contentValues = convert(sourceInfo);
|
||||
if (exist) {
|
||||
getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});
|
||||
} else {
|
||||
getWritableDatabase().insert(TABLE, null, contentValues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
close();
|
||||
}
|
||||
|
||||
private SourceInfo convert(Cursor cursor) {
|
||||
return new SourceInfo(
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME))
|
||||
);
|
||||
}
|
||||
|
||||
private ContentValues convert(SourceInfo sourceInfo) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_URL, sourceInfo.url);
|
||||
values.put(COLUMN_LENGTH, sourceInfo.length);
|
||||
values.put(COLUMN_MIME, sourceInfo.mime);
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
/**
|
||||
* {@link SourceInfoStorage} that does nothing.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class NoSourceInfoStorage implements SourceInfoStorage {
|
||||
|
||||
@Override
|
||||
public SourceInfo get(String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String url, SourceInfo sourceInfo) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import com.danikula.videocache.SourceInfo;
|
||||
|
||||
/**
|
||||
* Storage for {@link SourceInfo}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public interface SourceInfoStorage {
|
||||
|
||||
SourceInfo get(String url);
|
||||
|
||||
void put(String url, SourceInfo sourceInfo);
|
||||
|
||||
void release();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.danikula.videocache.sourcestorage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Simple factory for {@link SourceInfoStorage}.
|
||||
*
|
||||
* @author Alexey Danilov (danikula@gmail.com).
|
||||
*/
|
||||
public class SourceInfoStorageFactory {
|
||||
|
||||
public static SourceInfoStorage newSourceInfoStorage(Context context) {
|
||||
return new DatabaseSourceInfoStorage(context);
|
||||
}
|
||||
|
||||
public static SourceInfoStorage newEmptySourceInfoStorage() {
|
||||
return new NoSourceInfoStorage();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user