39 | * public onCreate(Bundle state) {
40 | * super.onCreate(state);
41 | *
42 | * HttpProxyCacheServer proxy = getProxy();
43 | * String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
44 | * videoView.setVideoPath(proxyUrl);
45 | * }
46 | *
47 | * private HttpProxyCacheServer getProxy() {
48 | * // should return single instance of HttpProxyCacheServer shared for whole app.
49 | * }
50 | *
51 | *
52 | * @author Alexey Danilov (danikula@gmail.com).
53 | */
54 | public class HttpProxyCacheServer {
55 |
56 | private static final String PROXY_HOST = "127.0.0.1";
57 |
58 | private final Object clientsLock = new Object();
59 | private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
60 | private final Map95 | * If file for this url is fully cached (it means method {@link #isCached(String)} returns {@code true}) 96 | * then file:// uri to cached file will be returned. 97 | *
98 | * Calling this method has same effect as calling {@link #getProxyUrl(String, boolean)} with 2nd parameter set to {@code true}. 99 | * 100 | * @param url a url to file that should be cached. 101 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise. 102 | */ 103 | public String getProxyUrl(String url) { 104 | return getProxyUrl(url, true); 105 | } 106 | 107 | /** 108 | * Returns url that wrap original url and should be used for client (MediaPlayer, ExoPlayer, etc). 109 | *
110 | * If parameter {@code allowCachedFileUri} is {@code true} and file for this url is fully cached 111 | * (it means method {@link #isCached(String)} returns {@code true}) then file:// uri to cached file will be returned. 112 | * 113 | * @param url a url to file that should be cached. 114 | * @param allowCachedFileUri {@code true} if allow to return file:// uri if url is fully cached 115 | * @return a wrapped by proxy url if file is not fully cached or url pointed to cache file otherwise (if {@code allowCachedFileUri} is {@code true}). 116 | */ 117 | public String getProxyUrl(String url, boolean allowCachedFileUri) { 118 | if (allowCachedFileUri && isCached(url)) { 119 | File cacheFile = getCacheFile(url); 120 | touchFileSafely(cacheFile); 121 | return Uri.fromFile(cacheFile).toString(); 122 | } 123 | return isAlive() ? appendToProxyUrl(url) : url; 124 | } 125 | 126 | public void registerCacheListener(CacheListener cacheListener, String url) { 127 | checkAllNotNull(cacheListener, url); 128 | synchronized (clientsLock) { 129 | try { 130 | getClients(url).registerCacheListener(cacheListener); 131 | } catch (ProxyCacheException e) { 132 | LogU.d("Error registering cache listener", e); 133 | } 134 | } 135 | } 136 | 137 | public void unregisterCacheListener(CacheListener cacheListener, String url) { 138 | checkAllNotNull(cacheListener, url); 139 | synchronized (clientsLock) { 140 | try { 141 | getClients(url).unregisterCacheListener(cacheListener); 142 | } catch (ProxyCacheException e) { 143 | LogU.d("Error registering cache listener", e); 144 | } 145 | } 146 | } 147 | 148 | public void unregisterCacheListener(CacheListener cacheListener) { 149 | checkNotNull(cacheListener); 150 | synchronized (clientsLock) { 151 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 152 | clients.unregisterCacheListener(cacheListener); 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Checks is cache contains fully cached file for particular url. 159 | * 160 | * @param url an url cache file will be checked for. 161 | * @return {@code true} if cache contains fully cached file for passed in parameters url. 162 | */ 163 | public boolean isCached(String url) { 164 | checkNotNull(url, "Url can't be null!"); 165 | return getCacheFile(url).exists(); 166 | } 167 | 168 | public void shutdown() { 169 | LogU.d("Shutdown proxy server"); 170 | 171 | shutdownClients(); 172 | 173 | config.sourceInfoStorage.release(); 174 | 175 | waitConnectionThread.interrupt(); 176 | try { 177 | if (!serverSocket.isClosed()) { 178 | serverSocket.close(); 179 | } 180 | } catch (IOException e) { 181 | onError(new ProxyCacheException("Error shutting down proxy server", e)); 182 | } 183 | } 184 | 185 | private boolean isAlive() { 186 | return pinger.ping(3, 70); // 70+140+280=max~500ms 187 | } 188 | 189 | private String appendToProxyUrl(String url) { 190 | return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url)); 191 | } 192 | 193 | private File getCacheFile(String url) { 194 | File cacheDir = config.cacheRoot; 195 | String fileName = config.fileNameGenerator.generate(url); 196 | return new File(cacheDir, fileName); 197 | } 198 | 199 | private void touchFileSafely(File cacheFile) { 200 | try { 201 | config.diskUsage.touch(cacheFile); 202 | } catch (IOException e) { 203 | LogU.e("Error touching file " + cacheFile, e); 204 | } 205 | } 206 | 207 | private void shutdownClients() { 208 | synchronized (clientsLock) { 209 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 210 | clients.shutdown(); 211 | } 212 | clientsMap.clear(); 213 | } 214 | } 215 | 216 | private void waitForRequest() { 217 | try { 218 | while (!Thread.currentThread().isInterrupted()) { 219 | Socket socket = serverSocket.accept(); 220 | LogU.d("Accept new socket " + socket); 221 | socketProcessor.submit(new SocketProcessorRunnable(socket)); 222 | } 223 | } catch (IOException e) { 224 | onError(new ProxyCacheException("Error during waiting connection", e)); 225 | } 226 | } 227 | 228 | private void processSocket(Socket socket) { 229 | try { 230 | GetRequest request = GetRequest.read(socket.getInputStream()); 231 | 232 | String originUrl = request.uri; 233 | String proxyUrl = originUrl; 234 | boolean isPreLoad = false; 235 | if (originUrl.contains(preUrlPx)) { 236 | //预下载 237 | isPreLoad = true; 238 | proxyUrl = originUrl.split(preUrlPx)[0]; 239 | } 240 | 241 | if (isPreLoad) { 242 | request.isPreLoad = true; 243 | request.percentsPreLoad = Integer.parseInt(originUrl.split(preUrlPx)[1].split("_")[1]); 244 | } else { 245 | request.isPreLoad = false; 246 | if (preLoader.isLoading(originUrl)) { 247 | preLoader.stopLoad(originUrl); 248 | } 249 | } 250 | 251 | LogU.d("Request to cache proxy:" + request); 252 | String url = ProxyCacheUtils.decode(proxyUrl); 253 | 254 | if (pinger.isPingRequest(url)) { 255 | pinger.responseToPing(socket); 256 | } else { 257 | 258 | HttpProxyCacheServerClients clients = getClients(url); 259 | clients.processRequest(request, socket); 260 | } 261 | } catch (SocketException e) { 262 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 263 | // So just to prevent log flooding don't log stacktrace 264 | LogU.d("Closing socket… Socket is closed by client."); 265 | } catch (ProxyCacheException | IOException e) { 266 | onError(new ProxyCacheException("Error processing request", e)); 267 | } finally { 268 | releaseSocket(socket); 269 | LogU.d("Opened connections: " + getClientsCount()); 270 | } 271 | } 272 | 273 | private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException { 274 | synchronized (clientsLock) { 275 | HttpProxyCacheServerClients clients = clientsMap.get(url); 276 | if (clients == null) { 277 | clients = new HttpProxyCacheServerClients(url, config); 278 | clientsMap.put(url, clients); 279 | } 280 | return clients; 281 | } 282 | } 283 | 284 | 285 | public boolean preLoad(final String url, final int percentsPreLoad) { 286 | final String proxyUrlOrigin = getProxyUrl(url, false); 287 | final String proxyUrl = proxyUrlOrigin + preUrlPx + "_" + percentsPreLoad; 288 | 289 | 290 | if (preLoader.isLoading(proxyUrl)) { 291 | return false; 292 | } else { 293 | // preLoader.startLoad(proxyUrl, percentsPreLoad, Math.abs(21474836)); 294 | preLoader.cachedThreadPool.execute(new Runnable() { 295 | @Override 296 | public void run() { 297 | HttpProxyCacheServerClients clients = null; 298 | try { 299 | clients = getClients(url); 300 | final HttpProxyCache cacheProxy = clients.startProcessRequest(); 301 | if (!cacheProxy.cache.isCompleted()) { 302 | long length = cacheProxy.source.length(); 303 | long cacheLen = cacheProxy.cache.available(); 304 | LogU.d("预加载文件大小" + length+" 本地缓存大小 "+cacheLen+ " "+(cacheLen < Math.abs(length) * (percentsPreLoad / 100.0))); 305 | if (cacheLen < Math.abs(length) * (percentsPreLoad / 100.0)) { 306 | preLoader.startLoad(proxyUrl, percentsPreLoad, Math.abs(length)); 307 | } 308 | } 309 | } catch (ProxyCacheException e) { 310 | e.printStackTrace(); 311 | } 312 | } 313 | }); 314 | 315 | } 316 | return true; 317 | } 318 | 319 | private boolean isLoading(String url) { 320 | HttpProxyCacheServerClients clients = clientsMap.get(url); 321 | return clients == null; 322 | } 323 | 324 | private int getClientsCount() { 325 | synchronized (clientsLock) { 326 | int count = 0; 327 | for (HttpProxyCacheServerClients clients : clientsMap.values()) { 328 | count += clients.getClientsCount(); 329 | } 330 | return count; 331 | } 332 | } 333 | 334 | private void releaseSocket(Socket socket) { 335 | closeSocketInput(socket); 336 | closeSocketOutput(socket); 337 | closeSocket(socket); 338 | } 339 | 340 | private void closeSocketInput(Socket socket) { 341 | try { 342 | if (!socket.isInputShutdown()) { 343 | socket.shutdownInput(); 344 | } 345 | } catch (SocketException e) { 346 | // There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458 347 | // So just to prevent log flooding don't log stacktrace 348 | LogU.d("Releasing input stream… Socket is closed by client."); 349 | } catch (IOException e) { 350 | onError(new ProxyCacheException("Error closing socket input stream", e)); 351 | } 352 | } 353 | 354 | private void closeSocketOutput(Socket socket) { 355 | try { 356 | if (!socket.isOutputShutdown()) { 357 | socket.shutdownOutput(); 358 | } 359 | } catch (IOException e) { 360 | LogU.d("Failed to close socket on proxy side: {}. It seems client have already closed connection.", e); 361 | } 362 | } 363 | 364 | private void closeSocket(Socket socket) { 365 | try { 366 | if (!socket.isClosed()) { 367 | socket.close(); 368 | } 369 | } catch (IOException e) { 370 | onError(new ProxyCacheException("Error closing socket", e)); 371 | } 372 | } 373 | 374 | private void onError(Throwable e) { 375 | LogU.e("HttpProxyCacheServer error", e); 376 | } 377 | 378 | private final class WaitRequestsRunnable implements Runnable { 379 | 380 | private final CountDownLatch startSignal; 381 | 382 | public WaitRequestsRunnable(CountDownLatch startSignal) { 383 | this.startSignal = startSignal; 384 | } 385 | 386 | @Override 387 | public void run() { 388 | startSignal.countDown(); 389 | waitForRequest(); 390 | } 391 | } 392 | 393 | private final class SocketProcessorRunnable implements Runnable { 394 | 395 | private final Socket socket; 396 | 397 | public SocketProcessorRunnable(Socket socket) { 398 | this.socket = socket; 399 | } 400 | 401 | @Override 402 | public void run() { 403 | processSocket(socket); 404 | } 405 | } 406 | 407 | /** 408 | * Builder for {@link HttpProxyCacheServer}. 409 | */ 410 | public static final class Builder { 411 | 412 | private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024; 413 | 414 | private File cacheRoot; 415 | private FileNameGenerator fileNameGenerator; 416 | private DiskUsage diskUsage; 417 | private SourceInfoStorage sourceInfoStorage; 418 | private HeaderInjector headerInjector; 419 | 420 | public Builder(Context context) { 421 | this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context); 422 | this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context); 423 | this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE); 424 | this.fileNameGenerator = new Md5FileNameGenerator(); 425 | this.headerInjector = new EmptyHeadersInjector(); 426 | } 427 | 428 | /** 429 | * Overrides default cache folder to be used for caching files. 430 | *
431 | * By default AndroidVideoCache uses 432 | * '/Android/data/[app_package_name]/cache/video-cache/' if card is mounted and app has appropriate permission 433 | * or 'video-cache' subdirectory in default application's cache directory otherwise. 434 | *
435 | * Note directory must be used only for AndroidVideoCache files. 436 | * 437 | * @param file a cache directory, can't be null. 438 | * @return a builder. 439 | */ 440 | public Builder cacheDirectory(File file) { 441 | this.cacheRoot = checkNotNull(file); 442 | return this; 443 | } 444 | 445 | /** 446 | * Overrides default cache file name generator {@link Md5FileNameGenerator} . 447 | * 448 | * @param fileNameGenerator a new file name generator. 449 | * @return a builder. 450 | */ 451 | public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) { 452 | this.fileNameGenerator = checkNotNull(fileNameGenerator); 453 | return this; 454 | } 455 | 456 | /** 457 | * Sets max cache size in bytes. 458 | *459 | * All files that exceeds limit will be deleted using LRU strategy. 460 | * Default value is 512 Mb. 461 | *
462 | * Note this method overrides result of calling {@link #maxCacheFilesCount(int)} 463 | * 464 | * @param maxSize max cache size in bytes. 465 | * @return a builder. 466 | */ 467 | public Builder maxCacheSize(long maxSize) { 468 | this.diskUsage = new TotalSizeLruDiskUsage(maxSize); 469 | return this; 470 | } 471 | 472 | /** 473 | * Sets max cache files count. 474 | * All files that exceeds limit will be deleted using LRU strategy. 475 | * Note this method overrides result of calling {@link #maxCacheSize(long)} 476 | * 477 | * @param count max cache files count. 478 | * @return a builder. 479 | */ 480 | public Builder maxCacheFilesCount(int count) { 481 | this.diskUsage = new TotalCountLruDiskUsage(count); 482 | return this; 483 | } 484 | 485 | /** 486 | * Set custom DiskUsage logic for handling when to keep or clean cache. 487 | * 488 | * @param diskUsage a disk usage strategy, cant be {@code null}. 489 | * @return a builder. 490 | */ 491 | public Builder diskUsage(DiskUsage diskUsage) { 492 | this.diskUsage = checkNotNull(diskUsage); 493 | return this; 494 | } 495 | 496 | /** 497 | * Add headers along the request to the server 498 | * 499 | * @param headerInjector to inject header base on url 500 | * @return a builder 501 | */ 502 | public Builder headerInjector(HeaderInjector headerInjector) { 503 | this.headerInjector = checkNotNull(headerInjector); 504 | return this; 505 | } 506 | 507 | /** 508 | * Builds new instance of {@link HttpProxyCacheServer}. 509 | * 510 | * @return proxy cache. Only single instance should be used across whole app. 511 | */ 512 | public HttpProxyCacheServer build() { 513 | Config config = buildConfig(); 514 | return new HttpProxyCacheServer(config); 515 | } 516 | 517 | private Config buildConfig() { 518 | return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector); 519 | } 520 | 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /videocache/src/main/java/com/danikula/videocache/HttpProxyCacheServerClients.java: -------------------------------------------------------------------------------- 1 | package com.danikula.videocache; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.os.Message; 6 | 7 | import com.danikula.videocache.file.FileCache; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.net.Socket; 12 | import java.util.List; 13 | import java.util.concurrent.CopyOnWriteArrayList; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static com.danikula.videocache.Preconditions.checkNotNull; 17 | 18 | /** 19 | * Client for {@link HttpProxyCacheServer} 20 | * 21 | * @author Alexey Danilov (danikula@gmail.com). 22 | */ 23 | final class HttpProxyCacheServerClients { 24 | 25 | private final AtomicInteger clientsCount = new AtomicInteger(0); 26 | private final String url; 27 | private volatile HttpProxyCache proxyCache; 28 | private final List
16 | * It is important to ignore system proxy for localhost connection.
17 | *
18 | * @author Alexey Danilov (danikula@gmail.com).
19 | */
20 | class IgnoreHostProxySelector extends ProxySelector {
21 |
22 | private static final List
45 | * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and
46 | * {@link Context#getCacheDir() Context.getCacheDir()} returns null).
47 | */
48 | private static File getCacheDirectory(Context context, boolean preferExternal) {
49 | File appCacheDir = null;
50 | String externalStorageState;
51 | try {
52 | externalStorageState = Environment.getExternalStorageState();
53 | } catch (NullPointerException e) { // (sh)it happens
54 | externalStorageState = "";
55 | }
56 | if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState)) {
57 | appCacheDir = getExternalCacheDir(context);
58 | }
59 | if (appCacheDir == null) {
60 | appCacheDir = context.getCacheDir();
61 | }
62 | if (appCacheDir == null) {
63 | String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
64 | LogU.d("Can't define system cache directory! '" + cacheDirPath + "%s' will be used.");
65 | appCacheDir = new File(cacheDirPath);
66 | }
67 | return appCacheDir;
68 | }
69 |
70 | private static File getExternalCacheDir(Context context) {
71 | File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
72 | File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
73 | if (!appCacheDir.exists()) {
74 | if (!appCacheDir.mkdirs()) {
75 | LogU.d("Unable to create external cache directory");
76 | return null;
77 | }
78 | }
79 | return appCacheDir;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/videocache/src/main/java/com/danikula/videocache/file/DiskUsage.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 |
6 | /**
7 | * Declares how {@link FileCache} will use disc space.
8 | *
9 | * @author Alexey Danilov (danikula@gmail.com).
10 | */
11 | public interface DiskUsage {
12 |
13 | void touch(File file) throws IOException;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/videocache/src/main/java/com/danikula/videocache/file/FileCache.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | import com.danikula.videocache.Cache;
4 | import com.danikula.videocache.LogU;
5 | import com.danikula.videocache.ProxyCacheException;
6 |
7 | import java.io.File;
8 | import java.io.IOException;
9 | import java.io.RandomAccessFile;
10 |
11 | /**
12 | * {@link Cache} that uses file for storing data.
13 | *
14 | * @author Alexey Danilov (danikula@gmail.com).
15 | */
16 | public class FileCache implements Cache {
17 |
18 | private static final String TEMP_POSTFIX = ".download";
19 |
20 | private final DiskUsage diskUsage;
21 | public File file;
22 | private RandomAccessFile dataFile;
23 |
24 | public FileCache(File file) throws ProxyCacheException {
25 | this(file, new UnlimitedDiskUsage());
26 | }
27 |
28 | public FileCache(File file, DiskUsage diskUsage) throws ProxyCacheException {
29 | try {
30 | if (diskUsage == null) {
31 | throw new NullPointerException();
32 | }
33 | this.diskUsage = diskUsage;
34 | File directory = file.getParentFile();
35 | Files.makeDir(directory);
36 | boolean completed = file.exists();
37 | this.file = completed ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
38 | this.dataFile = new RandomAccessFile(this.file, completed ? "r" : "rw");
39 | long size = this.file.length();
40 | LogU.d("临时文件大小"+size+" dataFile "+dataFile.length()+"file path"+file.getAbsolutePath());
41 | } catch (IOException e) {
42 | throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
43 | }
44 | }
45 |
46 | @Override
47 | public synchronized long available() throws ProxyCacheException {
48 | try {
49 | return (int) dataFile.length();
50 | } catch (IOException e) {
51 | throw new ProxyCacheException("Error reading length of file " + file, e);
52 | }
53 | }
54 |
55 | @Override
56 | public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
57 | try {
58 | dataFile.seek(offset);
59 | LogU.d(":从文件缓存里读 "+offset+" "+length);
60 | return dataFile.read(buffer, 0, length);
61 | } catch (IOException e) {
62 | String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
63 | throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
64 | }
65 | }
66 |
67 | @Override
68 | public synchronized void append(byte[] data, int length) throws ProxyCacheException {
69 | try {
70 | if (isCompleted()) {
71 | throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
72 | }
73 | dataFile.seek(available());
74 | dataFile.write(data, 0, length);
75 | } catch (IOException e) {
76 | String format = "Error writing %d bytes to %s from buffer with size %d";
77 | throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
78 | }
79 | }
80 |
81 | @Override
82 | public synchronized void close() throws ProxyCacheException {
83 | try {
84 | dataFile.close();
85 | diskUsage.touch(file);
86 | } catch (IOException e) {
87 | throw new ProxyCacheException("Error closing file " + file, e);
88 | }
89 | }
90 |
91 | @Override
92 | public synchronized void complete() throws ProxyCacheException {
93 | if (isCompleted()) {
94 | return;
95 | }
96 |
97 | close();
98 | String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
99 | File completedFile = new File(file.getParentFile(), fileName);
100 | boolean renamed = file.renameTo(completedFile);
101 | if (!renamed) {
102 | throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
103 | }
104 | file = completedFile;
105 | try {
106 | dataFile = new RandomAccessFile(file, "r");
107 | diskUsage.touch(file);
108 | } catch (IOException e) {
109 | throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
110 | }
111 | }
112 |
113 | @Override
114 | public synchronized boolean isCompleted() {
115 | return !isTempFile(file);
116 | }
117 |
118 | /**
119 | * Returns file to be used fo caching. It may as original file passed in constructor as some temp file for not completed cache.
120 | *
121 | * @return file for caching.
122 | */
123 | public File getFile() {
124 | return file;
125 | }
126 |
127 | private boolean isTempFile(File file) {
128 | return file.getName().endsWith(TEMP_POSTFIX);
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/videocache/src/main/java/com/danikula/videocache/file/FileNameGenerator.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 | /**
4 | * Generator for files to be used for caching.
5 | *
6 | * @author Alexey Danilov (danikula@gmail.com).
7 | */
8 | public interface FileNameGenerator {
9 |
10 | String generate(String url);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/videocache/src/main/java/com/danikula/videocache/file/Files.java:
--------------------------------------------------------------------------------
1 | package com.danikula.videocache.file;
2 |
3 |
4 |
5 | import com.danikula.videocache.LogU;
6 |
7 | import java.io.File;
8 | import java.io.IOException;
9 | import java.io.RandomAccessFile;
10 | import java.util.Arrays;
11 | import java.util.Collections;
12 | import java.util.Comparator;
13 | import java.util.Date;
14 | import java.util.LinkedList;
15 | import java.util.List;
16 |
17 | /**
18 | * Utils for work with files.
19 | *
20 | * @author Alexey Danilov (danikula@gmail.com).
21 | */
22 | class Files {
23 |
24 |
25 |
26 | static void makeDir(File directory) throws IOException {
27 | if (directory.exists()) {
28 | if (!directory.isDirectory()) {
29 | throw new IOException("File " + directory + " is not directory!");
30 | }
31 | } else {
32 | boolean isCreated = directory.mkdirs();
33 | if (!isCreated) {
34 | throw new IOException(String.format("Directory %s can't be created", directory.getAbsolutePath()));
35 | }
36 | }
37 | }
38 |
39 | static List