瀏覽代碼

新增强制缓存模式

drake 3 年之前
父節點
當前提交
29a005e6f9

+ 10 - 0
net/src/main/java/com/drake/net/NetConfig.kt

@@ -18,6 +18,7 @@ package com.drake.net
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Context
+import com.drake.net.cache.ForceCache
 import com.drake.net.convert.NetConverter
 import com.drake.net.convert.NetConverter
 import com.drake.net.interceptor.RequestInterceptor
 import com.drake.net.interceptor.RequestInterceptor
 import com.drake.net.interfaces.NetDialogFactory
 import com.drake.net.interfaces.NetDialogFactory
@@ -25,6 +26,7 @@ import com.drake.net.interfaces.NetErrorHandler
 import com.drake.net.okhttp.toNetOkhttp
 import com.drake.net.okhttp.toNetOkhttp
 import okhttp3.Call
 import okhttp3.Call
 import okhttp3.OkHttpClient
 import okhttp3.OkHttpClient
+import okhttp3.OkHttpUtils
 import java.lang.ref.WeakReference
 import java.lang.ref.WeakReference
 import java.util.concurrent.ConcurrentLinkedQueue
 import java.util.concurrent.ConcurrentLinkedQueue
 
 
@@ -44,8 +46,16 @@ object NetConfig {
     var okHttpClient: OkHttpClient = OkHttpClient.Builder().toNetOkhttp().build()
     var okHttpClient: OkHttpClient = OkHttpClient.Builder().toNetOkhttp().build()
         set(value) {
         set(value) {
             field = value.toNetOkhttp()
             field = value.toNetOkhttp()
+            forceCache = field.cache?.let { ForceCache(OkHttpUtils.diskLruCache(it)) }
         }
         }
 
 
+    /**
+     * 强制缓存配置. 不允许直接设置, 因为整个框架只允许存在一个缓存配置管理, 所以请使用[OkHttpClient.Builder.cache]
+     * 强制缓存会无视标准Http协议强制缓存任何数据
+     * 缓存目录和缓存最大值设置见: [okhttp3.Cache]
+     */
+    internal var forceCache: ForceCache? = null
+
     /** 是否启用日志 */
     /** 是否启用日志 */
     @Deprecated("命名变更", ReplaceWith("NetConfig.debug"))
     @Deprecated("命名变更", ReplaceWith("NetConfig.debug"))
     var logEnabled
     var logEnabled

+ 16 - 0
net/src/main/java/com/drake/net/cache/CacheMode.kt

@@ -0,0 +1,16 @@
+package com.drake.net.cache
+
+enum class CacheMode {
+
+    /** 只读取缓存, 本操作并不会请求网络故不存在写入缓存 */
+    READ,
+
+    /** 根据Http缓存协议决定是否读取缓存, 强制写入缓存 */
+    WRITE,
+
+    /** 先从缓存读取,如果失败再从网络读取, 强制写入缓存 */
+    READ_THEN_REQUEST,
+
+    /** 先从网络读取,如果失败再从缓存读取, 强制写入缓存 */
+    REQUEST_THEN_READ,
+}

+ 719 - 0
net/src/main/java/com/drake/net/cache/ForceCache.kt

@@ -0,0 +1,719 @@
+/*
+ * 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.drake.net.cache
+
+import com.drake.net.request.tagOf
+import com.drake.net.tag.NetTag
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.internal.EMPTY_HEADERS
+import okhttp3.internal.cache.CacheRequest
+import okhttp3.internal.cache.DiskLruCache
+import okhttp3.internal.closeQuietly
+import okhttp3.internal.http.StatusLine
+import okhttp3.internal.platform.Platform
+import okhttp3.internal.toLongOrDefault
+import okio.*
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.encodeUtf8
+import okio.ByteString.Companion.toByteString
+import java.io.Closeable
+import java.io.File
+import java.io.Flushable
+import java.io.IOException
+import java.security.cert.Certificate
+import java.security.cert.CertificateEncodingException
+import java.security.cert.CertificateException
+import java.security.cert.CertificateFactory
+import java.util.*
+
+/**
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
+ * bandwidth.
+ *
+ * ## Cache Optimization
+ *
+ * To measure cache effectiveness, this class tracks three statistics:
+ *
+ *  * **[Request Count:][requestCount]** the number of HTTP requests issued since this cache was
+ *    created.
+ *  * **[Network Count:][networkCount]** the number of those requests that required network use.
+ *  * **[Hit Count:][hitCount]** the number of those requests whose responses were served by the
+ *    cache.
+ *
+ * Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of
+ * the response, the client will issue a conditional `GET`. The server will then send either
+ * the updated response if it has changed, or a short 'not modified' response if the client's copy
+ * is still valid. Such responses increment both the network count and hit count.
+ *
+ * The best way to improve the cache hit rate is by configuring the web server to return cacheable
+ * responses. Although this client honors all [HTTP/1.1 (RFC 7234)][rfc_7234] cache headers, it
+ * doesn't cache partial responses.
+ *
+ * ## Force a Network Response
+ *
+ * In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip
+ * the cache, and fetch data directly from the server. To force a full refresh, add the `no-cache`
+ * directive:
+ *
+ * ```
+ * Request request = new Request.Builder()
+ *     .cacheControl(new CacheControl.Builder().noCache().build())
+ *     .url("http://publicobject.com/helloworld.txt")
+ *     .build();
+ * ```
+ *
+ * If it is only necessary to force a cached response to be validated by the server, use the more
+ * efficient `max-age=0` directive instead:
+ *
+ * ```
+ * Request request = new Request.Builder()
+ *     .cacheControl(new CacheControl.Builder()
+ *         .maxAge(0, TimeUnit.SECONDS)
+ *         .build())
+ *     .url("http://publicobject.com/helloworld.txt")
+ *     .build();
+ * ```
+ *
+ * ## Force a Cache Response
+ *
+ * Sometimes you'll want to show resources if they are available immediately, but not otherwise.
+ * This can be used so your application can show *something* while waiting for the latest data to be
+ * downloaded. To restrict a request to locally-cached resources, add the `only-if-cached`
+ * directive:
+ *
+ * ```
+ * Request request = new Request.Builder()
+ *     .cacheControl(new CacheControl.Builder()
+ *         .onlyIfCached()
+ *         .build())
+ *     .url("http://publicobject.com/helloworld.txt")
+ *     .build();
+ * Response forceCacheResponse = client.newCall(request).execute();
+ * if (forceCacheResponse.code() != 504) {
+ *   // The resource was cached! Show it.
+ * } else {
+ *   // The resource was not cached.
+ * }
+ * ```
+ *
+ * This technique works even better in situations where a stale response is better than no response.
+ * To permit stale cached responses, use the `max-stale` directive with the maximum staleness in
+ * seconds:
+ *
+ * ```
+ * Request request = new Request.Builder()
+ *     .cacheControl(new CacheControl.Builder()
+ *         .maxStale(365, TimeUnit.DAYS)
+ *         .build())
+ *     .url("http://publicobject.com/helloworld.txt")
+ *     .build();
+ * ```
+ *
+ * The [CacheControl] class can configure request caching directives and parse response caching
+ * directives. It even offers convenient constants [CacheControl.FORCE_NETWORK] and
+ * [CacheControl.FORCE_CACHE] that address the use cases above.
+ *
+ * [rfc_7234]: http://tools.ietf.org/html/rfc7234
+ */
+class ForceCache internal constructor(
+    val cache: DiskLruCache
+) : Closeable, Flushable {
+
+    // read and write statistics, all guarded by 'this'.
+    internal var writeSuccessCount = 0
+    internal var writeAbortCount = 0
+
+    val isClosed: Boolean
+        get() = cache.isClosed()
+
+    internal fun get(request: Request): Response? {
+        val snapshot: DiskLruCache.Snapshot = try {
+            cache[key(request)] ?: return null
+        } catch (_: IOException) {
+            return null // Give up because the cache cannot be read.
+        }
+
+        val entry: Entry = try {
+            Entry(snapshot.getSource(ENTRY_METADATA))
+        } catch (_: IOException) {
+            snapshot.closeQuietly()
+            return null
+        }
+
+        val response = entry.response(snapshot, request.body)
+        if (!entry.matches(request, response)) {
+            response.body?.closeQuietly()
+            return null
+        }
+
+        return response
+    }
+
+    internal fun put(response: Response): CacheRequest? {
+        val entry = Entry(response)
+        var editor: DiskLruCache.Editor? = null
+        try {
+            editor = cache.edit(key(response.request)) ?: return null
+            entry.writeTo(editor)
+            return RealCacheRequest(editor)
+        } catch (_: IOException) {
+            abortQuietly(editor)
+            return null
+        }
+    }
+
+    @Throws(IOException::class)
+    internal fun remove(request: Request) {
+        cache.remove(key(request))
+    }
+
+    internal fun update(cached: Response, network: Response) {
+        val entry = Entry(network)
+        val snapshot = (cached.body as CacheResponseBody).snapshot
+        var editor: DiskLruCache.Editor? = null
+        try {
+            editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.
+            entry.writeTo(editor)
+            editor.commit()
+        } catch (_: IOException) {
+            abortQuietly(editor)
+        }
+    }
+
+    private fun abortQuietly(editor: DiskLruCache.Editor?) {
+        // Give up because the cache cannot be written.
+        try {
+            editor?.abort()
+        } catch (_: IOException) {
+        }
+    }
+
+    /**
+     * Initialize the cache. This will include reading the journal files from the storage and building
+     * up the necessary in-memory cache information.
+     *
+     * The initialization time may vary depending on the journal file size and the current actual
+     * cache size. The application needs to be aware of calling this function during the
+     * initialization phase and preferably in a background worker thread.
+     *
+     * Note that if the application chooses to not call this method to initialize the cache. By
+     * default, OkHttp will perform lazy initialization upon the first usage of the cache.
+     */
+    @Throws(IOException::class)
+    fun initialize() {
+        cache.initialize()
+    }
+
+    /**
+     * 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.
+     */
+    @Throws(IOException::class)
+    fun delete() {
+        cache.delete()
+    }
+
+    /**
+     * Deletes all values stored in the cache. In-flight writes to the cache will complete normally,
+     * but the corresponding responses will not be stored.
+     */
+    @Throws(IOException::class)
+    fun evictAll() {
+        cache.evictAll()
+    }
+
+    /**
+     * Returns an iterator over the URLs in this cache. This iterator doesn't throw
+     * `ConcurrentModificationException`, but if new responses are added while iterating, their URLs
+     * will not be returned. If existing responses are evicted during iteration, they will be absent
+     * (unless they were already returned).
+     *
+     * The iterator supports [MutableIterator.remove]. Removing a URL from the iterator evicts the
+     * corresponding response from the cache. Use this to evict selected responses.
+     */
+    @Throws(IOException::class)
+    fun urls(): MutableIterator<String> {
+        return object : MutableIterator<String> {
+            private val delegate: MutableIterator<DiskLruCache.Snapshot> = cache.snapshots()
+            private var nextUrl: String? = null
+            private var canRemove = false
+
+            override fun hasNext(): Boolean {
+                if (nextUrl != null) return true
+
+                canRemove = false // Prevent delegate.remove() on the wrong item!
+                while (delegate.hasNext()) {
+                    try {
+                        delegate.next().use { snapshot ->
+                            val metadata = snapshot.getSource(ENTRY_METADATA).buffer()
+                            nextUrl = metadata.readUtf8LineStrict()
+                            return true
+                        }
+                    } catch (_: IOException) {
+                        // We couldn't read the metadata for this snapshot; possibly because the host filesystem
+                        // has disappeared! Skip it.
+                    }
+                }
+
+                return false
+            }
+
+            override fun next(): String {
+                if (!hasNext()) throw NoSuchElementException()
+                val result = nextUrl!!
+                nextUrl = null
+                canRemove = true
+                return result
+            }
+
+            override fun remove() {
+                check(canRemove) { "remove() before next()" }
+                delegate.remove()
+            }
+        }
+    }
+
+    @Synchronized
+    fun writeAbortCount(): Int = writeAbortCount
+
+    @Synchronized
+    fun writeSuccessCount(): Int = writeSuccessCount
+
+    @Throws(IOException::class)
+    fun size(): Long = cache.size()
+
+    /** Max size of the cache (in bytes). */
+    fun maxSize(): Long = cache.maxSize
+
+    @Throws(IOException::class)
+    override fun flush() {
+        cache.flush()
+    }
+
+    @Throws(IOException::class)
+    override fun close() {
+        cache.close()
+    }
+
+    @get:JvmName("directory")
+    val directory: File
+        get() = cache.directory
+
+    @JvmName("-deprecated_directory")
+    @Deprecated(
+        message = "moved to val",
+        replaceWith = ReplaceWith(expression = "directory"),
+        level = DeprecationLevel.ERROR
+    )
+    fun directory(): File = cache.directory
+
+    private inner class RealCacheRequest(
+        private val editor: DiskLruCache.Editor
+    ) : CacheRequest {
+        private val cacheOut: Sink = editor.newSink(ENTRY_BODY)
+        private val body: Sink
+        var done = false
+
+        init {
+            this.body = object : ForwardingSink(cacheOut) {
+                @Throws(IOException::class)
+                override fun close() {
+                    synchronized(this@ForceCache) {
+                        if (done) return
+                        done = true
+                        writeSuccessCount++
+                    }
+                    super.close()
+                    editor.commit()
+                }
+            }
+        }
+
+        override fun abort() {
+            synchronized(this@ForceCache) {
+                if (done) return
+                done = true
+                writeAbortCount++
+            }
+            cacheOut.closeQuietly()
+            try {
+                editor.abort()
+            } catch (_: IOException) {
+            }
+        }
+
+        override fun body(): Sink = body
+    }
+
+    private class Entry {
+        private val url: String
+        private val varyHeaders: Headers
+        private val requestMethod: String
+        private val protocol: Protocol
+        private val code: Int
+        private val message: String
+        private val responseHeaders: Headers
+        private val handshake: Handshake?
+        private val sentRequestMillis: Long
+        private val receivedResponseMillis: Long
+
+        private val isHttps: Boolean get() = url.startsWith("https://")
+
+        /**
+         * Reads an entry from an input stream. A typical entry looks like this:
+         *
+         * ```
+         * http://google.com/foo
+         * GET
+         * 2
+         * Accept-Language: fr-CA
+         * Accept-Charset: UTF-8
+         * HTTP/1.1 200 OK
+         * 3
+         * Content-Type: image/png
+         * Content-Length: 100
+         * Cache-Control: max-age=600
+         * ```
+         *
+         * A typical HTTPS file looks like this:
+         *
+         * ```
+         * https://google.com/foo
+         * GET
+         * 2
+         * Accept-Language: fr-CA
+         * Accept-Charset: UTF-8
+         * HTTP/1.1 200 OK
+         * 3
+         * Content-Type: image/png
+         * Content-Length: 100
+         * Cache-Control: max-age=600
+         *
+         * AES_256_WITH_MD5
+         * 2
+         * base64-encoded peerCertificate[0]
+         * base64-encoded peerCertificate[1]
+         * -1
+         * TLSv1.2
+         * ```
+         *
+         * The file is newline separated. The first two lines are the URL and the request method. Next
+         * is the number of HTTP Vary request header lines, followed by those lines.
+         *
+         * Next is the response status line, followed by the number of HTTP response header lines,
+         * followed by those lines.
+         *
+         * HTTPS responses also contain SSL session information. This begins with a blank line, and then
+         * a line containing the cipher suite. Next is the length of the peer certificate chain. These
+         * certificates are base64-encoded and appear each on their own line. The next line contains the
+         * length of the local certificate chain. These certificates are also base64-encoded and appear
+         * each on their own line. A length of -1 is used to encode a null array. The last line is
+         * optional. If present, it contains the TLS version.
+         */
+        @Throws(IOException::class)
+        constructor(rawSource: Source) {
+            try {
+                val source = rawSource.buffer()
+                url = source.readUtf8LineStrict()
+                requestMethod = source.readUtf8LineStrict()
+                val varyHeadersBuilder = Headers.Builder()
+                val varyRequestHeaderLineCount = readInt(source)
+                for (i in 0 until varyRequestHeaderLineCount) {
+                    OkHttpUtils.addLenient(varyHeadersBuilder, source.readUtf8LineStrict())
+                }
+                varyHeaders = varyHeadersBuilder.build()
+
+                val statusLine = StatusLine.parse(source.readUtf8LineStrict())
+                protocol = statusLine.protocol
+                code = statusLine.code
+                message = statusLine.message
+                val responseHeadersBuilder = Headers.Builder()
+                val responseHeaderLineCount = readInt(source)
+                for (i in 0 until responseHeaderLineCount) {
+                    OkHttpUtils.addLenient(responseHeadersBuilder, source.readUtf8LineStrict())
+                }
+                val sendRequestMillisString = responseHeadersBuilder[SENT_MILLIS]
+                val receivedResponseMillisString = responseHeadersBuilder[RECEIVED_MILLIS]
+                responseHeadersBuilder.removeAll(SENT_MILLIS)
+                responseHeadersBuilder.removeAll(RECEIVED_MILLIS)
+                sentRequestMillis = sendRequestMillisString?.toLong() ?: 0L
+                receivedResponseMillis = receivedResponseMillisString?.toLong() ?: 0L
+                responseHeaders = responseHeadersBuilder.build()
+
+                if (isHttps) {
+                    val blank = source.readUtf8LineStrict()
+                    if (blank.isNotEmpty()) {
+                        throw IOException("expected \"\" but was \"$blank\"")
+                    }
+                    val cipherSuiteString = source.readUtf8LineStrict()
+                    val cipherSuite = CipherSuite.forJavaName(cipherSuiteString)
+                    val peerCertificates = readCertificateList(source)
+                    val localCertificates = readCertificateList(source)
+                    val tlsVersion = if (!source.exhausted()) {
+                        TlsVersion.forJavaName(source.readUtf8LineStrict())
+                    } else {
+                        TlsVersion.SSL_3_0
+                    }
+                    handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates)
+                } else {
+                    handshake = null
+                }
+            } finally {
+                rawSource.close()
+            }
+        }
+
+        constructor(response: Response) {
+            this.url = response.request.url.toString()
+            this.varyHeaders = response.varyHeaders()
+            this.requestMethod = response.request.method
+            this.protocol = response.protocol
+            this.code = response.code
+            this.message = response.message
+            this.responseHeaders = response.headers
+            this.handshake = response.handshake
+            this.sentRequestMillis = response.sentRequestAtMillis
+            this.receivedResponseMillis = response.receivedResponseAtMillis
+        }
+
+        @Throws(IOException::class)
+        fun writeTo(editor: DiskLruCache.Editor) {
+            editor.newSink(ENTRY_METADATA).buffer().use { sink ->
+                sink.writeUtf8(url).writeByte('\n'.toInt())
+                sink.writeUtf8(requestMethod).writeByte('\n'.toInt())
+                sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
+                for (i in 0 until varyHeaders.size) {
+                    sink.writeUtf8(varyHeaders.name(i))
+                        .writeUtf8(": ")
+                        .writeUtf8(varyHeaders.value(i))
+                        .writeByte('\n'.toInt())
+                }
+
+                sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
+                sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
+                for (i in 0 until responseHeaders.size) {
+                    sink.writeUtf8(responseHeaders.name(i))
+                        .writeUtf8(": ")
+                        .writeUtf8(responseHeaders.value(i))
+                        .writeByte('\n'.toInt())
+                }
+                sink.writeUtf8(SENT_MILLIS)
+                    .writeUtf8(": ")
+                    .writeDecimalLong(sentRequestMillis)
+                    .writeByte('\n'.toInt())
+                sink.writeUtf8(RECEIVED_MILLIS)
+                    .writeUtf8(": ")
+                    .writeDecimalLong(receivedResponseMillis)
+                    .writeByte('\n'.toInt())
+
+                if (isHttps) {
+                    sink.writeByte('\n'.toInt())
+                    sink.writeUtf8(handshake!!.cipherSuite.javaName).writeByte('\n'.toInt())
+                    writeCertList(sink, handshake.peerCertificates)
+                    writeCertList(sink, handshake.localCertificates)
+                    sink.writeUtf8(handshake.tlsVersion.javaName).writeByte('\n'.toInt())
+                }
+            }
+        }
+
+        @Throws(IOException::class)
+        private fun readCertificateList(source: BufferedSource): List<Certificate> {
+            val length = readInt(source)
+            if (length == -1) return emptyList() // OkHttp v1.2 used -1 to indicate null.
+
+            try {
+                val certificateFactory = CertificateFactory.getInstance("X.509")
+                val result = ArrayList<Certificate>(length)
+                for (i in 0 until length) {
+                    val line = source.readUtf8LineStrict()
+                    val bytes = Buffer()
+                    bytes.write(line.decodeBase64()!!)
+                    result.add(certificateFactory.generateCertificate(bytes.inputStream()))
+                }
+                return result
+            } catch (e: CertificateException) {
+                throw IOException(e.message)
+            }
+        }
+
+        @Throws(IOException::class)
+        private fun writeCertList(sink: BufferedSink, certificates: List<Certificate>) {
+            try {
+                sink.writeDecimalLong(certificates.size.toLong()).writeByte('\n'.toInt())
+                for (i in 0 until certificates.size) {
+                    val bytes = certificates[i].encoded
+                    val line = bytes.toByteString().base64()
+                    sink.writeUtf8(line).writeByte('\n'.toInt())
+                }
+            } catch (e: CertificateEncodingException) {
+                throw IOException(e.message)
+            }
+        }
+
+        fun matches(request: Request, response: Response): Boolean {
+            return url == request.url.toString() &&
+                    requestMethod == request.method &&
+                    varyMatches(response, varyHeaders, request)
+        }
+
+        fun response(snapshot: DiskLruCache.Snapshot, requestBody: RequestBody?): Response {
+            val contentType = responseHeaders["Content-Type"]
+            val contentLength = responseHeaders["Content-Length"]
+            val cacheRequest = Request.Builder()
+                .url(url)
+                .method(requestMethod, requestBody)
+                .headers(varyHeaders)
+                .build()
+            return Response.Builder()
+                .request(cacheRequest)
+                .protocol(protocol)
+                .code(code)
+                .message(message)
+                .headers(responseHeaders)
+                .body(CacheResponseBody(snapshot, contentType, contentLength))
+                .handshake(handshake)
+                .sentRequestAtMillis(sentRequestMillis)
+                .receivedResponseAtMillis(receivedResponseMillis)
+                .build()
+        }
+
+        companion object {
+            /** Synthetic response header: the local time when the request was sent. */
+            private val SENT_MILLIS = "${Platform.get().getPrefix()}-Sent-Millis"
+
+            /** Synthetic response header: the local time when the response was received. */
+            private val RECEIVED_MILLIS = "${Platform.get().getPrefix()}-Received-Millis"
+        }
+    }
+
+    private class CacheResponseBody(
+        val snapshot: DiskLruCache.Snapshot,
+        private val contentType: String?,
+        private val contentLength: String?
+    ) : ResponseBody() {
+        private val bodySource: BufferedSource
+
+        init {
+            val source = snapshot.getSource(ENTRY_BODY)
+            bodySource = object : ForwardingSource(source) {
+                @Throws(IOException::class)
+                override fun close() {
+                    snapshot.close()
+                    super.close()
+                }
+            }.buffer()
+        }
+
+        override fun contentType(): MediaType? = contentType?.toMediaTypeOrNull()
+
+        override fun contentLength(): Long = contentLength?.toLongOrDefault(-1L) ?: -1L
+
+        override fun source(): BufferedSource = bodySource
+    }
+
+    companion object {
+        private const val ENTRY_METADATA = 0
+        private const val ENTRY_BODY = 1
+
+        @JvmStatic
+        fun key(request: Request): String {
+            val key = request.tagOf<NetTag.CacheKey>()?.value ?: (request.method + request.url.toString())
+            return key.encodeUtf8().sha1().hex()
+        }
+
+        @Throws(IOException::class)
+        internal fun readInt(source: BufferedSource): Int {
+            try {
+                val result = source.readDecimalLong()
+                val line = source.readUtf8LineStrict()
+                if (result < 0L || result > Integer.MAX_VALUE || line.isNotEmpty()) {
+                    throw IOException("expected an int but was \"$result$line\"")
+                }
+                return result.toInt()
+            } catch (e: NumberFormatException) {
+                throw IOException(e.message)
+            }
+        }
+
+        /**
+         * Returns true if none of the Vary headers have changed between [cachedRequest] and
+         * [newRequest].
+         */
+        fun varyMatches(
+            cachedResponse: Response,
+            cachedRequest: Headers,
+            newRequest: Request
+        ): Boolean {
+            return cachedResponse.headers.varyFields().none {
+                cachedRequest.values(it) != newRequest.headers(it)
+            }
+        }
+
+        /** Returns true if a Vary header contains an asterisk. Such responses cannot be cached. */
+        fun Response.hasVaryAll() = "*" in headers.varyFields()
+
+        /**
+         * Returns the names of the request headers that need to be checked for equality when caching.
+         */
+        private fun Headers.varyFields(): Set<String> {
+            var result: MutableSet<String>? = null
+            for (i in 0 until size) {
+                if (!"Vary".equals(name(i), ignoreCase = true)) {
+                    continue
+                }
+
+                val value = value(i)
+                if (result == null) {
+                    result = TreeSet(String.CASE_INSENSITIVE_ORDER)
+                }
+                for (varyField in value.split(',')) {
+                    result.add(varyField.trim())
+                }
+            }
+            return result ?: emptySet()
+        }
+
+        /**
+         * Returns the subset of the headers in this's request that impact the content of this's body.
+         */
+        fun Response.varyHeaders(): Headers {
+            // Use the request headers sent over the network, since that's what the response varies on.
+            // Otherwise OkHttp-supplied headers like "Accept-Encoding: gzip" may be lost.
+            val requestHeaders = networkResponse!!.request.headers
+            val responseHeaders = headers
+            return varyHeaders(requestHeaders, responseHeaders)
+        }
+
+        /**
+         * Returns the subset of the headers in [requestHeaders] that impact the content of the
+         * response's body.
+         */
+        private fun varyHeaders(requestHeaders: Headers, responseHeaders: Headers): Headers {
+            val varyFields = responseHeaders.varyFields()
+            if (varyFields.isEmpty()) return EMPTY_HEADERS
+
+            val result = Headers.Builder()
+            for (i in 0 until requestHeaders.size) {
+                val fieldName = requestHeaders.name(i)
+                if (fieldName in varyFields) {
+                    result.add(fieldName, requestHeaders.value(i))
+                }
+            }
+            return result.build()
+        }
+    }
+}

