pull/34/head
赵文文 5 years ago
parent d8125dcede
commit 892cc697ed
  1. 16
      .idea/codeStyles/Project.xml
  2. 5
      .idea/gradle.xml
  3. 2
      .idea/misc.xml
  4. 105
      OpenNSFW/src/main/java/com/zwy/opennsfw/core/Classifier.kt
  5. 2
      OpenNSFW/src/main/java/com/zwy/opennsfw/utils/kt.kt
  6. 0
      app/.gitignore
  7. 49
      app/build.gradle
  8. 0
      app/proguard-rules.pro
  9. 0
      app/src/androidTest/java/com/zww/sample/ExampleInstrumentedTest.kt
  10. 7
      app/src/main/AndroidManifest.xml
  11. 0
      app/src/main/assets/img/test_img2.jpeg
  12. 0
      app/src/main/assets/img/test_img3.jpg
  13. BIN
      app/src/main/assets/nsfw.tflite
  14. 27
      app/src/main/java/com/zww/sample/MainActivity.kt
  15. 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  16. 0
      app/src/main/res/drawable/ic_launcher_background.xml
  17. 0
      app/src/main/res/drawable/splash_bg.jpg
  18. 6
      app/src/main/res/layout/activity_main.xml
  19. 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  20. 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  21. 0
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  22. 0
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  23. 0
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  24. 0
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  25. 0
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  26. 0
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  27. 0
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  28. 0
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  29. 0
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  30. 0
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  31. 0
      app/src/main/res/values/colors.xml
  32. 0
      app/src/main/res/values/strings.xml
  33. 0
      app/src/main/res/values/styles.xml
  34. 0
      app/src/main/res/xml/network_security_config.xml
  35. 0
      app/src/test/java/com/zww/sample/ExampleUnitTest.kt
  36. 1
      build.gradle
  37. 1
      initializer/.gitignore
  38. 53
      initializer/build.gradle
  39. 0
      initializer/consumer-rules.pro
  40. 21
      initializer/proguard-rules.pro
  41. 24
      initializer/src/androidTest/java/com/zwy/nsfw_initializer/ExampleInstrumentedTest.kt
  42. 11
      initializer/src/main/AndroidManifest.xml
  43. 45
      initializer/src/main/java/com/zwy/nsfw_initializer/NSFWInitProvider.kt
  44. 17
      initializer/src/test/java/com/zwy/nsfw_initializer/ExampleUnitTest.kt
  45. 1
      nsfw/.gitignore
  46. 54
      nsfw/build.gradle
  47. 0
      nsfw/consumer-rules.pro
  48. 21
      nsfw/proguard-rules.pro
  49. 24
      nsfw/src/androidTest/java/com/zwy/nsfw/ExampleInstrumentedTest.kt
  50. 3
      nsfw/src/main/AndroidManifest.xml
  51. 191
      nsfw/src/main/java/com/zwy/nsfw/NSFWHelper.kt
  52. 43
      nsfw/src/main/java/com/zwy/nsfw/utils.kt
  53. 17
      nsfw/src/test/java/com/zwy/nsfw/ExampleUnitTest.kt
  54. 85
      sample/build.gradle
  55. 70
      sample/src/main/java/com/zww/sample/BaseApplication.kt
  56. 88
      sample/src/main/java/com/zww/sample/MainActivity.kt
  57. 175
      sample/src/main/java/com/zww/sample/base/BaseActivity.kt
  58. 33
      sample/src/main/java/com/zww/sample/base/BaseViewModel.kt
  59. 12
      sample/src/main/java/com/zww/sample/interceptors/BufferListener.kt
  60. 36
      sample/src/main/java/com/zww/sample/interceptors/I.kt
  61. 42
      sample/src/main/java/com/zww/sample/interceptors/Level.kt
  62. 19
      sample/src/main/java/com/zww/sample/interceptors/Logger.kt
  63. 332
      sample/src/main/java/com/zww/sample/interceptors/LoggingInterceptor.kt
  64. 345
      sample/src/main/java/com/zww/sample/interceptors/Printer.kt
  65. 47
      sample/src/main/java/com/zww/sample/kt.kt
  66. 17
      sample/src/main/java/com/zww/sample/model/ApiService.kt
  67. 136
      sample/src/main/java/com/zww/sample/model/AppRepository.kt
  68. 31
      sample/src/main/java/com/zww/sample/model/Repository.kt
  69. 72
      sample/src/main/java/com/zww/sample/model/modules.kt
  70. 63
      sample/src/main/java/com/zww/sample/ui/SplashActivity.kt
  71. 12
      sample/src/main/java/com/zww/sample/ui/databindingadapters/kt.kt
  72. 74
      sample/src/main/java/com/zww/sample/ui/viewmodels/MainViewModel.kt
  73. 74
      sample/src/main/java/com/zww/sample/ui/viewmodels/SplashViewModel.kt
  74. 47
      sample/src/main/res/layout/activity_main.xml
  75. 10
      sample/src/main/res/layout/activity_splash.xml
  76. 48
      sample/src/main/res/layout/item_main.xml
  77. 6
      settings.gradle

