()
+ val mAppRepository by inject(AppRepository::class.java)
+ val mContext by inject(Application::class.java)
private var mCompositeDisposable: CompositeDisposable? = null
/**
@@ -22,9 +24,8 @@ open class BaseViewModel : ViewModel() {
mCompositeDisposable!!.add(subscribe)
}
- override fun onCleared() {
+ public override fun onCleared() {
super.onCleared()
- Log.d(TAG, "页面销毁,取消全部网络请求")
if (mCompositeDisposable != null) mCompositeDisposable!!.clear()
}
diff --git a/sample/src/main/java/com/zww/sample/interceptors/BufferListener.kt b/sample/src/main/java/com/zww/sample/interceptors/BufferListener.kt
new file mode 100644
index 00000000..1987a9fb
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/BufferListener.kt
@@ -0,0 +1,12 @@
+package com.zww.sample.interceptors
+
+import okhttp3.Request
+import java.io.IOException
+
+/**
+ * @author ihsan on 8/12/18.
+ */
+interface BufferListener {
+ @Throws(IOException::class)
+ fun getJsonResponse(request: Request?): String?
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/interceptors/I.kt b/sample/src/main/java/com/zww/sample/interceptors/I.kt
new file mode 100644
index 00000000..ba3aa26c
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/I.kt
@@ -0,0 +1,36 @@
+package com.zww.sample.interceptors
+
+import okhttp3.internal.platform.Platform.Companion.INFO
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * @author ihsan on 10/02/2017.
+ */
+internal open class I protected constructor() {
+ companion object {
+ private val prefix = arrayOf(". ", " .")
+ private var index = 0
+ fun log(type: Int, tag: String, msg: String?, isLogHackEnable: Boolean) {
+ val finalTag = getFinalTag(tag, isLogHackEnable)
+ val logger = Logger.getLogger(if (isLogHackEnable) finalTag else tag)
+ when (type) {
+ INFO -> logger.log(Level.INFO, msg)
+ else -> logger.log(Level.WARNING, msg)
+ }
+ }
+
+ private fun getFinalTag(tag: String, isLogHackEnable: Boolean): String {
+ return if (isLogHackEnable) {
+ index = index xor 1
+ prefix[index] + tag
+ } else {
+ tag
+ }
+ }
+ }
+
+ init {
+ throw UnsupportedOperationException()
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/interceptors/Level.kt b/sample/src/main/java/com/zww/sample/interceptors/Level.kt
new file mode 100644
index 00000000..ecfa31a1
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/Level.kt
@@ -0,0 +1,42 @@
+package com.zww.sample.interceptors
+
+/**
+ * @author ihsan on 21/02/2017.
+ */
+enum class Level {
+ /**
+ * No logs.
+ */
+ NONE,
+
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Headers
+ * - Body
+ `
*
+ */
+ BASIC,
+
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Headers
+ `
*
+ */
+ HEADERS,
+
+ /**
+ *
+ * Example:
+ * `- URL
+ * - Method
+ * - Body
+ `
*
+ */
+ BODY
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/interceptors/Logger.kt b/sample/src/main/java/com/zww/sample/interceptors/Logger.kt
new file mode 100644
index 00000000..2cfb673c
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/Logger.kt
@@ -0,0 +1,19 @@
+package com.zww.sample.interceptors
+
+import okhttp3.internal.platform.Platform
+import okhttp3.internal.platform.Platform.Companion.INFO
+
+/**
+ * @author ihsan on 11/07/2017.
+ */
+interface Logger {
+ fun log(level: Int = INFO, tag: String? = null, msg: String? = null)
+
+ companion object {
+ val DEFAULT: Logger = object : Logger {
+ override fun log(level: Int, tag: String?, msg: String?) {
+ Platform.get().log("$msg", level, null)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/interceptors/LoggingInterceptor.kt b/sample/src/main/java/com/zww/sample/interceptors/LoggingInterceptor.kt
new file mode 100644
index 00000000..ac13410a
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/LoggingInterceptor.kt
@@ -0,0 +1,332 @@
+package com.zww.sample.interceptors
+
+import com.google.gson.Gson
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.ResponseBody.Companion.toResponseBody
+import okhttp3.internal.platform.Platform.Companion.INFO
+import okio.Buffer
+import java.io.IOException
+import java.net.URLDecoder
+import java.nio.charset.Charset
+import java.util.*
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+class LoggingInterceptor private constructor(private val builder: Builder) : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+
+ //将请求体中的数据进行封装并返回新的request
+// val topRequest = formartParams(chain.request())
+
+ val request = addQueryAndHeaders(chain.request())
+
+ if (builder.level == Level.NONE) {
+ return chain.proceed(request)
+ }
+
+ printlnRequestLog(request)
+
+ val startNs = System.nanoTime()
+ val response: Response
+ try {
+ response = proceedResponse(chain, request)
+ } catch (e: Exception) {
+ Printer.printFailed(builder.getTag(false), builder)
+ throw e
+ }
+ val receivedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
+
+ printlnResponseLog(receivedMs, response, request)
+ return response
+ }
+
+ private fun formartParams(request: Request): Request? {
+ val requestBody = request.body
+ val buffer = Buffer()
+ try {
+ requestBody?.writeTo(buffer)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ return null
+ }
+ //编码设为UTF-8
+ var charset: Charset = Charset.forName("UTF-8")
+ val contentType = requestBody?.contentType()
+ if (contentType != null) {
+ charset = contentType.charset(Charset.forName("UTF-8"))!!
+ }
+ //拿到request
+ val requestString: String = buffer.readString(charset)
+ if (!requestString.contains("requestData")) {
+ val formBody = FormBody.Builder(charset = charset)
+ val requestOtherMap = hashMapOf()
+// val sb = StringBuffer()
+ if (requestString.isNotEmpty()) {
+ requestString.split("&").forEach {
+ //{"requestSystem":"user","requestUri":"/userapi/securityCode","requestData":{"mobile":"15317026229"}}}}
+ val key = it.split("=")[0]
+ val value = it.split("=")[1]
+ if (key == "requestSystem") {
+ formBody.add(key, value)
+ } else if (key == "requestUri") {
+ formBody.add(key, URLDecoder.decode(value, "UTF-8"))
+ } else {
+ requestOtherMap.put(key, value)
+ }
+ }
+ }
+ if (requestOtherMap.isNotEmpty()) {
+ formBody.add("requestData", URLDecoder.decode(Gson().toJson(requestOtherMap)))
+ }
+ val requestBuilder = request.newBuilder()
+
+ requestBuilder.method(
+ request.method,
+ formBody.build()
+ )
+ return requestBuilder.build()
+ } else return request
+
+ }
+
+
+ private fun printlnResponseLog(receivedMs: Long, response: Response, request: Request) {
+ Printer.printJsonResponse(
+ builder,
+ receivedMs,
+ response.isSuccessful,
+ response.code,
+ response.headers,
+ response,
+ request.url.encodedPathSegments,
+ response.message,
+ request.url.toString()
+ )
+ }
+
+ private fun printlnRequestLog(request: Request) {
+ Printer.printJsonRequest(
+ builder,
+ request.body,
+ request.url.toUrl().toString(),
+ request.headers,
+ request.method
+ )
+ }
+
+ private fun proceedResponse(chain: Interceptor.Chain, request: Request): Response {
+ return if (builder.isMockEnabled && builder.listener != null) {
+ TimeUnit.MILLISECONDS.sleep(builder.sleepMs)
+ Response.Builder()
+ .body(
+ builder.listener!!.getJsonResponse(request)
+ ?.toResponseBody("application/json".toMediaTypeOrNull())
+ )
+ .request(chain.request())
+ .protocol(Protocol.HTTP_2)
+ .message("Mock data from LoggingInterceptor")
+ .code(200)
+ .build()
+ } else chain.proceed(request)
+ }
+
+ private fun addQueryAndHeaders(request: Request): Request {
+ val requestBuilder = request.newBuilder()
+ builder.headers.keys.forEach { key ->
+ builder.headers[key]?.let {
+ requestBuilder.addHeader(key, it)
+ }
+ }
+ val httpUrlBuilder: HttpUrl.Builder? = request.url.newBuilder(request.url.toString())
+ httpUrlBuilder?.let {
+ builder.httpUrl.keys.forEach { key ->
+ httpUrlBuilder.addQueryParameter(key, builder.httpUrl[key])
+ }
+ }
+ return requestBuilder.url(httpUrlBuilder?.build()!!).build()
+ }
+
+ @Suppress("unused")
+ class Builder {
+ val headers: HashMap = HashMap()
+ val httpUrl: HashMap = HashMap()
+ var isLogHackEnable = false
+ private set
+ var isDebugAble = false
+ var type: Int = INFO
+ private set
+ private var requestTag: String? = null
+ private var responseTag: String? = null
+ var level = Level.BASIC
+ private set
+ var logger: Logger? = null
+ private set
+ var isMockEnabled = false
+ var sleepMs: Long = 0
+ var listener: BufferListener? = null
+
+ /**
+ * @param level set log level
+ * @return Builder
+ * @see Level
+ */
+ fun setLevel(level: Level): Builder {
+ this.level = level
+ return this
+ }
+
+ fun getTag(isRequest: Boolean): String {
+ return when (isRequest) {
+ true -> if (requestTag.isNullOrEmpty()) TAG else requestTag!!
+ false -> if (responseTag.isNullOrEmpty()) TAG else responseTag!!
+ }
+ }
+
+ /**
+ * @param name Filed
+ * @param value Value
+ * @return Builder
+ * Add a field with the specified value
+ */
+ fun addHeader(name: String, value: String): Builder {
+ headers[name] = value
+ return this
+ }
+
+ /**
+ * @param name Filed
+ * @param value Value
+ * @return Builder
+ * Add a field with the specified value
+ */
+ fun addQueryParam(name: String, value: String): Builder {
+ httpUrl[name] = value
+ return this
+ }
+
+ /**
+ * Set request and response each log tag
+ *
+ * @param tag general log tag
+ * @return Builder
+ */
+ fun tag(tag: String): Builder {
+ TAG = tag
+ return this
+ }
+
+ /**
+ * Set request log tag
+ *
+ * @param tag request log tag
+ * @return Builder
+ */
+ fun request(tag: String?): Builder {
+ requestTag = tag
+ return this
+ }
+
+ /**
+ * Set response log tag
+ *
+ * @param tag response log tag
+ * @return Builder
+ */
+ fun response(tag: String?): Builder {
+ responseTag = tag
+ return this
+ }
+
+ /**
+ * @param isDebug set can sending log output
+ * @return Builder
+ */
+ @Deprecated(
+ message = "Set level based on your requirement",
+ replaceWith = ReplaceWith(expression = "setLevel(Level.Basic)"),
+ level = DeprecationLevel.ERROR
+ )
+ fun loggable(isDebug: Boolean): Builder {
+ this.isDebugAble = isDebug
+ return this
+ }
+
+ /**
+ * @param type set sending log output type
+ * @return Builder
+ * @see okhttp3.internal.platform.Platform
+ */
+ fun log(type: Int): Builder {
+ this.type = type
+ return this
+ }
+
+ /**
+ * @param logger manuel logging interface
+ * @return Builder
+ * @see Logger
+ */
+ fun logger(logger: Logger?): Builder {
+ this.logger = logger
+ return this
+ }
+
+ /**
+ * @param executor manual executor for printing
+ * @return Builder
+ * @see Logger
+ */
+ @Deprecated(
+ message = "Create your own Logcat filter for best result",
+ level = DeprecationLevel.ERROR
+ )
+ fun executor(executor: Executor?): Builder {
+ TODO("Deprecated")
+ }
+
+ /**
+ * @param useMock let you use json file from asset
+ * @param sleep let you see progress dialog when you request
+ * @return Builder
+ * @see LoggingInterceptor
+ */
+ fun enableMock(useMock: Boolean, sleep: Long, listener: BufferListener?): Builder {
+ isMockEnabled = useMock
+ sleepMs = sleep
+ this.listener = listener
+ return this
+ }
+
+ /**
+ * Call this if you want to have formatted pretty output in Android Studio logCat.
+ * By default this 'hack' is not applied.
+ *
+ * @param useHack setup builder to use hack for Android Studio v3+ in order to have nice
+ * output as it was in previous A.S. versions.
+ * @return Builder
+ * @see Logger
+ */
+ @Deprecated(
+ message = "Android studio has resolved problem for latest versions",
+ level = DeprecationLevel.WARNING
+ )
+ fun enableAndroidStudioV3LogsHack(useHack: Boolean): Builder {
+ isLogHackEnable = useHack
+ return this
+ }
+
+ fun build(): LoggingInterceptor {
+ return LoggingInterceptor(this)
+ }
+
+ companion object {
+ private var TAG = "LoggingI"
+ }
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/interceptors/Printer.kt b/sample/src/main/java/com/zww/sample/interceptors/Printer.kt
new file mode 100644
index 00000000..bbe2d90c
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/interceptors/Printer.kt
@@ -0,0 +1,345 @@
+package com.zww.sample.interceptors
+
+import okhttp3.Headers
+import okhttp3.RequestBody
+import okhttp3.Response
+import okhttp3.internal.http.promisesBody
+import okio.Buffer
+import okio.GzipSource
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.EOFException
+import java.io.IOException
+import java.net.URLDecoder
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets
+
+/**
+ * @author ihsan on 09/02/2017.
+ */
+class Printer private constructor() {
+ companion object {
+ private const val JSON_INDENT = 3
+ private val LINE_SEPARATOR = System.getProperty("line.separator")
+ private val DOUBLE_SEPARATOR = LINE_SEPARATOR + LINE_SEPARATOR
+ private const val N = "\n"
+ private const val T = "\t"
+ private const val REQUEST_UP_LINE =
+ "┌────── Request ────────────────────────────────────────────────────────────────────────"
+ private const val END_LINE =
+ "└───────────────────────────────────────────────────────────────────────────────────────"
+ private const val RESPONSE_UP_LINE =
+ "┌────── Response ───────────────────────────────────────────────────────────────────────"
+ private const val BODY_TAG = "Body:"
+ private const val URL_TAG = "URL: "
+ private const val METHOD_TAG = "Method: @"
+ private const val HEADERS_TAG = "Headers:"
+ private const val STATUS_CODE_TAG = "Status Code: "
+ private const val RECEIVED_TAG = "Received in: "
+ private const val DEFAULT_LINE = "│ "
+ private val OOM_OMITTED = LINE_SEPARATOR + "Output omitted because of Object size."
+ private fun isEmpty(line: String): Boolean {
+ return line.isEmpty() || N == line || T == line || line.trim { it <= ' ' }.isEmpty()
+ }
+
+ fun printJsonRequest(
+ builder: LoggingInterceptor.Builder,
+ body: RequestBody?,
+ url: String,
+ header: Headers,
+ method: String
+ ) {
+ val requestBody = body?.let {
+ LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + bodyToString(body, header)
+ } ?: ""
+ val tag = builder.getTag(true)
+ if (builder.logger == null) I.log(
+ builder.type,
+ tag,
+ REQUEST_UP_LINE,
+ builder.isLogHackEnable
+ )
+ logLines(
+ builder.type,
+ tag,
+ arrayOf(URL_TAG + url),
+ builder.logger,
+ false,
+ builder.isLogHackEnable
+ )
+ logLines(
+ builder.type,
+ tag,
+ getRequest(builder.level, header, method),
+ builder.logger,
+ true,
+ builder.isLogHackEnable
+ )
+ if (builder.level == Level.BASIC || builder.level == Level.BODY) {
+ logLines(
+ builder.type,
+ tag,
+ requestBody.split(LINE_SEPARATOR).toTypedArray(),
+ builder.logger,
+ true,
+ builder.isLogHackEnable
+ )
+ }
+ if (builder.logger == null) I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+
+ fun printJsonResponse(
+ builder: LoggingInterceptor.Builder,
+ chainMs: Long,
+ isSuccessful: Boolean,
+ code: Int,
+ headers: Headers,
+ response: Response,
+ segments: List,
+ message: String,
+ responseUrl: String
+ ) {
+ val responseBody =
+ LINE_SEPARATOR + BODY_TAG + LINE_SEPARATOR + getResponseBody(response)
+ val tag = builder.getTag(false)
+ val urlLine = arrayOf(URL_TAG + responseUrl, N)
+ val responseString = getResponse(
+ headers, chainMs, code, isSuccessful,
+ builder.level, segments, message
+ )
+ if (builder.logger == null) {
+ I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable)
+ }
+ logLines(builder.type, tag, urlLine, builder.logger, true, builder.isLogHackEnable)
+ logLines(
+ builder.type,
+ tag,
+ responseString,
+ builder.logger,
+ true,
+ builder.isLogHackEnable
+ )
+ if (builder.level == Level.BASIC || builder.level == Level.BODY) {
+ logLines(
+ builder.type,
+ tag,
+ responseBody.split(LINE_SEPARATOR).toTypedArray(),
+ builder.logger,
+ true,
+ builder.isLogHackEnable
+ )
+ }
+ if (builder.logger == null) {
+ I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+ }
+
+ private fun getResponseBody(response: Response): String {
+ val responseBody = response.body!!
+ val headers = response.headers
+ val contentLength = responseBody.contentLength()
+ if (!response.promisesBody()) {
+ return "End request - Promises Body"
+ } else if (bodyHasUnknownEncoding(response.headers)) {
+ return "encoded body omitted"
+ } else {
+ val source = responseBody.source()
+ source.request(Long.MAX_VALUE) // Buffer the entire body.
+ var buffer = source.buffer
+
+ var gzippedLength: Long? = null
+ if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) {
+ gzippedLength = buffer.size
+ GzipSource(buffer.clone()).use { gzippedResponseBody ->
+ buffer = Buffer()
+ buffer.writeAll(gzippedResponseBody)
+ }
+ }
+
+ val contentType = responseBody.contentType()
+ val charset: Charset = contentType?.charset(StandardCharsets.UTF_8)
+ ?: StandardCharsets.UTF_8
+
+ if (!buffer.isProbablyUtf8()) {
+ return "End request - binary ${buffer.size}:byte body omitted"
+ }
+
+ if (contentLength != 0L) {
+ return getJsonString(buffer.clone().readString(charset))
+ }
+
+ return if (gzippedLength != null) {
+ "End request - ${buffer.size}:byte, $gzippedLength-gzipped-byte body"
+ } else {
+ "End request - ${buffer.size}:byte body"
+ }
+ }
+ }
+
+ private fun getRequest(level: Level, headers: Headers, method: String): Array {
+ val log: String
+ val loggableHeader = level == Level.HEADERS || level == Level.BASIC
+ log = METHOD_TAG + method + DOUBLE_SEPARATOR +
+ if (isEmpty("$headers")) "" else if (loggableHeader) HEADERS_TAG + LINE_SEPARATOR + dotHeaders(
+ headers
+ ) else ""
+ return log.split(LINE_SEPARATOR).toTypedArray()
+ }
+
+ private fun getResponse(
+ headers: Headers, tookMs: Long, code: Int, isSuccessful: Boolean,
+ level: Level, segments: List, message: String
+ ): Array {
+ val log: String
+ val loggableHeader = level == Level.HEADERS || level == Level.BASIC
+ val segmentString = slashSegments(segments)
+ log = ((if (segmentString.isNotEmpty()) "$segmentString - " else "") + "[is success : "
+ + isSuccessful + "] - " + RECEIVED_TAG + tookMs + "ms" + DOUBLE_SEPARATOR + STATUS_CODE_TAG +
+ code + " / " + message + DOUBLE_SEPARATOR + when {
+ isEmpty("$headers") -> ""
+ loggableHeader -> HEADERS_TAG + LINE_SEPARATOR +
+ dotHeaders(headers)
+ else -> ""
+ })
+ return log.split(LINE_SEPARATOR).toTypedArray()
+ }
+
+ private fun slashSegments(segments: List): String {
+ val segmentString = StringBuilder()
+ for (segment in segments) {
+ segmentString.append("/").append(segment)
+ }
+ return segmentString.toString()
+ }
+
+ private fun dotHeaders(headers: Headers): String {
+ val builder = StringBuilder()
+ headers.forEach { pair ->
+ builder.append("${pair.first}: ${pair.second}").append(N)
+ }
+ return builder.dropLast(1).toString()
+ }
+
+ private fun logLines(
+ type: Int, tag: String, lines: Array, logger: Logger?,
+ withLineSize: Boolean, useLogHack: Boolean
+ ) {
+ for (line in lines) {
+ val lineLength = line.length
+ val maxLogSize = if (withLineSize) 110 else lineLength
+ for (i in 0..lineLength / maxLogSize) {
+ val start = i * maxLogSize
+ var end = (i + 1) * maxLogSize
+ end = if (end > line.length) line.length else end
+ if (logger == null) {
+ I.log(type, tag, DEFAULT_LINE + line.substring(start, end), useLogHack)
+ } else {
+ logger.log(type, tag, line.substring(start, end))
+ }
+ }
+ }
+ }
+
+ private fun bodyToString(requestBody: RequestBody?, headers: Headers): String {
+ return requestBody?.let {
+ return try {
+ when {
+ bodyHasUnknownEncoding(headers) -> {
+ return "encoded body omitted)"
+ }
+ requestBody.isDuplex() -> {
+ return "duplex request body omitted"
+ }
+ requestBody.isOneShot() -> {
+ return "one-shot body omitted"
+ }
+ else -> {
+ val buffer = Buffer()
+ requestBody.writeTo(buffer)
+
+ val contentType = requestBody.contentType()
+ val charset: Charset = contentType?.charset(StandardCharsets.UTF_8)
+ ?: StandardCharsets.UTF_8
+
+ return if (buffer.isProbablyUtf8()) {
+ getJsonString(buffer.readString(charset)) + LINE_SEPARATOR + "${requestBody.contentLength()}-byte body"
+ } else {
+ "binary ${requestBody.contentLength()}-byte body omitted"
+ }
+ }
+ }
+ } catch (e: IOException) {
+ "{\"err\": \"" + e.message + "\"}"
+ }
+ } ?: ""
+ }
+
+ private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
+ val contentEncoding = headers["Content-Encoding"] ?: return false
+ return !contentEncoding.equals("identity", ignoreCase = true) &&
+ !contentEncoding.equals("gzip", ignoreCase = true)
+ }
+
+ private fun getJsonString(msg: String): String {
+ val message: String
+ message = try {
+ when {
+ msg.startsWith("{") -> {
+ val jsonObject = JSONObject(msg)
+ jsonObject.toString(JSON_INDENT)
+ }
+ msg.startsWith("[") -> {
+ val jsonArray = JSONArray(msg)
+ jsonArray.toString(JSON_INDENT)
+ }
+ else -> {
+ msg
+ }
+ }
+ } catch (e: JSONException) {
+ URLDecoder.decode(msg)
+ } catch (e1: OutOfMemoryError) {
+ OOM_OMITTED
+ }
+ try {
+ return URLDecoder.decode(message)
+ } catch (e: Exception) {
+ return URLDecoder.decode(message.replace("%", "%25"))
+ }
+ }
+
+ fun printFailed(tag: String, builder: LoggingInterceptor.Builder) {
+ I.log(builder.type, tag, RESPONSE_UP_LINE, builder.isLogHackEnable)
+ I.log(builder.type, tag, DEFAULT_LINE + "Response failed", builder.isLogHackEnable)
+ I.log(builder.type, tag, END_LINE, builder.isLogHackEnable)
+ }
+ }
+
+ init {
+ throw UnsupportedOperationException()
+ }
+}
+
+/**
+ * @see 'https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/src/main/java/okhttp3/logging/utf8.kt'
+ * */
+internal fun Buffer.isProbablyUtf8(): Boolean {
+ try {
+ val prefix = Buffer()
+ val byteCount = size.coerceAtMost(64)
+ copyTo(prefix, 0, byteCount)
+ for (i in 0 until 16) {
+ if (prefix.exhausted()) {
+ break
+ }
+ val codePoint = prefix.readUtf8CodePoint()
+ if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
+ return false
+ }
+ }
+ return true
+ } catch (_: EOFException) {
+ return false // Truncated UTF-8 sequence.
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/kt.kt b/sample/src/main/java/com/zww/sample/kt.kt
new file mode 100644
index 00000000..88f42fef
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/kt.kt
@@ -0,0 +1,47 @@
+package com.zww.sample
+
+import NsfwBean
+import android.content.Intent
+import android.util.Log
+
+/**
+ * 控制UI的实体
+ */
+data class AppLiveData(
+ //UI改变类型
+ var type: AppLiveDataType,
+ //可空参数,页面跳转时不能为空
+ var intent: Intent? = null,
+ //需要回掉时传入
+ var requestCode: Int? = 0x00,
+ //跳转页面时是否关闭当前页面,默认不关闭
+ var isCloseThisActivity: Boolean = false
+)
+
+/**
+ * 控制UI的实体
+ */
+enum class AppLiveDataType {
+ showDialog,
+ dissmissDialog,
+ finish,
+ toNextPage,
+ toNextPageResult,
+}
+
+fun String.d() = Log.d("离线鉴黄App", this)
+fun String.e() = Log.e("离线鉴黄App", this)
+
+/**
+ * [path]文件路径
+ * [nsfwBean]扫描结果
+ * [type]文件类型 0 资源文件 1 相册文件
+ * [times]扫描耗时
+ */
+data class ScoreBean(val path: String, val nsfwBean: MyNSFWBean, val type: Int,val times:Int)
+
+data class MyNSFWBean(val nsfwScore: Float = 0.0f, val sfwScore: Float = 0.0f) {
+ override fun toString(): String {
+ return "NSFW score:${nsfwScore}\nSFW socre:${sfwScore}"
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/model/ApiService.kt b/sample/src/main/java/com/zww/sample/model/ApiService.kt
new file mode 100644
index 00000000..18cd0ce3
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/model/ApiService.kt
@@ -0,0 +1,17 @@
+package com.zww.sample.model
+
+import io.reactivex.Observable
+import okhttp3.ResponseBody
+import retrofit2.http.*
+import java.io.File
+
+/**
+ * 网络请求类
+ */
+interface ApiService {
+
+ @Streaming
+// @GET("/nsfw.tflite")
+ @GET("/nsfw.tflite")
+ fun dowloadNSFWModelFile(): Observable
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/model/AppRepository.kt b/sample/src/main/java/com/zww/sample/model/AppRepository.kt
new file mode 100644
index 00000000..44221d65
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/model/AppRepository.kt
@@ -0,0 +1,135 @@
+package com.zww.sample.model
+
+import android.app.Application
+import android.graphics.BitmapFactory
+import android.os.Environment
+import com.google.gson.Gson
+import com.zww.sample.MyNSFWBean
+import com.zww.sample.ScoreBean
+import com.zww.sample.d
+import com.zww.sample.e
+import com.zwy.opennsfw.core.Classifier
+import getNsfwScore
+import io.reactivex.Observable
+import okhttp3.ResponseBody
+import org.koin.java.KoinJavaComponent.inject
+import java.io.File
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.util.*
+import kotlin.collections.ArrayList
+
+/**
+ * 数据源 这里决定是从缓存获取还是从网络获取
+ */
+class AppRepository constructor(
+ private val mApiService: ApiService,
+ val mGson: Gson
+) :
+ Repository {
+
+ val mContext by inject(Application::class.java)
+
+ val nsfwPath by lazy {
+ "${Environment.getExternalStorageDirectory().absolutePath}/nsfw.tflite"
+ }
+
+ //NSFW是否初始化完成
+ var isSDKInit = false
+
+ override fun dowloadNSFWModelFile(): Observable {
+ return mApiService.dowloadNSFWModelFile()
+ }
+
+ /**
+ * 保存模型文件
+ */
+ override fun saveNSFWModel(body: ResponseBody) {
+ try {
+ //文件大小
+ val contentLength: Long = body.contentLength()
+ //读取文件
+ val inputStream: InputStream = body.byteStream()
+
+ //创建一个文件夹
+ val file = File(Environment.getExternalStorageDirectory(), "nsfw.tflite")
+ val outputStream = FileOutputStream(file)
+ val bytes = ByteArray(1024)
+ var len = 0
+ //循环读取文件的内容,把他放到新的文件目录里面
+ while (inputStream.read(bytes).also({ len = it }) != -1) {
+ outputStream.write(bytes, 0, len)
+ val length = file.length()
+ //获取下载的大小,并把它传给页面
+ val progress = (length * 100 / contentLength).toInt()
+ }
+ "文件保存成功".d()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ "文件保存失败,$e".e()
+ }
+ }
+
+ /**
+ * 模型是否已下载
+ */
+ override fun isFileDownloaded(): Boolean {
+ return File(nsfwPath).exists()
+ }
+
+ /**
+ * 初始化模型
+ */
+ override fun initNSFW() {
+ Classifier.Build().context(mContext).isOpenGPU(true).nsfwModuleFilePath(nsfwPath).build()
+ "NSFW初始化成功".d()
+ isSDKInit = true
+ }
+
+ /**
+ * 扫描文件并返回扫描结果,传入参数为空时扫描资源文件
+ */
+ override fun getNSFWScoreDataList(files: List?): ArrayList {
+ return arrayListOf().also { returnList ->
+ if (files == null) {
+ mContext.resources.assets.also { asset ->
+ asset.list("img")?.forEach {
+ "img/$it".also { assetFilePath ->
+ val startTime = Date().time
+ BitmapFactory.decodeStream(asset.open(assetFilePath)).also {
+ it.getNsfwScore().apply {
+ returnList.add(
+ ScoreBean(
+ assetFilePath,
+ MyNSFWBean(this.nsfw, this.sfw),
+ 0x00,
+ (Date().time - startTime).toInt()
+ )
+ )
+ }
+ it.recycle()
+ }
+
+ }
+ }
+ }
+ } else {
+ files.forEach {
+ val startTime = Date().time
+ it.getNsfwScore().apply {
+ returnList.add(
+ ScoreBean(
+ it.path,
+ MyNSFWBean(this.nsfw, this.sfw),
+ 0x01,
+ (Date().time - startTime).toInt()
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+}
+
diff --git a/sample/src/main/java/com/zww/sample/model/Repository.kt b/sample/src/main/java/com/zww/sample/model/Repository.kt
new file mode 100644
index 00000000..4a68e002
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/model/Repository.kt
@@ -0,0 +1,31 @@
+package com.zww.sample.model
+
+import com.zww.sample.ScoreBean
+import io.reactivex.Observable
+import okhttp3.ResponseBody
+import java.io.File
+
+
+interface Repository {
+ fun dowloadNSFWModelFile(): Observable
+
+ /**
+ * 保存模型文件
+ */
+ fun saveNSFWModel(body: ResponseBody)
+
+ /**
+ * 模型是否已下载
+ */
+ fun isFileDownloaded():Boolean
+
+ /**
+ * 初始化模型
+ */
+ fun initNSFW()
+
+ /**
+ * 扫描文件并返回扫描结果,传入参数为空时扫描资源文件
+ */
+ fun getNSFWScoreDataList(files:List?):ArrayList
+}
\ No newline at end of file
diff --git a/demo/src/main/java/com/zwy/demo/utils/modules.kt b/sample/src/main/java/com/zww/sample/model/modules.kt
similarity index 52%
rename from demo/src/main/java/com/zwy/demo/utils/modules.kt
rename to sample/src/main/java/com/zww/sample/model/modules.kt
index f6813866..e3e4dac0 100644
--- a/demo/src/main/java/com/zwy/demo/utils/modules.kt
+++ b/sample/src/main/java/com/zww/sample/model/modules.kt
@@ -1,13 +1,14 @@
-package com.zwy.demo.utils
+package com.zww.sample.model
import com.google.gson.Gson
-import com.zwy.demo.models.MainViewModel
-import com.zwy.demo.models.ScanPicViewModel
-import com.zwy.demo.repositorys.ApiService
-import com.zwy.demo.repositorys.AppRepository
+import com.zww.sample.BuildConfig
+import com.zww.sample.interceptors.Level
+import com.zww.sample.interceptors.LoggingInterceptor
+import com.zww.sample.ui.viewmodels.MainViewModel
+import com.zww.sample.ui.viewmodels.SplashViewModel
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.internal.platform.Platform
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import retrofit2.Retrofit
@@ -15,50 +16,57 @@ import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
-
val networkModule = module {
+ //OkHttpClient
single {
OkHttpClient().newBuilder()
- .connectTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
- .readTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
- .writeTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(
- HttpLoggingInterceptor()
- .setLevel(HttpLoggingInterceptor.Level.BODY)
+ LoggingInterceptor.Builder()
+ .setLevel(Level.BASIC)
+ .log(Platform.INFO)
+ .tag("离线鉴黄App")
+ .build()
)
.build()
}
-
+ //Retrofit
single {
Retrofit.Builder()
- .baseUrl(BASE_URL)
- .client(get())
+ .baseUrl(BuildConfig.BASE_URL)
+ .client(get())
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.addConverterFactory(GsonConverterFactory.create())
.build()
}
+ //ApiService
single {
get().create(ApiService::class.java)
}
+ //Gson
single {
Gson()
}
-
}
+
val repositoryModule = module {
+
single {
- AppRepository(get(), DBHelper.get(), get())
+ AppRepository(get(), get())
}
}
-
val viewModelModules = module {
viewModel {
- ScanPicViewModel(get())
+ SplashViewModel()
}
viewModel {
- MainViewModel(get())
+ MainViewModel()
}
-}
\ No newline at end of file
+
+
+}
diff --git a/sample/src/main/java/com/zww/sample/ui/SplashActivity.kt b/sample/src/main/java/com/zww/sample/ui/SplashActivity.kt
new file mode 100644
index 00000000..25c3fc00
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/ui/SplashActivity.kt
@@ -0,0 +1,63 @@
+package com.zww.sample.ui
+
+import android.os.Bundle
+import com.blankj.utilcode.util.ToastUtils
+import com.yanzhenjie.permission.AndPermission
+import com.yanzhenjie.permission.runtime.Permission.Group.*
+import com.yanzhenjie.permission.runtime.Permission.READ_PHONE_STATE
+import com.zww.sample.R
+import com.zww.sample.databinding.SplashLayoutBinding
+import com.zww.sample.ui.viewmodels.SplashViewModel
+import shop.gedian.www.base.BaseActivity
+
+
+/**
+ * =====================================================
+ * 创建时间:2020/6/22 13:28
+ * 作者:赵文文
+ * 说明:App闪屏页面
+ * =====================================================
+ */
+class SplashActivity : BaseActivity() {
+
+ /**
+ *
+ * 父类onCreate执行完成后调用
+ */
+ override fun initData() {
+ getPermission()
+ }
+
+ private fun getPermission() {
+ val isHavePermission = AndPermission.hasPermissions(
+ this,
+ STORAGE,
+ CAMERA
+ )
+ if (isHavePermission) {
+ mViewModel.doSomeThing()
+ return
+ }
+
+ //需要获取权限
+ AndPermission.with(this)
+ .runtime()
+ .permission(STORAGE, CAMERA)
+ .onGranted {
+ //同意
+ mViewModel.doSomeThing()
+ }
+ .onDenied {
+ //拒绝
+ ToastUtils.showShort("为了您更好的使用软件,请开启权限后再试")
+ }
+ .start()
+ }
+
+ /**
+ * 初始化根布局
+ *
+ * @return 布局layout的id
+ */
+ override fun initContentView(savedInstanceState: Bundle?): Int = R.layout.activity_splash
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/ui/databindingadapters/kt.kt b/sample/src/main/java/com/zww/sample/ui/databindingadapters/kt.kt
new file mode 100644
index 00000000..0dd2372a
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/ui/databindingadapters/kt.kt
@@ -0,0 +1,12 @@
+package com.zww.sample.ui.databindingadapters
+
+import androidx.databinding.BindingAdapter
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.chad.library.adapter.base.BaseQuickAdapter
+
+@BindingAdapter(requireAll = false, value = arrayOf("bindAdapter"))
+fun RecyclerView.bind(adapter: BaseQuickAdapter<*, *>) {
+ this.adapter = adapter
+ this.layoutManager = LinearLayoutManager(context)
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/ui/viewmodels/MainViewModel.kt b/sample/src/main/java/com/zww/sample/ui/viewmodels/MainViewModel.kt
new file mode 100644
index 00000000..a8409f32
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/ui/viewmodels/MainViewModel.kt
@@ -0,0 +1,69 @@
+package com.zww.sample.ui.viewmodels
+
+import android.graphics.BitmapFactory
+import android.widget.ImageView
+import androidx.lifecycle.MutableLiveData
+import com.blankj.utilcode.util.ToastUtils
+import com.bumptech.glide.Glide
+import com.chad.library.adapter.base.BaseQuickAdapter
+import com.chad.library.adapter.base.viewholder.BaseViewHolder
+import com.zww.sample.R
+import com.zww.sample.ScoreBean
+import shop.gedian.www.base.BaseViewModel
+import java.io.File
+import java.text.DecimalFormat
+
+class MainViewModel : BaseViewModel() {
+ val bName = MutableLiveData("扫描资源文件")
+ val selectPhoto = MutableLiveData(0)
+ val adapter = object : BaseQuickAdapter(R.layout.item_main) {
+ override fun convert(holder: BaseViewHolder, item: ScoreBean) {
+ holder.getView(R.id.iv).also { iv ->
+ if (item.type == 0x00) {
+ BitmapFactory.decodeStream(context.resources.assets.open(item.path)).apply {
+ Glide.with(context).load(this).into(iv)
+ }
+ } else {
+ Glide.with(context).load(item.path).into(iv)
+ }
+ }
+
+ holder.setText(
+ R.id.tvContent,
+ "涉黄程度:${(DecimalFormat("##0.00").format(item.nsfwBean.nsfwScore)).toFloat() * 100}%"
+ )
+ }
+ }.also {
+ it.setOnItemClickListener { adapter, view, position ->
+ ToastUtils.showLong(
+ "${it.getItem(position).nsfwBean.toString()}\n扫描耗时:${it.getItem(
+ position
+ ).times} ms"
+ )
+ }
+ }
+
+ fun changeFrom() {
+ if (!mAppRepository.isSDKInit) {
+ ToastUtils.showShort("SDK未初始化完成,如果为第一次打开请耐心等待下载完成")
+ return
+ }
+ when (bName.value) {
+ "扫描资源文件" -> {
+ adapter.setNewInstance(mAppRepository.getNSFWScoreDataList(null))
+ }
+ "扫描相册" -> {
+ selectPhoto.value = 1
+ }
+ }
+ changeText()
+ }
+
+ fun toScorePhoto(files: List) {
+ adapter.setNewInstance(mAppRepository.getNSFWScoreDataList(files))
+ }
+
+ private fun changeText() {
+ bName.value = if (bName.value.equals("扫描相册")) "扫描资源文件" else "扫描相册"
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/java/com/zww/sample/ui/viewmodels/SplashViewModel.kt b/sample/src/main/java/com/zww/sample/ui/viewmodels/SplashViewModel.kt
new file mode 100644
index 00000000..df17281c
--- /dev/null
+++ b/sample/src/main/java/com/zww/sample/ui/viewmodels/SplashViewModel.kt
@@ -0,0 +1,73 @@
+package com.zww.sample.ui.viewmodels
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import com.blankj.utilcode.util.ToastUtils
+import com.zww.sample.MainActivity
+import com.zww.sample.AppLiveData
+import com.zww.sample.AppLiveDataType
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.ResponseBody
+import shop.gedian.www.base.BaseViewModel
+
+
+/**
+ * =====================================================
+ * 创建时间:2020/6/22 13:30
+ * 作者:赵文文
+ * 说明:
+ * =====================================================
+ */
+class SplashViewModel : BaseViewModel() {
+
+ /**
+ * 休眠后跳转进其他页面
+ */
+ @SuppressLint("CheckResult")
+ fun doSomeThing() {
+ if (mAppRepository.isFileDownloaded()) {
+ Observable.create {
+ mAppRepository.initNSFW()
+ }.subscribeOn(Schedulers.io()).subscribe()
+ } else {
+ downloadModelFile()
+ }
+ toMainPage()
+ }
+
+ private fun downloadModelFile() {
+ mAppRepository.dowloadNSFWModelFile()
+ .subscribeOn(Schedulers.io()).subscribeOn(AndroidSchedulers.mainThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : Observer {
+ override fun onComplete() {
+ mAppRepository.initNSFW()
+ }
+
+ override fun onSubscribe(d: Disposable) {
+
+ }
+
+ override fun onNext(body: ResponseBody) {
+ mAppRepository.saveNSFWModel(body)
+ }
+
+ override fun onError(e: Throwable) {
+ ToastUtils.showLong("模型下载失败,$e")
+ }
+ })
+ }
+
+ private fun toMainPage() {
+ mUILiveData.value =
+ AppLiveData(
+ type = AppLiveDataType.toNextPage,
+ intent = Intent(mContext, MainActivity::class.java),
+ isCloseThisActivity = true
+ )
+ }
+}
\ No newline at end of file
diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..912980c8
--- /dev/null
+++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml
similarity index 99%
rename from app/src/main/res/drawable/ic_launcher_background.xml
rename to sample/src/main/res/drawable/ic_launcher_background.xml
index e2561079..6ae213eb 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/sample/src/main/res/drawable/ic_launcher_background.xml
@@ -5,7 +5,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/activity_splash.xml b/sample/src/main/res/layout/activity_splash.xml
new file mode 100644
index 00000000..807c82c7
--- /dev/null
+++ b/sample/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/layout/item_main.xml b/sample/src/main/res/layout/item_main.xml
new file mode 100644
index 00000000..a1536435
--- /dev/null
+++ b/sample/src/main/res/layout/item_main.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a571e600
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..61da551c
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c41dd285
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..db5080a7
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..6dba46da
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..da31a871
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..15ac6817
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b216f2d3
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f25a4197
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e96783cc
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml
new file mode 100644
index 00000000..8b606e40
--- /dev/null
+++ b/sample/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+ #00000000
+ #000000
+ #3F51B5
+ #ECECEC
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3c586cb1
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ 离线鉴黄
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml
new file mode 100644
index 00000000..6d65b125
--- /dev/null
+++ b/sample/src/main/res/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/sample/src/main/res/xml/network_security_config.xml
similarity index 100%
rename from app/src/main/res/xml/network_security_config.xml
rename to sample/src/main/res/xml/network_security_config.xml
diff --git a/demo/src/test/java/com/zwy/demo/ExampleUnitTest.kt b/sample/src/test/java/com/zww/sample/ExampleUnitTest.kt
similarity index 82%
rename from demo/src/test/java/com/zwy/demo/ExampleUnitTest.kt
rename to sample/src/test/java/com/zww/sample/ExampleUnitTest.kt
index 336c332e..f912f63c 100644
--- a/demo/src/test/java/com/zwy/demo/ExampleUnitTest.kt
+++ b/sample/src/test/java/com/zww/sample/ExampleUnitTest.kt
@@ -1,8 +1,9 @@
-package com.zwy.demo
+package com.zww.sample
-import org.junit.Assert.assertEquals
import org.junit.Test
+import org.junit.Assert.*
+
/**
* Example local unit test, which will execute on the development machine (host).
*
diff --git a/settings.gradle b/settings.gradle
index c364d2f2..20fc9e9d 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
-include ':demo' //开启新demo必须注释 include ':app' 并打开gradle.properties中 注释的两行代码 因为新demo使用了androidx与app模块冲突 demo模块完成:Assets目录识别、网络图片识别
+include ':sample'
+//include ':demo' //开启新demo必须注释 include ':app' 并打开gradle.properties中 注释的两行代码 因为新demo使用了androidx与app模块冲突 demo模块完成:Assets目录识别、网络图片识别
//include ':app'
include ':OpenNSFW'