+ 5 - 1
net/src/main/java/com/drake/net/exception/NoCacheException.kt

@@ -3,7 +3,11 @@ package com.drake.net.exception
 import okhttp3.Request
 import okhttp3.Request
 
 
 /**
 /**
- * 该异常暂未实现, 属于保留异常
+ * 读取缓存失败
+ * 仅当设置强制缓存模式[com.drake.net.cache.CacheMode.READ]和[com.drake.net.cache.CacheMode.REQUEST_THEN_READ]才会发生此异常
+ * @param request 请求信息
+ * @param message 错误描述信息
+ * @param cause 错误原因
  */
  */
 class NoCacheException(
 class NoCacheException(
     request: Request,
     request: Request,

+ 120 - 10
net/src/main/java/com/drake/net/interceptor/NetOkHttpInterceptor.kt

@@ -3,17 +3,30 @@ package com.drake.net.interceptor
 import com.drake.net.NetConfig
 import com.drake.net.NetConfig
 import com.drake.net.body.toNetRequestBody
 import com.drake.net.body.toNetRequestBody
 import com.drake.net.body.toNetResponseBody
 import com.drake.net.body.toNetResponseBody
+import com.drake.net.cache.CacheMode
+import com.drake.net.cache.ForceCache
 import com.drake.net.exception.*
 import com.drake.net.exception.*
-import com.drake.net.okhttp.attachToNet
-import com.drake.net.okhttp.detachFromNet
 import com.drake.net.request.downloadListeners
 import com.drake.net.request.downloadListeners
+import com.drake.net.request.tagOf
 import com.drake.net.request.uploadListeners
 import com.drake.net.request.uploadListeners
 import com.drake.net.utils.isNetworking
 import com.drake.net.utils.isNetworking
+import okhttp3.CacheControl
+import okhttp3.Call
 import okhttp3.Interceptor
 import okhttp3.Interceptor
 import okhttp3.Response
 import okhttp3.Response
+import okhttp3.internal.cache.CacheRequest
+import okhttp3.internal.discard
+import okhttp3.internal.http.ExchangeCodec
+import okhttp3.internal.http.RealResponseBody
+import okio.Buffer
+import okio.Source
+import okio.buffer
+import java.io.IOException
+import java.lang.ref.WeakReference
 import java.net.ConnectException
 import java.net.ConnectException
 import java.net.SocketTimeoutException
 import java.net.SocketTimeoutException
 import java.net.UnknownHostException
 import java.net.UnknownHostException
+import java.util.concurrent.TimeUnit
 
 
 /**
 /**
  * Net代理OkHttp的拦截器
  * Net代理OkHttp的拦截器
@@ -22,11 +35,39 @@ object NetOkHttpInterceptor : Interceptor {
 
 
     override fun intercept(chain: Interceptor.Chain): Response {
     override fun intercept(chain: Interceptor.Chain): Response {
         var request = chain.request()
         var request = chain.request()
-        val netRequestBody = request.body?.toNetRequestBody(request.uploadListeners())
-        request = request.newBuilder().method(request.method, netRequestBody).build()
-        val response = try {
-            chain.call().attachToNet()
-            chain.proceed(request)
+        val reqBody = request.body?.toNetRequestBody(request.uploadListeners())
+        val cache = request.tagOf<ForceCache>() ?: NetConfig.forceCache
+        request = request.newBuilder().apply {
+            if (cache != null) cacheControl(CacheControl.Builder().noCache().noStore().build())
+        }.method(request.method, reqBody).build()
+
+        try {
+            attach(chain)
+            val response = if (cache != null) {
+                when (request.tagOf<CacheMode>()) {
+                    CacheMode.READ -> cache.get(request) ?: throw NoCacheException(request)
+                    CacheMode.READ_THEN_REQUEST -> cache.get(request) ?: chain.proceed(request).run {
+                        cacheWritingResponse(cache.put(this), this)
+                    }
+                    CacheMode.REQUEST_THEN_READ -> try {
+                        chain.proceed(request).run {
+                            cacheWritingResponse(cache.put(this), this)
+                        }
+                    } catch (e: Exception) {
+                        cache.get(request) ?: throw NoCacheException(request)
+                    }
+                    CacheMode.WRITE -> chain.proceed(request).run {
+                        cacheWritingResponse(cache.put(this), this)
+                    }
+                    else -> chain.proceed(request)
+                }
+            } else {
+                chain.proceed(request)
+            }
+            val respBody = response.body?.toNetResponseBody(request.downloadListeners()) {
+                detach(chain.call())
+            }
+            return response.newBuilder().body(respBody).build()
         } catch (e: SocketTimeoutException) {
         } catch (e: SocketTimeoutException) {
             throw NetSocketTimeoutException(request, e.message, e)
             throw NetSocketTimeoutException(request, e.message, e)
         } catch (e: ConnectException) {
         } catch (e: ConnectException) {
@@ -45,9 +86,78 @@ object NetOkHttpInterceptor : Interceptor {
         } catch (e: Throwable) {
         } catch (e: Throwable) {
             throw HttpFailureException(request, cause = e)
             throw HttpFailureException(request, cause = e)
         }
         }
-        val netResponseBody = response.body?.toNetResponseBody(request.downloadListeners()) {
-            chain.call().detachFromNet()
+    }
+
+    private fun attach(chain: Interceptor.Chain) {
+        NetConfig.runningCalls.add(WeakReference(chain.call()))
+    }
+
+    private fun detach(call: Call) {
+        val iterator = NetConfig.runningCalls.iterator()
+        while (iterator.hasNext()) {
+            if (iterator.next().get() == call) {
+                iterator.remove()
+                return
+            }
+        }
+    }
+
+    /** 缓存网络响应 */
+    @Throws(IOException::class)
+    private fun cacheWritingResponse(cacheRequest: CacheRequest?, response: Response): Response {
+        // Some apps return a null body; for compatibility we treat that like a null cache request.
+        if (cacheRequest == null) return response
+        val cacheBodyUnbuffered = cacheRequest.body()
+
+        val source = response.body!!.source()
+        val cacheBody = cacheBodyUnbuffered.buffer()
+
+        val cacheWritingSource = object : Source {
+            private var cacheRequestClosed = false
+
+            @Throws(IOException::class)
+            override fun read(sink: Buffer, byteCount: Long): Long {
+                val bytesRead: Long
+                try {
+                    bytesRead = source.read(sink, byteCount)
+                } catch (e: IOException) {
+                    if (!cacheRequestClosed) {
+                        cacheRequestClosed = true
+                        cacheRequest.abort() // Failed to write a complete cache response.
+                    }
+                    throw e
+                }
+
+                if (bytesRead == -1L) {
+                    if (!cacheRequestClosed) {
+                        cacheRequestClosed = true
+                        cacheBody.close() // The cache response is complete!
+                    }
+                    return -1
+                }
+
+                sink.copyTo(cacheBody.buffer, sink.size - bytesRead, bytesRead)
+                cacheBody.emitCompleteSegments()
+                return bytesRead
+            }
+
+            override fun timeout() = source.timeout()
+
+            @Throws(IOException::class)
+            override fun close() {
+                if (!cacheRequestClosed &&
+                    !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
+                    cacheRequestClosed = true
+                    cacheRequest.abort()
+                }
+                source.close()
+            }
         }
         }
-        return response.newBuilder().body(netResponseBody).build()
+
+        val contentType = response.header("Content-Type")
+        val contentLength = response.body?.contentLength() ?: 0
+        return response.newBuilder()
+            .body(RealResponseBody(contentType, contentLength, cacheWritingSource.buffer()))
+            .build()
     }
     }
 }
 }