@ -25,6 +25,22 @@
</formatting-settings> </formatting-settings>
</DBN-SQL> </DBN-SQL>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings> <MarkdownNavigatorCodeStyleSettings>

@ -11,8 +11,9 @@
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/OpenNSFW" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/sample" /> <option value="$PROJECT_DIR$/initializer" />
<option value="$PROJECT_DIR$/nsfw" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />

@ -39,7 +39,7 @@
</profile-state> </profile-state>
</entry> </entry>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

@ -112,17 +112,13 @@ class Classifier private constructor(config: Config) {
} }
} }
//"/data/user/0/com.zwy.demo/files/nsfw.tflite"
init { init {
// val file = File(
// config.nsfwModuleFilePath
// ?: throw java.lang.NullPointerException("未配置模型路径,请调用Classifier.Build().nsfwModuleFilePath(模型路径)初始化")
// )
// if (!file.exists()) throw NullPointerException("模型加载失败,请确认路径是否正确")
try { try {
tflite = tflite =
Interpreter(loadModelFile(context = config.context!!), getTfLiteOptions(config.isOpenGPU)) Interpreter(
loadModelFile(context = config.context!!),
getTfLiteOptions(config.isOpenGPU)
)
if (config.isOpenGPU) "开启GPU加速成功".d() if (config.isOpenGPU) "开启GPU加速成功".d()
} catch (e: Exception) { } catch (e: Exception) {
"不支持GPU加速".e() "不支持GPU加速".e()
@ -160,33 +156,25 @@ class Classifier private constructor(config: Config) {
private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { private fun convertBitmapToByteBuffer(bitmap_: Bitmap) {
SystemClock.uptimeMillis().let { startTime ->
imgData.rewind() imgData.rewind()
val W = bitmap_.width
val H = bitmap_.height
val w_off = max((W - INPUT_WIDTH) / 2, 0)
val h_off = max((H - INPUT_HEIGHT) / 2, 0)
//把每个像素的颜色值转为int 存入intValues //把每个像素的颜色值转为int 存入intValues
bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) bitmap_.getPixels(
intValues,
val startTime = SystemClock.uptimeMillis() 0,
INPUT_WIDTH,
max((bitmap_.height - INPUT_HEIGHT) / 2, 0),
max((bitmap_.width - INPUT_WIDTH) / 2, 0),
INPUT_WIDTH,
INPUT_HEIGHT
)
for (color in intValues) { for (color in intValues) {
val r1 = Color.red(color) imgData.putFloat((Color.blue(color) - 104).toFloat())
val g1 = Color.green(color) imgData.putFloat((Color.green(color) - 117).toFloat())
val b1 = Color.blue(color) imgData.putFloat((Color.red(color) - 123).toFloat())
}
val rr1 = r1 - 123 "数据装载成功,耗时:${(SystemClock.uptimeMillis() - startTime)} ms".d()
val gg1 = g1 - 117
val bb1 = b1 - 104
imgData.putFloat(bb1.toFloat())
imgData.putFloat(gg1.toFloat())
imgData.putFloat(rr1.toFloat())
} }
val endTime = SystemClock.uptimeMillis()
"数据装载成功,耗时:${(endTime - startTime)} ms".d()
} }
// # 根据路径获取图片 Image.open(path) // # 根据路径获取图片 Image.open(path)
@ -203,41 +191,38 @@ class Classifier private constructor(config: Config) {
// # 删除所有单维度的条目 // # 删除所有单维度的条目
// # 输出扫描结果 // # 输出扫描结果
fun run(bitmap: Bitmap): NsfwBean { fun run(bitmap: Bitmap): NsfwBean {
SystemClock.uptimeMillis().let { startTime ->
//缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要 //缩放位图时是否应使用双线性过滤。如果这是正确的,则在缩放时将使用双线性滤波,从而以较差的性能为代价具有更好的图像质量。如果这是错误的,则使用最近邻居缩放,这将使图像质量较差但速度更快。推荐的默认设置是将滤镜设置为“ true”,因为双线性滤镜的成本通常很小,并且改善的图像质量非常重要
val stream = ByteArrayOutputStream() ByteArrayOutputStream().let { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
stream.close() stream.close()
val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) convertBitmapToByteBuffer(Bitmap.createScaledBitmap(bitmap, 256, 256, true))
convertBitmapToByteBuffer(bitmap_256)
//
val startTime = SystemClock.uptimeMillis()
// out // out
val outArray = Array(1) { FloatArray(2) } Array(1) { FloatArray(2) }.apply {
tflite.run(imgData, this)
tflite.run(imgData, outArray) val nsfw = NsfwBean(this[0][0], this[0][1])
"扫描完成[${nsfw}]耗时: ${(SystemClock.uptimeMillis() - startTime)} ms".d()
val endTime = SystemClock.uptimeMillis()
val nsfw = NsfwBean(outArray[0][0], outArray[0][1])
"扫描完成[${nsfw}]耗时: ${(endTime - startTime)} ms".d()
return nsfw return nsfw
} }
fun bitmap2RGB(bitmap: Bitmap): ByteArray? {
val bytes = bitmap.byteCount //返回可用于储存此位图像素的最小字节数
val buffer =
ByteBuffer.allocate(bytes) // 使用allocate()静态方法创建字节缓冲区
bitmap.copyPixelsToBuffer(buffer) // 将位图的像素复制到指定的缓冲区
val rgba = buffer.array()
val pixels = ByteArray(rgba.size / 4 * 3)
val count = rgba.size / 4
//Bitmap像素点的色彩通道排列顺序是RGBA
for (i in 0 until count) {
pixels[i * 3] = rgba[i * 4] //R
pixels[i * 3 + 1] = rgba[i * 4 + 1] //G
pixels[i * 3 + 2] = rgba[i * 4 + 2] //B
} }
return pixels
} }
}
// fun bitmap2RGB(bitmap: Bitmap): ByteArray? {
// val bytes = bitmap.byteCount //返回可用于储存此位图像素的最小字节数
// val buffer =
// ByteBuffer.allocate(bytes) // 使用allocate()静态方法创建字节缓冲区
// bitmap.copyPixelsToBuffer(buffer) // 将位图的像素复制到指定的缓冲区
// val rgba = buffer.array()
// val pixels = ByteArray(rgba.size / 4 * 3)
// val count = rgba.size / 4
//
// //Bitmap像素点的色彩通道排列顺序是RGBA
// for (i in 0 until count) {
// pixels[i * 3] = rgba[i * 4] //R
// pixels[i * 3 + 1] = rgba[i * 4 + 1] //G
// pixels[i * 3 + 2] = rgba[i * 4 + 2] //B
// }
// return pixels
// }
} }

@ -14,7 +14,7 @@ fun String.d() = Log.d(TAG, this)
fun String.e() = Log.e(TAG, this) fun String.e() = Log.e(TAG, this)
class NsfwBean(val sfw: Float, val nsfw: Float) { open class NsfwBean(val sfw: Float, val nsfw: Float) {
override fun toString(): String { override fun toString(): String {
val floatFormat = DecimalFormat("0.0000") val floatFormat = DecimalFormat("0.0000")
floatFormat.roundingMode = RoundingMode.HALF_UP floatFormat.roundingMode = RoundingMode.HALF_UP

@ -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'
}

@ -9,7 +9,6 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<application <application
android:name=".BaseApplication"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -18,17 +17,13 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity android:name=".MainActivity"> <activity android:name=".MainActivity">
</activity>
<activity
android:name=".ui.SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

@ -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.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -9,6 +9,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.0.0' classpath 'com.android.tools.build:gradle:4.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'
classpath "guru.stefma.bintrayrelease:bintrayrelease:1.0.0"
} }
} }

@ -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)
}
}

1
nsfw/.gitignore vendored

@ -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'

Loading…
Cancel
Save