parent
39a43c836d
commit
d3fb30d8b9
@ -1,81 +0,0 @@ |
||||
package com.arialyy.frame.cache; |
||||
|
||||
import android.text.TextUtils; |
||||
import com.arialyy.frame.util.show.L; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* Created by “AriaLyy@outlook.com” on 2015/4/9. |
||||
*/ |
||||
public class CacheHelp { |
||||
|
||||
/** |
||||
* 设置缓存有效时间 |
||||
*/ |
||||
public static void setCacheAvailableTime(long availableTime) { |
||||
CacheObj.CACHE_INTERVAL = availableTime; |
||||
} |
||||
|
||||
/** |
||||
* 储存缓存 |
||||
*/ |
||||
public static void saveCache(CacheUtil util, String key, String value) { |
||||
if (TextUtils.isEmpty(value)) { |
||||
L.e("缓存数据位null"); |
||||
return; |
||||
} |
||||
CacheObj cache = util.getObjectCache(CacheObj.class, key); |
||||
if (cache == null) { |
||||
cache = new CacheObj(); |
||||
} |
||||
cache.setCacheTime(System.currentTimeMillis()); |
||||
cache.setAvailableNum(cache.getAvailableNum() + 1); |
||||
cache.setData(value); |
||||
util.putObjectCache(CacheObj.class, key, cache); |
||||
} |
||||
|
||||
/** |
||||
* 获取有效的缓存 |
||||
*/ |
||||
public static CacheObj getAvailableCache(CacheUtil util, String url, Map<String, String> param) { |
||||
if (TextUtils.isEmpty(url)) { |
||||
return null; |
||||
} |
||||
String key = getKey(url, param); |
||||
CacheObj cache = util.getObjectCache(CacheObj.class, key); |
||||
if (cache == null) { |
||||
return null; |
||||
} |
||||
if (cache.isAvailable()) { |
||||
return cache; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* 获取缓存,有效或无效 |
||||
*/ |
||||
public static CacheObj getCache(CacheUtil util, String url, Map<String, String> param) { |
||||
if (TextUtils.isEmpty(url)) { |
||||
return null; |
||||
} |
||||
String key = getKey(url, param); |
||||
return util.getObjectCache(CacheObj.class, key); |
||||
} |
||||
|
||||
/** |
||||
* 通过url和请求参数获取key |
||||
*/ |
||||
public static String getKey(String url, Map<String, String> param) { |
||||
if (param == null || param.size() == 0) { |
||||
return url; |
||||
} |
||||
String p = ""; |
||||
for (Map.Entry<String, String> entry : param.entrySet()) { |
||||
p += "[" + entry.getKey() + "," + entry.getValue() + "];"; |
||||
} |
||||
url += ";" + p; |
||||
return url; |
||||
} |
||||
} |
@ -1,56 +0,0 @@ |
||||
package com.arialyy.frame.cache; |
||||
|
||||
/** |
||||
* Created by “AriaLyy@outlook.com” on 2015/4/9. |
||||
* 缓存对象 |
||||
*/ |
||||
class CacheObj { |
||||
/** |
||||
* 缓存有效请求时间 |
||||
*/ |
||||
public static long CACHE_INTERVAL = 120 * 1000; |
||||
/** |
||||
* 缓存有效请求次数 |
||||
*/ |
||||
public static final int CACHE_NUM_INTERVAL = 5; |
||||
private long cacheTime = 0; |
||||
private long availableNum = 0; |
||||
|
||||
private String data; |
||||
|
||||
/** |
||||
* 缓存是否有效 |
||||
*/ |
||||
public boolean isAvailable() { |
||||
if (System.currentTimeMillis() - cacheTime > CACHE_INTERVAL) { |
||||
return false; |
||||
} else if (availableNum >= CACHE_NUM_INTERVAL) { |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
public String getData() { |
||||
return data; |
||||
} |
||||
|
||||
public void setData(String data) { |
||||
this.data = data; |
||||
} |
||||
|
||||
public long getCacheTime() { |
||||
return cacheTime; |
||||
} |
||||
|
||||
public void setCacheTime(long cacheTime) { |
||||
this.cacheTime = cacheTime; |
||||
} |
||||
|
||||
public long getAvailableNum() { |
||||
return availableNum; |
||||
} |
||||
|
||||
public void setAvailableNum(long availableNum) { |
||||
this.availableNum = availableNum; |
||||
} |
||||
} |
@ -1,951 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2011 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 com.arialyy.frame.cache.diskcache; |
||||
|
||||
import java.io.BufferedWriter; |
||||
import java.io.Closeable; |
||||
import java.io.EOFException; |
||||
import java.io.File; |
||||
import java.io.FileInputStream; |
||||
import java.io.FileNotFoundException; |
||||
import java.io.FileOutputStream; |
||||
import java.io.FilterOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.OutputStream; |
||||
import java.io.OutputStreamWriter; |
||||
import java.io.Writer; |
||||
import java.util.ArrayList; |
||||
import java.util.Iterator; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.concurrent.Callable; |
||||
import java.util.concurrent.LinkedBlockingQueue; |
||||
import java.util.concurrent.ThreadPoolExecutor; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
/** |
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache |
||||
* entry has a string key and a fixed number of values. Each key must match |
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences, |
||||
* accessible as streams or files. Each value must be between {@code 0} and |
||||
* {@code Integer.MAX_VALUE} bytes in length. |
||||
* |
||||
* <p>The cache stores its data in a directory on the filesystem. This |
||||
* directory must be exclusive to the cache; the cache may delete or overwrite |
||||
* files from its directory. It is an error for multiple processes to use the |
||||
* same cache directory at the same time. |
||||
* |
||||
* <p>This cache limits the number of bytes that it will store on the |
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will |
||||
* remove entries in the background until the limit is satisfied. The limit is |
||||
* not strict: the cache may temporarily exceed it while waiting for files to be |
||||
* deleted. The limit does not include filesystem overhead or the cache |
||||
* journal so space-sensitive applications should set a conservative limit. |
||||
* |
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An |
||||
* entry may have only one editor at one time; if a value is not available to be |
||||
* edited then {@link #edit} will return null. |
||||
* <ul> |
||||
* <li>When an entry is being <strong>created</strong> it is necessary to |
||||
* supply a full set of values; the empty value should be used as a |
||||
* placeholder if necessary. |
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary |
||||
* to supply data for every value; values default to their previous |
||||
* value. |
||||
* </ul> |
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit} |
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set |
||||
* of values as they were before or after the commit, but never a mix of values. |
||||
* |
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will |
||||
* observe the value at the time that {@link #get} was called. Updates and |
||||
* removals after the call do not impact ongoing reads. |
||||
* |
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the |
||||
* filesystem, the corresponding entries will be dropped from the cache. If |
||||
* an error occurs while writing a cache value, the edit will fail silently. |
||||
* Callers should handle other problems by catching {@code IOException} and |
||||
* responding appropriately. |
||||
*/ |
||||
public final class DiskLruCache implements Closeable { |
||||
static final String JOURNAL_FILE = "journal"; |
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp"; |
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp"; |
||||
static final String MAGIC = "libcore.io.DiskLruCache"; |
||||
static final String VERSION_1 = "1"; |
||||
static final long ANY_SEQUENCE_NUMBER = -1; |
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}"; |
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN); |
||||
private static final String CLEAN = "CLEAN"; |
||||
private static final String DIRTY = "DIRTY"; |
||||
private static final String REMOVE = "REMOVE"; |
||||
private static final String READ = "READ"; |
||||
|
||||
/* |
||||
* This cache uses a journal file named "journal". A typical journal file |
||||
* looks like this: |
||||
* libcore.io.DiskLruCache |
||||
* 1 |
||||
* 100 |
||||
* 2 |
||||
* |
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 |
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52 |
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 |
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52 |
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a |
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 |
||||
* READ 335c4c6028171cfddfbaae1a9c313c52 |
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 |
||||
* |
||||
* The first five lines of the journal form its header. They are the |
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version, |
||||
* the application's version, the value count, and a blank line. |
||||
* |
||||
* Each of the subsequent lines in the file is a record of the state of a |
||||
* cache entry. Each line contains space-separated values: a state, a key, |
||||
* and optional state-specific values. |
||||
* o DIRTY lines track that an entry is actively being created or updated. |
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE |
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that |
||||
* temporary files may need to be deleted. |
||||
* o CLEAN lines track a cache entry that has been successfully published |
||||
* and may be read. A publish line is followed by the lengths of each of |
||||
* its values. |
||||
* o READ lines track accesses for LRU. |
||||
* o REMOVE lines track entries that have been deleted. |
||||
* |
||||
* The journal file is appended to as cache operations occur. The journal may |
||||
* occasionally be compacted by dropping redundant lines. A temporary file named |
||||
* "journal.tmp" will be used during compaction; that file should be deleted if |
||||
* it exists when the cache is opened. |
||||
*/ |
||||
|
||||
private final File directory; |
||||
private final File journalFile; |
||||
private final File journalFileTmp; |
||||
private final File journalFileBackup; |
||||
private final int appVersion; |
||||
private long maxSize; |
||||
private final int valueCount; |
||||
private long size = 0; |
||||
private Writer journalWriter; |
||||
private final LinkedHashMap<String, Entry> lruEntries = |
||||
new LinkedHashMap<String, Entry>(0, 0.75f, true); |
||||
private int redundantOpCount; |
||||
|
||||
/** |
||||
* To differentiate between old and current snapshots, each entry is given |
||||
* a sequence number each time an edit is committed. A snapshot is stale if |
||||
* its sequence number is not equal to its entry's sequence number. |
||||
*/ |
||||
private long nextSequenceNumber = 0; |
||||
|
||||
/** This cache uses a single background thread to evict entries. */ |
||||
final ThreadPoolExecutor executorService = |
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); |
||||
private final Callable<Void> cleanupCallable = new Callable<Void>() { |
||||
public Void call() throws Exception { |
||||
synchronized (DiskLruCache.this) { |
||||
if (journalWriter == null) { |
||||
return null; // Closed.
|
||||
} |
||||
trimToSize(); |
||||
if (journalRebuildRequired()) { |
||||
rebuildJournal(); |
||||
redundantOpCount = 0; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
}; |
||||
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { |
||||
this.directory = directory; |
||||
this.appVersion = appVersion; |
||||
this.journalFile = new File(directory, JOURNAL_FILE); |
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); |
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); |
||||
this.valueCount = valueCount; |
||||
this.maxSize = maxSize; |
||||
} |
||||
|
||||
/** |
||||
* Opens the cache in {@code directory}, creating a cache if none exists |
||||
* there. |
||||
* |
||||
* @param directory a writable directory |
||||
* @param valueCount the number of values per cache entry. Must be positive. |
||||
* @param maxSize the maximum number of bytes this cache should use to store |
||||
* @throws IOException if reading or writing the cache directory fails |
||||
*/ |
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) |
||||
throws IOException { |
||||
if (maxSize <= 0) { |
||||
throw new IllegalArgumentException("maxSize <= 0"); |
||||
} |
||||
if (valueCount <= 0) { |
||||
throw new IllegalArgumentException("valueCount <= 0"); |
||||
} |
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP); |
||||
if (backupFile.exists()) { |
||||
File journalFile = new File(directory, JOURNAL_FILE); |
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) { |
||||
backupFile.delete(); |
||||
} else { |
||||
renameTo(backupFile, journalFile, false); |
||||
} |
||||
} |
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); |
||||
if (cache.journalFile.exists()) { |
||||
try { |
||||
cache.readJournal(); |
||||
cache.processJournal(); |
||||
return cache; |
||||
} catch (IOException journalIsCorrupt) { |
||||
System.out.println("DiskLruCache " |
||||
+ directory |
||||
+ " is corrupt: " |
||||
+ journalIsCorrupt.getMessage() |
||||
+ ", removing"); |
||||
cache.delete(); |
||||
} |
||||
} |
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs(); |
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); |
||||
cache.rebuildJournal(); |
||||
return cache; |
||||
} |
||||
|
||||
private void readJournal() throws IOException { |
||||
StrictLineReader reader = |
||||
new StrictLineReader(new FileInputStream(journalFile), DiskLruCacheUtil.US_ASCII); |
||||
try { |
||||
String magic = reader.readLine(); |
||||
String version = reader.readLine(); |
||||
String appVersionString = reader.readLine(); |
||||
String valueCountString = reader.readLine(); |
||||
String blank = reader.readLine(); |
||||
if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion) |
||||
.equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !"" |
||||
.equals(blank)) { |
||||
throw new IOException("unexpected journal header: [" |
||||
+ magic |
||||
+ ", " |
||||
+ version |
||||
+ ", " |
||||
+ valueCountString |
||||
+ ", " |
||||
+ blank |
||||
+ "]"); |
||||
} |
||||
|
||||
int lineCount = 0; |
||||
while (true) { |
||||
try { |
||||
readJournalLine(reader.readLine()); |
||||
lineCount++; |
||||
} catch (EOFException endOfJournal) { |
||||
break; |
||||
} |
||||
} |
||||
redundantOpCount = lineCount - lruEntries.size(); |
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) { |
||||
rebuildJournal(); |
||||
} else { |
||||
journalWriter = new BufferedWriter( |
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), |
||||
DiskLruCacheUtil.US_ASCII)); |
||||
} |
||||
} finally { |
||||
DiskLruCacheUtil.closeQuietly(reader); |
||||
} |
||||
} |
||||
|
||||
private void readJournalLine(String line) throws IOException { |
||||
int firstSpace = line.indexOf(' '); |
||||
if (firstSpace == -1) { |
||||
throw new IOException("unexpected journal line: " + line); |
||||
} |
||||
|
||||
int keyBegin = firstSpace + 1; |
||||
int secondSpace = line.indexOf(' ', keyBegin); |
||||
final String key; |
||||
if (secondSpace == -1) { |
||||
key = line.substring(keyBegin); |
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { |
||||
lruEntries.remove(key); |
||||
return; |
||||
} |
||||
} else { |
||||
key = line.substring(keyBegin, secondSpace); |
||||
} |
||||
|
||||
Entry entry = lruEntries.get(key); |
||||
if (entry == null) { |
||||
entry = new Entry(key); |
||||
lruEntries.put(key, entry); |
||||
} |
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { |
||||
String[] parts = line.substring(secondSpace + 1).split(" "); |
||||
entry.readable = true; |
||||
entry.currentEditor = null; |
||||
entry.setLengths(parts); |
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { |
||||
entry.currentEditor = new Editor(entry); |
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { |
||||
// This work was already done by calling lruEntries.get().
|
||||
} else { |
||||
throw new IOException("unexpected journal line: " + line); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Computes the initial size and collects garbage as a part of opening the |
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted. |
||||
*/ |
||||
private void processJournal() throws IOException { |
||||
deleteIfExists(journalFileTmp); |
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { |
||||
Entry entry = i.next(); |
||||
if (entry.currentEditor == null) { |
||||
for (int t = 0; t < valueCount; t++) { |
||||
size += entry.lengths[t]; |
||||
} |
||||
} else { |
||||
entry.currentEditor = null; |
||||
for (int t = 0; t < valueCount; t++) { |
||||
deleteIfExists(entry.getCleanFile(t)); |
||||
deleteIfExists(entry.getDirtyFile(t)); |
||||
} |
||||
i.remove(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates a new journal that omits redundant information. This replaces the |
||||
* current journal if it exists. |
||||
*/ |
||||
private synchronized void rebuildJournal() throws IOException { |
||||
if (journalWriter != null) { |
||||
journalWriter.close(); |
||||
} |
||||
|
||||
Writer writer = new BufferedWriter( |
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), DiskLruCacheUtil.US_ASCII)); |
||||
try { |
||||
writer.write(MAGIC); |
||||
writer.write("\n"); |
||||
writer.write(VERSION_1); |
||||
writer.write("\n"); |
||||
writer.write(Integer.toString(appVersion)); |
||||
writer.write("\n"); |
||||
writer.write(Integer.toString(valueCount)); |
||||
writer.write("\n"); |
||||
writer.write("\n"); |
||||
|
||||
for (Entry entry : lruEntries.values()) { |
||||
if (entry.currentEditor != null) { |
||||
writer.write(DIRTY + ' ' + entry.key + '\n'); |
||||
} else { |
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); |
||||
} |
||||
} |
||||
} finally { |
||||
writer.close(); |
||||
} |
||||
|
||||
if (journalFile.exists()) { |
||||
renameTo(journalFile, journalFileBackup, true); |
||||
} |
||||
renameTo(journalFileTmp, journalFile, false); |
||||
journalFileBackup.delete(); |
||||
|
||||
journalWriter = new BufferedWriter( |
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), DiskLruCacheUtil.US_ASCII)); |
||||
} |
||||
|
||||
private static void deleteIfExists(File file) throws IOException { |
||||
if (file.exists() && !file.delete()) { |
||||
throw new IOException(); |
||||
} |
||||
} |
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { |
||||
if (deleteDestination) { |
||||
deleteIfExists(to); |
||||
} |
||||
if (!from.renameTo(to)) { |
||||
throw new IOException(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't |
||||
* exist is not currently readable. If a value is returned, it is moved to |
||||
* the head of the LRU queue. |
||||
*/ |
||||
public synchronized Snapshot get(String key) throws IOException { |
||||
checkNotClosed(); |
||||
validateKey(key); |
||||
Entry entry = lruEntries.get(key); |
||||
if (entry == null) { |
||||
return null; |
||||
} |
||||
|
||||
if (!entry.readable) { |
||||
return null; |
||||
} |
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount]; |
||||
try { |
||||
for (int i = 0; i < valueCount; i++) { |
||||
ins[i] = new FileInputStream(entry.getCleanFile(i)); |
||||
} |
||||
} catch (FileNotFoundException e) { |
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) { |
||||
if (ins[i] != null) { |
||||
DiskLruCacheUtil.closeQuietly(ins[i]); |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
redundantOpCount++; |
||||
journalWriter.append(READ + ' ' + key + '\n'); |
||||
if (journalRebuildRequired()) { |
||||
executorService.submit(cleanupCallable); |
||||
} |
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); |
||||
} |
||||
|
||||
/** |
||||
* Returns an editor for the entry named {@code key}, or null if another |
||||
* edit is in progress. |
||||
*/ |
||||
public Editor edit(String key) throws IOException { |
||||
return edit(key, ANY_SEQUENCE_NUMBER); |
||||
} |
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { |
||||
checkNotClosed(); |
||||
validateKey(key); |
||||
Entry entry = lruEntries.get(key); |
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null |
||||
|| entry.sequenceNumber != expectedSequenceNumber)) { |
||||
return null; // Snapshot is stale.
|
||||
} |
||||
if (entry == null) { |
||||
entry = new Entry(key); |
||||
lruEntries.put(key, entry); |
||||
} else if (entry.currentEditor != null) { |
||||
return null; // Another edit is in progress.
|
||||
} |
||||
|
||||
Editor editor = new Editor(entry); |
||||
entry.currentEditor = editor; |
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n'); |
||||
journalWriter.flush(); |
||||
return editor; |
||||
} |
||||
|
||||
/** Returns the directory where this cache stores its data. */ |
||||
public File getDirectory() { |
||||
return directory; |
||||
} |
||||
|
||||
/** |
||||
* Returns the maximum number of bytes that this cache should use to store |
||||
* its data. |
||||
*/ |
||||
public synchronized long getMaxSize() { |
||||
return maxSize; |
||||
} |
||||
|
||||
/** |
||||
* Changes the maximum number of bytes the cache can store and queues a job |
||||
* to trim the existing store, if necessary. |
||||
*/ |
||||
public synchronized void setMaxSize(long maxSize) { |
||||
this.maxSize = maxSize; |
||||
executorService.submit(cleanupCallable); |
||||
} |
||||
|
||||
/** |
||||
* Returns the number of bytes currently being used to store the values in |
||||
* this cache. This may be greater than the max size if a background |
||||
* deletion is pending. |
||||
*/ |
||||
public synchronized long size() { |
||||
return size; |
||||
} |
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException { |
||||
Entry entry = editor.entry; |
||||
if (entry.currentEditor != editor) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) { |
||||
for (int i = 0; i < valueCount; i++) { |
||||
if (!editor.written[i]) { |
||||
editor.abort(); |
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i); |
||||
} |
||||
if (!entry.getDirtyFile(i).exists()) { |
||||
editor.abort(); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
for (int i = 0; i < valueCount; i++) { |
||||
File dirty = entry.getDirtyFile(i); |
||||
if (success) { |
||||
if (dirty.exists()) { |
||||
File clean = entry.getCleanFile(i); |
||||
dirty.renameTo(clean); |
||||
long oldLength = entry.lengths[i]; |
||||
long newLength = clean.length(); |
||||
entry.lengths[i] = newLength; |
||||
size = size - oldLength + newLength; |
||||
} |
||||
} else { |
||||
deleteIfExists(dirty); |
||||
} |
||||
} |
||||
|
||||
redundantOpCount++; |
||||
entry.currentEditor = null; |
||||
if (entry.readable | success) { |
||||
entry.readable = true; |
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); |
||||
if (success) { |
||||
entry.sequenceNumber = nextSequenceNumber++; |
||||
} |
||||
} else { |
||||
lruEntries.remove(entry.key); |
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n'); |
||||
} |
||||
journalWriter.flush(); |
||||
|
||||
if (size > maxSize || journalRebuildRequired()) { |
||||
executorService.submit(cleanupCallable); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* We only rebuild the journal when it will halve the size of the journal |
||||
* and eliminate at least 2000 ops. |
||||
*/ |
||||
private boolean journalRebuildRequired() { |
||||
final int redundantOpCompactThreshold = 2000; |
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size(); |
||||
} |
||||
|
||||
/** |
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries |
||||
* actively being edited cannot be removed. |
||||
* |
||||
* @return true if an entry was removed. |
||||
*/ |
||||
public synchronized boolean remove(String key) throws IOException { |
||||
checkNotClosed(); |
||||
validateKey(key); |
||||
Entry entry = lruEntries.get(key); |
||||
if (entry == null || entry.currentEditor != null) { |
||||
return false; |
||||
} |
||||
|
||||
for (int i = 0; i < valueCount; i++) { |
||||
File file = entry.getCleanFile(i); |
||||
if (file.exists() && !file.delete()) { |
||||
throw new IOException("failed to delete " + file); |
||||
} |
||||
size -= entry.lengths[i]; |
||||
entry.lengths[i] = 0; |
||||
} |
||||
|
||||
redundantOpCount++; |
||||
journalWriter.append(REMOVE + ' ' + key + '\n'); |
||||
lruEntries.remove(key); |
||||
|
||||
if (journalRebuildRequired()) { |
||||
executorService.submit(cleanupCallable); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** Returns true if this cache has been closed. */ |
||||
public synchronized boolean isClosed() { |
||||
return journalWriter == null; |
||||
} |
||||
|
||||
private void checkNotClosed() { |
||||
if (journalWriter == null) { |
||||
throw new IllegalStateException("cache is closed"); |
||||
} |
||||
} |
||||
|
||||
/** Force buffered operations to the filesystem. */ |
||||
public synchronized void flush() throws IOException { |
||||
checkNotClosed(); |
||||
trimToSize(); |
||||
journalWriter.flush(); |
||||
} |
||||
|
||||
/** Closes this cache. Stored values will remain on the filesystem. */ |
||||
public synchronized void close() throws IOException { |
||||
if (journalWriter == null) { |
||||
return; // Already closed.
|
||||
} |
||||
for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { |
||||
if (entry.currentEditor != null) { |
||||
entry.currentEditor.abort(); |
||||
} |
||||
} |
||||
trimToSize(); |
||||
journalWriter.close(); |
||||
journalWriter = null; |
||||
} |
||||
|
||||
private void trimToSize() throws IOException { |
||||
while (size > maxSize) { |
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); |
||||
remove(toEvict.getKey()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Closes the cache and deletes all of its stored values. This will delete |
||||
* all files in the cache directory including files that weren't created by |
||||
* the cache. |
||||
*/ |
||||
public void delete() throws IOException { |
||||
close(); |
||||
DiskLruCacheUtil.deleteContents(directory); |
||||
} |
||||
|
||||
private void validateKey(String key) { |
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); |
||||
if (!matcher.matches()) { |
||||
throw new IllegalArgumentException( |
||||
"keys must match regex " + STRING_KEY_PATTERN + ": \"" + key + "\""); |
||||
} |
||||
} |
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException { |
||||
return DiskLruCacheUtil.readFully(new InputStreamReader(in, DiskLruCacheUtil.UTF_8)); |
||||
} |
||||
|
||||
/** A snapshot of the values for an entry. */ |
||||
public final class Snapshot implements Closeable { |
||||
private final String key; |
||||
private final long sequenceNumber; |
||||
private final InputStream[] ins; |
||||
private final long[] lengths; |
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { |
||||
this.key = key; |
||||
this.sequenceNumber = sequenceNumber; |
||||
this.ins = ins; |
||||
this.lengths = lengths; |
||||
} |
||||
|
||||
/** |
||||
* Returns an editor for this snapshot's entry, or null if either the |
||||
* entry has changed since this snapshot was created or if another edit |
||||
* is in progress. |
||||
*/ |
||||
public Editor edit() throws IOException { |
||||
return DiskLruCache.this.edit(key, sequenceNumber); |
||||
} |
||||
|
||||
/** Returns the unbuffered stream with the value for {@code index}. */ |
||||
public InputStream getInputStream(int index) { |
||||
return ins[index]; |
||||
} |
||||
|
||||
/** Returns the string value for {@code index}. */ |
||||
public String getString(int index) throws IOException { |
||||
return inputStreamToString(getInputStream(index)); |
||||
} |
||||
|
||||
/** Returns the byte length of the value for {@code index}. */ |
||||
public long getLength(int index) { |
||||
return lengths[index]; |
||||
} |
||||
|
||||
public void close() { |
||||
for (InputStream in : ins) { |
||||
DiskLruCacheUtil.closeQuietly(in); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { |
||||
@Override public void write(int b) throws IOException { |
||||
// Eat all writes silently. Nom nom.
|
||||
} |
||||
}; |
||||
|
||||
/** Edits the values for an entry. */ |
||||
public final class Editor { |
||||
private final Entry entry; |
||||
private final boolean[] written; |
||||
private boolean hasErrors; |
||||
private boolean committed; |
||||
|
||||
private Editor(Entry entry) { |
||||
this.entry = entry; |
||||
this.written = (entry.readable) ? null : new boolean[valueCount]; |
||||
} |
||||
|
||||
/** |
||||
* Returns an unbuffered input stream to read the last committed value, |
||||
* or null if no value has been committed. |
||||
*/ |
||||
public InputStream newInputStream(int index) throws IOException { |
||||
synchronized (DiskLruCache.this) { |
||||
if (entry.currentEditor != this) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
if (!entry.readable) { |
||||
return null; |
||||
} |
||||
try { |
||||
return new FileInputStream(entry.getCleanFile(index)); |
||||
} catch (FileNotFoundException e) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the last committed value as a string, or null if no value |
||||
* has been committed. |
||||
*/ |
||||
public String getString(int index) throws IOException { |
||||
InputStream in = newInputStream(index); |
||||
return in != null ? inputStreamToString(in) : null; |
||||
} |
||||
|
||||
/** |
||||
* Returns a new unbuffered output stream to write the value at |
||||
* {@code index}. If the underlying output stream encounters errors |
||||
* when writing to the filesystem, this edit will be aborted when |
||||
* {@link #commit} is called. The returned output stream does not throw |
||||
* IOExceptions. |
||||
*/ |
||||
public OutputStream newOutputStream(int index) throws IOException { |
||||
if (index < 0 || index >= valueCount) { |
||||
throw new IllegalArgumentException("Expected index " |
||||
+ index |
||||
+ " to " |
||||
+ "be greater than 0 and less than the maximum value count " |
||||
+ "of " |
||||
+ valueCount); |
||||
} |
||||
synchronized (DiskLruCache.this) { |
||||
if (entry.currentEditor != this) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
if (!entry.readable) { |
||||
written[index] = true; |
||||
} |
||||
File dirtyFile = entry.getDirtyFile(index); |
||||
FileOutputStream outputStream; |
||||
try { |
||||
outputStream = new FileOutputStream(dirtyFile); |
||||
} catch (FileNotFoundException e) { |
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs(); |
||||
try { |
||||
outputStream = new FileOutputStream(dirtyFile); |
||||
} catch (FileNotFoundException e2) { |
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM; |
||||
} |
||||
} |
||||
return new FaultHidingOutputStream(outputStream); |
||||
} |
||||
} |
||||
|
||||
/** Sets the value at {@code index} to {@code value}. */ |
||||
public void set(int index, String value) throws IOException { |
||||
Writer writer = null; |
||||
try { |
||||
writer = new OutputStreamWriter(newOutputStream(index), DiskLruCacheUtil.UTF_8); |
||||
writer.write(value); |
||||
} finally { |
||||
DiskLruCacheUtil.closeQuietly(writer); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Commits this edit so it is visible to readers. This releases the |
||||
* edit lock so another edit may be started on the same key. |
||||
*/ |
||||
public void commit() throws IOException { |
||||
if (hasErrors) { |
||||
completeEdit(this, false); |
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else { |
||||
completeEdit(this, true); |
||||
} |
||||
committed = true; |
||||
} |
||||
|
||||
/** |
||||
* Aborts this edit. This releases the edit lock so another edit may be |
||||
* started on the same key. |
||||
*/ |
||||
public void abort() throws IOException { |
||||
completeEdit(this, false); |
||||
} |
||||
|
||||
public void abortUnlessCommitted() { |
||||
if (!committed) { |
||||
try { |
||||
abort(); |
||||
} catch (IOException ignored) { |
||||
} |
||||
} |
||||
} |
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream { |
||||
private FaultHidingOutputStream(OutputStream out) { |
||||
super(out); |
||||
} |
||||
|
||||
@Override public void write(int oneByte) { |
||||
try { |
||||
out.write(oneByte); |
||||
} catch (IOException e) { |
||||
hasErrors = true; |
||||
} |
||||
} |
||||
|
||||
@Override public void write(byte[] buffer, int offset, int length) { |
||||
try { |
||||
out.write(buffer, offset, length); |
||||
} catch (IOException e) { |
||||
hasErrors = true; |
||||
} |
||||
} |
||||
|
||||
@Override public void close() { |
||||
try { |
||||
out.close(); |
||||
} catch (IOException e) { |
||||
hasErrors = true; |
||||
} |
||||
} |
||||
|
||||
@Override public void flush() { |
||||
try { |
||||
out.flush(); |
||||
} catch (IOException e) { |
||||
hasErrors = true; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private final class Entry { |
||||
private final String key; |
||||
|
||||
/** Lengths of this entry's files. */ |
||||
private final long[] lengths; |
||||
|
||||
/** True if this entry has ever been published. */ |
||||
private boolean readable; |
||||
|
||||
/** The ongoing edit or null if this entry is not being edited. */ |
||||
private Editor currentEditor; |
||||
|
||||
/** The sequence number of the most recently committed edit to this entry. */ |
||||
private long sequenceNumber; |
||||
|
||||
private Entry(String key) { |
||||
this.key = key; |
||||
this.lengths = new long[valueCount]; |
||||
} |
||||
|
||||
public String getLengths() throws IOException { |
||||
StringBuilder result = new StringBuilder(); |
||||
for (long size : lengths) { |
||||
result.append(' ').append(size); |
||||
} |
||||
return result.toString(); |
||||
} |
||||
|
||||
/** Set lengths using decimal numbers like "10123". */ |
||||
private void setLengths(String[] strings) throws IOException { |
||||
if (strings.length != valueCount) { |
||||
throw invalidLengths(strings); |
||||
} |
||||
|
||||
try { |
||||
for (int i = 0; i < strings.length; i++) { |
||||
lengths[i] = Long.parseLong(strings[i]); |
||||
} |
||||
} catch (NumberFormatException e) { |
||||
throw invalidLengths(strings); |
||||
} |
||||
} |
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException { |
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); |
||||
} |
||||
|
||||
public File getCleanFile(int i) { |
||||
return new File(directory, key + "." + i); |
||||
} |
||||
|
||||
public File getDirtyFile(int i) { |
||||
return new File(directory, key + "." + i + ".tmp"); |
||||
} |
||||
} |
||||
} |
@ -1,77 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2010 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 com.arialyy.frame.cache.diskcache; |
||||
|
||||
import java.io.Closeable; |
||||
import java.io.File; |
||||
import java.io.IOException; |
||||
import java.io.Reader; |
||||
import java.io.StringWriter; |
||||
import java.nio.charset.Charset; |
||||
|
||||
/** Junk drawer of utility methods. */ |
||||
final class DiskLruCacheUtil { |
||||
static final Charset US_ASCII = Charset.forName("US-ASCII"); |
||||
static final Charset UTF_8 = Charset.forName("UTF-8"); |
||||
|
||||
private DiskLruCacheUtil() { |
||||
} |
||||
|
||||
static String readFully(Reader reader) throws IOException { |
||||
try { |
||||
StringWriter writer = new StringWriter(); |
||||
char[] buffer = new char[1024]; |
||||
int count; |
||||
while ((count = reader.read(buffer)) != -1) { |
||||
writer.write(buffer, 0, count); |
||||
} |
||||
return writer.toString(); |
||||
} finally { |
||||
reader.close(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file |
||||
* could not be deleted, or if {@code dir} is not a readable directory. |
||||
*/ |
||||
static void deleteContents(File dir) throws IOException { |
||||
File[] files = dir.listFiles(); |
||||
if (files == null) { |
||||
throw new IOException("not a readable directory: " + dir); |
||||
} |
||||
for (File file : files) { |
||||
if (file.isDirectory()) { |
||||
deleteContents(file); |
||||
} |
||||
if (!file.delete()) { |
||||
throw new IOException("failed to delete file: " + file); |
||||
} |
||||
} |
||||
} |
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) { |
||||
if (closeable != null) { |
||||
try { |
||||
closeable.close(); |
||||
} catch (RuntimeException rethrown) { |
||||
throw rethrown; |
||||
} catch (Exception ignored) { |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,194 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2012 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 com.arialyy.frame.cache.diskcache; |
||||
|
||||
import java.io.ByteArrayOutputStream; |
||||
import java.io.Closeable; |
||||
import java.io.EOFException; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.nio.charset.Charset; |
||||
|
||||
/** |
||||
* Buffers input from an {@link InputStream} for reading lines. |
||||
* |
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends |
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated |
||||
* line at end of input is invalid and will be ignored, the caller may use {@code |
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}. |
||||
* |
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based |
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction |
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different |
||||
* end-of-input reporting and a more restrictive definition of a line. |
||||
* |
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 |
||||
* and 10, respectively, and the representation of no other character contains these values. |
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. |
||||
* The default charset is US_ASCII. |
||||
*/ |
||||
class StrictLineReader implements Closeable { |
||||
private static final byte CR = (byte) '\r'; |
||||
private static final byte LF = (byte) '\n'; |
||||
|
||||
private final InputStream in; |
||||
private final Charset charset; |
||||
|
||||
/* |
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end |
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is |
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying |
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. |
||||
*/ |
||||
private byte[] buf; |
||||
private int pos; |
||||
private int end; |
||||
|
||||
/** |
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity. |
||||
* |
||||
* @param in the {@code InputStream} to read data from. |
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are |
||||
* supported. |
||||
* @throws NullPointerException if {@code in} or {@code charset} is null. |
||||
* @throws IllegalArgumentException if the specified charset is not supported. |
||||
*/ |
||||
public StrictLineReader(InputStream in, Charset charset) { |
||||
this(in, 8192, charset); |
||||
} |
||||
|
||||
/** |
||||
* Constructs a new {@code LineReader} with the specified capacity and charset. |
||||
* |
||||
* @param in the {@code InputStream} to read data from. |
||||
* @param capacity the capacity of the buffer. |
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are |
||||
* supported. |
||||
* @throws NullPointerException if {@code in} or {@code charset} is null. |
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero |
||||
* or the specified charset is not supported. |
||||
*/ |
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) { |
||||
if (in == null || charset == null) { |
||||
throw new NullPointerException(); |
||||
} |
||||
if (capacity < 0) { |
||||
throw new IllegalArgumentException("capacity <= 0"); |
||||
} |
||||
if (!(charset.equals(DiskLruCacheUtil.US_ASCII))) { |
||||
throw new IllegalArgumentException("Unsupported encoding"); |
||||
} |
||||
|
||||
this.in = in; |
||||
this.charset = charset; |
||||
buf = new byte[capacity]; |
||||
} |
||||
|
||||
/** |
||||
* Closes the reader by closing the underlying {@code InputStream} and |
||||
* marking this reader as closed. |
||||
* |
||||
* @throws IOException for errors when closing the underlying {@code InputStream}. |
||||
*/ |
||||
public void close() throws IOException { |
||||
synchronized (in) { |
||||
if (buf != null) { |
||||
buf = null; |
||||
in.close(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, |
||||
* this end of line marker is not included in the result. |
||||
* |
||||
* @return the next line from the input. |
||||
* @throws IOException for underlying {@code InputStream} errors. |
||||
* @throws EOFException for the end of source stream. |
||||
*/ |
||||
public String readLine() throws IOException { |
||||
synchronized (in) { |
||||
if (buf == null) { |
||||
throw new IOException("LineReader is closed"); |
||||
} |
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) { |
||||
fillBuf(); |
||||
} |
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) { |
||||
if (buf[i] == LF) { |
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; |
||||
String res = new String(buf, pos, lineEnd - pos, charset.name()); |
||||
pos = i + 1; |
||||
return res; |
||||
} |
||||
} |
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { |
||||
@Override public String toString() { |
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; |
||||
try { |
||||
return new String(buf, 0, length, charset.name()); |
||||
} catch (UnsupportedEncodingException e) { |
||||
throw new AssertionError(e); // Since we control the charset this will never happen.
|
||||
} |
||||
} |
||||
}; |
||||
|
||||
while (true) { |
||||
out.write(buf, pos, end - pos); |
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1; |
||||
fillBuf(); |
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) { |
||||
if (buf[i] == LF) { |
||||
if (i != pos) { |
||||
out.write(buf, pos, i - pos); |
||||
} |
||||
pos = i + 1; |
||||
return out.toString(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
public boolean hasUnterminatedLine() { |
||||
return end == -1; |
||||
} |
||||
|
||||
/** |
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1, |
||||
* depending on the desired outcome if the function throws. |
||||
*/ |
||||
private void fillBuf() throws IOException { |
||||
int result = in.read(buf, 0, buf.length); |
||||
if (result == -1) { |
||||
throw new EOFException(); |
||||
} |
||||
pos = 0; |
||||
end = result; |
||||
} |
||||
} |
@ -0,0 +1,67 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common; |
||||
|
||||
import android.os.Handler; |
||||
|
||||
/** |
||||
* 线程任务状态 |
||||
*/ |
||||
public interface IThreadState extends Handler.Callback { |
||||
int STATE_STOP = 0x01; |
||||
int STATE_FAIL = 0x02; |
||||
int STATE_CANCEL = 0x03; |
||||
int STATE_COMPLETE = 0x04; |
||||
int STATE_RUNNING = 0x05; |
||||
int STATE_UPDATE_PROGRESS = 0x06; |
||||
String KEY_RETRY = "KEY_RETRY"; |
||||
String KEY_ERROR_INFO = "KEY_ERROR_INFO"; |
||||
|
||||
/** |
||||
* 任务是否已经停止 |
||||
* |
||||
* @return true 任务已停止 |
||||
*/ |
||||
boolean isStop(); |
||||
|
||||
/** |
||||
* 任务是否已经失败 |
||||
* |
||||
* @return true 任务已失败 |
||||
*/ |
||||
boolean isFail(); |
||||
|
||||
/** |
||||
* 任务是否已经完成 |
||||
* |
||||
* @return true 任务已完成 |
||||
*/ |
||||
boolean isComplete(); |
||||
|
||||
/** |
||||
* 任务是否已经取消 |
||||
* |
||||
* @return true 任务已取消 |
||||
*/ |
||||
boolean isCancel(); |
||||
|
||||
/** |
||||
* 获取当前任务进度 |
||||
* |
||||
* @return 任务当前进度 |
||||
*/ |
||||
long getCurrentProgress(); |
||||
} |
@ -0,0 +1,190 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common; |
||||
|
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import com.arialyy.aria.core.download.BaseDListener; |
||||
import com.arialyy.aria.core.inf.AbsNormalEntity; |
||||
import com.arialyy.aria.core.inf.AbsTaskWrapper; |
||||
import com.arialyy.aria.core.inf.IEventListener; |
||||
import com.arialyy.aria.core.manager.ThreadTaskManager; |
||||
import com.arialyy.aria.util.ALog; |
||||
import java.io.File; |
||||
|
||||
public abstract class NormalFileer<ENTITY extends AbsNormalEntity, TASK_WRAPPER extends AbsTaskWrapper<ENTITY>> |
||||
extends AbsFileer<ENTITY, TASK_WRAPPER> { |
||||
private ThreadStateManager mStateManager; |
||||
private Handler mStateHandler; |
||||
protected int mTotalThreadNum; //总线程数
|
||||
|
||||
protected NormalFileer(IEventListener listener, TASK_WRAPPER wrapper) { |
||||
super(listener, wrapper); |
||||
} |
||||
|
||||
/** |
||||
* 处理新任务 |
||||
* |
||||
* @return {@code true}创建新任务成功 |
||||
*/ |
||||
protected abstract boolean handleNewTask(); |
||||
|
||||
/** |
||||
* 选择单任务线程的类型 |
||||
*/ |
||||
protected abstract AbsThreadTask selectThreadTask(SubThreadConfig<TASK_WRAPPER> config); |
||||
|
||||
/** |
||||
* 设置最大下载/上传速度 |
||||
* |
||||
* @param maxSpeed 单位为:kb |
||||
*/ |
||||
public void setMaxSpeed(int maxSpeed) { |
||||
for (int i = 0; i < mTotalThreadNum; i++) { |
||||
AbsThreadTask task = getTaskList().get(i); |
||||
if (task != null) { |
||||
task.setMaxSpeed(maxSpeed); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override protected void onPostPre() { |
||||
super.onPostPre(); |
||||
mTotalThreadNum = mRecord.threadNum; |
||||
} |
||||
|
||||
@Override protected IThreadState getStateManager(Looper looper) { |
||||
mStateManager = new ThreadStateManager(looper, mRecord, mListener); |
||||
mStateHandler = new Handler(looper, mStateManager); |
||||
return mStateManager; |
||||
} |
||||
|
||||
@Override protected void handleTask() { |
||||
if (mTaskWrapper.isSupportBP()) { |
||||
handleBreakpoint(); |
||||
} else { |
||||
handleNoSupportBP(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 启动断点任务时,创建单线程任务 |
||||
* |
||||
* @param record 线程记录 |
||||
* @param startNum 启动的线程数 |
||||
*/ |
||||
private AbsThreadTask createSingThreadTask(ThreadRecord record, int startNum) { |
||||
SubThreadConfig<TASK_WRAPPER> config = new SubThreadConfig<>(); |
||||
config.url = mEntity.isRedirect() ? mEntity.getRedirectUrl() : mEntity.getUrl(); |
||||
config.tempFile = |
||||
mRecord.isBlock ? new File( |
||||
String.format(RecordHandler.SUB_PATH, mTempFile.getPath(), record.threadId)) |
||||
: mTempFile; |
||||
config.isBlock = mRecord.isBlock; |
||||
config.isOpenDynamicFile = mRecord.isOpenDynamicFile; |
||||
config.startThreadNum = startNum; |
||||
config.taskWrapper = mTaskWrapper; |
||||
config.record = record; |
||||
config.stateHandler = mStateHandler; |
||||
return selectThreadTask(config); |
||||
} |
||||
|
||||
private void handleBreakpoint() { |
||||
long fileLength = mEntity.getFileSize(); |
||||
long blockSize = fileLength / mTotalThreadNum; |
||||
long currentProgress = 0; |
||||
|
||||
mRecord.fileLength = fileLength; |
||||
if (mTaskWrapper.isNewTask() && !handleNewTask()) { |
||||
return; |
||||
} |
||||
|
||||
int startNum = mRecord.threadNum; |
||||
for (ThreadRecord tr : mRecord.threadRecords) { |
||||
if (!tr.isComplete) { |
||||
startNum++; |
||||
} |
||||
} |
||||
|
||||
for (int i = 0; i < mTotalThreadNum; i++) { |
||||
long startL = i * blockSize, endL = (i + 1) * blockSize; |
||||
ThreadRecord tr = mRecord.threadRecords.get(i); |
||||
|
||||
if (tr.isComplete) {//该线程已经完成
|
||||
currentProgress += endL - startL; |
||||
ALog.d(TAG, String.format("任务【%s】线程__%s__已完成", mTaskWrapper.getEntity().getFileName(), i)); |
||||
mStateHandler.obtainMessage(IThreadState.STATE_COMPLETE).sendToTarget(); |
||||
if (mStateManager.isComplete()) { |
||||
mRecord.deleteData(); |
||||
mListener.onComplete(); |
||||
return; |
||||
} |
||||
continue; |
||||
} |
||||
|
||||
//如果有记录,则恢复任务
|
||||
long r = tr.startLocation; |
||||
//记录的位置需要在线程区间中
|
||||
if (startL < r && r <= (i == (mTotalThreadNum - 1) ? fileLength : endL)) { |
||||
currentProgress += r - startL; |
||||
} |
||||
ALog.d(TAG, String.format("任务【%s】线程__%s__恢复任务", mEntity.getFileName(), i)); |
||||
|
||||
AbsThreadTask task = createSingThreadTask(tr, startNum); |
||||
if (task == null) return; |
||||
getTaskList().put(tr.threadId, task); |
||||
} |
||||
if (currentProgress != 0 && currentProgress != mEntity.getCurrentProgress()) { |
||||
ALog.d(TAG, String.format("进度修正,当前进度:%s", currentProgress)); |
||||
mEntity.setCurrentProgress(currentProgress); |
||||
} |
||||
mStateHandler.obtainMessage(IThreadState.STATE_UPDATE_PROGRESS, currentProgress) |
||||
.sendToTarget(); |
||||
startThreadTask(); |
||||
} |
||||
|
||||
/** |
||||
* 启动单线程任务 |
||||
*/ |
||||
private void startThreadTask() { |
||||
if (isBreak()) { |
||||
return; |
||||
} |
||||
if (mStateManager.getCurrentProgress() > 0) { |
||||
mListener.onResume(mStateManager.getCurrentProgress()); |
||||
} else { |
||||
mListener.onStart(mStateManager.getCurrentProgress()); |
||||
} |
||||
for (int i = 0; i < getTaskList().size(); i++) { |
||||
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), getTaskList().get(i)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 处理不支持断点的任务 |
||||
*/ |
||||
private void handleNoSupportBP() { |
||||
if (mListener instanceof BaseDListener) { |
||||
((BaseDListener) mListener).supportBreakpoint(false); |
||||
} |
||||
|
||||
AbsThreadTask task = createSingThreadTask(mRecord.threadRecords.get(0), 1); |
||||
if (task == null) return; |
||||
getTaskList().put(0, task); |
||||
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task); |
||||
mListener.onStart(0); |
||||
} |
||||
} |
@ -0,0 +1,419 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common; |
||||
|
||||
import com.arialyy.aria.core.config.Configuration; |
||||
import com.arialyy.aria.core.download.DTaskWrapper; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.core.download.m3u8.M3U8FileLoader; |
||||
import com.arialyy.aria.core.inf.AbsNormalEntity; |
||||
import com.arialyy.aria.core.inf.AbsTaskWrapper; |
||||
import com.arialyy.aria.core.inf.ITaskWrapper; |
||||
import com.arialyy.aria.core.upload.UploadEntity; |
||||
import com.arialyy.aria.orm.DbEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import com.arialyy.aria.util.DbDataHelper; |
||||
import java.io.File; |
||||
import java.util.ArrayList; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Properties; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* 处理任务记录,分配线程区间 |
||||
*/ |
||||
public class RecordHandler { |
||||
private final String TAG = "RecordHandler"; |
||||
|
||||
public static final int TYPE_DOWNLOAD = 1; |
||||
public static final int TYPE_UPLOAD = 2; |
||||
|
||||
private static final String STATE = "_state_"; |
||||
private static final String RECORD = "_record_"; |
||||
/** |
||||
* 小于1m的文件不启用多线程 |
||||
*/ |
||||
private static final long SUB_LEN = 1024 * 1024; |
||||
|
||||
/** |
||||
* 分块文件路径 |
||||
*/ |
||||
public static final String SUB_PATH = "%s.%s.part"; |
||||
|
||||
@Deprecated private File mConfigFile; |
||||
private TaskRecord mRecord; |
||||
private AbsTaskWrapper mTaskWrapper; |
||||
private AbsNormalEntity mEntity; |
||||
|
||||
RecordHandler(AbsTaskWrapper wrapper) { |
||||
mTaskWrapper = wrapper; |
||||
mEntity = (AbsNormalEntity) mTaskWrapper.getEntity(); |
||||
} |
||||
|
||||
/** |
||||
* 获取任务记录,如果任务记录存在,检查任务记录 |
||||
* 检查记录 对于分块任务: 子分块不存在或被删除,子线程将重新下载 |
||||
* 对于普通任务: 预下载文件不存在,则任务任务呗删除 |
||||
* 如果任务记录不存在或线程记录不存在,初始化记录 |
||||
* |
||||
* @return 任务记录 |
||||
*/ |
||||
TaskRecord getRecord() { |
||||
mConfigFile = new File(CommonUtil.getFileConfigPath(false, mEntity.getFileName())); |
||||
if (mConfigFile.exists()) { |
||||
convertDb(); |
||||
} else { |
||||
mRecord = DbDataHelper.getTaskRecord(getFilePath()); |
||||
if (mRecord == null) { |
||||
initRecord(true); |
||||
} else { |
||||
if (mRecord.threadRecords == null || mRecord.threadRecords.isEmpty()) { |
||||
initRecord(false); |
||||
} |
||||
|
||||
if (mTaskWrapper.getRequestType() == ITaskWrapper.M3U8_FILE) { |
||||
handleM3U8Record(); |
||||
} else { |
||||
if (mRecord.isBlock) { |
||||
handleBlockRecord(); |
||||
} else if (!mTaskWrapper.isSupportBP()) { |
||||
handleNoSupportBPRecord(); |
||||
} else { |
||||
handleSingleThreadRecord(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
saveRecord(); |
||||
return mRecord; |
||||
} |
||||
|
||||
/** |
||||
* 处理m3u8记录, |
||||
* 1、如果分片文件存在,并且分片文件的记录没有完成,则需要删除该分片文件 |
||||
* 2、如果记录显示已完成,但是分片文件不存在,则重新开始该分片 |
||||
* 3、如果记录显示已完成,并且文件存在,记录当前任务进度 |
||||
*/ |
||||
private void handleM3U8Record() { |
||||
DTaskWrapper wrapper = (DTaskWrapper) mTaskWrapper; |
||||
String cacheDir = wrapper.asM3U8().getCacheDir(); |
||||
long currentProgress = 0; |
||||
for (ThreadRecord record : mRecord.threadRecords) { |
||||
File temp = new File(M3U8FileLoader.getTsFilePath(cacheDir, record.threadId)); |
||||
if (!record.isComplete) { |
||||
if (temp.exists()) { |
||||
temp.delete(); |
||||
} |
||||
record.startLocation = 0; |
||||
ALog.d(TAG, String.format("分片【%s】未完成,将重新下载该分片", record.threadId)); |
||||
} else { |
||||
if (!temp.exists()) { |
||||
record.startLocation = 0; |
||||
record.isComplete = false; |
||||
ALog.w(TAG, String.format("分片【%s】不存在,将重新下载该分片", record.threadId)); |
||||
} else { |
||||
currentProgress += temp.length(); |
||||
} |
||||
} |
||||
} |
||||
wrapper.getEntity().setCurrentProgress(currentProgress); |
||||
} |
||||
|
||||
/** |
||||
* 处理不支持断点的记录 |
||||
*/ |
||||
private void handleNoSupportBPRecord() { |
||||
ThreadRecord tr = mRecord.threadRecords.get(0); |
||||
tr.startLocation = 0; |
||||
tr.endLocation = mEntity.getFileSize(); |
||||
tr.key = mRecord.filePath; |
||||
tr.blockLen = tr.endLocation; |
||||
tr.isComplete = false; |
||||
} |
||||
|
||||
/** |
||||
* 处理单线程的任务的记录 |
||||
*/ |
||||
private void handleSingleThreadRecord() { |
||||
File file = new File(mRecord.filePath); |
||||
ThreadRecord tr = mRecord.threadRecords.get(0); |
||||
if (!file.exists()) { |
||||
ALog.w(TAG, String.format("文件【%s】不存在,任务将重新开始", file.getPath())); |
||||
tr.startLocation = 0; |
||||
tr.isComplete = false; |
||||
tr.endLocation = mEntity.getFileSize(); |
||||
} else if (mRecord.isOpenDynamicFile) { |
||||
if (file.length() > mEntity.getFileSize()) { |
||||
ALog.i(TAG, String.format("文件【%s】错误,任务重新开始", file.getPath())); |
||||
file.delete(); |
||||
tr.startLocation = 0; |
||||
tr.isComplete = false; |
||||
tr.endLocation = mEntity.getFileSize(); |
||||
} else if (file.length() == mEntity.getFileSize()) { |
||||
tr.isComplete = true; |
||||
} else { |
||||
if (file.length() != tr.startLocation) { |
||||
ALog.i(TAG, String.format("修正【%s】的进度记录为:%s", file.getPath(), file.length())); |
||||
tr.startLocation = file.length(); |
||||
tr.isComplete = false; |
||||
} |
||||
} |
||||
} |
||||
mTaskWrapper.setNewTask(false); |
||||
} |
||||
|
||||
/** |
||||
* 处理分块任务的记录,分块文件(blockFileLen)长度必须需要小于等于线程区间(threadRectLen)的长度 |
||||
*/ |
||||
private void handleBlockRecord() { |
||||
// 默认线程分块长度
|
||||
long normalRectLen = mEntity.getFileSize() / mRecord.threadRecords.size(); |
||||
for (ThreadRecord tr : mRecord.threadRecords) { |
||||
long threadRect = tr.blockLen; |
||||
|
||||
File temp = new File(String.format(SUB_PATH, mRecord.filePath, tr.threadId)); |
||||
if (!temp.exists()) { |
||||
ALog.i(TAG, String.format("分块文件【%s】不存在,该分块将重新开始", temp.getPath())); |
||||
tr.isComplete = false; |
||||
tr.startLocation = tr.threadId * normalRectLen; |
||||
} else { |
||||
if (!tr.isComplete) { |
||||
ALog.i(TAG, String.format( |
||||
"startLocation = %s; endLocation = %s; block = %s; tempLen = %s; threadId = %s", |
||||
tr.startLocation, tr.endLocation, threadRect, temp.length(), tr.threadId)); |
||||
|
||||
long blockFileLen = temp.length(); // 磁盘中的分块文件长度
|
||||
/* |
||||
* 检查磁盘中的分块文件 |
||||
*/ |
||||
if (blockFileLen > threadRect) { |
||||
ALog.i(TAG, String.format("分块【%s】错误,分块长度【%s】 > 线程区间长度【%s】,将重新开始该分块", |
||||
tr.threadId, blockFileLen, threadRect)); |
||||
temp.delete(); |
||||
tr.startLocation = tr.threadId * threadRect; |
||||
continue; |
||||
} |
||||
|
||||
long realLocation = |
||||
tr.threadId * normalRectLen + blockFileLen; //正常情况下,该线程的startLocation的位置
|
||||
/* |
||||
* 检查记录文件 |
||||
*/ |
||||
if (blockFileLen == threadRect) { |
||||
ALog.i(TAG, String.format("分块【%s】已完成,更新记录", temp.getPath())); |
||||
tr.startLocation = blockFileLen; |
||||
tr.isComplete = true; |
||||
} else if (tr.startLocation != realLocation) { // 处理记录小于分块文件长度的情况
|
||||
ALog.i(TAG, String.format("修正分块【%s】的进度记录为:%s", temp.getPath(), realLocation)); |
||||
tr.startLocation = realLocation; |
||||
} |
||||
} else { |
||||
ALog.i(TAG, String.format("分块【%s】已完成", temp.getPath())); |
||||
} |
||||
} |
||||
} |
||||
mTaskWrapper.setNewTask(false); |
||||
} |
||||
|
||||
/** |
||||
* convertDb 是兼容性代码 从3.4.1开始,线程配置信息将存储在数据库中。 将配置文件的内容复制到数据库中,并将配置文件删除 |
||||
*/ |
||||
private void convertDb() { |
||||
List<RecordWrapper> records = |
||||
DbEntity.findRelationData(RecordWrapper.class, "TaskRecord.filePath=?", |
||||
getFilePath()); |
||||
if (records == null || records.size() == 0) { |
||||
Properties pro = CommonUtil.loadConfig(mConfigFile); |
||||
if (pro.isEmpty()) { |
||||
ALog.d(TAG, "老版本的线程记录为空,任务为新任务"); |
||||
initRecord(true); |
||||
return; |
||||
} |
||||
|
||||
Set<Object> keys = pro.keySet(); |
||||
// 老版本记录是5s存一次,但是5s中内,如果线程执行完成,record记录是没有的,只有state记录...
|
||||
// 第一步应该是record 和 state去重取正确的线程数
|
||||
Set<Integer> set = new HashSet<>(); |
||||
for (Object key : keys) { |
||||
String str = String.valueOf(key); |
||||
int i = Integer.parseInt(str.substring(str.length() - 1)); |
||||
set.add(i); |
||||
} |
||||
int threadNum = set.size(); |
||||
if (threadNum == 0) { |
||||
ALog.d(TAG, "线程数为空,任务为新任务"); |
||||
initRecord(true); |
||||
return; |
||||
} |
||||
mTaskWrapper.setNewTask(false); |
||||
mRecord = createTaskRecord(threadNum); |
||||
mRecord.isOpenDynamicFile = false; |
||||
mRecord.isBlock = false; |
||||
File tempFile = new File(getFilePath()); |
||||
for (int i = 0; i < threadNum; i++) { |
||||
ThreadRecord tRecord = new ThreadRecord(); |
||||
tRecord.key = mRecord.filePath; |
||||
Object state = pro.getProperty(tempFile.getName() + STATE + i); |
||||
Object record = pro.getProperty(tempFile.getName() + RECORD + i); |
||||
if (state != null && Integer.parseInt(String.valueOf(state)) == 1) { |
||||
tRecord.isComplete = true; |
||||
continue; |
||||
} |
||||
if (record != null) { |
||||
long temp = Long.parseLong(String.valueOf(record)); |
||||
tRecord.startLocation = temp > 0 ? temp : 0; |
||||
} else { |
||||
tRecord.startLocation = 0; |
||||
} |
||||
mRecord.threadRecords.add(tRecord); |
||||
} |
||||
mConfigFile.delete(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 初始化任务记录,分配线程区间 |
||||
* |
||||
* @param newRecord {@code true} 需要创建新{@link TaskRecord} |
||||
*/ |
||||
private void initRecord(boolean newRecord) { |
||||
if (newRecord) { |
||||
mRecord = createTaskRecord(getNewTaskThreadNum()); |
||||
} |
||||
mTaskWrapper.setNewTask(true); |
||||
// 处理线程区间记录
|
||||
int requestType = mTaskWrapper.getRequestType(); |
||||
long blockSize = mEntity.getFileSize() / mRecord.threadNum; |
||||
for (int i = 0; i < mRecord.threadNum; i++) { |
||||
long startL = i * blockSize, endL = (i + 1) * blockSize; |
||||
ThreadRecord tr; |
||||
tr = new ThreadRecord(); |
||||
tr.key = mRecord.filePath; |
||||
tr.threadId = i; |
||||
tr.startLocation = startL; |
||||
tr.isComplete = false; |
||||
if (requestType == ITaskWrapper.M3U8_FILE) { |
||||
tr.startLocation = 0; |
||||
tr.threadType = TaskRecord.TYPE_M3U8; |
||||
tr.m3u8url = ((DTaskWrapper) mTaskWrapper).asM3U8().getUrls().get(i); |
||||
} else { |
||||
tr.threadType = TaskRecord.TYPE_HTTP_FTP; |
||||
//最后一个线程的结束位置即为文件的总长度
|
||||
if (i == (mRecord.threadNum - 1)) { |
||||
endL = mEntity.getFileSize(); |
||||
} |
||||
tr.endLocation = endL; |
||||
tr.blockLen = CommonUtil.getBlockLen(mEntity.getFileSize(), i, mRecord.threadNum); |
||||
} |
||||
mRecord.threadRecords.add(tr); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 创建任务记录 |
||||
* |
||||
* @param threadNum 线程总数 |
||||
*/ |
||||
private TaskRecord createTaskRecord(int threadNum) { |
||||
TaskRecord record = new TaskRecord(); |
||||
record.fileName = mEntity.getFileName(); |
||||
record.filePath = getFilePath(); |
||||
record.threadRecords = new ArrayList<>(); |
||||
record.threadNum = threadNum; |
||||
// 处理分块和动态文件参数
|
||||
if (getRecordType() == TYPE_DOWNLOAD) { |
||||
record.isBlock = threadNum > 1 && Configuration.getInstance().downloadCfg.isUseBlock(); |
||||
// 线程数为1,或者使用了分块,则认为是使用动态长度文件
|
||||
record.isOpenDynamicFile = threadNum == 1 || record.isBlock; |
||||
} else { |
||||
record.isBlock = false; |
||||
} |
||||
if (mTaskWrapper.getRequestType() == ITaskWrapper.M3U8_FILE) { |
||||
record.taskType = TaskRecord.TYPE_M3U8; |
||||
record.isOpenDynamicFile = true; |
||||
} else { |
||||
record.taskType = TaskRecord.TYPE_HTTP_FTP; |
||||
} |
||||
record.isGroupRecord = mEntity.isGroupChild(); |
||||
if (record.isGroupRecord) { |
||||
if (mEntity instanceof DownloadEntity) { |
||||
record.dGroupHash = ((DownloadEntity) mEntity).getGroupHash(); |
||||
} |
||||
} |
||||
return record; |
||||
} |
||||
|
||||
/** |
||||
* 保存任务记录 |
||||
*/ |
||||
private void saveRecord() { |
||||
mRecord.threadNum = mRecord.threadRecords.size(); |
||||
mRecord.save(); |
||||
DbEntity.saveAll(mRecord.threadRecords); |
||||
ALog.d(TAG, String.format("保存记录,线程记录数:%s", mRecord.threadRecords.size())); |
||||
} |
||||
|
||||
/** |
||||
* 获取记录类型 |
||||
* |
||||
* @return {@link #TYPE_DOWNLOAD}、{@link #TYPE_UPLOAD} |
||||
*/ |
||||
private int getRecordType() { |
||||
if (mEntity instanceof DownloadEntity) { |
||||
return TYPE_DOWNLOAD; |
||||
} else { |
||||
return TYPE_UPLOAD; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取任务路径 |
||||
* |
||||
* @return 任务文件路径 |
||||
*/ |
||||
private String getFilePath() { |
||||
if (mEntity instanceof DownloadEntity) { |
||||
return ((DownloadEntity) mTaskWrapper.getEntity()).getFilePath(); |
||||
} else { |
||||
return ((UploadEntity) mTaskWrapper.getEntity()).getFilePath(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 小于1m的文件或是任务组的子任务、线程数强制为1 |
||||
* 不支持断点或chunked模式的线程数都为,线程数强制为1 |
||||
*/ |
||||
private int getNewTaskThreadNum() { |
||||
if (getRecordType() == TYPE_DOWNLOAD) { |
||||
if (mTaskWrapper.getRequestType() == ITaskWrapper.M3U8_FILE) { |
||||
return ((DTaskWrapper) mTaskWrapper).asM3U8().getUrls().size(); |
||||
} |
||||
if (!mTaskWrapper.isSupportBP() || mTaskWrapper.asHttp().isChunked()) { |
||||
return 1; |
||||
} |
||||
int threadNum = Configuration.getInstance().downloadCfg.getThreadNum(); |
||||
return mEntity.getFileSize() <= SUB_LEN |
||||
|| mEntity.isGroupChild() |
||||
|| threadNum == 1 |
||||
? 1 |
||||
: threadNum; |
||||
} else { |
||||
return 1; |
||||
} |
||||
} |
||||
} |
@ -1,82 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common; |
||||
|
||||
import com.arialyy.aria.util.ALog; |
||||
|
||||
/** |
||||
* Created by lyy on 2017/1/18. |
||||
* 状态常量 |
||||
*/ |
||||
public class StateConstance { |
||||
private static final String TAG = "StateConstance"; |
||||
public int CANCEL_NUM = 0; |
||||
public int STOP_NUM = 0; |
||||
public int FAIL_NUM = 0; |
||||
public int COMPLETE_THREAD_NUM = 0; |
||||
public int START_THREAD_NUM; //启动的线程数
|
||||
public long CURRENT_LOCATION = 0; //当前下载进度
|
||||
public boolean isCancel = false; |
||||
public boolean isStop = false; |
||||
public TaskRecord TASK_RECORD; |
||||
|
||||
StateConstance() { |
||||
} |
||||
|
||||
public void resetState() { |
||||
isCancel = false; |
||||
isStop = false; |
||||
CANCEL_NUM = 0; |
||||
STOP_NUM = 0; |
||||
FAIL_NUM = 0; |
||||
COMPLETE_THREAD_NUM = 0; |
||||
START_THREAD_NUM = 0; |
||||
CURRENT_LOCATION = 0; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经停止 |
||||
*/ |
||||
public boolean isStop() { |
||||
//ALog.d(TAG, String.format("stop_thread_num=%s; start_thread_num=%s; complete_thread_num=%s",
|
||||
// STOP_NUM, START_THREAD_NUM, COMPLETE_THREAD_NUM));
|
||||
return STOP_NUM == START_THREAD_NUM || STOP_NUM + COMPLETE_THREAD_NUM == START_THREAD_NUM; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经失败 |
||||
*/ |
||||
public boolean isFail() { |
||||
//ALog.d(TAG, String.format("fail_thread_num=%s; start_thread_num=%s; complete_thread_num=%s",
|
||||
// FAIL_NUM, START_THREAD_NUM, COMPLETE_THREAD_NUM));
|
||||
return COMPLETE_THREAD_NUM != START_THREAD_NUM |
||||
&& (FAIL_NUM == START_THREAD_NUM || FAIL_NUM + COMPLETE_THREAD_NUM == START_THREAD_NUM); |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经完成 |
||||
*/ |
||||
public boolean isComplete() { |
||||
return COMPLETE_THREAD_NUM == START_THREAD_NUM; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经取消 |
||||
*/ |
||||
public boolean isCancel() { |
||||
return CANCEL_NUM == START_THREAD_NUM; |
||||
} |
||||
} |
@ -0,0 +1,223 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common; |
||||
|
||||
import android.os.Bundle; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.os.Message; |
||||
import com.arialyy.aria.core.inf.IEventListener; |
||||
import com.arialyy.aria.exception.BaseException; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.FileUtil; |
||||
import java.io.File; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 线程任务管理器,用于处理多线程下载时任务的状态回调 |
||||
*/ |
||||
public class ThreadStateManager implements IThreadState { |
||||
private final String TAG = "ThreadTaskStateManager"; |
||||
|
||||
/** |
||||
* 任务状态回调 |
||||
*/ |
||||
private IEventListener mListener; |
||||
private int mThreadNum; // 启动的线程总数
|
||||
private int mCancelNum = 0; // 已经取消的线程的数
|
||||
private int mStopNum = 0; // 已经停止的线程数
|
||||
private int mFailNum = 0; // 失败的线程数
|
||||
private int mCompleteNum = 0; // 完成的线程数
|
||||
private long mProgress; //当前总进度
|
||||
private TaskRecord mTaskRecord; // 任务记录
|
||||
private Looper mLooper; |
||||
|
||||
/** |
||||
* @param taskRecord 任务记录 |
||||
* @param listener 任务事件 |
||||
*/ |
||||
ThreadStateManager(Looper looper, TaskRecord taskRecord, IEventListener listener) { |
||||
mLooper = looper; |
||||
mTaskRecord = taskRecord; |
||||
mThreadNum = mTaskRecord.threadNum; |
||||
mListener = listener; |
||||
} |
||||
|
||||
@Override public boolean handleMessage(Message msg) { |
||||
switch (msg.what) { |
||||
case STATE_STOP: |
||||
mStopNum++; |
||||
if (isStop()) { |
||||
mListener.onStop(mProgress); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_CANCEL: |
||||
mCancelNum++; |
||||
if (isCancel()) { |
||||
ALog.d(TAG, "icCancel"); |
||||
mListener.onCancel(); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_FAIL: |
||||
mFailNum++; |
||||
if (isFail()) { |
||||
Bundle b = msg.getData(); |
||||
mListener.onFail(b.getBoolean(KEY_RETRY, true), |
||||
(BaseException) b.getSerializable(KEY_ERROR_INFO)); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_COMPLETE: |
||||
mCompleteNum++; |
||||
if (isComplete()) { |
||||
ALog.d(TAG, "isComplete, completeNum = " + mCompleteNum); |
||||
if (mTaskRecord.isBlock) { |
||||
if (mergeFile()) { |
||||
mListener.onComplete(); |
||||
} else { |
||||
mListener.onFail(false, null); |
||||
} |
||||
} else { |
||||
mListener.onComplete(); |
||||
} |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_RUNNING: |
||||
mProgress += (long) msg.obj; |
||||
break; |
||||
case STATE_UPDATE_PROGRESS: |
||||
if (msg.obj == null) { |
||||
mProgress = updateBlockProgress(); |
||||
} else { |
||||
mProgress = (long) msg.obj; |
||||
} |
||||
break; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* 退出looper循环 |
||||
*/ |
||||
private void quitLooper() { |
||||
mLooper.quit(); |
||||
} |
||||
|
||||
/** |
||||
* 获取当前任务下载进度 |
||||
* |
||||
* @return 当前任务下载进度 |
||||
*/ |
||||
@Override |
||||
public long getCurrentProgress() { |
||||
return mProgress; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经停止 |
||||
*/ |
||||
@Override |
||||
public boolean isStop() { |
||||
//ALog.d(TAG,
|
||||
// String.format("isStop; stopNum: %s, cancelNum: %s, failNum: %s, completeNum: %s", mStopNum,
|
||||
// mCancelNum, mFailNum, mCompleteNum));
|
||||
return mStopNum == mThreadNum || mStopNum + mCompleteNum == mThreadNum; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经失败 |
||||
*/ |
||||
@Override |
||||
public boolean isFail() { |
||||
//ALog.d(TAG,
|
||||
// String.format("isFail; stopNum: %s, cancelNum: %s, failNum: %s, completeNum: %s", mStopNum,
|
||||
// mCancelNum, mFailNum, mCompleteNum));
|
||||
return mCompleteNum != mThreadNum |
||||
&& (mFailNum == mThreadNum || mFailNum + mCompleteNum == mThreadNum); |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经完成 |
||||
*/ |
||||
@Override |
||||
public boolean isComplete() { |
||||
//ALog.d(TAG,
|
||||
// String.format("isComplete; stopNum: %s, cancelNum: %s, failNum: %s, completeNum: %s",
|
||||
// mStopNum,
|
||||
// mCancelNum, mFailNum, mCompleteNum));
|
||||
return mCompleteNum == mThreadNum; |
||||
} |
||||
|
||||
/** |
||||
* 所有子线程是否都已经取消 |
||||
*/ |
||||
@Override |
||||
public boolean isCancel() { |
||||
//ALog.d(TAG, String.format("isCancel; stopNum: %s, cancelNum: %s, failNum: %s, completeNum: %s",
|
||||
// mStopNum,
|
||||
// mCancelNum, mFailNum, mCompleteNum));
|
||||
return mCancelNum == mThreadNum; |
||||
} |
||||
|
||||
/** |
||||
* 更新分块任务s的真实进度 |
||||
*/ |
||||
private long updateBlockProgress() { |
||||
long size = 0; |
||||
for (int i = 0, len = mTaskRecord.threadRecords.size(); i < len; i++) { |
||||
File temp = new File(String.format(RecordHandler.SUB_PATH, mTaskRecord.filePath, i)); |
||||
if (temp.exists()) { |
||||
size += temp.length(); |
||||
} |
||||
} |
||||
return size; |
||||
} |
||||
|
||||
/** |
||||
* 合并文件 |
||||
* |
||||
* @return {@code true} 合并成功,{@code false}合并失败 |
||||
*/ |
||||
private boolean mergeFile() { |
||||
List<String> partPath = new ArrayList<>(); |
||||
for (int i = 0, len = mTaskRecord.threadNum; i < len; i++) { |
||||
partPath.add(String.format(RecordHandler.SUB_PATH, mTaskRecord.filePath, i)); |
||||
} |
||||
boolean isSuccess = FileUtil.mergeFile(mTaskRecord.filePath, partPath); |
||||
if (isSuccess) { |
||||
for (String pp : partPath) { |
||||
File f = new File(pp); |
||||
if (f.exists()) { |
||||
f.delete(); |
||||
} |
||||
} |
||||
File targetFile = new File(mTaskRecord.filePath); |
||||
if (targetFile.exists() && targetFile.length() > mTaskRecord.fileLength) { |
||||
ALog.e(TAG, String.format("任务【%s】分块文件合并失败,下载长度超出文件真实长度,downloadLen: %s,fileSize: %s", |
||||
targetFile.getName(), targetFile.length(), mTaskRecord.fileLength)); |
||||
return false; |
||||
} |
||||
return true; |
||||
} else { |
||||
ALog.e(TAG, "合并失败"); |
||||
return false; |
||||
} |
||||
} |
||||
} |
@ -1,88 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common.ftp; |
||||
|
||||
import com.arialyy.aria.core.FtpUrlEntity; |
||||
import com.arialyy.aria.core.inf.ITargetHeadDelegate; |
||||
import java.net.Proxy; |
||||
|
||||
/** |
||||
* fTP任务设置的信息,如:用户名、密码、端口等信息 |
||||
*/ |
||||
public class FtpTaskDelegate implements ITargetHeadDelegate { |
||||
|
||||
/** |
||||
* 账号和密码 |
||||
*/ |
||||
private FtpUrlEntity urlEntity; |
||||
|
||||
private Proxy proxy; |
||||
|
||||
/** |
||||
* 字符编码,默认为"utf-8" |
||||
*/ |
||||
private String charSet = "utf-8"; |
||||
|
||||
/** |
||||
* 上传拦截器 |
||||
*/ |
||||
private IFtpUploadInterceptor uploadInterceptor; |
||||
|
||||
/** |
||||
* 上传到服务器文件的新文件名{@link FtpInterceptHandler#getNewFileName()} |
||||
*/ |
||||
private String newFileName; |
||||
|
||||
public String getNewFileName() { |
||||
return newFileName; |
||||
} |
||||
|
||||
public void setNewFileName(String newFileName) { |
||||
this.newFileName = newFileName; |
||||
} |
||||
|
||||
public IFtpUploadInterceptor getUploadInterceptor() { |
||||
return uploadInterceptor; |
||||
} |
||||
|
||||
public void setUploadInterceptor(IFtpUploadInterceptor uploadInterceptor) { |
||||
this.uploadInterceptor = uploadInterceptor; |
||||
} |
||||
|
||||
public FtpUrlEntity getUrlEntity() { |
||||
return urlEntity; |
||||
} |
||||
|
||||
public void setUrlEntity(FtpUrlEntity urlEntity) { |
||||
this.urlEntity = urlEntity; |
||||
} |
||||
|
||||
public void setProxy(Proxy proxy) { |
||||
this.proxy = proxy; |
||||
} |
||||
|
||||
public Proxy getProxy() { |
||||
return proxy; |
||||
} |
||||
|
||||
public String getCharSet() { |
||||
return charSet; |
||||
} |
||||
|
||||
public void setCharSet(String charSet) { |
||||
this.charSet = charSet; |
||||
} |
||||
} |
@ -1,144 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common.http; |
||||
|
||||
import android.support.annotation.NonNull; |
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.inf.AbsTarget; |
||||
import com.arialyy.aria.core.inf.AbsTaskWrapper; |
||||
import com.arialyy.aria.core.inf.IHttpFileLenAdapter; |
||||
import com.arialyy.aria.core.inf.IHttpHeaderDelegate; |
||||
import com.arialyy.aria.util.ALog; |
||||
import java.net.Proxy; |
||||
import java.util.Collection; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* Created by laoyuyu on 2018/3/9. |
||||
* D_HTTP header参数设置委托类 |
||||
*/ |
||||
public class HttpHeaderDelegate<TARGET extends AbsTarget> |
||||
implements IHttpHeaderDelegate<TARGET> { |
||||
private static final String TAG = "HttpHeaderDelegate"; |
||||
private TARGET mTarget; |
||||
|
||||
public HttpHeaderDelegate(TARGET target) { |
||||
mTarget = target; |
||||
} |
||||
|
||||
/** |
||||
* 给url请求添加Header数据 |
||||
* 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据 |
||||
* |
||||
* @param key header对应的key |
||||
* @param value header对应的value |
||||
*/ |
||||
@Override |
||||
public TARGET addHeader(@NonNull String key, @NonNull String value) { |
||||
if (TextUtils.isEmpty(key)) { |
||||
ALog.w(TAG, "设置header失败,header对应的key不能为null"); |
||||
return mTarget; |
||||
} else if (TextUtils.isEmpty(value)) { |
||||
ALog.w(TAG, "设置header失败,header对应的value不能为null"); |
||||
return mTarget; |
||||
} |
||||
addHeader(mTarget.getTaskWrapper(), key, value); |
||||
return mTarget; |
||||
} |
||||
|
||||
/** |
||||
* 给url请求添加一组header数据 |
||||
* 如果新的header数据和数据保存的不一致,则更新数据库中对应的header数据 |
||||
* |
||||
* @param headers 一组http header数据 |
||||
*/ |
||||
@Override |
||||
public TARGET addHeaders(@NonNull Map<String, String> headers) { |
||||
if (headers.size() == 0) { |
||||
ALog.w(TAG, "设置header失败,map没有header数据"); |
||||
return mTarget; |
||||
} |
||||
addHeaders(mTarget.getTaskWrapper(), headers); |
||||
return mTarget; |
||||
} |
||||
|
||||
@Override public TARGET setUrlProxy(Proxy proxy) { |
||||
mTarget.getTaskWrapper().asHttp().setProxy(proxy); |
||||
return mTarget; |
||||
} |
||||
|
||||
public TARGET setFileLenAdapter(IHttpFileLenAdapter adapter) { |
||||
if (adapter == null) { |
||||
throw new IllegalArgumentException("adapter为空"); |
||||
} |
||||
mTarget.getTaskWrapper().asHttp().setFileLenAdapter(adapter); |
||||
return mTarget; |
||||
} |
||||
|
||||
private void addHeader(AbsTaskWrapper taskWrapper, String key, String value) { |
||||
HttpTaskDelegate taskDelegate = taskWrapper.asHttp(); |
||||
if (taskDelegate.getHeaders().get(key) == null) { |
||||
taskDelegate.getHeaders().put(key, value); |
||||
} else if (!taskDelegate.getHeaders().get(key).equals(value)) { |
||||
taskDelegate.getHeaders().put(key, value); |
||||
} |
||||
} |
||||
|
||||
private void addHeaders(AbsTaskWrapper taskWrapper, Map<String, String> headers) { |
||||
HttpTaskDelegate taskDelegate = taskWrapper.asHttp(); |
||||
/* |
||||
两个map比较逻辑 |
||||
1、比对key是否相同 |
||||
2、如果key相同,比对value是否相同 |
||||
3、只有当上面两个步骤中key 和 value都相同时才能任务两个map数据一致 |
||||
*/ |
||||
boolean mapEquals = false; |
||||
if (taskDelegate.getHeaders().size() == headers.size()) { |
||||
int i = 0; |
||||
Set<String> keys = taskDelegate.getHeaders().keySet(); |
||||
for (String key : keys) { |
||||
if (headers.containsKey(key)) { |
||||
i++; |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
if (i == taskDelegate.getHeaders().size()) { |
||||
int j = 0; |
||||
Collection<String> values = taskDelegate.getHeaders().values(); |
||||
for (String value : values) { |
||||
if (headers.containsValue(value)) { |
||||
j++; |
||||
} else { |
||||
break; |
||||
} |
||||
} |
||||
if (j == taskDelegate.getHeaders().size()) { |
||||
mapEquals = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (!mapEquals) { |
||||
taskDelegate.getHeaders().clear(); |
||||
Set<String> keys = headers.keySet(); |
||||
for (String key : keys) { |
||||
taskDelegate.getHeaders().put(key, headers.get(key)); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,199 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.common.http; |
||||
|
||||
import com.arialyy.aria.core.common.RequestEnum; |
||||
import com.arialyy.aria.core.inf.IHttpFileLenAdapter; |
||||
import com.arialyy.aria.core.inf.ITargetHeadDelegate; |
||||
import java.lang.ref.WeakReference; |
||||
import java.net.CookieManager; |
||||
import java.net.Proxy; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* Http任务设置的信息,如:cookie、请求参数 |
||||
*/ |
||||
public class HttpTaskDelegate implements ITargetHeadDelegate { |
||||
|
||||
private CookieManager cookieManager; |
||||
|
||||
/** |
||||
* 请求参数 |
||||
*/ |
||||
private Map<String, String> params; |
||||
|
||||
/** |
||||
* http 请求头 |
||||
*/ |
||||
private Map<String, String> headers = new HashMap<>(); |
||||
|
||||
/** |
||||
* 字符编码,默认为"utf-8" |
||||
*/ |
||||
private String charSet = "utf-8"; |
||||
|
||||
/** |
||||
* 网络请求类型 |
||||
*/ |
||||
private RequestEnum requestEnum = RequestEnum.GET; |
||||
|
||||
/** |
||||
* 是否使用服务器通过content-disposition传递的文件名,内容格式{@code attachment; filename="filename.jpg"} {@code true} |
||||
* 使用 |
||||
*/ |
||||
private boolean useServerFileName = false; |
||||
|
||||
/** |
||||
* 重定向链接 |
||||
*/ |
||||
private String redirectUrl = ""; |
||||
|
||||
/** |
||||
* 是否是chunk模式 |
||||
*/ |
||||
private boolean isChunked = false; |
||||
/** |
||||
* 文件上传需要的key |
||||
*/ |
||||
private String attachment; |
||||
/** |
||||
* 上传的文件类型 |
||||
*/ |
||||
private String contentType = "multipart/form-data"; |
||||
private String userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)"; |
||||
|
||||
private Proxy proxy; |
||||
/** |
||||
* 文件上传表单 |
||||
*/ |
||||
private Map<String, String> formFields = new HashMap<>(); |
||||
|
||||
private IHttpFileLenAdapter fileLenAdapter; |
||||
|
||||
public IHttpFileLenAdapter getFileLenAdapter() { |
||||
return fileLenAdapter; |
||||
} |
||||
|
||||
public void setFileLenAdapter(IHttpFileLenAdapter fileLenAdapter) { |
||||
this.fileLenAdapter = fileLenAdapter; |
||||
} |
||||
|
||||
public Map<String, String> getFormFields() { |
||||
return formFields; |
||||
} |
||||
|
||||
public void setFormFields(Map<String, String> formFields) { |
||||
this.formFields = formFields; |
||||
} |
||||
|
||||
public String getAttachment() { |
||||
return attachment; |
||||
} |
||||
|
||||
public void setAttachment(String attachment) { |
||||
this.attachment = attachment; |
||||
} |
||||
|
||||
public String getContentType() { |
||||
return contentType; |
||||
} |
||||
|
||||
public void setContentType(String contentType) { |
||||
this.contentType = contentType; |
||||
} |
||||
|
||||
public String getUserAgent() { |
||||
return userAgent; |
||||
} |
||||
|
||||
public void setUserAgent(String userAgent) { |
||||
this.userAgent = userAgent; |
||||
} |
||||
|
||||
public boolean isChunked() { |
||||
return isChunked; |
||||
} |
||||
|
||||
public void setChunked(boolean chunked) { |
||||
isChunked = chunked; |
||||
} |
||||
|
||||
public CookieManager getCookieManager() { |
||||
return cookieManager; |
||||
} |
||||
|
||||
public void setCookieManager(CookieManager cookieManager) { |
||||
this.cookieManager = cookieManager; |
||||
} |
||||
|
||||
public Proxy getProxy() { |
||||
return proxy; |
||||
} |
||||
|
||||
public void setProxy(Proxy proxy) { |
||||
this.proxy = proxy; |
||||
} |
||||
|
||||
public Map<String, String> getHeaders() { |
||||
return headers; |
||||
} |
||||
|
||||
public void setHeaders(Map<String, String> headers) { |
||||
this.headers = headers; |
||||
} |
||||
|
||||
public String getCharSet() { |
||||
return charSet; |
||||
} |
||||
|
||||
public void setCharSet(String charSet) { |
||||
this.charSet = charSet; |
||||
} |
||||
|
||||
public RequestEnum getRequestEnum() { |
||||
return requestEnum; |
||||
} |
||||
|
||||
public void setRequestEnum(RequestEnum requestEnum) { |
||||
this.requestEnum = requestEnum; |
||||
} |
||||
|
||||
public boolean isUseServerFileName() { |
||||
return useServerFileName; |
||||
} |
||||
|
||||
public void setUseServerFileName(boolean useServerFileName) { |
||||
this.useServerFileName = useServerFileName; |
||||
} |
||||
|
||||
public String getRedirectUrl() { |
||||
return redirectUrl; |
||||
} |
||||
|
||||
public void setRedirectUrl(String redirectUrl) { |
||||
this.redirectUrl = redirectUrl; |
||||
} |
||||
|
||||
public Map<String, String> getParams() { |
||||
return params; |
||||
} |
||||
|
||||
public void setParams(Map<String, String> params) { |
||||
this.params = params; |
||||
} |
||||
} |
@ -1,159 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download; |
||||
|
||||
import android.support.annotation.CheckResult; |
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.inf.AbsEntity; |
||||
import com.arialyy.aria.core.inf.IGroupTarget; |
||||
import com.arialyy.aria.core.queue.DownloadGroupTaskQueue; |
||||
import com.arialyy.aria.orm.DbEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import java.io.File; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Created by lyy on 2019/4/9. |
||||
* 下载组合任务功能 |
||||
*/ |
||||
abstract class AbsGroupDelegate<TARGET extends AbsDGTarget> implements IGroupTarget { |
||||
protected String TAG; |
||||
private TARGET mTarget; |
||||
private DGTaskWrapper mWrapper; |
||||
/** |
||||
* 组任务名 |
||||
*/ |
||||
private String mGroupHash; |
||||
/** |
||||
* 文件夹临时路径 |
||||
*/ |
||||
private String mDirPathTemp; |
||||
/** |
||||
* 是否需要修改路径 |
||||
*/ |
||||
private boolean needModifyPath = false; |
||||
|
||||
AbsGroupDelegate(TARGET target, DGTaskWrapper wrapper) { |
||||
TAG = CommonUtil.getClassName(getClass()); |
||||
mTarget = target; |
||||
mWrapper = wrapper; |
||||
setGroupHash(wrapper.getKey()); |
||||
mTarget.setTaskWrapper(wrapper); |
||||
if (getEntity() != null) { |
||||
mDirPathTemp = getEntity().getDirPath(); |
||||
} |
||||
} |
||||
|
||||
@Override public boolean isRunning() { |
||||
DownloadGroupTask task = DownloadGroupTaskQueue.getInstance().getTask(getEntity().getKey()); |
||||
return task != null && task.isRunning(); |
||||
} |
||||
|
||||
@CheckResult |
||||
TARGET setDirPath(String dirPath) { |
||||
mDirPathTemp = dirPath; |
||||
return mTarget; |
||||
} |
||||
|
||||
/** |
||||
* 改变任务组文件夹路径,修改文件夹路径会将子任务所有路径更换 |
||||
* |
||||
* @param newDirPath 新的文件夹路径 |
||||
*/ |
||||
void reChangeDirPath(String newDirPath) { |
||||
ALog.d(TAG, String.format("修改新路径为:%s", newDirPath)); |
||||
List<DTaskWrapper> subTasks = mWrapper.getSubTaskWrapper(); |
||||
if (subTasks != null && !subTasks.isEmpty()) { |
||||
List<DownloadEntity> des = new ArrayList<>(); |
||||
for (DTaskWrapper dte : subTasks) { |
||||
DownloadEntity de = dte.getEntity(); |
||||
String oldPath = de.getDownloadPath(); |
||||
String newPath = newDirPath + "/" + de.getFileName(); |
||||
File file = new File(oldPath); |
||||
if (file.exists()) { |
||||
file.renameTo(new File(newPath)); |
||||
} |
||||
de.setDownloadPath(newPath); |
||||
des.add(de); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 检查并设置文件夹路径 |
||||
* |
||||
* @return {@code true} 合法 |
||||
*/ |
||||
@Override public boolean checkDirPath() { |
||||
if (TextUtils.isEmpty(mDirPathTemp)) { |
||||
ALog.e(TAG, "文件夹路径不能为null"); |
||||
return false; |
||||
} else if (!mDirPathTemp.startsWith("/")) { |
||||
ALog.e(TAG, "文件夹路径【" + mDirPathTemp + "】错误"); |
||||
return false; |
||||
} |
||||
File file = new File(mDirPathTemp); |
||||
if (file.isFile()) { |
||||
ALog.e(TAG, "路径【" + mDirPathTemp + "】是文件,请设置文件夹路径"); |
||||
return false; |
||||
} |
||||
|
||||
if (TextUtils.isEmpty(getEntity().getDirPath()) || !getEntity().getDirPath() |
||||
.equals(mDirPathTemp)) { |
||||
if (!file.exists()) { |
||||
file.mkdirs(); |
||||
} |
||||
needModifyPath = true; |
||||
getEntity().setDirPath(mDirPathTemp); |
||||
ALog.i(TAG, String.format("文件夹路径改变,将更新文件夹路径为:%s", mDirPathTemp)); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override public DownloadGroupEntity getEntity() { |
||||
return mWrapper.getEntity(); |
||||
} |
||||
|
||||
@Override public boolean taskExists() { |
||||
return DbEntity.checkDataExist(DownloadGroupEntity.class, "groupHash=?", mWrapper.getKey()); |
||||
} |
||||
|
||||
DGTaskWrapper getTaskWrapper() { |
||||
return mWrapper; |
||||
} |
||||
|
||||
boolean isNeedModifyPath() { |
||||
return needModifyPath; |
||||
} |
||||
|
||||
String getDirPathTemp() { |
||||
return mDirPathTemp; |
||||
} |
||||
|
||||
TARGET getTarget() { |
||||
return mTarget; |
||||
} |
||||
|
||||
public String getGroupHash() { |
||||
return mGroupHash; |
||||
} |
||||
|
||||
public void setGroupHash(String groupHash) { |
||||
this.mGroupHash = groupHash; |
||||
} |
||||
} |
@ -1,201 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download; |
||||
|
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.inf.ITargetHandler; |
||||
import com.arialyy.aria.core.inf.INormalTarget; |
||||
import com.arialyy.aria.core.manager.TaskWrapperManager; |
||||
import com.arialyy.aria.core.queue.DownloadTaskQueue; |
||||
import com.arialyy.aria.orm.DbEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CheckUtil; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import java.io.File; |
||||
|
||||
/** |
||||
* Created by AriaL on 2019/4/5. |
||||
* 普通下载任务通用功能处理 |
||||
*/ |
||||
class DNormalDelegate<TARGET extends AbsDTarget> implements INormalTarget { |
||||
private final String TAG = "DNormalDelegate"; |
||||
private DownloadEntity mEntity; |
||||
|
||||
private TARGET mTarget; |
||||
private String mNewUrl; |
||||
/** |
||||
* 设置的文件保存路径的临时变量 |
||||
*/ |
||||
private String mTempFilePath; |
||||
|
||||
/** |
||||
* {@code true}强制下载,不考虑文件路径是否被占用 |
||||
*/ |
||||
private boolean forceDownload = false; |
||||
/** |
||||
* 资源地址 |
||||
*/ |
||||
private String mUrl; |
||||
|
||||
DNormalDelegate(TARGET target, String url, String targetName) { |
||||
this.mTarget = target; |
||||
initTarget(url, targetName); |
||||
} |
||||
|
||||
private void initTarget(String url, String targetName) { |
||||
DTaskWrapper taskWrapper = |
||||
TaskWrapperManager.getInstance().getHttpTaskWrapper(DTaskWrapper.class, url); |
||||
mEntity = taskWrapper.getEntity(); |
||||
|
||||
mUrl = url; |
||||
mTarget.setTargetName(targetName); |
||||
mTarget.setTaskWrapper(taskWrapper); |
||||
if (mEntity != null) { |
||||
mTempFilePath = mEntity.getDownloadPath(); |
||||
} |
||||
} |
||||
|
||||
TARGET updateUrl(String newUrl) { |
||||
if (TextUtils.isEmpty(newUrl)) { |
||||
ALog.e(TAG, "url更新失败,newUrl为null"); |
||||
return mTarget; |
||||
} |
||||
if (mUrl.equals(newUrl)) { |
||||
ALog.e(TAG, "url更新失败,新的下载url和旧的url一致"); |
||||
return mTarget; |
||||
} |
||||
mNewUrl = newUrl; |
||||
mTarget.getTaskWrapper().setRefreshInfo(true); |
||||
return mTarget; |
||||
} |
||||
|
||||
@Override public DownloadEntity getEntity() { |
||||
return mTarget.getEntity(); |
||||
} |
||||
|
||||
@Override public boolean taskExists() { |
||||
return DbEntity.checkDataExist(DownloadEntity.class, "url=?", mUrl); |
||||
} |
||||
|
||||
@Override public boolean isRunning() { |
||||
DownloadTask task = DownloadTaskQueue.getInstance().getTask(mEntity.getKey()); |
||||
return task != null && task.isRunning(); |
||||
} |
||||
|
||||
@Override public boolean checkEntity() { |
||||
boolean b = checkUrl() && checkFilePath(); |
||||
if (b) { |
||||
mEntity.save(); |
||||
} |
||||
return b; |
||||
} |
||||
|
||||
@Override public boolean checkFilePath() { |
||||
String filePath = mTempFilePath; |
||||
if (TextUtils.isEmpty(filePath)) { |
||||
ALog.e(TAG, "下载失败,文件保存路径为null"); |
||||
return false; |
||||
} else if (!filePath.startsWith("/")) { |
||||
ALog.e(TAG, "下载失败,文件保存路径【" + filePath + "】错误"); |
||||
return false; |
||||
} |
||||
File file = new File(filePath); |
||||
if (file.isDirectory()) { |
||||
if (mTarget.getTargetType() == ITargetHandler.D_HTTP) { |
||||
ALog.e(TAG, "下载失败,保存路径【" + filePath + "】不能为文件夹,路径需要是完整的文件路径,如:/mnt/sdcard/game.zip"); |
||||
return false; |
||||
} else if (mTarget.getTargetType() == ITargetHandler.D_FTP) { |
||||
filePath += mEntity.getFileName(); |
||||
} |
||||
} else { |
||||
// http文件名设置
|
||||
if (TextUtils.isEmpty(mEntity.getFileName())) { |
||||
mEntity.setFileName(file.getName()); |
||||
} |
||||
} |
||||
|
||||
//设置文件保存路径,如果新文件路径和旧文件路径不同,则修改路径
|
||||
if (!filePath.equals(mEntity.getFilePath())) { |
||||
// 检查路径冲突
|
||||
if (DbEntity.checkDataExist(DownloadEntity.class, "downloadPath=?", filePath)) { |
||||
if (!forceDownload) { |
||||
ALog.e(TAG, "下载失败,保存路径【" + filePath + "】已经被其它任务占用,请设置其它保存路径"); |
||||
return false; |
||||
} else { |
||||
ALog.w(TAG, "保存路径【" + filePath + "】已经被其它任务占用,当前任务将覆盖该路径的文件"); |
||||
CommonUtil.delTaskRecord(filePath, 1); |
||||
mTarget.setTaskWrapper( |
||||
TaskWrapperManager.getInstance() |
||||
.getHttpTaskWrapper(DTaskWrapper.class, mUrl)); |
||||
} |
||||
} |
||||
File oldFile = new File(mEntity.getDownloadPath()); |
||||
File newFile = new File(filePath); |
||||
mEntity.setDownloadPath(filePath); |
||||
mEntity.setFileName(newFile.getName()); |
||||
// 如过使用Content-Disposition中的文件名,将不会执行重命名工作
|
||||
if (mTarget.getTaskWrapper().asHttp().isUseServerFileName()) { |
||||
return true; |
||||
} |
||||
if (oldFile.exists()) { |
||||
// 处理普通任务的重命名
|
||||
CommonUtil.modifyTaskRecord(oldFile.getPath(), newFile.getPath()); |
||||
ALog.i(TAG, String.format("将任务重命名为:%s", newFile.getName())); |
||||
} else if (CommonUtil.blockTaskExists(oldFile.getPath())) { |
||||
// 处理分块任务的重命名
|
||||
CommonUtil.modifyTaskRecord(oldFile.getPath(), newFile.getPath()); |
||||
ALog.i(TAG, String.format("将分块任务重命名为:%s", newFile.getName())); |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override public boolean checkUrl() { |
||||
final String url = mEntity.getUrl(); |
||||
if (TextUtils.isEmpty(url)) { |
||||
ALog.e(TAG, "下载失败,url为null"); |
||||
return false; |
||||
} else if (!CheckUtil.checkUrlNotThrow(url)) { |
||||
ALog.e(TAG, "下载失败,url【" + url + "】错误"); |
||||
return false; |
||||
} |
||||
int index = url.indexOf("://"); |
||||
if (index == -1) { |
||||
ALog.e(TAG, "下载失败,url【" + url + "】不合法"); |
||||
return false; |
||||
} |
||||
if (!TextUtils.isEmpty(mNewUrl)) { |
||||
mEntity.setUrl(mNewUrl); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
void setForceDownload(boolean forceDownload) { |
||||
this.forceDownload = forceDownload; |
||||
} |
||||
|
||||
void setUrl(String url) { |
||||
this.mUrl = url; |
||||
} |
||||
|
||||
String getUrl() { |
||||
return mUrl; |
||||
} |
||||
|
||||
void setTempFilePath(String mTempFilePath) { |
||||
this.mTempFilePath = mTempFilePath; |
||||
} |
||||
} |
@ -1,91 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download; |
||||
|
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.FtpUrlEntity; |
||||
import com.arialyy.aria.core.inf.AbsTaskWrapper; |
||||
import com.arialyy.aria.util.ALog; |
||||
|
||||
/** |
||||
* Created by lyy on 2017/4/9. |
||||
* ftp文件夹下载功能代理 |
||||
*/ |
||||
class FtpDirDelegate extends AbsGroupDelegate<FtpDirDownloadTarget> { |
||||
FtpDirDelegate(FtpDirDownloadTarget target, DGTaskWrapper wrapper) { |
||||
super(target, wrapper); |
||||
wrapper.setRequestType(AbsTaskWrapper.D_FTP_DIR); |
||||
} |
||||
|
||||
@Override public boolean checkEntity() { |
||||
boolean b = checkDirPath() && checkUrl(); |
||||
if (b) { |
||||
getEntity().save(); |
||||
if (getTaskWrapper().getSubTaskWrapper() != null) { |
||||
//初始化子项的登录信息
|
||||
FtpUrlEntity tUrlEntity = getTaskWrapper().asFtp().getUrlEntity(); |
||||
for (DTaskWrapper wrapper : getTaskWrapper().getSubTaskWrapper()) { |
||||
FtpUrlEntity urlEntity = wrapper.asFtp().getUrlEntity(); |
||||
urlEntity.needLogin = tUrlEntity.needLogin; |
||||
urlEntity.account = tUrlEntity.account; |
||||
urlEntity.user = tUrlEntity.user; |
||||
urlEntity.password = tUrlEntity.password; |
||||
// 处理ftps详细
|
||||
if (tUrlEntity.isFtps) { |
||||
urlEntity.isFtps = true; |
||||
urlEntity.protocol = tUrlEntity.protocol; |
||||
urlEntity.storePath = tUrlEntity.storePath; |
||||
urlEntity.storePass = tUrlEntity.storePass; |
||||
urlEntity.keyAlias = tUrlEntity.keyAlias; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if (getTaskWrapper().asFtp().getUrlEntity().isFtps) { |
||||
if (TextUtils.isEmpty(getTaskWrapper().asFtp().getUrlEntity().storePath)) { |
||||
ALog.e(TAG, "证书路径为空"); |
||||
return false; |
||||
} |
||||
if (TextUtils.isEmpty(getTaskWrapper().asFtp().getUrlEntity().keyAlias)) { |
||||
ALog.e(TAG, "证书别名为空"); |
||||
return false; |
||||
} |
||||
} |
||||
return b; |
||||
} |
||||
|
||||
/** |
||||
* 检查普通任务的下载地址 |
||||
* |
||||
* @return {@code true}地址合法 |
||||
*/ |
||||
private boolean checkUrl() { |
||||
final String url = getGroupHash(); |
||||
if (TextUtils.isEmpty(url)) { |
||||
ALog.e(TAG, "下载失败,url为null"); |
||||
return false; |
||||
} else if (!url.startsWith("ftp")) { |
||||
ALog.e(TAG, "下载失败,url【" + url + "】错误"); |
||||
return false; |
||||
} |
||||
int index = url.indexOf("://"); |
||||
if (index == -1) { |
||||
ALog.e(TAG, "下载失败,url【" + url + "】不合法"); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -1,247 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download; |
||||
|
||||
import android.support.annotation.CheckResult; |
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.common.RequestEnum; |
||||
import com.arialyy.aria.orm.DbEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.HashSet; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* Created by lyy on 2019/4/9. |
||||
* |
||||
* http组合任务功能代理 |
||||
*/ |
||||
class HttpGroupDelegate extends AbsGroupDelegate<DownloadGroupTarget> { |
||||
|
||||
/** |
||||
* 子任务下载地址, |
||||
*/ |
||||
private List<String> mUrls = new ArrayList<>(); |
||||
|
||||
/** |
||||
* 子任务文件名 |
||||
*/ |
||||
private List<String> mSubNameTemp = new ArrayList<>(); |
||||
|
||||
HttpGroupDelegate(DownloadGroupTarget target, DGTaskWrapper wrapper) { |
||||
super(target, wrapper); |
||||
mUrls.addAll(wrapper.getEntity().getUrls()); |
||||
} |
||||
|
||||
@CheckResult |
||||
DownloadGroupTarget setGroupUrl(List<String> urls) { |
||||
mUrls.clear(); |
||||
mUrls.addAll(urls); |
||||
return getTarget(); |
||||
} |
||||
|
||||
/** |
||||
* 设置子任务文件名,该方法必须在{@link #setDirPath(String)}之后调用,否则不生效 |
||||
*/ |
||||
@CheckResult |
||||
DownloadGroupTarget setSubFileName(List<String> subTaskFileName) { |
||||
if (subTaskFileName == null || subTaskFileName.isEmpty()) { |
||||
ALog.w(TAG, "修改子任务的文件名失败:列表为null"); |
||||
return getTarget(); |
||||
} |
||||
if (subTaskFileName.size() != getTaskWrapper().getSubTaskWrapper().size()) { |
||||
ALog.w(TAG, "修改子任务的文件名失败:子任务文件名列表数量和子任务的数量不匹配"); |
||||
return getTarget(); |
||||
} |
||||
mSubNameTemp.clear(); |
||||
mSubNameTemp.addAll(subTaskFileName); |
||||
return getTarget(); |
||||
} |
||||
|
||||
/** |
||||
* 更新组合任务下载地址 |
||||
* |
||||
* @param urls 新的组合任务下载地址列表 |
||||
*/ |
||||
@CheckResult |
||||
DownloadGroupTarget updateUrls(List<String> urls) { |
||||
if (urls == null || urls.isEmpty()) { |
||||
throw new NullPointerException("下载地址列表为空"); |
||||
} |
||||
if (urls.size() != mUrls.size()) { |
||||
throw new IllegalArgumentException("新下载地址数量和旧下载地址数量不一致"); |
||||
} |
||||
mUrls.clear(); |
||||
mUrls.addAll(urls); |
||||
String newHash = CommonUtil.getMd5Code(urls); |
||||
setGroupHash(newHash); |
||||
getEntity().setGroupHash(newHash); |
||||
getEntity().update(); |
||||
if (getEntity().getSubEntities() != null && !getEntity().getSubEntities().isEmpty()) { |
||||
for (DownloadEntity de : getEntity().getSubEntities()) { |
||||
de.setGroupHash(newHash); |
||||
de.update(); |
||||
} |
||||
} |
||||
return getTarget(); |
||||
} |
||||
|
||||
@Override public boolean checkEntity() { |
||||
if (!checkDirPath()) { |
||||
return false; |
||||
} |
||||
|
||||
if (!checkSubName()) { |
||||
return false; |
||||
} |
||||
|
||||
if (!checkUrls()) { |
||||
return false; |
||||
} |
||||
|
||||
if (!getTaskWrapper().isUnknownSize() && getTaskWrapper().getEntity().getFileSize() == 0) { |
||||
ALog.e(TAG, "组合任务必须设置文件文件大小,默认需要强制设置文件大小。如果无法获取到总长度,请调用#unknownSize()来标志该组合任务"); |
||||
return false; |
||||
} |
||||
|
||||
if (getTaskWrapper().asHttp().getRequestEnum() == RequestEnum.POST) { |
||||
for (DTaskWrapper subTask : getTaskWrapper().getSubTaskWrapper()) { |
||||
subTask.asHttp().setRequestEnum(RequestEnum.POST); |
||||
} |
||||
} |
||||
|
||||
if (isNeedModifyPath()) { |
||||
reChangeDirPath(getDirPathTemp()); |
||||
} |
||||
|
||||
if (!mSubNameTemp.isEmpty()) { |
||||
updateSingleSubFileName(); |
||||
} |
||||
saveEntity(); |
||||
return true; |
||||
} |
||||
|
||||
private void saveEntity() { |
||||
getEntity().save(); |
||||
DbEntity.saveAll(getEntity().getSubEntities()); |
||||
} |
||||
|
||||
/** |
||||
* 更新所有改动的子任务文件名 |
||||
*/ |
||||
private void updateSingleSubFileName() { |
||||
List<DTaskWrapper> entities = getTaskWrapper().getSubTaskWrapper(); |
||||
int i = 0; |
||||
for (DTaskWrapper taskWrapper : entities) { |
||||
if (i < mSubNameTemp.size()) { |
||||
String newName = mSubNameTemp.get(i); |
||||
DownloadEntity entity = taskWrapper.getEntity(); |
||||
if (!newName.equals(entity.getFileName())) { |
||||
String oldPath = getEntity().getDirPath() + "/" + entity.getFileName(); |
||||
String newPath = getEntity().getDirPath() + "/" + newName; |
||||
if (DbEntity.checkDataExist(DownloadEntity.class, "downloadPath=? or isComplete='true'", |
||||
newPath)) { |
||||
ALog.w(TAG, String.format("更新文件名失败,路径【%s】已存在或文件已下载", newPath)); |
||||
return; |
||||
} |
||||
|
||||
CommonUtil.modifyTaskRecord(oldPath, newPath); |
||||
entity.setDownloadPath(newPath); |
||||
entity.setFileName(newName); |
||||
} |
||||
} |
||||
i++; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 检查urls是否合法,并删除不合法的子任务 |
||||
* |
||||
* @return {@code true} 合法 |
||||
*/ |
||||
private boolean checkUrls() { |
||||
if (mUrls.isEmpty()) { |
||||
ALog.e(TAG, "下载失败,子任务下载列表为null"); |
||||
return false; |
||||
} |
||||
|
||||
Set<String> repeated = new HashSet<>(); |
||||
List<String> results = new ArrayList<>(); |
||||
for (String url : mUrls) { |
||||
if (!repeated.add(url)) { |
||||
results.add(url); |
||||
} |
||||
} |
||||
if (!results.isEmpty()) { |
||||
ALog.e(TAG, String.format("组合任务中有url重复,重复的url:%s", Arrays.toString(results.toArray()))); |
||||
return false; |
||||
} |
||||
|
||||
Set<Integer> delItem = new HashSet<>(); |
||||
|
||||
int i = 0; |
||||
for (String url : mUrls) { |
||||
if (TextUtils.isEmpty(url)) { |
||||
ALog.e(TAG, "子任务url为null,即将删除该子任务。"); |
||||
delItem.add(i); |
||||
continue; |
||||
} else if (!url.startsWith("http")) { |
||||
ALog.e(TAG, "子任务url【" + url + "】错误,即将删除该子任务。"); |
||||
delItem.add(i); |
||||
continue; |
||||
} |
||||
int index = url.indexOf("://"); |
||||
if (index == -1) { |
||||
ALog.e(TAG, "子任务url【" + url + "】不合法,即将删除该子任务。"); |
||||
delItem.add(i); |
||||
continue; |
||||
} |
||||
|
||||
i++; |
||||
} |
||||
|
||||
for (int index : delItem) { |
||||
mUrls.remove(index); |
||||
if (mSubNameTemp != null && !mSubNameTemp.isEmpty()) { |
||||
mSubNameTemp.remove(index); |
||||
} |
||||
} |
||||
|
||||
getEntity().setGroupHash(CommonUtil.getMd5Code(mUrls)); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* 如果用户设置了子任务文件名,检查子任务文件名 |
||||
* |
||||
* @return {@code true} 合法 |
||||
*/ |
||||
private boolean checkSubName() { |
||||
if (mSubNameTemp == null || mSubNameTemp.isEmpty()) { |
||||
return true; |
||||
} |
||||
if (mUrls.size() != mSubNameTemp.size()) { |
||||
ALog.e(TAG, "子任务文件名必须和子任务数量一致"); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* M3U8 #EXTINF 信息处理器 |
||||
*/ |
||||
public interface IM3U8UrlExtInfHandler { |
||||
|
||||
/** |
||||
* 处理#EXTINF信息,对于某些服务器,返回的切片信息有可能是相对地址,因此,你需要自行转换为可下载http连接 |
||||
* |
||||
* @param extInf #EXTINF 切片信息列表 |
||||
* @return 根据切片信息转换后的http连接列表,如果你的切片信息是可以直接下载的http连接,直接返回extInf便可 |
||||
*/ |
||||
List<String> handler(List<String> extInf); |
||||
} |
@ -0,0 +1,158 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import android.net.TrafficStats; |
||||
import android.os.Process; |
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.AriaManager; |
||||
import com.arialyy.aria.core.common.CompleteInfo; |
||||
import com.arialyy.aria.core.common.OnFileInfoCallback; |
||||
import com.arialyy.aria.core.common.http.HttpTaskConfig; |
||||
import com.arialyy.aria.core.download.DTaskWrapper; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.core.download.downloader.ConnectionHelp; |
||||
import com.arialyy.aria.exception.M3U8Exception; |
||||
import com.arialyy.aria.exception.TaskException; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CheckUtil; |
||||
import java.io.BufferedReader; |
||||
import java.io.IOException; |
||||
import java.io.InputStreamReader; |
||||
import java.net.HttpURLConnection; |
||||
import java.net.URL; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.UUID; |
||||
|
||||
/** |
||||
* 解析url中获取到到m3u信息 |
||||
* https://www.cnblogs.com/renhui/p/10351870.html
|
||||
*/ |
||||
public class M3U8FileInfoThread implements Runnable { |
||||
private final String TAG = "M3U8FileInfoThread"; |
||||
private DownloadEntity mEntity; |
||||
private DTaskWrapper mTaskWrapper; |
||||
private int mConnectTimeOut; |
||||
private OnFileInfoCallback onFileInfoCallback; |
||||
private HttpTaskConfig mTaskDelegate; |
||||
|
||||
public M3U8FileInfoThread(DTaskWrapper taskWrapper, OnFileInfoCallback callback) { |
||||
this.mTaskWrapper = taskWrapper; |
||||
mEntity = taskWrapper.getEntity(); |
||||
mConnectTimeOut = |
||||
AriaManager.getInstance(AriaManager.APP).getDownloadConfig().getConnectTimeOut(); |
||||
onFileInfoCallback = callback; |
||||
mTaskDelegate = taskWrapper.asHttp(); |
||||
} |
||||
|
||||
@Override public void run() { |
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
||||
TrafficStats.setThreadStatsTag(UUID.randomUUID().toString().hashCode()); |
||||
HttpURLConnection conn = null; |
||||
try { |
||||
URL url = ConnectionHelp.handleUrl(mEntity.getUrl(), mTaskDelegate); |
||||
conn = ConnectionHelp.handleConnection(url, mTaskDelegate); |
||||
ConnectionHelp.setConnectParam(mTaskDelegate, conn); |
||||
conn.setConnectTimeout(mConnectTimeOut); |
||||
conn.connect(); |
||||
handleConnect(conn); |
||||
} catch (IOException e) { |
||||
e.printStackTrace(); |
||||
} finally { |
||||
if (conn != null) { |
||||
conn.disconnect(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void handleConnect(HttpURLConnection conn) throws IOException { |
||||
int code = conn.getResponseCode(); |
||||
if (code == HttpURLConnection.HTTP_OK) { |
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); |
||||
String line = reader.readLine(); |
||||
if (TextUtils.isEmpty(line) || !line.equalsIgnoreCase("#EXTM3U")) { |
||||
failDownload("读取M3U8信息失败,读取不到#EXTM3U标签", false); |
||||
return; |
||||
} |
||||
List<String> extInf = new ArrayList<>(); |
||||
boolean isDes = false; |
||||
while ((line = reader.readLine()) != null) { |
||||
if (line.startsWith("#EXT-X-ENDLIST")) { |
||||
break; |
||||
} |
||||
if (line.startsWith("#EXTINF")) { |
||||
isDes = true; |
||||
} else if (isDes) { |
||||
extInf.add(line); |
||||
isDes = false; |
||||
} |
||||
} |
||||
if (extInf.isEmpty()) { |
||||
failDownload("获取M3U8下载地址列表失败", false); |
||||
return; |
||||
} |
||||
CompleteInfo info = new CompleteInfo(); |
||||
info.obj = extInf; |
||||
onFileInfoCallback.onComplete(mEntity.getKey(), info); |
||||
} else if (code == HttpURLConnection.HTTP_MOVED_TEMP |
||||
|| code == HttpURLConnection.HTTP_MOVED_PERM |
||||
|| code == HttpURLConnection.HTTP_SEE_OTHER |
||||
|| code == HttpURLConnection.HTTP_CREATED // 201 跳转
|
||||
|| code == 307) { |
||||
handleUrlReTurn(conn, conn.getHeaderField("Location")); |
||||
} else if (code == HttpURLConnection.HTTP_NOT_FOUND) { |
||||
failDownload("404错误", false); |
||||
} else { |
||||
failDownload(String.format("不支持的响应,code: %s", code), true); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 处理30x跳转 |
||||
*/ |
||||
private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException { |
||||
ALog.d(TAG, "30x跳转,新url为【" + newUrl + "】"); |
||||
if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null") || !newUrl.startsWith( |
||||
"http")) { |
||||
if (onFileInfoCallback != null) { |
||||
onFileInfoCallback.onFail(mEntity, new TaskException(TAG, "获取重定向链接失败"), false); |
||||
} |
||||
return; |
||||
} |
||||
if (!CheckUtil.checkUrlNotThrow(newUrl)) { |
||||
failDownload("下载失败,重定向url错误", false); |
||||
return; |
||||
} |
||||
mTaskDelegate.setRedirectUrl(newUrl); |
||||
mEntity.setRedirect(true); |
||||
mEntity.setRedirectUrl(newUrl); |
||||
String cookies = conn.getHeaderField("Set-Cookie"); |
||||
URL url = ConnectionHelp.handleUrl(newUrl, mTaskDelegate); |
||||
conn = ConnectionHelp.handleConnection(url, mTaskDelegate); |
||||
ConnectionHelp.setConnectParam(mTaskDelegate, conn); |
||||
conn.setRequestProperty("Cookie", cookies); |
||||
conn.setConnectTimeout(mConnectTimeOut); |
||||
conn.connect(); |
||||
handleConnect(conn); |
||||
conn.disconnect(); |
||||
} |
||||
|
||||
private void failDownload(String errorInfo, boolean needRetry) { |
||||
onFileInfoCallback.onFail(mEntity, new M3U8Exception(TAG, errorInfo), needRetry); |
||||
} |
||||
} |
@ -0,0 +1,325 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import android.os.Bundle; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.os.Message; |
||||
import com.arialyy.aria.core.common.AbsFileer; |
||||
import com.arialyy.aria.core.common.IThreadState; |
||||
import com.arialyy.aria.core.common.SubThreadConfig; |
||||
import com.arialyy.aria.core.common.TaskRecord; |
||||
import com.arialyy.aria.core.common.ThreadRecord; |
||||
import com.arialyy.aria.core.download.DTaskWrapper; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.core.inf.IEventListener; |
||||
import com.arialyy.aria.core.manager.ThreadTaskManager; |
||||
import com.arialyy.aria.exception.BaseException; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import com.arialyy.aria.util.FileUtil; |
||||
import com.arialyy.aria.util.IdGenerator; |
||||
import java.io.File; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.concurrent.ArrayBlockingQueue; |
||||
import java.util.concurrent.locks.Condition; |
||||
import java.util.concurrent.locks.ReentrantLock; |
||||
|
||||
/** |
||||
* M3U8点播文件下载器 |
||||
*/ |
||||
public class M3U8FileLoader extends AbsFileer<DownloadEntity, DTaskWrapper> { |
||||
/** |
||||
* 最大执行数 |
||||
*/ |
||||
private static final int EXEC_MAX_NUM = 4; |
||||
private Handler mStateHandler; |
||||
private ArrayBlockingQueue<Long> mFlagQueue = new ArrayBlockingQueue<>(EXEC_MAX_NUM); |
||||
private M3U8ThreadStateManager mManager; |
||||
private ReentrantLock LOCK = new ReentrantLock(); |
||||
private Condition mCondition = LOCK.newCondition(); |
||||
|
||||
protected M3U8FileLoader(IEventListener listener, DTaskWrapper wrapper) { |
||||
super(listener, wrapper); |
||||
} |
||||
|
||||
@Override protected IThreadState getStateManager(Looper looper) { |
||||
mManager = new M3U8ThreadStateManager(looper, mRecord, mListener); |
||||
mStateHandler = new Handler(looper, mManager); |
||||
return mManager; |
||||
} |
||||
|
||||
@Override protected long delayTimer() { |
||||
return 2000; |
||||
} |
||||
|
||||
/** |
||||
* 获取ts文件保存路径 |
||||
* |
||||
* @param dirCache 缓存目录 |
||||
* @param threadId ts文件名 |
||||
*/ |
||||
public static String getTsFilePath(String dirCache, int threadId) { |
||||
return String.format("%s/%s.ts", dirCache, threadId); |
||||
} |
||||
|
||||
private String getCacheDir() { |
||||
String cacheDir = mTaskWrapper.asM3U8().getCacheDir(); |
||||
if (!new File(cacheDir).exists()) { |
||||
CommonUtil.createDir(cacheDir); |
||||
} |
||||
return cacheDir; |
||||
} |
||||
|
||||
@Override protected void handleTask() { |
||||
|
||||
new Thread(new Runnable() { |
||||
@Override public void run() { |
||||
int index = 0; |
||||
while (!isBreak()) { |
||||
try { |
||||
LOCK.lock(); |
||||
while (mFlagQueue.size() < EXEC_MAX_NUM) { |
||||
if (index == mRecord.threadRecords.size()) { |
||||
break; |
||||
} |
||||
ThreadRecord tr = mRecord.threadRecords.get(index); |
||||
index++; |
||||
if (tr.isComplete) { |
||||
continue; |
||||
} |
||||
|
||||
M3U8ThreadTask task = createThreadTask(getCacheDir(), tr); |
||||
getTaskList().put(tr.threadId, task); |
||||
mFlagQueue.offer(startThreadTask(task)); |
||||
} |
||||
if (mFlagQueue.size() > 0) { |
||||
mCondition.await(); |
||||
} |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} finally { |
||||
LOCK.unlock(); |
||||
} |
||||
} |
||||
} |
||||
}).start(); |
||||
} |
||||
|
||||
private void notifyLock() { |
||||
try { |
||||
LOCK.lock(); |
||||
long id = mFlagQueue.take(); |
||||
ALog.d(TAG, String.format("线程【%s】完成", id)); |
||||
mCondition.signalAll(); |
||||
} catch (InterruptedException e) { |
||||
e.printStackTrace(); |
||||
} finally { |
||||
LOCK.unlock(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 启动线程任务 |
||||
* |
||||
* @return 线程唯一id标志 |
||||
*/ |
||||
private long startThreadTask(M3U8ThreadTask task) { |
||||
ThreadTaskManager.getInstance().startThread(mTaskWrapper.getKey(), task); |
||||
return IdGenerator.getInstance().nextId(); |
||||
} |
||||
|
||||
/** |
||||
* 配置config |
||||
*/ |
||||
private M3U8ThreadTask createThreadTask(String cacheDir, ThreadRecord record) { |
||||
SubThreadConfig<DTaskWrapper> config = new SubThreadConfig<>(); |
||||
config.url = record.m3u8url; |
||||
config.tempFile = new File(getTsFilePath(cacheDir, record.threadId)); |
||||
config.isBlock = mRecord.isBlock; |
||||
config.isOpenDynamicFile = mRecord.isOpenDynamicFile; |
||||
config.taskWrapper = mTaskWrapper; |
||||
config.record = record; |
||||
config.stateHandler = mStateHandler; |
||||
if (!config.tempFile.exists()) { |
||||
CommonUtil.createFile(config.tempFile.getPath()); |
||||
} |
||||
return new M3U8ThreadTask(config); |
||||
} |
||||
|
||||
/** |
||||
* M3U8线程状态管理 |
||||
*/ |
||||
private class M3U8ThreadStateManager implements IThreadState { |
||||
private final String TAG = "M3U8ThreadStateManager"; |
||||
|
||||
/** |
||||
* 任务状态回调 |
||||
*/ |
||||
private IEventListener mListener; |
||||
private int mStartThreadNum; // 启动的线程总数
|
||||
private int mCancelNum = 0; // 已经取消的线程的数
|
||||
private int mStopNum = 0; // 已经停止的线程数
|
||||
private int mFailNum = 0; // 失败的线程数
|
||||
private int mCompleteNum = 0; // 完成的线程数
|
||||
private long mProgress; //当前总进度
|
||||
private TaskRecord mTaskRecord; // 任务记录
|
||||
private Looper mLooper; |
||||
|
||||
/** |
||||
* @param taskRecord 任务记录 |
||||
* @param listener 任务事件 |
||||
*/ |
||||
M3U8ThreadStateManager(Looper looper, TaskRecord taskRecord, IEventListener listener) { |
||||
mLooper = looper; |
||||
mTaskRecord = taskRecord; |
||||
for (ThreadRecord record : taskRecord.threadRecords) { |
||||
if (!record.isComplete) { |
||||
mStartThreadNum++; |
||||
} |
||||
} |
||||
mListener = listener; |
||||
} |
||||
|
||||
/** |
||||
* 退出looper循环 |
||||
*/ |
||||
private void quitLooper() { |
||||
ALog.d(TAG, "quitLooper"); |
||||
mLooper.quit(); |
||||
} |
||||
|
||||
@Override public boolean handleMessage(Message msg) { |
||||
switch (msg.what) { |
||||
case STATE_STOP: |
||||
mStopNum++; |
||||
if (isStop()) { |
||||
mListener.onStop(mProgress); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_CANCEL: |
||||
mCancelNum++; |
||||
if (isCancel()) { |
||||
ALog.d(TAG, "icCancel"); |
||||
mListener.onCancel(); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_FAIL: |
||||
mFailNum++; |
||||
if (isFail()) { |
||||
Bundle b = msg.getData(); |
||||
mListener.onFail(b.getBoolean(KEY_RETRY, true), |
||||
(BaseException) b.getSerializable(KEY_ERROR_INFO)); |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_COMPLETE: |
||||
mCompleteNum++; |
||||
ALog.d(TAG, "完成"); |
||||
notifyLock(); |
||||
if (isComplete()) { |
||||
ALog.d(TAG, "isComplete, completeNum = " + mCompleteNum); |
||||
if (mTaskRecord.isBlock) { |
||||
if (mergeFile()) { |
||||
mListener.onComplete(); |
||||
} else { |
||||
mListener.onFail(false, null); |
||||
} |
||||
} else { |
||||
mListener.onComplete(); |
||||
} |
||||
quitLooper(); |
||||
} |
||||
break; |
||||
case STATE_RUNNING: |
||||
mProgress += (long) msg.obj; |
||||
break; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override public boolean isStop() { |
||||
printInfo("isStop"); |
||||
return mStartThreadNum == mStopNum + mFailNum + mCompleteNum; |
||||
} |
||||
|
||||
@Override public boolean isFail() { |
||||
printInfo("isFail"); |
||||
return mStartThreadNum == mStopNum + mFailNum + mCompleteNum + mCancelNum; |
||||
} |
||||
|
||||
@Override public boolean isComplete() { |
||||
printInfo("isComplete"); |
||||
return mStartThreadNum == mCompleteNum; |
||||
} |
||||
|
||||
@Override public boolean isCancel() { |
||||
printInfo("isCancel"); |
||||
return mStartThreadNum == mCancelNum + mFailNum + mCompleteNum; |
||||
} |
||||
|
||||
@Override public long getCurrentProgress() { |
||||
return mProgress; |
||||
} |
||||
|
||||
private void printInfo(String tag) { |
||||
if (false) { |
||||
ALog.d(tag, String.format( |
||||
"startThreadNum = %s, stopNum = %s, cancelNum = %s, failNum = %s, completeNum = %s", |
||||
mStartThreadNum, mStopNum, mCancelNum, mFailNum, mCompleteNum)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 合并文件 |
||||
* |
||||
* @return {@code true} 合并成功,{@code false}合并失败 |
||||
*/ |
||||
private boolean mergeFile() { |
||||
String cacheDir = getCacheDir(); |
||||
|
||||
List<String> partPath = new ArrayList<>(); |
||||
|
||||
for (ThreadRecord tr : mTaskRecord.threadRecords) { |
||||
partPath.add(getTsFilePath(cacheDir, tr.threadId)); |
||||
} |
||||
|
||||
boolean isSuccess = FileUtil.mergeFile(mTaskRecord.filePath, partPath); |
||||
if (isSuccess) { |
||||
for (String pp : partPath) { |
||||
File f = new File(pp); |
||||
if (f.exists()) { |
||||
f.delete(); |
||||
} |
||||
} |
||||
File targetFile = new File(mTaskRecord.filePath); |
||||
if (targetFile.exists() && targetFile.length() > mTaskRecord.fileLength) { |
||||
ALog.e(TAG, String.format("任务【%s】分块文件合并失败,下载长度超出文件真实长度,downloadLen: %s,fileSize: %s", |
||||
targetFile.getName(), targetFile.length(), mTaskRecord.fileLength)); |
||||
return false; |
||||
} |
||||
return true; |
||||
} else { |
||||
ALog.e(TAG, "合并失败"); |
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,136 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import com.arialyy.aria.core.common.CompleteInfo; |
||||
import com.arialyy.aria.core.common.IUtil; |
||||
import com.arialyy.aria.core.common.OnFileInfoCallback; |
||||
import com.arialyy.aria.core.download.DTaskWrapper; |
||||
import com.arialyy.aria.core.inf.AbsEntity; |
||||
import com.arialyy.aria.core.inf.IDownloadListener; |
||||
import com.arialyy.aria.core.inf.ITaskWrapper; |
||||
import com.arialyy.aria.exception.BaseException; |
||||
import com.arialyy.aria.exception.M3U8Exception; |
||||
import com.arialyy.aria.util.ALog; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* M3U8点播文件下载工具 |
||||
* 工作流程: |
||||
* 1、创建一个和文件同父路径并且同名隐藏文件夹 |
||||
* 2、将所有m3u8的ts文件下载到该文件夹中 |
||||
* 3、完成所有分片下载后,合并ts文件 |
||||
* 4、删除该隐藏文件夹 |
||||
*/ |
||||
public class M3U8FileUtil implements IUtil { |
||||
private final String TAG = "M3U8DownloadUtil"; |
||||
|
||||
private DTaskWrapper mWrapper; |
||||
private IDownloadListener mListener; |
||||
private boolean isStop = false, isCancel = false; |
||||
private List<String> mUrls = new ArrayList<>(); |
||||
|
||||
public M3U8FileUtil(DTaskWrapper wrapper, IDownloadListener listener) { |
||||
mWrapper = wrapper; |
||||
mListener = listener; |
||||
} |
||||
|
||||
@Override public String getKey() { |
||||
return mWrapper.getKey(); |
||||
} |
||||
|
||||
@Override public long getFileSize() { |
||||
return 0; |
||||
} |
||||
|
||||
@Override public long getCurrentLocation() { |
||||
return 0; |
||||
} |
||||
|
||||
@Override public boolean isRunning() { |
||||
return false; |
||||
} |
||||
|
||||
@Override public void cancel() { |
||||
isCancel = true; |
||||
} |
||||
|
||||
@Override public void stop() { |
||||
isStop = true; |
||||
} |
||||
|
||||
@Override public void start() { |
||||
if (isStop || isCancel) { |
||||
return; |
||||
} |
||||
mListener.onPre(); |
||||
|
||||
new Thread(createInfoThread()).start(); |
||||
} |
||||
|
||||
@Override public void resume() { |
||||
start(); |
||||
} |
||||
|
||||
@Override public void setMaxSpeed(int speed) { |
||||
|
||||
} |
||||
|
||||
private Runnable createInfoThread() { |
||||
if (mWrapper.getRequestType() == ITaskWrapper.M3U8_FILE) { |
||||
return new M3U8FileInfoThread(mWrapper, new OnFileInfoCallback() { |
||||
@Override public void onComplete(String key, CompleteInfo info) { |
||||
IM3U8UrlExtInfHandler handler = mWrapper.asM3U8().getExtInfHandler(); |
||||
if (handler != null) { |
||||
mUrls.addAll(handler.handler((List<String>) info.obj)); |
||||
if (handler.getClass().isAnonymousClass()) { |
||||
mWrapper.asM3U8().setExtInfHandler(null); |
||||
} |
||||
} else { |
||||
mUrls.addAll((Collection<? extends String>) info.obj); |
||||
} |
||||
if (mUrls.isEmpty()) { |
||||
failDownload(new M3U8Exception(TAG, "获取地址失败"), false); |
||||
return; |
||||
} else if (!mUrls.get(0).startsWith("http")) { |
||||
failDownload(new M3U8Exception(TAG, "地址错误,请使用IM3U8UrlExtInfHandler处理你的url信息"), false); |
||||
return; |
||||
} |
||||
mWrapper.asM3U8().setUrls(mUrls); |
||||
new M3U8FileLoader(mListener, mWrapper).start(); |
||||
} |
||||
|
||||
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) { |
||||
failDownload(e, needRetry); |
||||
} |
||||
}); |
||||
} else if (mWrapper.getRequestType() == ITaskWrapper.M3U8_LIVE) { |
||||
return null; |
||||
} else { |
||||
ALog.e(TAG, "不支持的类型"); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
private void failDownload(BaseException e, boolean needRetry) { |
||||
if (isStop || isCancel) { |
||||
return; |
||||
} |
||||
mListener.onFail(needRetry, e); |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* m3u8任务配信息 |
||||
*/ |
||||
public class M3U8TaskConfig { |
||||
|
||||
/** |
||||
* 所有ts文件的下载地址 |
||||
*/ |
||||
private List<String> urls; |
||||
|
||||
/** |
||||
* #EXTINF 标签信息处理器 |
||||
*/ |
||||
private IM3U8UrlExtInfHandler extInfHandler; |
||||
|
||||
/** |
||||
* 缓存目录 |
||||
*/ |
||||
private String cacheDir; |
||||
|
||||
public IM3U8UrlExtInfHandler getExtInfHandler() { |
||||
return extInfHandler; |
||||
} |
||||
|
||||
public void setExtInfHandler(IM3U8UrlExtInfHandler extInfHandler) { |
||||
this.extInfHandler = extInfHandler; |
||||
} |
||||
|
||||
public List<String> getUrls() { |
||||
return urls; |
||||
} |
||||
|
||||
public void setUrls(List<String> urls) { |
||||
this.urls = urls; |
||||
} |
||||
|
||||
public String getCacheDir() { |
||||
return cacheDir; |
||||
} |
||||
|
||||
public void setCacheDir(String cacheDir) { |
||||
this.cacheDir = cacheDir; |
||||
} |
||||
} |
@ -0,0 +1,140 @@ |
||||
///*
|
||||
// * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
// *
|
||||
// * 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 com.arialyy.aria.core.download.m3u8;
|
||||
//
|
||||
//import android.os.Bundle;
|
||||
//import android.os.Looper;
|
||||
//import android.os.Message;
|
||||
//import com.arialyy.aria.core.common.IThreadState;
|
||||
//import com.arialyy.aria.core.common.TaskRecord;
|
||||
//import com.arialyy.aria.core.download.DTaskWrapper;
|
||||
//import com.arialyy.aria.core.inf.IEventListener;
|
||||
//import com.arialyy.aria.exception.BaseException;
|
||||
//import com.arialyy.aria.util.ALog;
|
||||
//
|
||||
///**
|
||||
// * M3U8线程状态管理
|
||||
// */
|
||||
//public class M3U8ThreadStateManager implements IThreadState {
|
||||
// private final String TAG = "M3U8ThreadStateManager";
|
||||
//
|
||||
// /**
|
||||
// * 任务状态回调
|
||||
// */
|
||||
// private IEventListener mListener;
|
||||
// private int mThreadNum; // 启动的线程总数
|
||||
// private int mCancelNum = 0; // 已经取消的线程的数
|
||||
// private int mStopNum = 0; // 已经停止的线程数
|
||||
// private int mFailNum = 0; // 失败的线程数
|
||||
// private int mCompleteNum = 0; // 完成的线程数
|
||||
// private long mProgress; //当前总进度
|
||||
// private TaskRecord mTaskRecord; // 任务记录
|
||||
// private Looper mLooper;
|
||||
//
|
||||
// /**
|
||||
// * @param taskRecord 任务记录
|
||||
// * @param listener 任务事件
|
||||
// */
|
||||
// M3U8ThreadStateManager(Looper looper, TaskRecord taskRecord, IEventListener listener) {
|
||||
// mLooper = looper;
|
||||
// mTaskRecord = taskRecord;
|
||||
// mThreadNum = mTaskRecord.threadNum;
|
||||
// mListener = listener;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 退出looper循环
|
||||
// */
|
||||
// private void quitLooper() {
|
||||
// mLooper.quit();
|
||||
// }
|
||||
//
|
||||
// @Override public boolean handleMessage(Message msg) {
|
||||
// switch (msg.what) {
|
||||
// case STATE_STOP:
|
||||
// mStopNum++;
|
||||
// if (isStop()) {
|
||||
// mListener.onStop(mProgress);
|
||||
// quitLooper();
|
||||
// }
|
||||
// break;
|
||||
// case STATE_CANCEL:
|
||||
// mCancelNum++;
|
||||
// if (isCancel()) {
|
||||
// ALog.d(TAG, "icCancel");
|
||||
// mListener.onCancel();
|
||||
// quitLooper();
|
||||
// }
|
||||
// break;
|
||||
// case STATE_FAIL:
|
||||
// mFailNum++;
|
||||
// if (isFail()) {
|
||||
// Bundle b = msg.getData();
|
||||
// mListener.onFail(b.getBoolean(KEY_RETRY, true),
|
||||
// (BaseException) b.getSerializable(KEY_ERROR_INFO));
|
||||
// quitLooper();
|
||||
// }
|
||||
// break;
|
||||
// case STATE_COMPLETE:
|
||||
// mCompleteNum++;
|
||||
// if (isComplete()) {
|
||||
// ALog.d(TAG, "isComplete, completeNum = " + mCompleteNum);
|
||||
// if (mTaskRecord.isBlock) {
|
||||
// if (mergeFile()) {
|
||||
// mListener.onComplete();
|
||||
// } else {
|
||||
// mListener.onFail(false, null);
|
||||
// }
|
||||
// } else {
|
||||
// mListener.onComplete();
|
||||
// }
|
||||
// quitLooper();
|
||||
// }
|
||||
// break;
|
||||
// case STATE_RUNNING:
|
||||
// mProgress += (long) msg.obj;
|
||||
// break;
|
||||
// case STATE_UPDATE_PROGRESS:
|
||||
// if (msg.obj == null) {
|
||||
// mProgress = updateBlockProgress();
|
||||
// } else {
|
||||
// mProgress = (long) msg.obj;
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Override public boolean isStop() {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Override public boolean isFail() {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Override public boolean isComplete() {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Override public boolean isCancel() {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// @Override public long getCurrentProgress() {
|
||||
// return mProgress;
|
||||
// }
|
||||
//}
|
@ -0,0 +1,247 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.download.m3u8; |
||||
|
||||
import com.arialyy.aria.core.common.AbsThreadTask; |
||||
import com.arialyy.aria.core.common.RequestEnum; |
||||
import com.arialyy.aria.core.common.SubThreadConfig; |
||||
import com.arialyy.aria.core.common.http.HttpTaskConfig; |
||||
import com.arialyy.aria.core.config.DownloadConfig; |
||||
import com.arialyy.aria.core.download.DTaskWrapper; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.core.download.downloader.ConnectionHelp; |
||||
import com.arialyy.aria.exception.AriaIOException; |
||||
import com.arialyy.aria.exception.TaskException; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.BufferedRandomAccessFile; |
||||
import java.io.BufferedInputStream; |
||||
import java.io.FileOutputStream; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.OutputStreamWriter; |
||||
import java.net.HttpURLConnection; |
||||
import java.net.MalformedURLException; |
||||
import java.net.URL; |
||||
import java.net.URLEncoder; |
||||
import java.nio.ByteBuffer; |
||||
import java.nio.channels.Channels; |
||||
import java.nio.channels.FileChannel; |
||||
import java.nio.channels.ReadableByteChannel; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* Created by lyy on 2017/1/18. 下载线程 |
||||
*/ |
||||
final class M3U8ThreadTask extends AbsThreadTask<DownloadEntity, DTaskWrapper> { |
||||
private final String TAG = "HttpThreadTask"; |
||||
|
||||
M3U8ThreadTask(SubThreadConfig<DTaskWrapper> config) { |
||||
super(config); |
||||
} |
||||
|
||||
@Override public M3U8ThreadTask call() throws Exception { |
||||
super.call(); |
||||
if (mRecord.isComplete) { |
||||
handleComplete(); |
||||
return this; |
||||
} |
||||
HttpURLConnection conn = null; |
||||
BufferedInputStream is = null; |
||||
try { |
||||
HttpTaskConfig taskDelegate = getTaskWrapper().asHttp(); |
||||
URL url = ConnectionHelp.handleUrl(getConfig().url, taskDelegate); |
||||
conn = ConnectionHelp.handleConnection(url, taskDelegate); |
||||
ALog.d(TAG, String.format("分片【%s】开始下载", mRecord.threadId)); |
||||
ConnectionHelp.setConnectParam(taskDelegate, conn); |
||||
conn.setConnectTimeout(getTaskConfig().getConnectTimeOut()); |
||||
conn.setReadTimeout(getTaskConfig().getIOTimeOut()); //设置读取流的等待时间,必须设置该参数
|
||||
if (taskDelegate.isChunked()) { |
||||
conn.setDoInput(true); |
||||
conn.setChunkedStreamingMode(0); |
||||
} |
||||
conn.connect(); |
||||
//Map<String, List<String>> header = conn.getHeaderFields();
|
||||
//int code = conn.getResponseCode();
|
||||
//String msg = conn.getResponseMessage();
|
||||
// 传递参数
|
||||
if (taskDelegate.getRequestEnum() == RequestEnum.POST) { |
||||
Map<String, String> params = taskDelegate.getParams(); |
||||
if (params != null) { |
||||
OutputStreamWriter dos = new OutputStreamWriter(conn.getOutputStream()); |
||||
Set<String> keys = params.keySet(); |
||||
StringBuilder sb = new StringBuilder(); |
||||
for (String key : keys) { |
||||
sb.append(key).append("=").append(URLEncoder.encode(params.get(key))).append("&"); |
||||
} |
||||
String paramStr = sb.toString(); |
||||
paramStr = paramStr.substring(0, paramStr.length() - 1); |
||||
dos.write(paramStr); |
||||
dos.flush(); |
||||
dos.close(); |
||||
} |
||||
} |
||||
|
||||
is = new BufferedInputStream(ConnectionHelp.convertInputStream(conn)); |
||||
if (taskDelegate.isChunked()) { |
||||
readChunked(is); |
||||
} else if (getConfig().isOpenDynamicFile) { |
||||
readDynamicFile(is); |
||||
} |
||||
} catch (MalformedURLException e) { |
||||
fail(mChildCurrentLocation, new TaskException(TAG, |
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", mRecord.threadId, |
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e)); |
||||
} catch (IOException e) { |
||||
fail(mChildCurrentLocation, new TaskException(TAG, |
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", mRecord.threadId, |
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e)); |
||||
} catch (Exception e) { |
||||
fail(mChildCurrentLocation, new TaskException(TAG, |
||||
String.format("分片【%s】下载失败,filePath: %s, url: %s", mRecord.threadId, |
||||
getConfig().tempFile.getPath(), getEntity().getUrl()), e)); |
||||
} finally { |
||||
try { |
||||
if (is != null) { |
||||
is.close(); |
||||
} |
||||
if (conn != null) { |
||||
conn.disconnect(); |
||||
} |
||||
} catch (IOException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* 读取chunked数据 |
||||
*/ |
||||
private void readChunked(InputStream is) { |
||||
FileOutputStream fos = null; |
||||
try { |
||||
fos = new FileOutputStream(getConfig().tempFile, true); |
||||
byte[] buffer = new byte[getTaskConfig().getBuffSize()]; |
||||
int len; |
||||
while (isLive() && (len = is.read(buffer)) != -1) { |
||||
if (isBreak()) { |
||||
break; |
||||
} |
||||
if (mSpeedBandUtil != null) { |
||||
mSpeedBandUtil.limitNextBytes(len); |
||||
} |
||||
fos.write(buffer, 0, len); |
||||
progress(len); |
||||
} |
||||
handleComplete(); |
||||
} catch (IOException e) { |
||||
fail(mChildCurrentLocation, new AriaIOException(TAG, |
||||
String.format("文件下载失败,savePath: %s, url: %s", getConfig().tempFile.getPath(), |
||||
getConfig().url), |
||||
e)); |
||||
} finally { |
||||
if (fos != null) { |
||||
try { |
||||
fos.close(); |
||||
} catch (IOException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 动态长度文件读取方式 |
||||
*/ |
||||
private void readDynamicFile(InputStream is) { |
||||
FileOutputStream fos = null; |
||||
FileChannel foc = null; |
||||
ReadableByteChannel fic = null; |
||||
try { |
||||
int len; |
||||
fos = new FileOutputStream(getConfig().tempFile, true); |
||||
foc = fos.getChannel(); |
||||
fic = Channels.newChannel(is); |
||||
ByteBuffer bf = ByteBuffer.allocate(getTaskConfig().getBuffSize()); |
||||
//如果要通过 Future 的 cancel 方法取消正在运行的任务,那么该任务必定是可以 对线程中断做出响应 的任务。
|
||||
|
||||
while (isLive() && (len = fic.read(bf)) != -1) { |
||||
if (isBreak()) { |
||||
break; |
||||
} |
||||
if (mSpeedBandUtil != null) { |
||||
mSpeedBandUtil.limitNextBytes(len); |
||||
} |
||||
bf.flip(); |
||||
foc.write(bf); |
||||
bf.compact(); |
||||
progress(len); |
||||
} |
||||
handleComplete(); |
||||
} catch (IOException e) { |
||||
fail(mChildCurrentLocation, new AriaIOException(TAG, |
||||
String.format("文件下载失败,savePath: %s, url: %s", getConfig().tempFile.getPath(), |
||||
getConfig().url), |
||||
e)); |
||||
} finally { |
||||
try { |
||||
if (fos != null) { |
||||
fos.flush(); |
||||
fos.close(); |
||||
} |
||||
if (foc != null) { |
||||
foc.close(); |
||||
} |
||||
if (fic != null) { |
||||
fic.close(); |
||||
} |
||||
} catch (IOException e) { |
||||
e.printStackTrace(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 处理完成配置文件的更新或事件回调 |
||||
*/ |
||||
private void handleComplete() { |
||||
if (isBreak()) { |
||||
return; |
||||
} |
||||
if (getTaskWrapper().asHttp().isChunked()) { |
||||
sendCompleteMsg(); |
||||
return; |
||||
} |
||||
|
||||
//支持断点的处理
|
||||
if (mTaskWrapper.isSupportBP()) { |
||||
writeConfig(true, getConfig().tempFile.length()); |
||||
sendCompleteMsg(); |
||||
} else { |
||||
sendCompleteMsg(); |
||||
} |
||||
} |
||||
|
||||
@Override public int getMaxSpeed() { |
||||
return mAridManager.getDownloadConfig().getMaxSpeed(); |
||||
} |
||||
|
||||
@Override protected DownloadConfig getTaskConfig() { |
||||
return getTaskWrapper().getConfig(); |
||||
} |
||||
} |
@ -1,58 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.inf; |
||||
|
||||
/** |
||||
* Created by lyy on 2019/4/5. |
||||
* 组合任务接收器功能接口 |
||||
*/ |
||||
public interface IGroupTarget { |
||||
|
||||
/** |
||||
* 获取实体 |
||||
*/ |
||||
AbsEntity getEntity(); |
||||
|
||||
/** |
||||
* 任务是否存在 |
||||
* |
||||
* @return {@code true}任务存在,{@code false} 任务不存在 |
||||
*/ |
||||
boolean taskExists(); |
||||
|
||||
/** |
||||
* 任务是否在执行 |
||||
* |
||||
* @return {@code true} 任务正在执行,{@code false} 任务没有执行 |
||||
*/ |
||||
boolean isRunning(); |
||||
|
||||
/** |
||||
* 检查实体是否合法 |
||||
* |
||||
* @return {@code true}合法 |
||||
*/ |
||||
boolean checkEntity(); |
||||
|
||||
/** |
||||
* 检查文件夹路径 |
||||
* 1、文件夹路径不能为空 |
||||
* 2、文件夹路径不能是文件 |
||||
* |
||||
* @return {@code true} 合法 |
||||
*/ |
||||
boolean checkDirPath(); |
||||
} |
@ -1,66 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.inf; |
||||
|
||||
/** |
||||
* Created by lyy on 2019/4/5. |
||||
* 普通任务接收器功能接口 |
||||
*/ |
||||
public interface INormalTarget { |
||||
|
||||
/** |
||||
* 获取实体 |
||||
*/ |
||||
AbsEntity getEntity(); |
||||
|
||||
/** |
||||
* 任务是否存在 |
||||
* |
||||
* @return {@code true}任务存在,{@code false} 任务不存在 |
||||
*/ |
||||
boolean taskExists(); |
||||
|
||||
/** |
||||
* 任务是否在执行 |
||||
* |
||||
* @return {@code true} 任务正在执行,{@code false} 任务没有执行 |
||||
*/ |
||||
boolean isRunning(); |
||||
|
||||
/** |
||||
* 检查下载实体,判断实体是否合法 合法标准为: |
||||
* 1、下载路径不为null,并且下载路径是正常的http或ftp路径 |
||||
* 2、保存路径不为null,并且保存路径是android文件系统路径 |
||||
* 3、保存路径不能重复 |
||||
* |
||||
* @return {@code true}合法 |
||||
*/ |
||||
boolean checkEntity(); |
||||
|
||||
/** |
||||
* 检查并设置普通任务的文件保存路径 |
||||
* |
||||
* @return {@code true}保存路径合法 |
||||
*/ |
||||
boolean checkFilePath(); |
||||
|
||||
/** |
||||
* 检查普通任务的下载地址 |
||||
* |
||||
* @return {@code true}地址合法 |
||||
*/ |
||||
boolean checkUrl(); |
||||
} |
@ -1,155 +0,0 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.core.upload; |
||||
|
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.inf.AbsEntity; |
||||
import com.arialyy.aria.core.common.ftp.IFtpUploadInterceptor; |
||||
import com.arialyy.aria.core.inf.INormalTarget; |
||||
import com.arialyy.aria.core.manager.TaskWrapperManager; |
||||
import com.arialyy.aria.core.queue.UploadTaskQueue; |
||||
import com.arialyy.aria.orm.DbEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CheckUtil; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import java.io.File; |
||||
|
||||
/** |
||||
* Created by Aria.Lao on 2019/4/5. |
||||
* 普通上传任务通用功能处理 |
||||
*/ |
||||
class UNormalDelegate<TARGET extends AbsUploadTarget> implements INormalTarget { |
||||
private String TAG = "UNormalDelegate"; |
||||
private UploadEntity mEntity; |
||||
private TARGET mTarget; |
||||
/** |
||||
* 上传路径 |
||||
*/ |
||||
private String mTempUrl; |
||||
|
||||
UNormalDelegate(TARGET target, String filePath, String targetName) { |
||||
mTarget = target; |
||||
initTarget(filePath, targetName); |
||||
} |
||||
|
||||
private void initTarget(String filePath, String targetName) { |
||||
UTaskWrapper taskWrapper = |
||||
TaskWrapperManager.getInstance().getHttpTaskWrapper(UTaskWrapper.class, filePath); |
||||
mEntity = taskWrapper.getEntity(); |
||||
File file = new File(filePath); |
||||
mEntity.setFileName(file.getName()); |
||||
mEntity.setFileSize(file.length()); |
||||
mTarget.setTargetName(targetName); |
||||
mTarget.setTaskWrapper(taskWrapper); |
||||
mTempUrl = mEntity.getUrl(); |
||||
} |
||||
|
||||
TARGET updateUrl(String newUrl) { |
||||
mTempUrl = newUrl; |
||||
return mTarget; |
||||
} |
||||
|
||||
TARGET setUploadInterceptor(IFtpUploadInterceptor uploadInterceptor) { |
||||
if (uploadInterceptor == null) { |
||||
throw new NullPointerException("ftp拦截器为空"); |
||||
} |
||||
mTarget.getTaskWrapper().asFtp().setUploadInterceptor(uploadInterceptor); |
||||
return mTarget; |
||||
} |
||||
|
||||
@Override public AbsEntity getEntity() { |
||||
return mEntity; |
||||
} |
||||
|
||||
@Override public boolean taskExists() { |
||||
return DbEntity.checkDataExist(UploadEntity.class, "key=?", mEntity.getFilePath()); |
||||
} |
||||
|
||||
@Override public boolean isRunning() { |
||||
UploadTask task = UploadTaskQueue.getInstance().getTask(mEntity.getKey()); |
||||
return task != null && task.isRunning(); |
||||
} |
||||
|
||||
@Override public boolean checkEntity() { |
||||
boolean b = checkUrl() && checkFilePath(); |
||||
if (b) { |
||||
mEntity.save(); |
||||
} |
||||
if (mTarget.getTaskWrapper().asFtp().getUrlEntity() != null && mTarget.getTaskWrapper() |
||||
.asFtp() |
||||
.getUrlEntity().isFtps) { |
||||
if (TextUtils.isEmpty(mTarget.getTaskWrapper().asFtp().getUrlEntity().storePath)) { |
||||
ALog.e(TAG, "证书路径为空"); |
||||
return false; |
||||
} |
||||
if (TextUtils.isEmpty(mTarget.getTaskWrapper().asFtp().getUrlEntity().keyAlias)) { |
||||
ALog.e(TAG, "证书别名为空"); |
||||
return false; |
||||
} |
||||
} |
||||
return b; |
||||
} |
||||
|
||||
@Override public boolean checkFilePath() { |
||||
String filePath = mEntity.getFilePath(); |
||||
if (TextUtils.isEmpty(filePath)) { |
||||
ALog.e(TAG, "上传失败,文件路径为null"); |
||||
return false; |
||||
} else if (!filePath.startsWith("/")) { |
||||
ALog.e(TAG, "上传失败,文件路径【" + filePath + "】不合法"); |
||||
return false; |
||||
} |
||||
|
||||
File file = new File(mEntity.getFilePath()); |
||||
if (!file.exists()) { |
||||
ALog.e(TAG, "上传失败,文件【" + filePath + "】不存在"); |
||||
return false; |
||||
} |
||||
if (file.isDirectory()) { |
||||
ALog.e(TAG, "上传失败,文件【" + filePath + "】不能是文件夹"); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Override public boolean checkUrl() { |
||||
|
||||
final String url = mTempUrl; |
||||
if (TextUtils.isEmpty(url)) { |
||||
ALog.e(TAG, "上传失败,url为null"); |
||||
return false; |
||||
} else if (!CheckUtil.checkUrlNotThrow(url)) { |
||||
ALog.e(TAG, "上传失败,url【" + url + "】错误"); |
||||
return false; |
||||
} |
||||
int index = url.indexOf("://"); |
||||
if (index == -1) { |
||||
ALog.e(TAG, "上传失败,url【" + url + "】不合法"); |
||||
return false; |
||||
} |
||||
mEntity.setUrl(url); |
||||
return true; |
||||
} |
||||
|
||||
void setTempUrl(String tempUrl) { |
||||
this.mTempUrl = tempUrl; |
||||
mTarget.getTaskWrapper().asFtp().setUrlEntity(CommonUtil.getFtpUrlInfo(tempUrl)); |
||||
} |
||||
|
||||
public String getTempUrl() { |
||||
return mTempUrl; |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.aria.exception; |
||||
|
||||
public class M3U8Exception extends BaseException{ |
||||
private static final String M3U8_EXCEPTION = "M3U8 Exception:"; |
||||
|
||||
public M3U8Exception(String tag, String message) { |
||||
super(tag, String.format("%s%s", M3U8_EXCEPTION, message)); |
||||
} |
||||
|
||||
public M3U8Exception(String tag, String message, Exception e){ |
||||
super(tag, message, e); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
package com.arialyy.aria.util; |
||||
|
||||
public class IdGenerator { |
||||
/** |
||||
* SnowFlake算法 64位Long类型生成唯一ID 第一位0,表明正数 2-42,41位,表示毫秒时间戳差值,起始值自定义 |
||||
* 43-52,10位,机器编号,5位数据中心编号,5位进程编号 53-64,12位,毫秒内计数器 本机内存生成,性能高 |
||||
* <p> |
||||
* 主要就是三部分: 时间戳,进程id,序列号 时间戳41,id10位,序列号12位 |
||||
* |
||||
* @author chiwei |
||||
* @param args |
||||
* @since JDK 1.6 |
||||
*/ |
||||
|
||||
private static volatile IdGenerator INSTANCE = null; |
||||
|
||||
private final static long beginTs = 1483200000000L; |
||||
|
||||
private long lastTs = 0L; |
||||
|
||||
private long processId; |
||||
private int processIdBits = 10; |
||||
|
||||
private long sequence = 0L; |
||||
private int sequenceBits = 12; |
||||
|
||||
private IdGenerator() { |
||||
|
||||
} |
||||
|
||||
public static synchronized IdGenerator getInstance() { |
||||
if (INSTANCE == null) { |
||||
INSTANCE = new IdGenerator(); |
||||
} |
||||
return INSTANCE; |
||||
} |
||||
|
||||
// 10位进程ID标识
|
||||
public IdGenerator(long processId) { |
||||
if (processId > ((1 << processIdBits) - 1)) { |
||||
throw new RuntimeException("进程ID超出范围,设置位数" + processIdBits + ",最大" |
||||
+ ((1 << processIdBits) - 1)); |
||||
} |
||||
this.processId = processId; |
||||
} |
||||
|
||||
private long timeGen() { |
||||
return System.currentTimeMillis(); |
||||
} |
||||
|
||||
public synchronized long nextId() { |
||||
long ts = timeGen(); |
||||
if (ts < lastTs) {// 刚刚生成的时间戳比上次的时间戳还小,出错
|
||||
throw new RuntimeException("时间戳顺序错误"); |
||||
} |
||||
if (ts == lastTs) {// 刚刚生成的时间戳跟上次的时间戳一样,则需要生成一个sequence序列号
|
||||
// sequence循环自增
|
||||
sequence = (sequence + 1) & ((1 << sequenceBits) - 1); |
||||
// 如果sequence=0则需要重新生成时间戳
|
||||
if (sequence == 0) { |
||||
// 且必须保证时间戳序列往后
|
||||
ts = nextTs(lastTs); |
||||
} |
||||
} else {// 如果ts>lastTs,时间戳序列已经不同了,此时可以不必生成sequence了,直接取0
|
||||
sequence = 0L; |
||||
} |
||||
lastTs = ts;// 更新lastTs时间戳
|
||||
return ((ts - beginTs) << (processIdBits + sequenceBits)) | (processId << sequenceBits) |
||||
| sequence; |
||||
} |
||||
|
||||
private long nextTs(long lastTs) { |
||||
long ts = timeGen(); |
||||
while (ts <= lastTs) { |
||||
ts = timeGen(); |
||||
} |
||||
return ts; |
||||
} |
||||
} |
@ -0,0 +1,268 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.simple.core.download.m3u8; |
||||
|
||||
import android.arch.lifecycle.Observer; |
||||
import android.arch.lifecycle.ViewModelProviders; |
||||
import android.os.Bundle; |
||||
import android.support.annotation.Nullable; |
||||
import android.util.Log; |
||||
import android.view.Menu; |
||||
import android.view.MenuItem; |
||||
import android.view.View; |
||||
import android.widget.Toast; |
||||
import com.arialyy.annotations.Download; |
||||
import com.arialyy.aria.core.Aria; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.core.download.DownloadTarget; |
||||
import com.arialyy.aria.core.download.DownloadTask; |
||||
import com.arialyy.aria.core.inf.IEntity; |
||||
import com.arialyy.aria.core.inf.IHttpFileLenAdapter; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.aria.util.CommonUtil; |
||||
import com.arialyy.frame.util.show.T; |
||||
import com.arialyy.simple.R; |
||||
import com.arialyy.simple.base.BaseActivity; |
||||
import com.arialyy.simple.common.ModifyPathDialog; |
||||
import com.arialyy.simple.common.ModifyUrlDialog; |
||||
import com.arialyy.simple.databinding.ActivityM3u8Binding; |
||||
import java.io.File; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
public class M3U8DownloadActivity extends BaseActivity<ActivityM3u8Binding> { |
||||
|
||||
private String mUrl; |
||||
private String mFilePath; |
||||
private M3U8Module mModule; |
||||
private DownloadTarget mTarget; |
||||
|
||||
@Override |
||||
protected void init(Bundle savedInstanceState) { |
||||
super.init(savedInstanceState); |
||||
setTitle("单任务下载"); |
||||
Aria.download(this).register(); |
||||
mModule = ViewModelProviders.of(this).get(M3U8Module.class); |
||||
mModule.getHttpDownloadInfo(this).observe(this, new Observer<DownloadEntity>() { |
||||
|
||||
@Override public void onChanged(@Nullable DownloadEntity entity) { |
||||
if (entity == null) { |
||||
return; |
||||
} |
||||
mTarget = Aria.download(M3U8DownloadActivity.this).load(entity.getUrl()); |
||||
if (mTarget.getTaskState() == IEntity.STATE_STOP) { |
||||
getBinding().setStateStr(getString(R.string.resume)); |
||||
} else if (mTarget.isRunning()) { |
||||
getBinding().setStateStr(getString(R.string.stop)); |
||||
} |
||||
|
||||
if (entity.getFileSize() != 0) { |
||||
getBinding().setFileSize(CommonUtil.formatFileSize(entity.getFileSize())); |
||||
getBinding().setProgress(entity.isComplete() ? 100 |
||||
: (int) (entity.getCurrentProgress() * 100 / entity.getFileSize())); |
||||
} |
||||
getBinding().setUrl(entity.getUrl()); |
||||
getBinding().setFilePath(entity.getFilePath()); |
||||
mUrl = entity.getUrl(); |
||||
mFilePath = entity.getFilePath(); |
||||
} |
||||
}); |
||||
getBinding().setViewModel(this); |
||||
} |
||||
|
||||
public void chooseUrl() { |
||||
ModifyUrlDialog dialog = |
||||
new ModifyUrlDialog(this, getString(R.string.modify_url_dialog_title), mUrl); |
||||
dialog.show(getSupportFragmentManager(), "ModifyUrlDialog"); |
||||
} |
||||
|
||||
public void chooseFilePath() { |
||||
ModifyPathDialog dialog = |
||||
new ModifyPathDialog(this, getString(R.string.modify_file_path), mFilePath); |
||||
dialog.show(getSupportFragmentManager(), "ModifyPathDialog"); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onCreateOptionsMenu(Menu menu) { |
||||
getMenuInflater().inflate(R.menu.menu_single_task_activity, menu); |
||||
return super.onCreateOptionsMenu(menu); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onMenuItemClick(MenuItem item) { |
||||
int speed = -1; |
||||
String msg = ""; |
||||
switch (item.getItemId()) { |
||||
case R.id.help: |
||||
msg = "一些小知识点:\n" |
||||
+ "1、你可以在注解中增加链接,用于指定被注解的方法只能被特定的下载任务回调,以防止progress乱跳\n" |
||||
+ "2、当遇到网络慢的情况时,你可以先使用onPre()更新UI界面,待连接成功时,再在onTaskPre()获取完整的task数据,然后给UI界面设置正确的数据\n" |
||||
+ "3、你可以在界面初始化时通过Aria.download(this).load(URL).getPercent()等方法快速获取相关任务的一些数据"; |
||||
showMsgDialog("tip", msg); |
||||
break; |
||||
case R.id.speed_0: |
||||
speed = 0; |
||||
break; |
||||
case R.id.speed_128: |
||||
speed = 128; |
||||
break; |
||||
case R.id.speed_256: |
||||
speed = 256; |
||||
break; |
||||
case R.id.speed_512: |
||||
speed = 512; |
||||
break; |
||||
case R.id.speed_1m: |
||||
speed = 1024; |
||||
break; |
||||
} |
||||
if (speed > -1) { |
||||
msg = item.getTitle().toString(); |
||||
Aria.download(this).setMaxSpeed(speed); |
||||
T.showShort(this, msg); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
@Download.onWait |
||||
void onWait(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
Log.d(TAG, "wait ==> " + task.getDownloadEntity().getFileName()); |
||||
} |
||||
} |
||||
|
||||
@Download.onPre |
||||
protected void onPre(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setStateStr(getString(R.string.stop)); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskStart |
||||
void taskStart(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setFileSize(task.getConvertFileSize()); |
||||
ALog.d(TAG, "isComplete = " + task.isComplete() + ", state = " + task.getState()); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskRunning |
||||
protected void running(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
ALog.d(TAG, "isRunning"); |
||||
//Log.d(TAG, task.getKey());
|
||||
long len = task.getFileSize(); |
||||
if (len == 0) { |
||||
getBinding().setProgress(0); |
||||
} else { |
||||
getBinding().setProgress(task.getPercent()); |
||||
} |
||||
getBinding().setSpeed(task.getConvertSpeed()); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskResume |
||||
void taskResume(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setStateStr(getString(R.string.stop)); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskStop |
||||
void taskStop(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setStateStr(getString(R.string.resume)); |
||||
getBinding().setSpeed(""); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskCancel |
||||
void taskCancel(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setProgress(0); |
||||
getBinding().setStateStr(getString(R.string.start)); |
||||
getBinding().setSpeed(""); |
||||
Log.d(TAG, "cancel"); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskFail |
||||
void taskFail(DownloadTask task, Exception e) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
Toast.makeText(M3U8DownloadActivity.this, getString(R.string.download_fail), |
||||
Toast.LENGTH_SHORT) |
||||
.show(); |
||||
getBinding().setStateStr(getString(R.string.start)); |
||||
} |
||||
} |
||||
|
||||
@Download.onTaskComplete |
||||
void taskComplete(DownloadTask task) { |
||||
if (task.getKey().equals(mUrl)) { |
||||
getBinding().setProgress(100); |
||||
Toast.makeText(M3U8DownloadActivity.this, getString(R.string.download_success), |
||||
Toast.LENGTH_SHORT).show(); |
||||
getBinding().setStateStr(getString(R.string.re_start)); |
||||
getBinding().setSpeed(""); |
||||
ALog.d(TAG, "md5: " + CommonUtil.getFileMD5(new File(task.getDownloadPath()))); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected int setLayoutId() { |
||||
return R.layout.activity_m3u8; |
||||
} |
||||
|
||||
public void onClick(View view) { |
||||
switch (view.getId()) { |
||||
case R.id.start: |
||||
if (mTarget.isRunning()) { |
||||
Aria.download(this).load(mUrl).stop(); |
||||
} else { |
||||
startD(); |
||||
} |
||||
break; |
||||
case R.id.cancel: |
||||
Aria.download(this).load(mUrl).cancel(true); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
private void startD() { |
||||
Aria.download(M3U8DownloadActivity.this) |
||||
.load(mUrl) |
||||
.useServerFileName(true) |
||||
.setFilePath(mFilePath, true) |
||||
.asM3U8() |
||||
.start(); |
||||
} |
||||
|
||||
@Override |
||||
protected void onStop() { |
||||
super.onStop(); |
||||
//Aria.download(this).unRegister();
|
||||
} |
||||
|
||||
@Override protected void dataCallback(int result, Object data) { |
||||
super.dataCallback(result, data); |
||||
if (result == ModifyUrlDialog.MODIFY_URL_DIALOG_RESULT) { |
||||
mModule.uploadUrl(this, String.valueOf(data)); |
||||
} else if (result == ModifyPathDialog.MODIFY_PATH_RESULT) { |
||||
mModule.updateFilePath(this, String.valueOf(data)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,95 @@ |
||||
/* |
||||
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
|
||||
* |
||||
* 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 com.arialyy.simple.core.download.m3u8; |
||||
|
||||
import android.arch.lifecycle.LiveData; |
||||
import android.arch.lifecycle.MutableLiveData; |
||||
import android.content.Context; |
||||
import android.os.Environment; |
||||
import android.text.TextUtils; |
||||
import com.arialyy.aria.core.Aria; |
||||
import com.arialyy.aria.core.download.DownloadEntity; |
||||
import com.arialyy.aria.util.ALog; |
||||
import com.arialyy.frame.base.BaseViewModule; |
||||
import com.arialyy.simple.util.AppUtil; |
||||
import java.io.File; |
||||
|
||||
public class M3U8Module extends BaseViewModule { |
||||
private final String M3U8_URL_KEY = "M3U8_URL_KEY"; |
||||
private final String M3U8_PATH_KEY = "M3U8_PATH_KEY"; |
||||
|
||||
private final String defUrl = "https://www.gaoya123.cn/2019/1557993797897.m3u8"; |
||||
private final String filePath = |
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath() |
||||
+ "/1557993797897.m3u8"; |
||||
|
||||
private MutableLiveData<DownloadEntity> liveData = new MutableLiveData<>(); |
||||
private DownloadEntity singDownloadInfo; |
||||
|
||||
/** |
||||
* 单任务下载的信息 |
||||
*/ |
||||
LiveData<DownloadEntity> getHttpDownloadInfo(Context context) { |
||||
String url = AppUtil.getConfigValue(context, M3U8_URL_KEY, defUrl); |
||||
String filePath = AppUtil.getConfigValue(context, M3U8_PATH_KEY, this.filePath); |
||||
|
||||
singDownloadInfo = Aria.download(context).getDownloadEntity(url); |
||||
if (singDownloadInfo == null) { |
||||
singDownloadInfo = new DownloadEntity(); |
||||
singDownloadInfo.setUrl(url); |
||||
File temp = new File(this.filePath); |
||||
singDownloadInfo.setDownloadPath(filePath); |
||||
singDownloadInfo.setFileName(temp.getName()); |
||||
} else { |
||||
AppUtil.setConfigValue(context, M3U8_PATH_KEY, singDownloadInfo.getDownloadPath()); |
||||
AppUtil.setConfigValue(context, M3U8_URL_KEY, singDownloadInfo.getUrl()); |
||||
} |
||||
liveData.postValue(singDownloadInfo); |
||||
|
||||
return liveData; |
||||
} |
||||
|
||||
/** |
||||
* 更新文件保存路径 |
||||
* |
||||
* @param filePath 文件保存路径 |
||||
*/ |
||||
void updateFilePath(Context context, String filePath) { |
||||
if (TextUtils.isEmpty(filePath)) { |
||||
ALog.e(TAG, "文件保存路径为空"); |
||||
return; |
||||
} |
||||
File temp = new File(filePath); |
||||
AppUtil.setConfigValue(context, M3U8_PATH_KEY, filePath); |
||||
singDownloadInfo.setFileName(temp.getName()); |
||||
singDownloadInfo.setDownloadPath(filePath); |
||||
liveData.postValue(singDownloadInfo); |
||||
} |
||||
|
||||
/** |
||||
* 更新url |
||||
*/ |
||||
void uploadUrl(Context context, String url) { |
||||
if (TextUtils.isEmpty(url)) { |
||||
ALog.e(TAG, "下载地址为空"); |
||||
return; |
||||
} |
||||
AppUtil.setConfigValue(context, M3U8_URL_KEY, url); |
||||
singDownloadInfo.setUrl(url); |
||||
liveData.postValue(singDownloadInfo); |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:height="24dp" |
||||
android:viewportHeight="1024" |
||||
android:viewportWidth="1024" |
||||
android:width="24dp"> |
||||
<path |
||||
android:fillColor="#F4F4F4" |
||||
android:pathData="M891.9,307.2v634.88c0,15.87 -12.8,28.16 -28.16,28.67H206.34c-11.78,0 -21.5,-9.22 -22.02,-21.5V90.62c0,-8.7 7.17,-15.87 15.87,-15.87h442.88c0.51,0 1.02,0.51 1.02,1.02s0,0.51 -0.51,0.51c-0.51,0.51 -0.51,1.02 0,1.54l247.81,228.86c0.51,0 0.51,0.51 0.51,0.51z"/> |
||||
<path |
||||
android:fillColor="#6200AF" |
||||
android:pathData="M643.58,291.84L643.58,74.75l248.32,231.94h-233.98c-7.68,0 -14.34,-6.66 -14.34,-14.85zM420.35,366.08L202.24,366.08c-58.37,0 -105.47,-47.1 -105.47,-105.47S143.36,154.62 202.24,154.62h218.11c58.37,0 105.47,47.1 105.47,105.47s-47.1,105.98 -105.47,105.98zM658.43,771.58h-235.52c-21.5,0 -38.4,-17.41 -38.91,-38.4v-178.69c0,-21.5 17.41,-38.4 38.4,-38.4h235.52c21.5,0 38.4,17.41 38.4,38.4v178.69c0,20.99 -16.9,38.4 -37.89,38.4zM422.91,529.41c-13.82,0 -24.58,11.26 -24.58,24.58v178.69c0,13.82 11.26,24.58 24.58,24.58h235.52c13.82,0 24.58,-11.26 24.58,-24.58v-178.69c0,-13.82 -11.26,-24.58 -24.58,-24.58h-235.52z"/> |
||||
<path |
||||
android:fillColor="#6200AF" |
||||
android:pathData="M437.76,771.58c-4.1,0 -7.17,-3.07 -7.17,-7.17L430.59,522.24c0,-4.1 3.07,-7.17 7.17,-7.17s7.17,3.07 7.17,7.17v242.18c0,4.1 -3.07,7.17 -7.17,7.17zM643.07,771.58c-4.1,0 -7.17,-3.07 -7.17,-7.17L635.9,522.24c0,-4.1 3.07,-7.17 7.17,-7.17s7.17,3.07 7.17,7.17v242.18c0,4.1 -3.07,7.17 -7.17,7.17z"/> |
||||
<path |
||||
android:fillColor="#6200AF" |
||||
android:pathData="M437.76,595.97h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.58,7.17 -7.17,7.17zM437.76,650.75h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.58,7.17 -7.17,7.17zM437.76,712.19h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.58,7.17 -7.17,7.17zM689.66,595.97h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.07,7.17 -7.17,7.17zM689.66,650.75h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.07,7.17 -7.17,7.17zM689.66,712.19h-46.59c-4.1,0 -7.17,-3.07 -7.17,-7.17s3.07,-7.17 7.17,-7.17h46.59c4.1,0 7.17,3.07 7.17,7.17s-3.07,7.17 -7.17,7.17zM509.44,693.25c-1.02,0 -2.56,-0.51 -3.58,-1.02 -2.05,-1.02 -3.58,-3.58 -3.58,-6.14v-86.02c0,-2.56 1.54,-4.61 3.58,-6.14 2.05,-1.02 4.61,-1.02 7.17,0l74.24,43.01c3.58,2.05 4.61,6.14 2.56,9.73 -0.51,1.02 -1.54,2.05 -2.56,2.56l-74.24,43.01c-1.02,1.02 -2.05,1.02 -3.58,1.02zM516.61,612.86v61.44l53.25,-30.72 -53.25,-30.72z"/> |
||||
<path |
||||
android:fillColor="#FFFFFF" |
||||
android:pathData="M284.16,301.57h-15.36L261.12,279.55h-33.79L220.16,301.57h-15.36l31.74,-84.99h15.87l31.74,84.99zM257.54,268.29L245.76,233.98c-0.51,-1.54 -1.02,-3.58 -1.02,-5.12 -0.51,1.54 -0.51,3.58 -1.02,5.12l-11.78,33.79h25.6zM360.96,216.06l-30.72,85.5h-15.87l-30.21,-85.5h15.36l20.99,64.51c0.51,2.56 1.02,4.61 1.54,7.17 0.51,-2.56 1.02,-5.12 1.54,-7.17l21.5,-64.51h15.87zM391.68,301.57h-14.34L377.34,216.06h14.34v85.5z"/> |
||||
<path |
||||
android:fillColor="#F4F4F4" |
||||
android:pathData="M891.9,307.2v634.88c0,15.87 -12.8,28.16 -28.16,28.67H206.34c-11.78,0 -21.5,-9.22 -22.02,-21.5V90.62c0,-8.7 7.17,-15.87 15.87,-15.87h442.88c0.51,0 1.02,0.51 1.02,1.02s0,0.51 -0.51,0.51c-0.51,0.51 -0.51,1.02 0,1.54l247.81,228.86c0.51,0 0.51,0.51 0.51,0.51z"/> |
||||
<path |
||||
android:fillColor="@color/icon_color" |
||||
android:pathData="M643.58,291.84V74.75l248.32,231.94h-233.98c-7.68,0 -14.34,-6.66 -14.34,-14.85zM420.35,366.08H202.24c-58.37,0 -105.47,-47.1 -105.47,-105.47S143.36,154.62 202.24,154.62h218.11c58.37,0 105.47,47.1 105.47,105.47s-47.1,105.98 -105.47,105.98z"/> |
||||
<path |
||||
android:fillColor="#FFFFFF" |
||||
android:pathData="M308.22,231.94h-24.06v69.63h-19.46V231.94H240.64v-15.87h67.58v15.87zM321.02,297.98v-18.94c3.58,3.07 7.17,5.12 11.26,6.66 4.1,1.54 8.19,2.05 12.29,2.05 2.56,0 4.61,0 6.14,-0.51s3.07,-1.02 4.61,-2.05c1.02,-1.02 2.05,-1.54 2.56,-2.56 0.51,-1.02 1.02,-2.05 1.02,-3.58 0,-1.54 -0.51,-3.07 -1.54,-4.61 -1.02,-1.54 -2.05,-2.56 -4.1,-3.58 -1.54,-1.02 -3.58,-2.05 -5.63,-3.07 -2.05,-1.02 -4.61,-2.05 -7.17,-3.07 -6.66,-2.56 -11.78,-6.14 -14.85,-10.24 -3.07,-4.1 -5.12,-8.7 -5.12,-14.34 0,-4.61 1.02,-8.19 2.56,-11.26 1.54,-3.07 4.1,-5.63 7.17,-7.68s6.66,-3.58 10.75,-4.61 8.19,-1.54 12.8,-1.54c4.61,0 8.19,0.51 11.78,1.02 3.58,0.51 6.66,1.54 9.73,2.56v17.92c-1.54,-1.02 -3.07,-2.05 -4.61,-2.56 -1.54,-0.51 -3.58,-1.54 -5.12,-2.05s-3.58,-1.02 -5.12,-1.02c-1.54,0 -3.58,-0.51 -5.12,-0.51 -2.05,0 -4.1,0 -6.14,0.51l-4.61,1.54c-1.02,0.51 -2.05,1.54 -3.07,2.56 -0.51,1.02 -1.02,2.05 -1.02,3.58 0,1.54 0.51,2.56 1.02,3.58 0.51,1.02 2.05,2.05 3.07,3.07 1.54,1.02 3.07,2.05 5.12,3.07 2.05,1.02 4.1,2.05 6.66,3.07 3.58,1.54 6.66,3.07 9.22,4.61 2.56,1.54 5.12,3.58 7.17,5.63 2.05,2.05 3.58,4.1 4.61,7.17s1.54,5.63 1.54,9.22c0,4.61 -1.02,8.7 -2.56,11.78s-4.1,5.63 -7.17,7.68 -6.66,3.58 -10.75,4.1c-4.1,1.02 -8.19,1.54 -12.8,1.54 -4.61,0 -9.22,-0.51 -13.31,-1.02 -4.61,-1.02 -8.19,-2.56 -11.26,-4.1z"/> |
||||
<path |
||||
android:fillColor="@color/icon_color" |
||||
android:pathData="M504.83,708.61l75.26,-43.01c5.12,-3.58 6.66,-10.75 3.58,-15.87 -1.02,-1.54 -2.05,-2.56 -3.58,-3.58l-75.26,-43.01c-4.61,-2.05 -10.24,0 -12.8,4.61 -0.51,1.54 -1.02,3.07 -1.02,5.12v85.5c-0.51,5.12 3.58,9.73 8.7,10.24 2.05,1.02 3.58,0.51 5.12,0zM660.48,527.36L416.26,527.36c-15.36,0 -28.16,14.34 -28.16,32.26v192.51c0,17.92 12.29,32.26 28.16,32.26L660.48,784.38c15.36,0 28.16,-14.34 28.16,-32.26v-192c0,-17.92 -12.8,-32.77 -28.16,-32.77zM444.42,752.13c0,5.63 -4.1,10.24 -9.22,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c-0.51,-5.63 4.1,-10.24 9.22,-10.75L435.2,720.38c5.63,0.51 9.73,5.12 9.22,10.75v20.99zM444.42,666.62c0.51,5.63 -4.1,10.24 -9.22,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c0,-5.63 4.1,-10.24 9.22,-10.75L435.2,634.88c5.63,0.51 9.73,5.12 9.22,10.75v20.99zM444.42,581.12c0.51,5.63 -4.1,10.24 -9.22,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c-0.51,-5.63 4.1,-10.24 9.22,-10.75L435.2,549.38c5.63,0.51 9.73,5.12 9.22,10.75v20.99zM613.38,762.88L463.36,762.88v-214.02h150.02L613.38,762.88zM669.7,752.13c0,5.63 -3.58,10.24 -9.22,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c-0.51,-5.63 4.1,-10.24 9.22,-10.75h18.43c5.63,0.51 9.73,5.12 9.73,10.75l-0.51,20.99zM669.7,666.62c0.51,5.63 -4.1,10.24 -9.73,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c-0.51,-5.63 4.1,-10.24 9.22,-10.75h18.94c5.63,0.51 9.73,5.12 9.73,10.75l-0.51,20.99zM669.7,581.12c0.51,5.63 -4.1,10.24 -9.73,10.75h-18.43c-5.63,-0.51 -9.73,-5.12 -9.22,-10.75v-20.99c-0.51,-5.63 4.1,-10.24 9.22,-10.75h18.94c5.63,0.51 9.73,5.12 9.73,10.75l-0.51,20.99z"/> |
||||
</vector> |
@ -0,0 +1,80 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:bind="http://schemas.android.com/apk/res-auto" |
||||
> |
||||
|
||||
<data> |
||||
<variable |
||||
name="fileSize" |
||||
type="String" |
||||
/> |
||||
<variable |
||||
name="speed" |
||||
type="String" |
||||
/> |
||||
<variable |
||||
name="progress" |
||||
type="int" |
||||
/> |
||||
<variable |
||||
name="stateStr" |
||||
type="String" |
||||
/> |
||||
|
||||
<variable |
||||
name="url" |
||||
type="String" |
||||
/> |
||||
<variable |
||||
name="filePath" |
||||
type="String" |
||||
/> |
||||
<variable |
||||
name="viewModel" |
||||
type="com.arialyy.simple.core.download.m3u8.M3U8DownloadActivity" |
||||
/> |
||||
</data> |
||||
|
||||
<LinearLayout |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:fitsSystemWindows="true" |
||||
android:orientation="vertical" |
||||
tools:context=".core.download.SingleTaskActivity" |
||||
> |
||||
|
||||
<include layout="@layout/layout_bar"/> |
||||
|
||||
<com.arialyy.simple.widget.SvgTextView |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginLeft="16dp" |
||||
android:layout_marginRight="16dp" |
||||
android:layout_marginTop="16dp" |
||||
bind:iconClickListener="@{() -> viewModel.chooseUrl()}" |
||||
bind:svg_text_view_icon="@drawable/ic_modify" |
||||
bind:text="@{@string/url(url)}" |
||||
/> |
||||
|
||||
<com.arialyy.simple.widget.SvgTextView |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginLeft="16dp" |
||||
android:layout_marginRight="16dp" |
||||
android:layout_marginTop="8dp" |
||||
bind:iconClickListener="@{() -> viewModel.chooseFilePath()}" |
||||
bind:svg_text_view_icon="@drawable/ic_choose_file" |
||||
bind:text="@{@string/file_path(filePath)}" |
||||
/> |
||||
|
||||
<include |
||||
layout="@layout/layout_content_single" |
||||
bind:fileSize="@{fileSize}" |
||||
bind:progress="@{progress}" |
||||
bind:speed="@{speed}" |
||||
bind:stateStr="@{stateStr}" |
||||
/> |
||||
|
||||
</LinearLayout> |
||||
</layout> |
Loading…
Reference in new issue