+ 0 - 25
net/src/main/java/com/drake/net/okhttp/CallExtension.kt

@@ -1,25 +0,0 @@
-package com.drake.net.okhttp
-
-import com.drake.net.NetConfig
-import okhttp3.Call
-import java.lang.ref.WeakReference
-
-/**
- * Call附着到Net上
- */
-fun Call.attachToNet() {
-    NetConfig.runningCalls.add(WeakReference(this))
-}
-
-/**
- * Call从Net上分离释放引用
- */
-fun Call.detachFromNet() {
-    val iterator = NetConfig.runningCalls.iterator()
-    while (iterator.hasNext()) {
-        if (iterator.next().get() == this) {
-            iterator.remove()
-            return
-        }
-    }
-}

+ 24 - 4
net/src/main/java/com/drake/net/request/BaseRequest.kt

@@ -18,6 +18,8 @@
 package com.drake.net.request
 package com.drake.net.request
 
 
 import com.drake.net.NetConfig
 import com.drake.net.NetConfig
+import com.drake.net.cache.CacheMode
+import com.drake.net.cache.ForceCache
 import com.drake.net.convert.NetConverter
 import com.drake.net.convert.NetConverter
 import com.drake.net.exception.URLParseException
 import com.drake.net.exception.URLParseException
 import com.drake.net.interfaces.ProgressListener
 import com.drake.net.interfaces.ProgressListener
