@ -0,0 +1,49 @@ |
||||
apply plugin: 'com.android.application' |
||||
apply plugin: 'kotlin-android' |
||||
apply plugin: 'kotlin-android-extensions' |
||||
apply plugin: 'kotlin-kapt' |
||||
android { |
||||
compileSdkVersion 30 |
||||
buildToolsVersion "30.0.2" |
||||
|
||||
defaultConfig { |
||||
applicationId "com.zww.sample" |
||||
minSdkVersion 21 |
||||
targetSdkVersion 30 |
||||
versionCode 136 |
||||
versionName "1.3.6" |
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
||||
} |
||||
|
||||
buildTypes { |
||||
release { |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
debug { |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
} |
||||
|
||||
aaptOptions { |
||||
noCompress "tflite" |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation fileTree(dir: "libs", include: ["*.jar"]) |
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |
||||
implementation 'androidx.core:core-ktx:1.3.1' |
||||
implementation 'androidx.appcompat:appcompat:1.2.0' |
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' |
||||
implementation project(path: ':initializer') |
||||
implementation project(path: ':nsfw') |
||||
testImplementation 'junit:junit:4.12' |
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2' |
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' |
||||
|
||||
//权限 |
||||
implementation 'com.yanzhenjie:permission:2.0.3' |
||||
} |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -0,0 +1,27 @@ |
||||
package com.zww.sample |
||||
|
||||
import android.graphics.BitmapFactory |
||||
import android.os.Bundle |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import com.zwy.nsfw.getNSFWScore |
||||
import java.util.* |
||||
|
||||
class MainActivity : AppCompatActivity() { |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_main) |
||||
|
||||
resources.assets.also { asset -> |
||||
asset.list("img")?.forEach { |
||||
"img/$it".also { assetFilePath -> |
||||
val startTime = Date().time |
||||
BitmapFactory.decodeStream(asset.open(assetFilePath)).also { |
||||
it.getNSFWScore() |
||||
it.recycle() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical"> |
||||
</LinearLayout> |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -0,0 +1 @@ |
||||
/build |
@ -0,0 +1,53 @@ |
||||
apply plugin: 'com.android.library' |
||||
apply plugin: 'kotlin-android' |
||||
apply plugin: 'kotlin-android-extensions' |
||||
apply plugin: "guru.stefma.bintrayrelease" |
||||
android { |
||||
compileSdkVersion 30 |
||||
buildToolsVersion "30.0.2" |
||||
|
||||
defaultConfig { |
||||
minSdkVersion 21 |
||||
targetSdkVersion 30 |
||||
versionCode 136 |
||||
versionName "1.3.6" |
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
||||
consumerProguardFiles "consumer-rules.pro" |
||||
} |
||||
|
||||
buildTypes { |
||||
release { |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation fileTree(dir: "libs", include: ["*.jar"]) |
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |
||||
implementation 'androidx.core:core-ktx:1.3.2' |
||||
implementation 'androidx.appcompat:appcompat:1.2.0' |
||||
api project(path: ':nsfw') |
||||
testImplementation 'junit:junit:4.12' |
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2' |
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' |
||||
|
||||
} |
||||
|
||||
version = "1.3.6" |
||||
group = "com.zwy.nsfw_initializer" |
||||
publish { |
||||
userOrg = 'devzwy' |
||||
repoName = 'maven' |
||||
artifactId = 'com.zwy.nsfw_initializer' |
||||
desc = 'android端离线鉴黄库快速初始化库' |
||||
website = 'https://github.com/devzwy' |
||||
} |
||||
|
||||
tasks.withType(Javadoc) {//防止编码问题 |
||||
options.addStringOption('Xdoclint:none', '-quiet') |
||||
options.addStringOption('encoding', 'UTF-8') |
||||
options.addStringOption('charSet', 'UTF-8') |
||||
} |
@ -0,0 +1,21 @@ |
||||
# Add project specific ProGuard rules here. |
||||
# You can control the set of applied configuration files using the |
||||
# proguardFiles setting in build.gradle. |
||||
# |
||||
# For more details, see |
||||
# http://developer.android.com/guide/developing/tools/proguard.html |
||||
|
||||
# If your project uses WebView with JS, uncomment the following |
||||
# and specify the fully qualified class name to the JavaScript interface |
||||
# class: |
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
||||
# public *; |
||||
#} |
||||
|
||||
# Uncomment this to preserve the line number information for |
||||
# debugging stack traces. |
||||
#-keepattributes SourceFile,LineNumberTable |
||||
|
||||
# If you keep the line number information, uncomment this to |
||||
# hide the original source file name. |
||||
#-renamesourcefileattribute SourceFile |
@ -0,0 +1,24 @@ |
||||
package com.zwy.nsfw_initializer |
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
|
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Instrumented test, which will execute on an Android device. |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
@RunWith(AndroidJUnit4::class) |
||||
class ExampleInstrumentedTest { |
||||
@Test |
||||
fun useAppContext() { |
||||
// Context of the app under test. |
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext |
||||
assertEquals("zwy.nsfw.initializer.test", appContext.packageName) |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.zwy.nsfw_initializer"> |
||||
|
||||
<application> |
||||
<provider |
||||
android:name="com.zwy.nsfw_initializer.NSFWInitProvider" |
||||
android:authorities="${applicationId}.nsfwinitprovider" |
||||
android:enabled="true" |
||||
android:exported="false" /> |
||||
</application> |
||||
</manifest> |
@ -0,0 +1,45 @@ |
||||
package com.zwy.nsfw_initializer |
||||
|
||||
import android.content.ContentProvider |
||||
import android.content.ContentValues |
||||
import android.database.Cursor |
||||
import android.net.Uri |
||||
import com.zwy.nsfw.NSFWHelper |
||||
|
||||
class NSFWInitProvider : ContentProvider() { |
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? { |
||||
return null |
||||
} |
||||
|
||||
override fun query( |
||||
uri: Uri, |
||||
projection: Array<out String>?, |
||||
selection: String?, |
||||
selectionArgs: Array<out String>?, |
||||
sortOrder: String? |
||||
): Cursor? { |
||||
return null |
||||
} |
||||
|
||||
override fun onCreate(): Boolean { |
||||
NSFWHelper.init(requireNotNull(context),null) |
||||
return true |
||||
} |
||||
|
||||
override fun update( |
||||
uri: Uri, |
||||
values: ContentValues?, |
||||
selection: String?, |
||||
selectionArgs: Array<out String>? |
||||
): Int { |
||||
return 0 |
||||
} |
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int { |
||||
return 0 |
||||
} |
||||
|
||||
override fun getType(uri: Uri): String? { |
||||
return null |
||||
} |
||||
} |
@ -0,0 +1,17 @@ |
||||
package com.zwy.nsfw_initializer |
||||
|
||||
import org.junit.Test |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Example local unit test, which will execute on the development machine (host). |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
class ExampleUnitTest { |
||||
@Test |
||||
fun addition_isCorrect() { |
||||
assertEquals(4, 2 + 2) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
/build |
@ -0,0 +1,54 @@ |
||||
apply plugin: 'com.android.library' |
||||
apply plugin: 'kotlin-android' |
||||
apply plugin: 'kotlin-android-extensions' |
||||
apply plugin: "guru.stefma.bintrayrelease" |
||||
android { |
||||
compileSdkVersion 30 |
||||
buildToolsVersion "30.0.2" |
||||
|
||||
defaultConfig { |
||||
minSdkVersion 21 |
||||
targetSdkVersion 30 |
||||
versionCode 136 |
||||
versionName "1.3.6" |
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
||||
consumerProguardFiles "consumer-rules.pro" |
||||
} |
||||
|
||||
buildTypes { |
||||
release { |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation fileTree(dir: "libs", include: ["*.jar"]) |
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |
||||
implementation 'androidx.core:core-ktx:1.3.2' |
||||
implementation 'androidx.appcompat:appcompat:1.2.0' |
||||
testImplementation 'junit:junit:4.12' |
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2' |
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' |
||||
|
||||
implementation 'org.tensorflow:tensorflow-lite:2.1.0' |
||||
implementation 'org.tensorflow:tensorflow-lite-gpu:2.1.0' |
||||
} |
||||
|
||||
version = "1.3.6" |
||||
group = "com.zwy.nsfw" |
||||
publish { |
||||
userOrg = 'devzwy' |
||||
repoName = 'maven' |
||||
artifactId = 'com.zwy.nsfw' |
||||
desc = 'android端离线鉴黄库' |
||||
website = 'https://github.com/devzwy' |
||||
} |
||||
|
||||
tasks.withType(Javadoc) {//防止编码问题 |
||||
options.addStringOption('Xdoclint:none', '-quiet') |
||||
options.addStringOption('encoding', 'UTF-8') |
||||
options.addStringOption('charSet', 'UTF-8') |
||||
} |
@ -0,0 +1,21 @@ |
||||
# Add project specific ProGuard rules here. |
||||
# You can control the set of applied configuration files using the |
||||
# proguardFiles setting in build.gradle. |
||||
# |
||||
# For more details, see |
||||
# http://developer.android.com/guide/developing/tools/proguard.html |
||||
|
||||
# If your project uses WebView with JS, uncomment the following |
||||
# and specify the fully qualified class name to the JavaScript interface |
||||
# class: |
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
||||
# public *; |
||||
#} |
||||
|
||||
# Uncomment this to preserve the line number information for |
||||
# debugging stack traces. |
||||
#-keepattributes SourceFile,LineNumberTable |
||||
|
||||
# If you keep the line number information, uncomment this to |
||||
# hide the original source file name. |
||||
#-renamesourcefileattribute SourceFile |
@ -0,0 +1,24 @@ |
||||
package com.zwy.nsfw |
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
|
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Instrumented test, which will execute on an Android device. |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
@RunWith(AndroidJUnit4::class) |
||||
class ExampleInstrumentedTest { |
||||
@Test |
||||
fun useAppContext() { |
||||
// Context of the app under test. |
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext |
||||
assertEquals("com.zwy.nsfw.test", appContext.packageName) |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
package="com.zwy.nsfw"> |
||||
</manifest> |
@ -0,0 +1,191 @@ |
||||
package com.zwy.nsfw |
||||
|
||||
import android.content.Context |
||||
import android.content.pm.PackageManager |
||||
import android.content.res.Resources |
||||
import android.graphics.Bitmap |
||||
import android.graphics.Color |
||||
import android.os.SystemClock |
||||
import org.tensorflow.lite.Interpreter |
||||
import org.tensorflow.lite.gpu.GpuDelegate |
||||
import java.io.ByteArrayOutputStream |
||||
import java.io.File |
||||
import java.io.FileInputStream |
||||
import java.io.FileNotFoundException |
||||
import java.nio.ByteBuffer |
||||
import java.nio.ByteOrder |
||||
import java.nio.MappedByteBuffer |
||||
import java.nio.channels.FileChannel |
||||
import java.security.AccessControlContext |
||||
|
||||
object NSFWHelper { |
||||
|
||||
private lateinit var mInterpreter: Interpreter |
||||
|
||||
private lateinit var imgData: ByteBuffer |
||||
|
||||
/** |
||||
* 数据宽高 |
||||
*/ |
||||
private val INPUT_WIDTH = 224 |
||||
|
||||
/** |
||||
* 数据宽高 |
||||
*/ |
||||
private val INPUT_HEIGHT = 224 |
||||
|
||||
/** |
||||
* 手动初始化时必须传入模型文件的路径否则会有异常抛出 |
||||
* 自动初始化需要将模型存放在资源文件assets根目录下,且名称必须为:'nsfw.tflite',不可改动 |
||||
*/ |
||||
fun init(context: Context? = null, modelPath: String?) { |
||||
if (modelPath != null && modelPath.isNotEmpty()) { |
||||
log("手动初始化...") |
||||
File(modelPath).let { modelFile -> |
||||
if (!modelFile.exists()) throw FileNotFoundException("模型文件路径配置错误,请重新配置,如果确定路径正确请检测文件权限是否申请") |
||||
mInterpreter = try { |
||||
initInterpreterByFile(modelFile, true) |
||||
} catch (e: Exception) { |
||||
initInterpreterByFile(modelFile) |
||||
} |
||||
} |
||||
} else { |
||||
loadModelFile(context!!).let { |
||||
if (it == null) throw Resources.NotFoundException("资源文件下未找到模型文件,请检测模型是否在assets目录下,名称是否为nsfw.tflite") |
||||
mInterpreter = try { |
||||
initInterpreterByMappedByteBuffer(it, true).also { |
||||
log("NSFW自动初始化成功,已开启GPU加速") |
||||
} |
||||
} catch (e: Exception) { |
||||
initInterpreterByMappedByteBuffer(it).also { |
||||
log("NSFW自动初始化成功,未开启GPU加速") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
imgData = ByteBuffer.allocateDirect(1 * INPUT_WIDTH * INPUT_HEIGHT * 3 * 4) |
||||
imgData.order(ByteOrder.LITTLE_ENDIAN) |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 关闭日志输出 |
||||
*/ |
||||
fun disEnableLog() { |
||||
isOpenLog = false |
||||
} |
||||
|
||||
/** |
||||
* 装载扫描数据 |
||||
*/ |
||||
private fun convertBitmapToByteBuffer(bitmap_: Bitmap): Long { |
||||
SystemClock.uptimeMillis().let { startTime -> |
||||
imgData.rewind() |
||||
IntArray(INPUT_WIDTH * INPUT_HEIGHT).let { |
||||
//把每个像素的颜色值转为int 存入intValues |
||||
bitmap_.getPixels( |
||||
it, |
||||
0, |
||||
INPUT_WIDTH, |
||||
Math.max((bitmap_.height - INPUT_HEIGHT) / 2, 0), |
||||
Math.max((bitmap_.width - INPUT_WIDTH) / 2, 0), |
||||
INPUT_WIDTH, |
||||
INPUT_HEIGHT |
||||
) |
||||
for (color in it) { |
||||
imgData.putFloat((Color.blue(color) - 104).toFloat()) |
||||
imgData.putFloat((Color.green(color) - 117).toFloat()) |
||||
imgData.putFloat((Color.red(color) - 123).toFloat()) |
||||
} |
||||
} |
||||
return SystemClock.uptimeMillis() - startTime |
||||
} |
||||
} |
||||
|
||||
// # 根据路径获取图片 Image.open(path) |
||||
// # 判断图片的像素格式是否为RGB,如果不是RGB则转换为RGB(24位彩色图像,每个像素用24个bit表示,分别表示红色、绿色和蓝色三个通道) |
||||
// # 重制图片大小为256*256并使用 官方解释:若要调整大小,请对所有可能影响输出值的像素使用线性插值计算输出像素值。对于其他变换,使用输入图像中2x2环境上的线性插值。 |
||||
// # 对resize的结果转换为io流并保存为JPEG格式 |
||||
// # Convert to 64-bit floating point.asType float32 定义变量存储转换后的32位float图片数据 |
||||
// # 获取图片的宽高,截取 224*224大小 x从16位开始,取到16+224位置 y亦是如此 |
||||
// # 将取的数值转换位float32 |
||||
// # 将每一个颜色值*255 |
||||
// # 将每一个颜色减去一定的阈值 104.... |
||||
// # [[[127.64.-18]]] 转换为 [[[[127.64.-18]]]] |
||||
// # 使用index关键字喂入模型 |
||||
// # 删除所有单维度的条目 |
||||
// # 输出扫描结果 |
||||
fun getNSFWScore(bitmap: Bitmap): NSFWScoreBean { |
||||
SystemClock.uptimeMillis().let { startTime -> |
||||
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要 |
||||
ByteArrayOutputStream().let { stream -> |
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) |
||||
stream.close() |
||||
convertBitmapToByteBuffer( |
||||
Bitmap.createScaledBitmap( |
||||
bitmap, |
||||
256, |
||||
256, |
||||
true |
||||
) |
||||
).let { timeConsumingToLoadData -> |
||||
// out |
||||
Array(1) { FloatArray(2) }.apply { |
||||
mInterpreter.run(imgData, this) |
||||
return NSFWScoreBean( |
||||
this[0][1], |
||||
this[0][0], |
||||
timeConsumingToLoadData, |
||||
SystemClock.uptimeMillis() - startTime |
||||
).also { |
||||
log("扫描完成 -> $it") |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
private fun initInterpreterByFile(modelFile: File, isOpenGPU: Boolean = false): Interpreter { |
||||
return Interpreter( |
||||
modelFile, |
||||
Interpreter.Options().also { |
||||
it.setNumThreads(4) |
||||
if (isOpenGPU) it.addDelegate(GpuDelegate()) |
||||
it.setAllowBufferHandleOutput(true) |
||||
it.setAllowFp16PrecisionForFp32(true) |
||||
}) |
||||
} |
||||
|
||||
@Throws(Exception::class) |
||||
private fun initInterpreterByMappedByteBuffer( |
||||
modelMappedByteBuffer: MappedByteBuffer, |
||||
isOpenGPU: Boolean = false |
||||
): Interpreter { |
||||
return Interpreter( |
||||
modelMappedByteBuffer, |
||||
Interpreter.Options().also { |
||||
it.setNumThreads(4) |
||||
if (isOpenGPU) it.addDelegate(GpuDelegate()) |
||||
it.setAllowBufferHandleOutput(true) |
||||
it.setAllowFp16PrecisionForFp32(true) |
||||
}) |
||||
} |
||||
|
||||
private fun loadModelFile(context: Context): MappedByteBuffer? { |
||||
try { |
||||
context.assets.openFd("nsfw.tflite").let { fileDescriptor -> |
||||
return FileInputStream(fileDescriptor.fileDescriptor).channel.map( |
||||
FileChannel.MapMode.READ_ONLY, |
||||
fileDescriptor.startOffset, |
||||
fileDescriptor.declaredLength |
||||
) |
||||
} |
||||
} catch (e: Exception) { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,43 @@ |
||||
package com.zwy.nsfw |
||||
|
||||
import android.graphics.Bitmap |
||||
import android.graphics.BitmapFactory |
||||
import android.util.Log |
||||
import java.io.File |
||||
|
||||
@JvmField |
||||
var isOpenLog = true |
||||
|
||||
fun Any.log(content: String) { |
||||
if (isOpenLog) |
||||
Log.d("NSFW", content) |
||||
} |
||||
|
||||
fun Any.logE(content: String) { |
||||
if (isOpenLog) |
||||
Log.e("NSFW", content) |
||||
} |
||||
|
||||
/** |
||||
* [nsfwScore]不适宜度 |
||||
* [sfwScore]适宜度 |
||||
* [TimeConsumingToLoadData]装载数据耗时 |
||||
* [TimeConsumingToScanData]扫描数据耗时 |
||||
*/ |
||||
data class NSFWScoreBean( |
||||
val nsfwScore: Float, |
||||
val sfwScore: Float, |
||||
val timeConsumingToLoadData: Long, |
||||
val timeConsumingToScanData: Long |
||||
) { |
||||
override fun toString(): String { |
||||
return "nsfwScore:$nsfwScore, sfwScore:$sfwScore, TimeConsumingToLoadData:$timeConsumingToLoadData ms, TimeConsumingToScanData=$timeConsumingToScanData ms)" |
||||
} |
||||
} |
||||
|
||||
fun Bitmap.getNSFWScore() = NSFWHelper.getNSFWScore(this) |
||||
fun File.getNSFWScore() = try { |
||||
BitmapFactory.decodeFile(this.path) |
||||
} catch (e: Exception) { |
||||
throw RuntimeException("File2Bitmap过程失败,请确认文件是否存在或是否为图片") |
||||
}.getNSFWScore() |
@ -0,0 +1,17 @@ |
||||
package com.zwy.nsfw |
||||
|
||||
import org.junit.Test |
||||
|
||||
import org.junit.Assert.* |
||||
|
||||
/** |
||||
* Example local unit test, which will execute on the development machine (host). |
||||
* |
||||
* See [testing documentation](http://d.android.com/tools/testing). |
||||
*/ |
||||
class ExampleUnitTest { |
||||
@Test |
||||
fun addition_isCorrect() { |
||||
assertEquals(4, 2 + 2) |
||||
} |
||||
} |
@ -1,85 +0,0 @@ |
||||
apply plugin: 'com.android.application' |
||||
apply plugin: 'kotlin-android' |
||||
apply plugin: 'kotlin-android-extensions' |
||||
apply plugin: 'kotlin-kapt' |
||||
android { |
||||
compileSdkVersion 30 |
||||
buildToolsVersion "30.0.2" |
||||
|
||||
defaultConfig { |
||||
applicationId "com.zww.sample" |
||||
minSdkVersion 21 |
||||
targetSdkVersion 30 |
||||
versionCode 2 |
||||
versionName "1.3.4" |
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
||||
} |
||||
|
||||
buildTypes { |
||||
release { |
||||
buildConfigField("String", "BASE_URL", '"https://www.jason1024.cn"') |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
debug { |
||||
buildConfigField("String", "BASE_URL", '"https://www.jason1024.cn"') |
||||
minifyEnabled false |
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
} |
||||
} |
||||
|
||||
|
||||
buildFeatures { |
||||
dataBinding = true |
||||
} |
||||
|
||||
|
||||
compileOptions { |
||||
sourceCompatibility JavaVersion.VERSION_1_8 |
||||
targetCompatibility JavaVersion.VERSION_1_8 |
||||
} |
||||
} |
||||
|
||||
dependencies { |
||||
implementation fileTree(dir: "libs", include: ["*.jar"]) |
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" |
||||
implementation 'androidx.core:core-ktx:1.3.1' |
||||
implementation 'androidx.appcompat:appcompat:1.2.0' |
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1' |
||||
testImplementation 'junit:junit:4.12' |
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2' |
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' |
||||
|
||||
//MVVM设计模式 |
||||
implementation "org.koin:koin-android:2.1.5" |
||||
implementation "org.koin:koin-androidx-viewmodel:2.1.5" |
||||
implementation "org.koin:koin-androidx-ext:2.1.5" |
||||
|
||||
//网络请求 |
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1" |
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.2' |
||||
implementation 'com.squareup.retrofit2:converter-gson:2.7.2' |
||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.2' |
||||
implementation "com.squareup.okhttp3:logging-interceptor:4.7.2" |
||||
implementation 'org.conscrypt:conscrypt-android:2.2.1' |
||||
//沉浸式状态栏 |
||||
implementation 'com.gyf.immersionbar:immersionbar:3.0.0' |
||||
implementation 'com.gyf.immersionbar:immersionbar-components:3.0.0' |
||||
//适配器快速实现 |
||||
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4' |
||||
//图片加载框架 |
||||
implementation 'com.github.bumptech.glide:glide:4.11.0' |
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0' |
||||
|
||||
//权限 |
||||
implementation 'com.yanzhenjie:permission:2.0.3' |
||||
|
||||
implementation 'com.blankj:utilcodex:1.29.0' |
||||
|
||||
implementation 'com.kongzue.dialog_v3:dialog:3.2.0' |
||||
|
||||
implementation 'com.github.devzwy:open_nsfw_android:1.3.4' |
||||
|
||||
implementation 'com.github.LuckSiege.PictureSelector:picture_library:2.2.5' |
||||
} |
@ -1,70 +0,0 @@ |
||||
package com.zww.sample |
||||
|
||||
import android.app.Application |
||||
import com.blankj.utilcode.util.Utils |
||||
import com.kongzue.dialog.util.DialogSettings |
||||
import com.zww.sample.model.networkModule |
||||
import com.zww.sample.model.repositoryModule |
||||
import com.zww.sample.model.viewModelModules |
||||
import org.koin.android.ext.koin.androidContext |
||||
import org.koin.android.ext.koin.androidLogger |
||||
import org.koin.core.context.startKoin |
||||
|
||||
class BaseApplication : Application() { |
||||
override fun onCreate() { |
||||
super.onCreate() |
||||
init() |
||||
} |
||||
|
||||
private fun init() { |
||||
Utils.init(this) |
||||
|
||||
//全局注入对象 |
||||
startKoin { |
||||
androidContext(this@BaseApplication) |
||||
androidLogger() |
||||
modules(networkModule) |
||||
modules(repositoryModule) |
||||
modules(viewModelModules) |
||||
} |
||||
|
||||
initDialogStyle() |
||||
} |
||||
|
||||
|
||||
private fun initDialogStyle() { |
||||
|
||||
// DialogSettings.isUseBlur = (boolean); //是否开启模糊效果,默认关闭 |
||||
// DialogSettings.modalDialog = (boolean); //是否开启模态窗口模式,一次显示多个对话框将以队列形式一个一个显示,默认关闭 |
||||
DialogSettings.style = |
||||
DialogSettings.STYLE.STYLE_IOS //全局主题风格,提供三种可选风格,STYLE_MATERIAL, STYLE_KONGZUE, STYLE_IOS |
||||
DialogSettings.theme = DialogSettings.THEME.LIGHT //全局对话框明暗风格,提供两种可选主题,LIGHT, DARK |
||||
DialogSettings.tipTheme = DialogSettings.THEME.LIGHT //全局提示框明暗风格,提供两种可选主题,LIGHT, DARK |
||||
// DialogSettings.titleTextInfo = new TextInfo(""); //全局对话框标题文字样式 |
||||
// DialogSettings.menuTitleInfo = (TextInfo); //全局菜单标题文字样式 |
||||
// DialogSettings.menuTextInfo = (TextInfo); //全局菜单列表文字样式 |
||||
// DialogSettings.contentTextInfo = (TextInfo); //全局正文文字样式 |
||||
// DialogSettings.buttonTextInfo = (TextInfo); //全局默认按钮文字样式 |
||||
// DialogSettings.buttonPositiveTextInfo = (TextInfo); //全局焦点按钮文字样式(一般指确定按钮) |
||||
// DialogSettings.inputInfo = (InputInfo); //全局输入框文本样式 |
||||
// DialogSettings.backgroundColor = (ColorInt); //全局对话框背景颜色,值0时不生效 |
||||
DialogSettings.cancelable = |
||||
true //全局对话框默认是否可以点击外围遮罩区域或返回键关闭,此开关不影响提示框(TipDialog)以及等待框(TipDialog) |
||||
DialogSettings.cancelableTipDialog = true //全局提示框及等待框(WaitDialog、TipDialog)默认是否可以关闭 |
||||
DialogSettings.DEBUGMODE = true //是否允许打印日志 |
||||
// DialogSettings.blurAlpha = (int); //开启模糊后的透明度(0~255) |
||||
// DialogSettings.systemDialogStyle = (styleResId); //自定义系统对话框style,注意设置此功能会导致原对话框风格和动画失效 |
||||
// DialogSettings.dialogLifeCycleListener = (DialogLifeCycleListener); //全局Dialog生命周期监听器 |
||||
// DialogSettings.defaultCancelButtonText = (String); //设置 BottomMenu 和 ShareDialog 默认“取消”按钮的文字 |
||||
// DialogSettings.tipBackgroundResId = (drawableResId); //设置 TipDialog 和 WaitDialog 的背景资源 |
||||
// DialogSettings.tipTextInfo = (InputInfo); //设置 TipDialog 和 WaitDialog 文字样式 |
||||
DialogSettings.autoShowInputKeyboard = false //设置 InputDialog 是否自动弹出输入法 |
||||
// DialogSettings.okButtonDrawable = (drawable); //设置确定按钮背景资源 |
||||
// DialogSettings.cancelButtonDrawable = (drawable); //设置取消按钮背景资源 |
||||
// DialogSettings.otherButtonDrawable = (drawable); //设置其他按钮背景资源 |
||||
|
||||
//检查 Renderscript 兼容性,若设备不支持,DialogSettings.isUseBlur 会自动关闭; |
||||
// boolean renderscriptSupport = DialogSettings.checkRenderscriptSupport(this); |
||||
DialogSettings.init() //初始化清空 BaseDialog 队列 |
||||
} |
||||
} |
@ -1,88 +0,0 @@ |
||||
package com.zww.sample |
||||
|
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import com.kongzue.dialog.v3.MessageDialog |
||||
import com.luck.picture.lib.PictureSelector |
||||
import com.luck.picture.lib.config.PictureConfig |
||||
import com.luck.picture.lib.config.PictureMimeType |
||||
import com.zww.sample.databinding.MainLayoutBinding |
||||
import com.zww.sample.ui.viewmodels.MainViewModel |
||||
import shop.gedian.www.base.BaseActivity |
||||
import java.io.File |
||||
import java.util.* |
||||
|
||||
class MainActivity : BaseActivity<MainLayoutBinding, MainViewModel>() { |
||||
/** |
||||
* |
||||
* 父类onCreate执行完成后调用 |
||||
*/ |
||||
override fun initData() { |
||||
mBinding.model = mViewModel |
||||
mViewModel.selectPhoto.observe(this, androidx.lifecycle.Observer<Int> |
||||
{ |
||||
if (it == 0x01) { |
||||
c() |
||||
} |
||||
}) |
||||
mViewModel.dialogShow.observe(this, androidx.lifecycle.Observer { |
||||
if (it){ |
||||
MessageDialog.build(this).also { |
||||
it.message = "后台正在下载模型文件,大约耗时1分钟,请等待完成。(简化demo未做进度条处理,完成后会有toast提示)" |
||||
it.title = "提示" |
||||
it.cancelable = false |
||||
it.okButton = "好的" |
||||
it.show() |
||||
} |
||||
}else{ |
||||
MessageDialog.build(this).also { |
||||
it.message = "模型已下载,等待模型初始化完成的toast后可点击右上角识别按钮" |
||||
it.title = "提示" |
||||
it.cancelable = false |
||||
it.okButton = "好的" |
||||
it.show() |
||||
} |
||||
} |
||||
}) |
||||
mViewModel.isShowDialog() |
||||
} |
||||
|
||||
override fun isInitImmersionBar() = true |
||||
|
||||
/** |
||||
* 初始化根布局 |
||||
* |
||||
* @return 布局layout的id |
||||
*/ |
||||
override fun initContentView(savedInstanceState: Bundle?): Int { |
||||
return R.layout.activity_main |
||||
} |
||||
|
||||
fun c() { |
||||
PictureSelector.create(this) |
||||
.openGallery(PictureMimeType.ofImage())//全部.ofAll()、图片.、视频.ofVideo()、音频.ofAudio() |
||||
.maxSelectNum(20)// 最大图片选择数量 int |
||||
.minSelectNum(1)// 最小选择数量 int |
||||
.imageSpanCount(3)// 每行显示个数 int |
||||
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 or PictureConfig.SINGLE |
||||
.previewImage(true)// 是否可预览图片 true or false |
||||
.isCamera(false)// 是否显示拍照按钮 true or false |
||||
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true |
||||
// .selectionMedia(selectList) |
||||
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效 |
||||
.previewEggs(true)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false |
||||
.forResult(0x00);//结果回调onActivityResult code } |
||||
} |
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
||||
super.onActivityResult(requestCode, resultCode, data) |
||||
if (data != null) { |
||||
mViewModel.toScorePhoto(arrayListOf<File>().also { |
||||
PictureSelector.obtainMultipleResult(data).forEach { localMedia -> |
||||
it.add(File(localMedia.path)) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
} |
@ -1,175 +0,0 @@ |
||||
package shop.gedian.www.base |
||||
|
||||
import android.os.Bundle |
||||
import android.view.MotionEvent |
||||
import android.view.View |
||||
import android.widget.EditText |
||||
import androidx.appcompat.app.AppCompatActivity |
||||
import androidx.databinding.DataBindingUtil |
||||
import androidx.databinding.ViewDataBinding |
||||
import androidx.lifecycle.Observer |
||||
import com.blankj.utilcode.util.KeyboardUtils |
||||
import com.gyf.immersionbar.ImmersionBar |
||||
import com.kongzue.dialog.interfaces.OnBackClickListener |
||||
import com.kongzue.dialog.v3.TipDialog |
||||
import com.kongzue.dialog.v3.WaitDialog |
||||
import com.zww.sample.R |
||||
import com.zww.sample.AppLiveDataType |
||||
import org.koin.java.KoinJavaComponent.get |
||||
import java.lang.reflect.ParameterizedType |
||||
import java.lang.reflect.Type |
||||
|
||||
|
||||
abstract class BaseActivity<V : ViewDataBinding, VM : BaseViewModel> : AppCompatActivity(), |
||||
OnBackClickListener { |
||||
|
||||
lateinit var mBinding: V |
||||
lateinit var mViewModel: VM |
||||
val dialog: TipDialog by lazy { |
||||
|
||||
WaitDialog.build(this).also { |
||||
it.message = "努力加载中..." |
||||
it.cancelable = true |
||||
it.cancelable = false |
||||
it.onBackClickListener = this |
||||
} |
||||
} |
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
initViewDataBinding(savedInstanceState) |
||||
registorUIChangeLiveDataCallBack() |
||||
if (isInitImmersionBar()) initTitleBar() |
||||
initData() |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 是否需要初始化沉浸式状态栏 默认初始化,如果不需要初始化时子类复写该函数 |
||||
*/ |
||||
open fun isInitImmersionBar() = true |
||||
|
||||
|
||||
private fun initTitleBar() { |
||||
ImmersionBar.with(this) |
||||
.fitsSystemWindows(true) |
||||
.statusBarColor(R.color.colorPrimary).init() |
||||
} |
||||
|
||||
/** |
||||
* 点击空白区域关闭输入框 |
||||
*/ |
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { |
||||
if (ev.action == MotionEvent.ACTION_DOWN) { |
||||
val v = currentFocus |
||||
if (isShouldHideKeyboard(v, ev)) { |
||||
KeyboardUtils.hideSoftInput(this) |
||||
} |
||||
} |
||||
return super.dispatchTouchEvent(ev) |
||||
} |
||||
|
||||
/** |
||||
* 点击空白区域关闭输入框 |
||||
*/ |
||||
private fun isShouldHideKeyboard( |
||||
v: View?, |
||||
event: MotionEvent |
||||
): Boolean { |
||||
if (v is EditText) { |
||||
val l = intArrayOf(0, 0) |
||||
v.getLocationOnScreen(l) |
||||
val left = l[0] |
||||
val top = l[1] |
||||
val bottom = top + v.getHeight() |
||||
val right = left + v.getWidth() |
||||
return !(event.rawX > left && event.rawX < right && event.rawY > top && event.rawY < bottom) |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/** |
||||
* 隐藏虚拟按键 |
||||
*/ |
||||
fun hideNavtionBar() { |
||||
window.decorView.systemUiVisibility = |
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE |
||||
} |
||||
|
||||
|
||||
/** |
||||
* 注入绑定 |
||||
*/ |
||||
private fun initViewDataBinding(savedInstanceState: Bundle?) { |
||||
mBinding = DataBindingUtil.setContentView(this, initContentView(savedInstanceState)) |
||||
val type: Type = javaClass.genericSuperclass!! |
||||
mViewModel = get((type as ParameterizedType).actualTypeArguments[1] as Class<VM>) |
||||
mBinding.lifecycleOwner = this |
||||
} |
||||
|
||||
/** |
||||
* 注册ViewModel与View的契约UI回调事件 |
||||
*/ |
||||
protected open fun registorUIChangeLiveDataCallBack() { |
||||
|
||||
mViewModel.mUILiveData.observe(this, Observer { |
||||
|
||||
when (it.type) { |
||||
AppLiveDataType.showDialog -> { |
||||
//加载进度条 |
||||
dialog.show() |
||||
} |
||||
AppLiveDataType.dissmissDialog -> { |
||||
//取消进度条 |
||||
if (dialog.isShow) dialog.doDismiss() |
||||
} |
||||
AppLiveDataType.finish -> { |
||||
//关闭页面 |
||||
finish() |
||||
} |
||||
//跳转页面 |
||||
AppLiveDataType.toNextPage -> { |
||||
startActivity(it.intent) |
||||
if (it.isCloseThisActivity) finish() |
||||
} |
||||
//跳转页面 |
||||
AppLiveDataType.toNextPageResult -> { |
||||
startActivityForResult(it.intent, it.requestCode!!) |
||||
if (it.isCloseThisActivity) finish() |
||||
} |
||||
} |
||||
|
||||
}) |
||||
} |
||||
|
||||
|
||||
/** |
||||
* |
||||
* 父类onCreate执行完成后调用 |
||||
*/ |
||||
abstract fun initData() |
||||
|
||||
/** |
||||
* 初始化根布局 |
||||
* |
||||
* @return 布局layout的id |
||||
*/ |
||||
abstract fun initContentView(savedInstanceState: Bundle?): Int |
||||
|
||||
override fun onDestroy() { |
||||
super.onDestroy() |
||||
mViewModel.onCleared() |
||||
mBinding.unbind() |
||||
} |
||||
|
||||
/** |
||||
* 页面进度条对话框被取消后的回掉 |
||||
*/ |
||||
override fun onBackClick(): Boolean { |
||||
TODO("Not yet implemented") |
||||
} |
||||
|
||||
|
||||
} |
||||
|
@ -1,33 +0,0 @@ |
||||
package shop.gedian.www.base |
||||
|
||||
import android.app.Application |
||||
import androidx.lifecycle.MutableLiveData |
||||
import androidx.lifecycle.ViewModel |
||||
import com.zww.sample.AppLiveData |
||||
import com.zww.sample.model.AppRepository |
||||
import io.reactivex.disposables.CompositeDisposable |
||||
import io.reactivex.disposables.Disposable |
||||
import org.koin.java.KoinJavaComponent.inject |
||||
|
||||
|
||||
open class BaseViewModel : ViewModel() { |
||||
val mUILiveData = MutableLiveData<AppLiveData>() |
||||
val mAppRepository by inject(AppRepository::class.java) |
||||
val mContext by inject(Application::class.java) |
||||
private var mCompositeDisposable: CompositeDisposable? = null |
||||
|
||||
/** |
||||
* 将所有请求加入集合,页面推出后统一取消 |
||||
*/ |
||||
fun addSubscribe(subscribe: Disposable) { |
||||
if (mCompositeDisposable == null) mCompositeDisposable = CompositeDisposable() |
||||
mCompositeDisposable!!.add(subscribe) |
||||
} |
||||
|
||||
public override fun onCleared() { |
||||
super.onCleared() |
||||
if (mCompositeDisposable != null) mCompositeDisposable!!.clear() |
||||
} |
||||
|
||||
|
||||
} |
@ -1,12 +0,0 @@ |
||||
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? |
||||
} |
@ -1,36 +0,0 @@ |
||||
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() |
||||
} |
||||
} |
@ -1,42 +0,0 @@ |
||||
package com.zww.sample.interceptors |
||||
|
||||
/** |
||||
* @author ihsan on 21/02/2017. |
||||
*/ |
||||
enum class Level { |
||||
/** |
||||
* No logs. |
||||
*/ |
||||
NONE, |
||||
|
||||
/** |
||||
* |
||||
* Example: |
||||
* <pre>`- URL |
||||
* - Method |
||||
* - Headers |
||||
* - Body |
||||
`</pre> * |
||||
*/ |
||||
BASIC, |
||||
|
||||
/** |
||||
* |
||||
* Example: |
||||
* <pre>`- URL |
||||
* - Method |
||||
* - Headers |
||||
`</pre> * |
||||
*/ |
||||
HEADERS, |
||||
|
||||
/** |
||||
* |
||||
* Example: |
||||
* <pre>`- URL |
||||
* - Method |
||||
* - Body |
||||
`</pre> * |
||||
*/ |
||||
BODY |
||||
} |
@ -1,19 +0,0 @@ |
||||
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) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,332 +0,0 @@ |
||||
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<String, Any>() |
||||
// 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<String, String> = HashMap() |
||||
val httpUrl: HashMap<String, String> = 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" |
||||
} |
||||
} |
||||
} |
@ -1,345 +0,0 @@ |
||||
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<String>, |
||||
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<String> { |
||||
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<String>, message: String |
||||
): Array<String> { |
||||
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>): 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<String>, 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. |
||||
} |
||||
} |
@ -1,47 +0,0 @@ |
||||
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}" |
||||
} |
||||
} |
@ -1,17 +0,0 @@ |
||||
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("/files/nsfw.tflite") |
||||
fun dowloadNSFWModelFile(): Observable<ResponseBody> |
||||
} |
@ -1,136 +0,0 @@ |
||||
package com.zww.sample.model |
||||
|
||||
import android.app.Application |
||||
import android.graphics.BitmapFactory |
||||
import android.os.Environment |
||||
import com.blankj.utilcode.util.ToastUtils |
||||
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.* |
||||
|
||||
/** |
||||
* 数据源 这里决定是从缓存获取还是从网络获取 |
||||
*/ |
||||
class AppRepository constructor( |
||||
private val mApiService: ApiService, |
||||
val mGson: Gson |
||||
) : |
||||
Repository { |
||||
|
||||
val mContext by inject(Application::class.java) |
||||
|
||||
val nsfwPath by lazy { |
||||
"/data/data/com.zww.sample/nsfw.tflite" |
||||
} |
||||
|
||||
//NSFW是否初始化完成 |
||||
var isSDKInit = false |
||||
|
||||
override fun dowloadNSFWModelFile(): Observable<ResponseBody> { |
||||
return mApiService.dowloadNSFWModelFile() |
||||
} |
||||
|
||||
/** |
||||
* 保存模型文件 |
||||
*/ |
||||
override fun saveNSFWModel(body: ResponseBody) { |
||||
try { |
||||
//文件大小 |
||||
val contentLength: Long = body.contentLength() |
||||
//读取文件 |
||||
val inputStream: InputStream = body.byteStream() |
||||
|
||||
//创建一个文件夹 |
||||
val file = File(nsfwPath) |
||||
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() |
||||
ToastUtils.showLong("NSFW初始化成功,可点击右上角识别按钮开始识别") |
||||
"NSFW初始化成功".d() |
||||
isSDKInit = true |
||||
} |
||||
|
||||
/** |
||||
* 扫描文件并返回扫描结果,传入参数为空时扫描资源文件 |
||||
*/ |
||||
override fun getNSFWScoreDataList(files: List<File>?): ArrayList<ScoreBean> { |
||||
return arrayListOf<ScoreBean>().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() |
||||
) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
@ -1,31 +0,0 @@ |
||||
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<ResponseBody> |
||||
|
||||
/** |
||||
* 保存模型文件 |
||||
*/ |
||||
fun saveNSFWModel(body: ResponseBody) |
||||
|
||||
/** |
||||
* 模型是否已下载 |
||||
*/ |
||||
fun isFileDownloaded():Boolean |
||||
|
||||
/** |
||||
* 初始化模型 |
||||
*/ |
||||
fun initNSFW() |
||||
|
||||
/** |
||||
* 扫描文件并返回扫描结果,传入参数为空时扫描资源文件 |
||||
*/ |
||||
fun getNSFWScoreDataList(files:List<File>?):ArrayList<ScoreBean> |
||||
} |
@ -1,72 +0,0 @@ |
||||
package com.zww.sample.model |
||||
|
||||
import com.google.gson.Gson |
||||
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.internal.platform.Platform |
||||
import org.koin.androidx.viewmodel.dsl.viewModel |
||||
import org.koin.dsl.module |
||||
import retrofit2.Retrofit |
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory |
||||
import retrofit2.converter.gson.GsonConverterFactory |
||||
import java.util.concurrent.TimeUnit |
||||
|
||||
val networkModule = module { |
||||
//OkHttpClient |
||||
single { |
||||
OkHttpClient().newBuilder() |
||||
.connectTimeout(10, TimeUnit.SECONDS) |
||||
.readTimeout(10, TimeUnit.SECONDS) |
||||
.writeTimeout(10, TimeUnit.SECONDS) |
||||
.addInterceptor( |
||||
LoggingInterceptor.Builder() |
||||
.setLevel(Level.BASIC) |
||||
.log(Platform.INFO) |
||||
.tag("离线鉴黄App") |
||||
.build() |
||||
) |
||||
.build() |
||||
} |
||||
//Retrofit |
||||
single { |
||||
Retrofit.Builder() |
||||
.baseUrl(BuildConfig.BASE_URL) |
||||
.client(get()) |
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) |
||||
.addConverterFactory(GsonConverterFactory.create()) |
||||
.build() |
||||
} |
||||
|
||||
//ApiService |
||||
single { |
||||
get<Retrofit>().create(ApiService::class.java) |
||||
} |
||||
|
||||
//Gson |
||||
single { |
||||
Gson() |
||||
} |
||||
} |
||||
|
||||
|
||||
val repositoryModule = module { |
||||
|
||||
single { |
||||
AppRepository(get(), get()) |
||||
} |
||||
} |
||||
val viewModelModules = module { |
||||
viewModel { |
||||
SplashViewModel() |
||||
} |
||||
viewModel { |
||||
MainViewModel() |
||||
} |
||||
|
||||
|
||||
} |
@ -1,63 +0,0 @@ |
||||
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<SplashLayoutBinding, SplashViewModel>() { |
||||
|
||||
/** |
||||
* |
||||
* 父类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 |
||||
} |
@ -1,12 +0,0 @@ |
||||
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) |
||||
} |
@ -1,74 +0,0 @@ |
||||
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<String>("扫描资源文件") |
||||
val selectPhoto = MutableLiveData<Int>(0) |
||||
val dialogShow = MutableLiveData<Boolean>(true) |
||||
val adapter = object : BaseQuickAdapter<ScoreBean, BaseViewHolder>(R.layout.item_main) { |
||||
override fun convert(holder: BaseViewHolder, item: ScoreBean) { |
||||
holder.getView<ImageView>(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 isShowDialog(){ |
||||
dialogShow.value = !mAppRepository.isFileDownloaded() |
||||
} |
||||
|
||||
fun changeFrom() { |
||||
if (!mAppRepository.isSDKInit) { |
||||
dialogShow.value = true |
||||
ToastUtils.showShort("SDK未初始化完成,如果为第一次打开请耐心等待下载完成") |
||||
return |
||||
} |
||||
when (bName.value) { |
||||
"扫描资源文件" -> { |
||||
adapter.setNewInstance(mAppRepository.getNSFWScoreDataList(null)) |
||||
} |
||||
"扫描相册" -> { |
||||
selectPhoto.value = 1 |
||||
} |
||||
} |
||||
changeText() |
||||
} |
||||
|
||||
fun toScorePhoto(files: List<File>) { |
||||
adapter.setNewInstance(mAppRepository.getNSFWScoreDataList(files)) |
||||
} |
||||
|
||||
private fun changeText() { |
||||
bName.value = if (bName.value.equals("扫描相册")) "扫描资源文件" else "扫描相册" |
||||
} |
||||
} |
@ -1,74 +0,0 @@ |
||||
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<Long> { |
||||
mAppRepository.initNSFW() |
||||
}.subscribeOn(Schedulers.io()).subscribe() |
||||
} else { |
||||
downloadModelFile() |
||||
} |
||||
toMainPage() |
||||
} |
||||
|
||||
private fun downloadModelFile() { |
||||
ToastUtils.showLong("开始后台下载模型文件") |
||||
mAppRepository.dowloadNSFWModelFile() |
||||
.subscribeOn(Schedulers.io()).subscribeOn(AndroidSchedulers.mainThread()) |
||||
.observeOn(AndroidSchedulers.mainThread()) |
||||
.subscribe(object : Observer<ResponseBody> { |
||||
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 |
||||
) |
||||
} |
||||
} |
@ -1,47 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<layout> |
||||
|
||||
<data class="MainLayoutBinding"> |
||||
|
||||
<variable |
||||
name="model" |
||||
type="com.zww.sample.ui.viewmodels.MainViewModel" /> |
||||
</data> |
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:orientation="vertical"> |
||||
|
||||
<RelativeLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="45dp" |
||||
android:background="@color/colorPrimary"> |
||||
|
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_centerInParent="true" |
||||
android:text="@string/app_name" |
||||
android:textColor="@color/white" /> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="match_parent" |
||||
android:layout_alignParentRight="true" |
||||
android:layout_centerVertical="true" |
||||
android:gravity="center" |
||||
android:onClick="@{()->model.changeFrom()}" |
||||
android:paddingRight="20dp" |
||||
android:text="@{model.bName}" |
||||
android:textColor="@color/white" /> |
||||
</RelativeLayout> |
||||
|
||||
<androidx.recyclerview.widget.RecyclerView |
||||
bindAdapter="@{model.adapter}" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="@color/bg" /> |
||||
</LinearLayout> |
||||
</layout> |
@ -1,10 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
||||
<data class="SplashLayoutBinding" /> |
||||
|
||||
<RelativeLayout |
||||
android:background="@color/transparent" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" /> |
||||
</layout> |
@ -1,48 +0,0 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginTop="1px" |
||||
android:background="@color/white" |
||||
android:padding="15dp"> |
||||
|
||||
<ImageView |
||||
android:id="@+id/iv" |
||||
android:layout_width="105dp" |
||||
android:layout_height="105dp" |
||||
android:scaleType="centerCrop" |
||||
android:src="@mipmap/ic_launcher" /> |
||||
|
||||
<LinearLayout |
||||
android:layout_width="match_parent" |
||||
android:layout_height="105dp" |
||||
android:layout_marginLeft="10dp" |
||||
android:layout_toRightOf="@+id/iv" |
||||
android:gravity="center" |
||||
android:orientation="vertical"> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:text="鉴别结果" |
||||
android:textColor="@color/colorPrimary" |
||||
android:textSize="18sp" /> |
||||
|
||||
<TextView |
||||
android:id="@+id/tvContent" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginTop="5dp" |
||||
android:text="涉黄程度:80%" |
||||
android:textColor="@color/black" |
||||
android:textSize="14sp" /> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:text="点击查看详细参数" |
||||
android:textColor="@color/see" |
||||
android:textSize="12sp" /> |
||||
</LinearLayout> |
||||
|
||||
</RelativeLayout> |
@ -1,4 +1,6 @@ |
||||
include ':sample' |
||||
include ':initializer' |
||||
include ':nsfw' |
||||
include ':app' |
||||
//include ':demo' //开启新demo必须注释 include ':app' 并打开gradle.properties中 注释的两行代码 因为新demo使用了androidx与app模块冲突 demo模块完成:Assets目录识别、网络图片识别 |
||||
//include ':app' |
||||
include ':OpenNSFW' |
||||
//include ':OpenNSFW' |
||||
|