@ -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 ':demo' //开启新demo必须注释 include ':app' 并打开gradle.properties中 注释的两行代码 因为新demo使用了androidx与app模块冲突 demo模块完成:Assets目录识别、网络图片识别 |
||||||
//include ':app' |
//include ':app' |
||||||
include ':OpenNSFW' |
//include ':OpenNSFW' |
||||||
|