@@ -51,6 +53,8 @@ abstract class BaseRequest {
     open var okHttpClient = NetConfig.okHttpClient
     open var okHttpClient = NetConfig.okHttpClient
         set(value) {
         set(value) {
             field = value.toNetOkhttp()
             field = value.toNetOkhttp()
+            val forceCache = field.cache?.let { ForceCache(OkHttpUtils.diskLruCache(it)) }
+            tagOf(forceCache)
         }
         }
 
 
     /**
     /**
@@ -269,11 +273,27 @@ abstract class BaseRequest {
     //<editor-fold desc="Cache">
     //<editor-fold desc="Cache">
 
 
     /**
     /**
-     * 设置请求头的缓存控制
+     * 设置Http缓存协议头的缓存控制
      */
      */
     fun setCacheControl(cacheControl: CacheControl) {
     fun setCacheControl(cacheControl: CacheControl) {
         okHttpRequest.cacheControl(cacheControl)
         okHttpRequest.cacheControl(cacheControl)
     }
     }
+
+    /**
+     * 设置缓存模式
+     * 缓存模式将无视Http缓存协议进行强制读取/写入缓存
+     */
+    fun setCacheMode(mode: CacheMode) {
+        tagOf(mode)
+    }
+
+    /**
+     * 自定义强制缓存使用的Key, 本方法对于Http缓存协议无效
+     * @param key 缓存的Key无论是自定义还是默认(使用URL作为Key)最终都会被进行SHA1编码, 所以无需考虑特殊字符问题
+     */
+    fun setCacheKey(key: String) {
+        tagOf(NetTag.CacheKey(key))
+    }
     //</editor-fold>
     //</editor-fold>
 
 
     //<editor-fold desc="Download">
     //<editor-fold desc="Download">
@@ -286,21 +306,21 @@ abstract class BaseRequest {
      * @see setDownloadFileNameConflict
      * @see setDownloadFileNameConflict
      * @see setDownloadDir
      * @see setDownloadDir
      */
      */
-    fun setDownloadFileName(name: String?) {
+    fun setDownloadFileName(name: String) {
         okHttpRequest.tagOf(NetTag.DownloadFileName(name))
         okHttpRequest.tagOf(NetTag.DownloadFileName(name))
     }
     }
 
 
     /**
     /**
      * 下载保存的目录, 也支持包含文件名称的完整路径, 如果使用完整路径则无视setDownloadFileName设置
      * 下载保存的目录, 也支持包含文件名称的完整路径, 如果使用完整路径则无视setDownloadFileName设置
      */
      */
-    fun setDownloadDir(name: String?) {
+    fun setDownloadDir(name: String) {
         okHttpRequest.tagOf(NetTag.DownloadFileDir(name))
         okHttpRequest.tagOf(NetTag.DownloadFileDir(name))
     }
     }
 
 
     /**
     /**
      * 下载保存的目录, 也支持包含文件名称的完整路径, 如果使用完整路径则无视setDownloadFileName设置
      * 下载保存的目录, 也支持包含文件名称的完整路径, 如果使用完整路径则无视setDownloadFileName设置
      */
      */
-    fun setDownloadDir(name: File?) {
+    fun setDownloadDir(name: File) {
         okHttpRequest.tagOf(NetTag.DownloadFileDir(name))
         okHttpRequest.tagOf(NetTag.DownloadFileDir(name))
     }
     }
 
 

+ 3 - 3
net/src/main/java/com/drake/net/request/RequestBuilder.kt

@@ -16,7 +16,7 @@ import kotlin.reflect.KType
 var Request.Builder.id: Any?
 var Request.Builder.id: Any?
     get() = tagOf<NetTag.RequestId>()
     get() = tagOf<NetTag.RequestId>()
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestId(value))
+        tagOf(value?.let { NetTag.RequestId(it) })
     }
     }
 
 
 /**
 /**
@@ -27,7 +27,7 @@ var Request.Builder.id: Any?
 var Request.Builder.group: Any?
 var Request.Builder.group: Any?
     get() = tagOf<NetTag.RequestGroup>()
     get() = tagOf<NetTag.RequestGroup>()
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestGroup(value))
+        tagOf(value?.let { NetTag.RequestGroup(it) })
     }
     }
 //</editor-fold>
 //</editor-fold>
 
 
@@ -37,7 +37,7 @@ var Request.Builder.group: Any?
 var Request.Builder.kType: KType?
 var Request.Builder.kType: KType?
     get() = tagOf<NetTag.RequestKType>()?.value
     get() = tagOf<NetTag.RequestKType>()?.value
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestKType(value))
+        tagOf(value?.let { NetTag.RequestKType(it) })
     }
     }
 
 
 /**
 /**

+ 9 - 9
net/src/main/java/com/drake/net/request/RequestExtension.kt

@@ -38,7 +38,7 @@ import kotlin.reflect.KType
 var Request.id: Any?
 var Request.id: Any?
     get() = tagOf<NetTag.RequestId>()
     get() = tagOf<NetTag.RequestId>()
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestId(value))
+        tagOf(value?.let { NetTag.RequestId(it) })
     }
     }
 
 
 /**
 /**
@@ -49,7 +49,7 @@ var Request.id: Any?
 var Request.group: Any?
 var Request.group: Any?
     get() = tagOf<NetTag.RequestGroup>()
     get() = tagOf<NetTag.RequestGroup>()
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestGroup(value))
+        tagOf(value?.let { NetTag.RequestGroup(it) })
     }
     }
 //</editor-fold>
 //</editor-fold>
 
 
@@ -59,7 +59,7 @@ var Request.group: Any?
 var Request.kType: KType?
 var Request.kType: KType?
     get() = tagOf<NetTag.RequestKType>()?.value
     get() = tagOf<NetTag.RequestKType>()?.value
     set(value) {
     set(value) {
-        tagOf(NetTag.RequestKType(value))
+        tagOf(value?.let { NetTag.RequestKType(it) })
     }
     }
 
 
 
 
@@ -139,28 +139,28 @@ fun Request.downloadListeners(): ConcurrentLinkedQueue<ProgressListener> {
  * 当指定下载目录存在同名文件是覆盖还是进行重命名, 重命名规则是: $文件名_($序号).$后缀
  * 当指定下载目录存在同名文件是覆盖还是进行重命名, 重命名规则是: $文件名_($序号).$后缀
  */
  */
 fun Request.downloadConflictRename(): Boolean {
 fun Request.downloadConflictRename(): Boolean {
-    return tagOf<NetTag.DownloadFileConflictRename>()?.enabled == true
+    return tagOf<NetTag.DownloadFileConflictRename>()?.value == true
 }
 }
 
 
 /**
 /**
  * 是否进行校验文件md5, 如果校验则匹配上既马上返回文件而不会进行下载
  * 是否进行校验文件md5, 如果校验则匹配上既马上返回文件而不会进行下载
  */
  */
 fun Request.downloadMd5Verify(): Boolean {
 fun Request.downloadMd5Verify(): Boolean {
-    return tagOf<NetTag.DownloadFileMD5Verify>()?.enabled == true
+    return tagOf<NetTag.DownloadFileMD5Verify>()?.value == true
 }
 }
 
 
 /**
 /**
  * 下载文件目录
  * 下载文件目录
  */
  */
 fun Request.downloadFileDir(): String {
 fun Request.downloadFileDir(): String {
-    return tagOf<NetTag.DownloadFileDir>()?.dir ?: NetConfig.app.filesDir.absolutePath
+    return tagOf<NetTag.DownloadFileDir>()?.value ?: NetConfig.app.filesDir.absolutePath
 }
 }
 
 
 /**
 /**
  * 下载文件名
  * 下载文件名
  */
  */
 fun Request.downloadFileName(): String? {
 fun Request.downloadFileName(): String? {
-    return tagOf<NetTag.DownloadFileName>()?.name
+    return tagOf<NetTag.DownloadFileName>()?.value
 }
 }
 
 
 /**
 /**
@@ -168,7 +168,7 @@ fun Request.downloadFileName(): String? {
  * 例如下载的文件名如果是中文, 服务器传输给你的会是被URL编码的字符串. 你使用URL解码后才是可读的中文名称
  * 例如下载的文件名如果是中文, 服务器传输给你的会是被URL编码的字符串. 你使用URL解码后才是可读的中文名称
  */
  */
 fun Request.downloadFileNameDecode(): Boolean {
 fun Request.downloadFileNameDecode(): Boolean {
-    return tagOf<NetTag.DownloadFileNameDecode>()?.enabled == true
+    return tagOf<NetTag.DownloadFileNameDecode>()?.value == true
 }
 }
 
 
 /**
 /**
@@ -178,7 +178,7 @@ fun Request.downloadFileNameDecode(): Boolean {
  *      下载文件名: install.apk, 临时文件名: install.apk.net-download
  *      下载文件名: install.apk, 临时文件名: install.apk.net-download
  */
  */
 fun Request.downloadTempFile(): Boolean {
 fun Request.downloadTempFile(): Boolean {
-    return tagOf<NetTag.DownloadTempFile>()?.enabled == true
+    return tagOf<NetTag.DownloadTempFile>()?.value == true
 }
 }
 //</editor-fold>
 //</editor-fold>
 
 

+ 1 - 1
net/src/main/java/com/drake/net/scope/DialogCoroutineScope.kt

@@ -61,7 +61,7 @@ class DialogCoroutineScope(
         }
         }
     }
     }
 
 
-    override fun readCache(succeed: Boolean) {
+    override fun previewFinish(succeed: Boolean) {
         if (succeed) {
         if (succeed) {
             dismiss()
             dismiss()
         }
         }

+ 24 - 17
net/src/main/java/com/drake/net/scope/NetCoroutineScope.kt

@@ -34,30 +34,35 @@ open class NetCoroutineScope(
     dispatcher: CoroutineDispatcher = Dispatchers.Main
     dispatcher: CoroutineDispatcher = Dispatchers.Main
 ) : AndroidScope(lifecycleOwner, lifeEvent, dispatcher) {
 ) : AndroidScope(lifecycleOwner, lifeEvent, dispatcher) {
 
 
-    protected var isReadCache = true
+    /** 预览模式 */
     protected var preview: (suspend CoroutineScope.() -> Unit)? = null
     protected var preview: (suspend CoroutineScope.() -> Unit)? = null
 
 
-    protected var isCacheSucceed = false
-        get() = if (preview != null) field else false
+    /** 是否可读取缓存 */
+    protected var isPreview = true
 
 
-    protected var error = true
-        get() = if (isCacheSucceed) field else true
+    /** 是否读取缓存成功 */
+    protected var previewSucceed = false
+        get() = if (preview != null) field else false
 
 
-    var animate: Boolean = false
+    /** 使用[preview]预览模式情况下读取缓存成功后, 网络请求失败是否处理错误信息 */
+    protected var previewBreakError = false
+        get() = if (previewSucceed) field else false
 
 
+    /** 使用[preview]预览模式情况下读取缓存成功后是否关闭加载动画 */
+    protected var previewBreakLoading: Boolean = true
 
 
     override fun launch(block: suspend CoroutineScope.() -> Unit): NetCoroutineScope {
     override fun launch(block: suspend CoroutineScope.() -> Unit): NetCoroutineScope {
         launch(EmptyCoroutineContext) {
         launch(EmptyCoroutineContext) {
             start()
             start()
-            if (preview != null && isReadCache) {
+            if (preview != null && isPreview) {
                 supervisorScope {
                 supervisorScope {
-                    isCacheSucceed = try {
+                    previewSucceed = try {
                         preview?.invoke(this)
                         preview?.invoke(this)
                         true
                         true
                     } catch (e: Exception) {
                     } catch (e: Exception) {
                         false
                         false
                     }
                     }
-                    readCache(isCacheSucceed)
+                    previewFinish(previewSucceed)
                 }
                 }
             }
             }
             block()
             block()
@@ -75,14 +80,14 @@ open class NetCoroutineScope(
      * 读取缓存回调
      * 读取缓存回调
      * @param succeed 缓存是否成功
      * @param succeed 缓存是否成功
      */
      */
-    protected open fun readCache(succeed: Boolean) {}
+    protected open fun previewFinish(succeed: Boolean) {}
 
 
     override fun handleError(e: Throwable) {
     override fun handleError(e: Throwable) {
         NetConfig.errorHandler.onError(e)
         NetConfig.errorHandler.onError(e)
     }
     }
 
 
     override fun catch(e: Throwable) {
     override fun catch(e: Throwable) {
-        catch?.invoke(this@NetCoroutineScope, e) ?: if (error) handleError(e)
+        catch?.invoke(this@NetCoroutineScope, e) ?: if (!previewBreakError) handleError(e)
     }
     }
 
 
     /**
     /**
@@ -91,17 +96,19 @@ open class NetCoroutineScope(
      * 该函数在作用域[NetCoroutineScope.launch]之前执行
      * 该函数在作用域[NetCoroutineScope.launch]之前执行
      * 函数内部所有的异常都不会被抛出, 也不会终止作用域执行
      * 函数内部所有的异常都不会被抛出, 也不会终止作用域执行
      *
      *
-     * @param ignore 是否在缓存读取成功但网络请求错误时吐司错误信息
-     * @param animate 是否在缓存成功后依然显示加载动画
+     * @param breakError 读取缓存成功后不再处理错误信息
+     * @param breakLoading 读取缓存成功后结束加载动画
      * @param block 该作用域内的所有异常都算缓存读取失败, 不会吐司和打印任何错误
      * @param block 该作用域内的所有异常都算缓存读取失败, 不会吐司和打印任何错误
+     *
+     * 这里指的读取缓存也可以替换为其他任务, 比如读取数据库或者其他接口数据
      */
      */
     fun preview(
     fun preview(
-        ignore: Boolean = false,
-        animate: Boolean = false,
+        breakError: Boolean = false,
+        breakLoading: Boolean = true,
         block: suspend CoroutineScope.() -> Unit
         block: suspend CoroutineScope.() -> Unit
     ): AndroidScope {
     ): AndroidScope {
-        this.animate = animate
-        this.error = ignore
+        this.previewBreakError = breakError
+        this.previewBreakLoading = breakLoading
         this.preview = block
         this.preview = block
         return this
         return this
     }
     }

+ 3 - 3
net/src/main/java/com/drake/net/scope/PageCoroutineScope.kt

@@ -44,12 +44,12 @@ class PageCoroutineScope(
     }
     }
 
 
     override fun start() {
     override fun start() {
-        isReadCache = !page.loaded
+        isPreview = !page.loaded
         page.trigger()
         page.trigger()
     }
     }
 
 
-    override fun readCache(succeed: Boolean) {
-        if (succeed && !animate) {
+    override fun previewFinish(succeed: Boolean) {
+        if (succeed && previewBreakLoading) {
             page.showContent()
             page.showContent()
         }
         }
         page.loaded = succeed
         page.loaded = succeed

+ 3 - 3
net/src/main/java/com/drake/net/scope/StateCoroutineScope.kt

@@ -43,11 +43,11 @@ class StateCoroutineScope(
     }
     }
 
 
     override fun start() {
     override fun start() {
-        isReadCache = !state.loaded
+        isPreview = !state.loaded
         state.trigger()
         state.trigger()
     }
     }
 
 
-    override fun readCache(succeed: Boolean) {
+    override fun previewFinish(succeed: Boolean) {
         if (succeed) {
         if (succeed) {
             state.showContent()
             state.showContent()
         }
         }
@@ -55,7 +55,7 @@ class StateCoroutineScope(
 
 
     override fun catch(e: Throwable) {
     override fun catch(e: Throwable) {
         super.catch(e)
         super.catch(e)
-        if (!isCacheSucceed) state.showError(e)
+        if (!previewSucceed) state.showError(e)
     }
     }
 
 
     override fun handleError(e: Throwable) {
     override fun handleError(e: Throwable) {

+ 32 - 14
net/src/main/java/com/drake/net/tag/NetTag.kt

@@ -23,20 +23,38 @@ import kotlin.reflect.KType
 
 
 sealed class NetTag {
 sealed class NetTag {
     class Extras : HashMap<String, Any?>()
     class Extras : HashMap<String, Any?>()
-
-    inline class RequestId(val value: Any?)
-    inline class RequestGroup(val value: Any?)
-    inline class RequestKType(val value: KType?)
-
-    inline class DownloadFileMD5Verify(val enabled: Boolean = true)
-    inline class DownloadFileNameDecode(val enabled: Boolean = true)
-    inline class DownloadTempFile(val enabled: Boolean = true)
-    inline class DownloadFileConflictRename(val enabled: Boolean = true)
-    inline class DownloadFileName(val name: String?)
-    inline class DownloadFileDir(val dir: String?) {
-        constructor(fileDir: File?) : this(fileDir?.absolutePath)
-    }
-
     class UploadListeners : ConcurrentLinkedQueue<ProgressListener>()
     class UploadListeners : ConcurrentLinkedQueue<ProgressListener>()
     class DownloadListeners : ConcurrentLinkedQueue<ProgressListener>()
     class DownloadListeners : ConcurrentLinkedQueue<ProgressListener>()
+
+    @JvmInline
+    value class RequestId(val value: Any)
+
+    @JvmInline
+    value class RequestGroup(val value: Any)
+
+    @JvmInline
+    value class RequestKType(val value: KType)
+
+    @JvmInline
+    value class DownloadFileMD5Verify(val value: Boolean = true)
+
+    @JvmInline
+    value class DownloadFileNameDecode(val value: Boolean = true)
+
+    @JvmInline
+    value class DownloadTempFile(val value: Boolean = true)
+
+    @JvmInline
+    value class DownloadFileConflictRename(val value: Boolean = true)
+
+    @JvmInline
+    value class DownloadFileName(val value: String)
+
+    @JvmInline
+    value class CacheKey(val value: String)
+
+    @JvmInline
+    value class DownloadFileDir(val value: String) {
+        constructor(fileDir: File) : this(fileDir.absolutePath)
+    }
 }
 }

+ 11 - 0
net/src/main/java/okhttp3/OkHttpUtils.java

@@ -5,6 +5,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map;
 
 
 import kotlin.jvm.JvmStatic;
 import kotlin.jvm.JvmStatic;
+import okhttp3.internal.cache.DiskLruCache;
 
 
 @SuppressWarnings("KotlinInternalInJava")
 @SuppressWarnings("KotlinInternalInJava")
 public class OkHttpUtils {
 public class OkHttpUtils {
@@ -43,4 +44,14 @@ public class OkHttpUtils {
     public static Headers.Builder headers(Request.Builder builder) {
     public static Headers.Builder headers(Request.Builder builder) {
         return builder.getHeaders$okhttp();
         return builder.getHeaders$okhttp();
     }
     }
+
+    @JvmStatic
+    public static Headers.Builder addLenient(Headers.Builder builder, String line) {
+        return builder.addLenient$okhttp(line);
+    }
+
+    @JvmStatic
+    public static DiskLruCache diskLruCache(Cache cache) {
+        return cache.getCache$okhttp();
+    }
 }
 }

+ 1 - 1
net/src/main/res/values/strings.xml

@@ -24,7 +24,7 @@
     <string name="net_host_error">无法找到指定服务器主机</string>
     <string name="net_host_error">无法找到指定服务器主机</string>
     <string name="net_connect_timeout_error">连接服务器超时,%s</string>
     <string name="net_connect_timeout_error">连接服务器超时,%s</string>
     <string name="net_download_error">下载过程发生错误</string>
     <string name="net_download_error">下载过程发生错误</string>
-    <string name="net_no_cache_error">读取缓存错误</string>
+    <string name="net_no_cache_error">读取缓存失败</string>
     <string name="net_parse_error">解析数据时发生异常</string>
     <string name="net_parse_error">解析数据时发生异常</string>
     <string name="request_failure">请求失败</string>
     <string name="request_failure">请求失败</string>
     <string name="net_request_error">请求参数错误</string>
     <string name="net_request_error">请求参数错误</string>

+ 2 - 2
sample/src/main/java/com/drake/net/sample/base/App.kt

@@ -35,6 +35,7 @@ import com.drake.statelayout.StateConfig
 import com.scwang.smart.refresh.footer.ClassicsFooter
 import com.scwang.smart.refresh.footer.ClassicsFooter
 import com.scwang.smart.refresh.header.MaterialHeader
 import com.scwang.smart.refresh.header.MaterialHeader
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
+import okhttp3.Cache
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeUnit
 
 
 class App : Application() {
 class App : Application() {
@@ -57,9 +58,8 @@ class App : Application() {
                     request.setHeader("token", "123456")
                     request.setHeader("token", "123456")
                 }
                 }
             })
             })
-
             setConverter(GsonConverter()) // 数据转换器
             setConverter(GsonConverter()) // 数据转换器
-
+            cache(Cache(cacheDir, 1024 * 1024 * 128)) // 缓存设置
             setDialogFactory { // 全局加载对话框
             setDialogFactory { // 全局加载对话框
                 ProgressDialog(it).apply {
                 ProgressDialog(it).apply {
                     setMessage("我是全局自定义的加载对话框...")
                     setMessage("我是全局自定义的加载对话框...")

+ 53 - 0
sample/src/main/java/com/drake/net/sample/ui/fragment/PreviewCacheFragment.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 Drake, Inc.
+ *
+ * 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.drake.net.sample.ui.fragment
+
+import android.util.Log
+import com.drake.engine.base.EngineFragment
+import com.drake.net.Get
+import com.drake.net.cache.CacheMode
+import com.drake.net.sample.R
+import com.drake.net.sample.databinding.FragmentReadCacheBinding
+import com.drake.net.utils.scopeNetLife
+
+
+/** 预览缓存. 其实不仅仅是双重加载缓存/网络也可以用于回退请求, 可以执行两次作用域并且忽略preview{}内的所有错误 */
+class PreviewCacheFragment : EngineFragment<FragmentReadCacheBinding>(R.layout.fragment_read_cache) {
+
+    override fun initView() {
+
+        // 一般用于秒开首页或者回退加载数据. 我们可以在preview{}只加载缓存. 然后再执行scopeNetLife来请求网络, 做到缓存+网络双重加载的效果
+
+        scopeNetLife {
+            // 然后执行这里(网络请求)
+            binding.tvFragment.text = Get<String>("api") {
+                setCacheMode(CacheMode.WRITE)
+            }.await()
+            Log.d("日志", "网络请求")
+        }.preview(true) {
+            // 先执行这里(仅读缓存), 任何异常都视为读取缓存失败
+            binding.tvFragment.text = Get<String>("api") {
+                setCacheMode(CacheMode.READ)
+            }.await()
+            Log.d("日志", "读取缓存")
+        }
+    }
+
+    override fun initData() {
+    }
+
+}

+ 10 - 7
sample/src/main/java/com/drake/net/sample/ui/fragment/ReadCacheFragment.kt

@@ -18,25 +18,28 @@ package com.drake.net.sample.ui.fragment
 
 
 import android.util.Log
 import android.util.Log
 import com.drake.engine.base.EngineFragment
 import com.drake.engine.base.EngineFragment
-import com.drake.net.Get
 import com.drake.net.Post
 import com.drake.net.Post
+import com.drake.net.cache.CacheMode
 import com.drake.net.sample.R
 import com.drake.net.sample.R
 import com.drake.net.sample.databinding.FragmentReadCacheBinding
 import com.drake.net.sample.databinding.FragmentReadCacheBinding
 import com.drake.net.utils.scopeNetLife
 import com.drake.net.utils.scopeNetLife
 
 
 
 
+/**
+ * 默认支持Http标准缓存协议
+ * 如果需要自定义缓存模式来强制读写缓存,可以使用[CacheMode], 这会覆盖默认的Http标准缓存协议.
+ * 可以缓存任何数据, 包括文件. 并且遵守LRU缓存策略限制最大缓存空间
+ */
 class ReadCacheFragment : EngineFragment<FragmentReadCacheBinding>(R.layout.fragment_read_cache) {
 class ReadCacheFragment : EngineFragment<FragmentReadCacheBinding>(R.layout.fragment_read_cache) {
 
 
     override fun initView() {
     override fun initView() {
         scopeNetLife {
         scopeNetLife {
-            // 然后执行这里(网络请求)
             binding.tvFragment.text =
             binding.tvFragment.text =
-                Post<String>("api").await()
+                Post<String>("api") {
+                    setCacheMode(CacheMode.REQUEST_THEN_READ) // 请求网络失败会读取缓存, 请断网测试
+                    // setCacheKey("自定义缓存KEY")
+                }.await()
             Log.d("日志", "网络请求")
             Log.d("日志", "网络请求")
-        }.preview {
-            // 先执行这里(仅读缓存), 任何异常都视为读取缓存失败
-            binding.tvFragment.text = Get<String>("api").await()
-            Log.d("日志", "读取缓存")
         }
         }
     }
     }
 
 

+ 12 - 0
sample/src/main/res/drawable/ic_preview_cache.xml

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="14dp"
+    android:height="16dp"
+    android:viewportWidth="14"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M0.4121,11.2265C0.4121,11.8077 0.562,12.2464 0.8619,12.5425C1.1618,12.8387 1.6059,12.9868 2.1942,12.9868L8.5641,12.9868C9.1559,12.9868 9.6009,12.8387 9.899,12.5425C10.1971,12.2464 10.3461,11.8077 10.3461,11.2265L10.3461,5.8619C10.3461,5.2803 10.1971,4.8414 9.899,4.5453C9.6009,4.2491 9.1559,4.101 8.5641,4.101L5.86,4.101L5.86,1.4599C5.86,1.3364 5.8125,1.2278 5.7177,1.1339C5.6228,1.04 5.511,0.9931 5.382,0.9931C5.2493,0.9931 5.1364,1.04 5.0433,1.1339C4.9501,1.2278 4.9036,1.3364 4.9036,1.4599L4.9036,4.101L2.1942,4.101C1.6059,4.101 1.1618,4.2482 0.8619,4.5426C0.562,4.837 0.4121,5.2768 0.4121,5.8619L0.4121,11.2265ZM5.382,9.4228C5.3178,9.4228 5.2571,9.4109 5.1999,9.3872C5.1427,9.3634 5.0839,9.3216 5.0236,9.2616L3.1554,7.459C3.0641,7.3674 3.0185,7.265 3.0185,7.152C3.0185,7.0304 3.0591,6.93 3.1403,6.8507C3.2215,6.7714 3.3236,6.7318 3.4464,6.7318C3.5797,6.7318 3.6888,6.7799 3.7737,6.8762L4.6741,7.8275L4.9427,8.1369L4.9036,7.3083L4.9036,4.5017C4.9036,4.3708 4.9475,4.2621 5.0353,4.1754C5.1231,4.0888 5.2387,4.0455 5.382,4.0455C5.5215,4.0455 5.636,4.0888 5.7256,4.1754C5.8152,4.2621 5.86,4.3708 5.86,4.5017L5.86,7.3083L5.8209,8.1369L6.0842,7.8275L6.9846,6.8762C7.0695,6.7799 7.1766,6.7318 7.306,6.7318C7.4293,6.7318 7.5323,6.7714 7.6153,6.8507C7.6982,6.93 7.7397,7.0304 7.7397,7.152C7.7397,7.265 7.6941,7.3674 7.6029,7.459L5.7399,9.2616C5.6761,9.3216 5.6156,9.3634 5.5584,9.3872C5.5012,9.4109 5.4424,9.4228 5.382,9.4228ZM5.4655,15.6064L11.3723,15.6064C11.961,15.6064 12.4043,15.4583 12.7022,15.1621C13.0002,14.866 13.1491,14.4254 13.1491,13.8402L13.1491,8.2741C13.1491,7.6904 13.0007,7.2513 12.7039,6.9567C12.4072,6.6621 11.9657,6.5143 11.3797,6.5132L11.1297,6.5132L11.1297,11.3011C11.1297,11.8366 11.035,12.2881 10.8454,12.6557C10.6559,13.0233 10.3755,13.3009 10.0042,13.4887C9.6329,13.6765 9.1778,13.7704 8.6388,13.7704L3.6829,13.7704L3.6829,13.8233C3.6844,14.4082 3.8305,14.8517 4.1211,15.1535C4.4118,15.4554 4.86,15.6064 5.4655,15.6064Z"
+      android:strokeWidth="1"
+      android:fillColor="#000000"
+      android:fillType="nonZero"
+      android:strokeColor="#00000000" />
+</vector>

+ 8 - 5
sample/src/main/res/menu/menu_main.xml

@@ -75,11 +75,14 @@
         android:id="@+id/state_layout"
         android:id="@+id/state_layout"
         android:icon="@drawable/ic_state_layout"
         android:icon="@drawable/ic_state_layout"
         android:title="自动缺省页" />
         android:title="自动缺省页" />
-    <!--缓存设计中-->
-    <!--<item-->
-    <!--    android:id="@+id/read_cache"-->
-    <!--    android:icon="@drawable/ic_read_cache"-->
-    <!--    android:title="预读缓存" />-->
+    <item
+        android:id="@+id/read_cache"
+        android:icon="@drawable/ic_read_cache"
+        android:title="强制缓存" />
+    <item
+        android:id="@+id/previewCacheFragment"
+        android:icon="@drawable/ic_preview_cache"
+        android:title="预读缓存" />
     <item
     <item
         android:id="@+id/fastest"
         android:id="@+id/fastest"
         android:icon="@drawable/ic_fastest"
         android:icon="@drawable/ic_fastest"

+ 4 - 0
sample/src/main/res/navigation/nav_main.xml

@@ -136,5 +136,9 @@
         android:name="com.drake.net.sample.ui.fragment.ViewModelRequestFragment"
         android:name="com.drake.net.sample.ui.fragment.ViewModelRequestFragment"
         android:label="ViewModel" />
         android:label="ViewModel" />
     <include app:graph="@navigation/nav_converter" />
     <include app:graph="@navigation/nav_converter" />
+    <fragment
+        android:id="@+id/previewCacheFragment"
+        android:name="com.drake.net.sample.ui.fragment.PreviewCacheFragment"
+        android:label="PreviewCacheFragment" />
 
 
 </navigation>
 </navigation>