parent
							
								
									66d0f48f05
								
							
						
					
					
						commit
						d8a2e0783f
					
				| @ -0,0 +1,22 @@ | ||||
| package com.zwy.opennsfw | ||||
| 
 | ||||
| import android.support.test.InstrumentationRegistry | ||||
| import android.support.test.runner.AndroidJUnit4 | ||||
| import org.junit.Assert.assertEquals | ||||
| import org.junit.Test | ||||
| import org.junit.runner.RunWith | ||||
| 
 | ||||
| /** | ||||
|  * 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.opennsfw.test", appContext.packageName) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,2 @@ | ||||
| <manifest | ||||
|         package="com.zwy.opennsfw"/> | ||||
| @ -0,0 +1,205 @@ | ||||
| //package com.zwy.opennsfw | ||||
| // | ||||
| //import android.content.Context | ||||
| //import android.content.res.AssetManager | ||||
| //import android.graphics.Bitmap | ||||
| //import android.graphics.Color | ||||
| //import android.os.SystemClock | ||||
| //import android.util.Log | ||||
| //import org.tensorflow.lite.Interpreter | ||||
| //import org.tensorflow.lite.gpu.GpuDelegate | ||||
| //import java.io.FileInputStream | ||||
| //import java.lang.Math.max | ||||
| //import java.nio.ByteBuffer | ||||
| //import java.nio.ByteOrder | ||||
| //import java.nio.MappedByteBuffer | ||||
| //import java.nio.channels.FileChannel | ||||
| // | ||||
| //class Classifier | ||||
| //private constructor(assetManager: AssetManager, isGPU: Boolean?, numThreads: Int) { | ||||
| // | ||||
| //    /** | ||||
| //     * 数据宽高 | ||||
| //     */ | ||||
| //    private val INPUT_WIDTH = 224 | ||||
| // | ||||
| //    /** | ||||
| //     * 数据宽高 | ||||
| //     */ | ||||
| //    private val INPUT_HEIGHT = 224 | ||||
| // | ||||
| //    /** | ||||
| //     * 通道 | ||||
| //     */ | ||||
| //    private val BYTES_PER_CHANNEL_NUM = 4 | ||||
| // | ||||
| //    /** | ||||
| //     * Resize后的数据源 | ||||
| //     */ | ||||
| //    private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT) | ||||
| // | ||||
| //    /** | ||||
| //     * 载入模型的客户端 | ||||
| //     */ | ||||
| //    private var tfliteModel: MappedByteBuffer? = null | ||||
| // | ||||
| //    /** | ||||
| //     * GPU代理 | ||||
| //     */ | ||||
| //    private var gpuDelegate: GpuDelegate? = null | ||||
| // | ||||
| //    /** | ||||
| //     * Tensorflow Lite | ||||
| //     */ | ||||
| //    private var tflite: Interpreter? = null | ||||
| // | ||||
| //    /** | ||||
| //     * 喂入模型的最终数据源 | ||||
| //     */ | ||||
| //    private val imgData: ByteBuffer? | ||||
| // | ||||
| // | ||||
| //    init { | ||||
| //        tfliteModel = loadModelFile(assetManager) | ||||
| //        val tfliteOptions = Interpreter.Options() | ||||
| //        if (isGPU == true) { | ||||
| //            gpuDelegate = GpuDelegate() | ||||
| //            tfliteOptions.addDelegate(gpuDelegate) | ||||
| //        } | ||||
| //        tfliteOptions.setNumThreads(numThreads) | ||||
| //        tflite = Interpreter(tfliteModel!!, tfliteOptions) | ||||
| // | ||||
| //        val tensor = tflite!!.getInputTensor(tflite!!.getInputIndex("input")) | ||||
| //        val stringBuilder = (" \n" | ||||
| //                + "dataType : " + | ||||
| //                tensor.dataType() + | ||||
| //                "\n" + | ||||
| //                "numBytes : " + | ||||
| //                tensor.numBytes() + | ||||
| //                "\n" + | ||||
| //                "numDimensions : " + | ||||
| //                tensor.numDimensions() + | ||||
| //                "\n" + | ||||
| //                "numElements : " + | ||||
| //                tensor.numElements() + | ||||
| //                "\n" + | ||||
| //                "shape : " + | ||||
| //                tensor.shape().size) | ||||
| //        Log.d(TAG, stringBuilder) | ||||
| // | ||||
| //        imgData = ByteBuffer.allocateDirect( | ||||
| //            DIM_BATCH_SIZE | ||||
| //                    * INPUT_WIDTH | ||||
| //                    * INPUT_HEIGHT | ||||
| //                    * DIM_PIXEL_SIZE | ||||
| //                    * BYTES_PER_CHANNEL_NUM | ||||
| //        ) | ||||
| // | ||||
| //        imgData!!.order(ByteOrder.LITTLE_ENDIAN) | ||||
| //        Log.d(TAG, "Tensorflow Lite Image Classifier Initialization Success.") | ||||
| //    } | ||||
| // | ||||
| //    /** | ||||
| //     * Memory-map the model file in Assets. | ||||
| //     */ | ||||
| //    private fun loadModelFile(assetManager: AssetManager): MappedByteBuffer { | ||||
| //        val context:Context | ||||
| //        val fileDescriptor = assetManager.openFd("nsfw.tflite") | ||||
| //        val inputStream = FileInputStream(fileDescriptor.fileDescriptor) | ||||
| //        val fileChannel = inputStream.channel | ||||
| //        val startOffset = fileDescriptor.startOffset | ||||
| //        val declaredLength = fileDescriptor.declaredLength | ||||
| //        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) | ||||
| //    } | ||||
| // | ||||
| // | ||||
| //    /** | ||||
| //     * Writes Image data into a `ByteBuffer`. | ||||
| //     */ | ||||
| //    private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { | ||||
| //        if (imgData == null || bitmap_ == null) { | ||||
| //            return | ||||
| //        } | ||||
| //        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 | ||||
| //        bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) | ||||
| //        // Convert the image to floating point. | ||||
| //        val startTime = SystemClock.uptimeMillis() | ||||
| //        for (color in intValues) { | ||||
| //            val r1 = Color.red(color) | ||||
| //            val g1 = Color.green(color) | ||||
| //            val b1 = Color.blue(color) | ||||
| // | ||||
| //            val rr1 = r1 - 123 | ||||
| //            val gg1 = g1 - 117 | ||||
| //            val bb1 = b1 - 104 | ||||
| // | ||||
| //            imgData.putFloat(bb1.toFloat()) | ||||
| //            imgData.putFloat(gg1.toFloat()) | ||||
| //            imgData.putFloat(rr1.toFloat()) | ||||
| //        } | ||||
| //        val endTime = SystemClock.uptimeMillis() | ||||
| //        Log.d(TAG, "Timecost to put values into ByteBuffer: " + (endTime - startTime) + "ms") | ||||
| //    } | ||||
| // | ||||
| //    fun run(bitmap: Bitmap): NsfwBean { | ||||
| // | ||||
| //        val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) | ||||
| // | ||||
| //        //Writes image data into byteBuffer | ||||
| //        convertBitmapToByteBuffer(bitmap_256) | ||||
| // | ||||
| //        val startTime = SystemClock.uptimeMillis() | ||||
| //        // out | ||||
| //        val outArray = Array(1) { FloatArray(2) } | ||||
| // | ||||
| //        tflite!!.run(imgData, outArray) | ||||
| // | ||||
| //        val endTime = SystemClock.uptimeMillis() | ||||
| // | ||||
| //        Log.d(TAG, "SFW score :" + outArray[0][0] + ",NSFW score :" + outArray[0][1]) | ||||
| //        Log.d(TAG, "Timecost to run model inference: " + (endTime - startTime) + "ms") | ||||
| //        return NsfwBean(outArray[0][0], outArray[0][1]) | ||||
| //    } | ||||
| // | ||||
| //    /** | ||||
| //     * Closes the interpreter and model to release resources. | ||||
| //     */ | ||||
| //    fun close() { | ||||
| //        if (tflite != null) { | ||||
| //            tflite!!.close() | ||||
| //            tflite = null | ||||
| //            Log.d(TAG, "Tensorflow Lite Image Classifier close.") | ||||
| //        } | ||||
| //        if (gpuDelegate != null) { | ||||
| //            gpuDelegate!!.close() | ||||
| //            Log.d(TAG, "Tensorflow Lite Image gpuDelegate close.") | ||||
| //            gpuDelegate = null | ||||
| //        } | ||||
| //        tfliteModel = null | ||||
| //        Log.d(TAG, "Tensorflow Lite destroyed.") | ||||
| //    } | ||||
| // | ||||
| //    companion object { | ||||
| // | ||||
| //        val TAG = "open_nsfw_android" | ||||
| //        /** | ||||
| //         * Dimensions of inputs. | ||||
| //         */ | ||||
| //        private val DIM_BATCH_SIZE = 1 | ||||
| // | ||||
| //        private val DIM_PIXEL_SIZE = 3 | ||||
| // | ||||
| //        fun create(assetManager: AssetManager, isAddGpuDelegate: Boolean?, numThreads: Int): Classifier { | ||||
| //            return Classifier(assetManager, isAddGpuDelegate!!, numThreads) | ||||
| //        } | ||||
| // | ||||
| //    } | ||||
| // | ||||
| //} | ||||
| @ -0,0 +1,180 @@ | ||||
| package com.zwy.opennsfw.core | ||||
| 
 | ||||
| import NsfwBean | ||||
| import android.content.Context | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.Color | ||||
| import android.os.SystemClock | ||||
| import d | ||||
| import mClassifier | ||||
| import org.tensorflow.lite.Interpreter | ||||
| import org.tensorflow.lite.gpu.GpuDelegate | ||||
| import java.io.FileInputStream | ||||
| import java.lang.Math.max | ||||
| import java.nio.ByteBuffer | ||||
| import java.nio.ByteOrder | ||||
| import java.nio.MappedByteBuffer | ||||
| import java.nio.channels.FileChannel | ||||
| 
 | ||||
| class Classifier private constructor(config: Config) { | ||||
| 
 | ||||
|     /** | ||||
|      * DIM_BATCH_SIZE | ||||
|      */ | ||||
|     private val DIM_BATCH_SIZE = 1 | ||||
| 
 | ||||
|     /** | ||||
|      * DIM_PIXEL_SIZE | ||||
|      */ | ||||
|     private val DIM_PIXEL_SIZE = 3 | ||||
| 
 | ||||
|     /** | ||||
|      * 数据宽高 | ||||
|      */ | ||||
|     private val INPUT_WIDTH = 224 | ||||
| 
 | ||||
|     /** | ||||
|      * 数据宽高 | ||||
|      */ | ||||
|     private val INPUT_HEIGHT = 224 | ||||
| 
 | ||||
|     /** | ||||
|      * 通道 | ||||
|      */ | ||||
|     private val BYTES_PER_CHANNEL_NUM = 4 | ||||
| 
 | ||||
|     /** | ||||
|      * Resize后的数据源 | ||||
|      */ | ||||
|     private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT) | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Tensorflow Lite | ||||
|      */ | ||||
|     private lateinit var tflite: Interpreter | ||||
| 
 | ||||
|     /** | ||||
|      * 喂入模型的最终数据源 | ||||
|      */ | ||||
|     private lateinit var imgData: ByteBuffer | ||||
| 
 | ||||
|     companion object { | ||||
|         private var config = Config(context = null) | ||||
|         private var instance: Classifier? = null | ||||
|             get() { | ||||
|                 if (field == null) { | ||||
|                     if (config.context == null) throw RuntimeException("context函数未调用,请使用Classifier.Build().context(context)初始化") | ||||
|                     field = Classifier(config) | ||||
|                     mClassifier = field | ||||
|                 } | ||||
|                 return field | ||||
|             } | ||||
| 
 | ||||
|         @Synchronized | ||||
|         private fun get(config: Config): Classifier { | ||||
|             this.config = config | ||||
|             return instance!! | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     class Build { | ||||
|         private val config = Config(context = null) | ||||
| 
 | ||||
|         fun isOpenGPU(isOpenGPU: Boolean): Build { | ||||
|             config.isOpenGPU = isOpenGPU | ||||
|             return this | ||||
|         } | ||||
| 
 | ||||
|         fun numThreads(numThreads: Int): Build { | ||||
|             config.numThreads = numThreads | ||||
|             return this | ||||
|         } | ||||
| 
 | ||||
|         fun context(context: Context): Build { | ||||
|             config.context = context | ||||
|             return this | ||||
|         } | ||||
| 
 | ||||
|         fun build(): Classifier { | ||||
|             return get(config) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
| 
 | ||||
|         val tfliteOptions = Interpreter.Options() | ||||
|         if (config.isOpenGPU) tfliteOptions.addDelegate(GpuDelegate()) | ||||
|         tfliteOptions.setNumThreads(config.numThreads) | ||||
|         tflite = Interpreter(loadModelFile(config.context!!), tfliteOptions) | ||||
| 
 | ||||
|         imgData = ByteBuffer.allocateDirect( | ||||
|             DIM_BATCH_SIZE | ||||
|                     * INPUT_WIDTH | ||||
|                     * INPUT_HEIGHT | ||||
|                     * DIM_PIXEL_SIZE | ||||
|                     * BYTES_PER_CHANNEL_NUM | ||||
|         ) | ||||
| 
 | ||||
|         imgData.order(ByteOrder.LITTLE_ENDIAN) | ||||
|     } | ||||
| 
 | ||||
|     private fun loadModelFile(context: Context): MappedByteBuffer { | ||||
|         val fileDescriptor = context.assets.openFd("nsfw.tflite") | ||||
|         val inputStream = FileInputStream(fileDescriptor.fileDescriptor) | ||||
|         val fileChannel = inputStream.channel | ||||
|         val startOffset = fileDescriptor.startOffset | ||||
|         val declaredLength = fileDescriptor.declaredLength | ||||
|         return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) | ||||
|     } | ||||
| 
 | ||||
|     private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { | ||||
|         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 | ||||
|         bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) | ||||
| 
 | ||||
|         val startTime = SystemClock.uptimeMillis() | ||||
| 
 | ||||
|         for (color in intValues) { | ||||
|             val r1 = Color.red(color) | ||||
|             val g1 = Color.green(color) | ||||
|             val b1 = Color.blue(color) | ||||
| 
 | ||||
|             val rr1 = r1 - 123 | ||||
|             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() | ||||
|     } | ||||
| 
 | ||||
|     fun run(bitmap: Bitmap): NsfwBean { | ||||
| 
 | ||||
|         val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) | ||||
| 
 | ||||
|         convertBitmapToByteBuffer(bitmap_256) | ||||
| 
 | ||||
|         val startTime = SystemClock.uptimeMillis() | ||||
|         // out | ||||
|         val outArray = Array(1) { FloatArray(2) } | ||||
| 
 | ||||
|         tflite.run(imgData, outArray) | ||||
| 
 | ||||
|         val endTime = SystemClock.uptimeMillis() | ||||
| 
 | ||||
|         "扫描完成,耗时: ${(endTime - startTime)} ms".d() | ||||
|         return NsfwBean(outArray[0][0], outArray[0][1]) | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| package com.zwy.opennsfw.core | ||||
| 
 | ||||
| import android.content.Context | ||||
| 
 | ||||
| data class Config( | ||||
|     /** | ||||
|      * 是否开启GPU加速 | ||||
|      */ | ||||
|     var isOpenGPU: Boolean = false, | ||||
|     var numThreads: Int = 1, | ||||
|     var context: Context? | ||||
| ) | ||||
| @ -0,0 +1,45 @@ | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.BitmapFactory | ||||
| import android.util.Log | ||||
| import com.zwy.opennsfw.core.Classifier | ||||
| import java.io.File | ||||
| import java.math.RoundingMode | ||||
| import java.text.DecimalFormat | ||||
| 
 | ||||
| private val TAG = "OpenNSFWLogTag" | ||||
| 
 | ||||
| var mClassifier: Classifier? = null | ||||
| 
 | ||||
| fun String.d() = Log.d(TAG, this) | ||||
| 
 | ||||
| fun String.e() = Log.e(TAG, this) | ||||
| 
 | ||||
| class NsfwBean(val sfw: Float, val nsfw: Float) { | ||||
|     override fun toString(): String { | ||||
|         val floatFormat = DecimalFormat("0.0000") | ||||
|         floatFormat.roundingMode = RoundingMode.HALF_UP | ||||
|         return "适宜度(非色情程度):${floatFormat.format(sfw * 100)}%,不适宜度(涉黄程度):${floatFormat.format(nsfw * 100)}%" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fun Bitmap.getNsfwScore(): NsfwBean { | ||||
|     checkNull() | ||||
|     return mClassifier!!.run(this) | ||||
| } | ||||
| 
 | ||||
| fun File.getNsfwScore(): NsfwBean { | ||||
|     checkNull() | ||||
|     val bitmap = try { | ||||
|         BitmapFactory.decodeFile(this.path) | ||||
|     } catch (e: Exception) { | ||||
|         val m = "File2Bitmap过程失败,请确认文件是否存在或是否为图片" | ||||
|         m.e() | ||||
|         throw RuntimeException(m) | ||||
|     } | ||||
|     return bitmap.getNsfwScore() | ||||
| } | ||||
| 
 | ||||
| private fun checkNull() { | ||||
|     if (mClassifier == null) throw RuntimeException("Classifier未初始化,请使用Classifier.Build().context(context)初始化后再试") | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,16 @@ | ||||
| package com.zwy.opennsfw | ||||
| 
 | ||||
| import org.junit.Assert.assertEquals | ||||
| import org.junit.Test | ||||
| 
 | ||||
| /** | ||||
|  * 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) | ||||
|     } | ||||
| } | ||||
| Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB | 
| 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,11 @@ | ||||
| package com.example.open_nsfw_android | ||||
| 
 | ||||
| import android.app.Application | ||||
| import com.zwy.opennsfw.core.Classifier | ||||
| 
 | ||||
| class DemoApplication : Application() { | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         Classifier.Build().context(this).build() | ||||
|     } | ||||
| } | ||||
| @ -1,174 +0,0 @@ | ||||
| package com.example.open_nsfw_android | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.ProgressDialog | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.graphics.BitmapFactory | ||||
| import android.os.Bundle | ||||
| import android.support.v4.app.ActivityCompat | ||||
| import android.support.v4.content.ContextCompat | ||||
| import android.support.v7.app.AppCompatActivity | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| import android.util.Log | ||||
| import android.view.View | ||||
| import android.widget.Toast | ||||
| import com.bumptech.glide.Glide | ||||
| import com.luck.picture.lib.PictureSelector | ||||
| import com.luck.picture.lib.config.PictureConfig | ||||
| import com.luck.picture.lib.config.PictureMimeType | ||||
| import com.luck.picture.lib.entity.LocalMedia | ||||
| import com.zwy.nsfw.api.NSFWHelper | ||||
| import com.zwy.nsfw.core.NSFWConfig | ||||
| import com.zwy.nsfw.kotlin.getNsfwScore | ||||
| import kotlinx.android.synthetic.main.activity_main.* | ||||
| import java.io.File | ||||
| import kotlin.concurrent.thread | ||||
| 
 | ||||
| 
 | ||||
| class MainActivity : AppCompatActivity(), View.OnClickListener { | ||||
| 
 | ||||
|     var nsfwHelper: NSFWHelper? = null | ||||
|     var mainAdapter: MainAdapter? = null | ||||
|     var index = 0 | ||||
|     var listData: ArrayList<MyNsfwBean> = ArrayList() | ||||
|     var selectList: List<LocalMedia>? = null | ||||
| 
 | ||||
|     var progressDialog: ProgressDialog? = null | ||||
|     @SuppressLint("SetTextI18n") | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         setContentView(R.layout.activity_main) | ||||
|         initNsfwHelper() | ||||
|         initAdapter() | ||||
|         initClickListener() | ||||
|         tv_version.text = "当前版本:${this.packageManager.getPackageInfo(packageName, 0).versionName}" | ||||
|         if (ContextCompat.checkSelfPermission( | ||||
|                 this, | ||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
|             ) != PackageManager.PERMISSION_GRANTED | ||||
|         ) { //表示未授权时 | ||||
|             //进行授权 | ||||
|             ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     override fun onClick(v: View) { | ||||
|         when (v.id) { | ||||
|             R.id.bt_sc_assets -> { | ||||
|                 reScAssetsImgs() | ||||
|             } | ||||
|             R.id.bt_sc_from_other -> { | ||||
|                 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(0x01);//结果回调onActivityResult code | ||||
|             } | ||||
|             R.id.bt_sc_from_internet ->{ | ||||
|                 startActivity(Intent(this,Main2Activity::class.java)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         super.onActivityResult(requestCode, resultCode, data) | ||||
|         if (requestCode == 0x01 && resultCode == RESULT_OK) { | ||||
|             selectList = PictureSelector.obtainMultipleResult(data) | ||||
|             if (selectList != null && selectList?.size ?: 0 > 0) | ||||
|                 reScFromImgs(selectList!!) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private fun initClickListener() { | ||||
|         bt_sc_assets.setOnClickListener(this) | ||||
|         bt_sc_from_other.setOnClickListener(this) | ||||
|         bt_sc_from_internet.setOnClickListener(this) | ||||
|     } | ||||
| 
 | ||||
|     private fun initAdapter() { | ||||
|         mainAdapter = MainAdapter(null) | ||||
|         rv.layoutManager = LinearLayoutManager(this) | ||||
|         rv.adapter = mainAdapter | ||||
|     } | ||||
| 
 | ||||
|     private fun initNsfwHelper() { | ||||
|         nsfwHelper = NSFWHelper.init(NSFWConfig(assets)) | ||||
|     } | ||||
| 
 | ||||
|     private fun reScFromImgs(list: List<LocalMedia>) { | ||||
|         progressDialog = ProgressDialog.show(this, "提示", "请稍后") | ||||
|         index = 0 | ||||
|         mainAdapter?.setNewData(null) | ||||
|         listData = ArrayList<MyNsfwBean>() | ||||
|         Thread(Runnable { | ||||
|             for (lm in list) { | ||||
|                 val bitmap = BitmapFactory.decodeFile(lm.path) | ||||
| 
 | ||||
|                 val nsfwScore = bitmap.getNsfwScore(assets) | ||||
| 
 | ||||
| //                listData.add(MyNsfwBean(0.0f, 0.0f, lm.path, bitmap)) | ||||
|                 listData.add(MyNsfwBean(nsfwScore.sfw, nsfwScore.nsfw, lm.path, bitmap)) | ||||
| //                val nsfwBean = nsfwHelper?.scanBitmap(bitmap)!! | ||||
| //                listData[index].sfw = nsfwBean.sfw | ||||
| //                listData[index].nsfw = nsfwBean.nsfw | ||||
| //                rv.scrollToPosition(index) | ||||
|                 index++ | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|             runOnUiThread { | ||||
|                 mainAdapter?.setNewData(listData) | ||||
|                 mainAdapter?.notifyDataSetChanged() | ||||
|                 progressDialog?.dismiss() | ||||
|             } | ||||
|         }).start() | ||||
|     } | ||||
| 
 | ||||
|     private fun reScAssetsImgs() { | ||||
|         progressDialog = ProgressDialog.show(this, "提示", "请稍后") | ||||
|         index = 0 | ||||
|         mainAdapter?.setNewData(null) | ||||
|         listData = ArrayList<MyNsfwBean>() | ||||
|         thread(true) { | ||||
|             for (a in resources.assets.list("img")) { | ||||
|                 val path = "img/${a}" | ||||
|                 val b = BitmapFactory.decodeStream(resources.assets.open(path)) | ||||
|                 listData.add(MyNsfwBean(0f, 0f, path, b)) | ||||
|                 val nsfwBean = nsfwHelper?.scanBitmap(b)!! | ||||
|                 listData[index].sfw = nsfwBean.sfw | ||||
|                 listData[index].nsfw = nsfwBean.nsfw | ||||
| 
 | ||||
|                 index++ | ||||
|             } | ||||
|             runOnUiThread { | ||||
|                 mainAdapter?.setNewData(listData) | ||||
|                 mainAdapter?.notifyDataSetChanged() | ||||
|                 progressDialog?.dismiss() | ||||
| //                rv.scrollToPosition(index) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     override fun onBackPressed() { | ||||
|         finish() | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         nsfwHelper?.destroyFactory() | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package com.example.open_nsfw_android | ||||
| package com.example.open_nsfw_android.util | ||||
| 
 | ||||
| import android.graphics.Bitmap | ||||
| 
 | ||||
| @ -0,0 +1,143 @@ | ||||
| package com.example.open_nsfw_android.view | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.content.Intent | ||||
| import android.content.pm.PackageManager | ||||
| import android.graphics.BitmapFactory | ||||
| import android.os.Bundle | ||||
| import android.support.v4.app.ActivityCompat | ||||
| import android.support.v4.content.ContextCompat | ||||
| import android.support.v7.app.AppCompatActivity | ||||
| import android.support.v7.widget.LinearLayoutManager | ||||
| import android.widget.Toast | ||||
| import com.example.open_nsfw_android.R | ||||
| import com.example.open_nsfw_android.util.MainAdapter | ||||
| import com.example.open_nsfw_android.util.MyNsfwBean | ||||
| import com.luck.picture.lib.PictureSelector | ||||
| import com.luck.picture.lib.config.PictureConfig | ||||
| import com.luck.picture.lib.config.PictureMimeType | ||||
| import com.luck.picture.lib.entity.LocalMedia | ||||
| import getNsfwScore | ||||
| import kotlinx.android.synthetic.main.activity_main.* | ||||
| import java.io.File | ||||
| 
 | ||||
| class MainAty : AppCompatActivity() { | ||||
| 
 | ||||
|     var selectList: List<LocalMedia>? = null | ||||
| 
 | ||||
|     lateinit var mMainAdapter: MainAdapter | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         setContentView(R.layout.activity_main) | ||||
| 
 | ||||
|         init() | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     fun init() { | ||||
|         //检测权限 | ||||
|         checkPermissions() | ||||
|         //初始化适配器 | ||||
|         initAdapter() | ||||
|         //识别assets目录 | ||||
|         bt_sc_assets.setOnClickListener { assetsListSc() } | ||||
|         //选择相册进行扫描 | ||||
|         bt_sc_from_other.setOnClickListener { selectImgFromD() } | ||||
|         //跳转网络图片识别页面 | ||||
|         bt_sc_from_internet.setOnClickListener { startActivity(Intent(this, Main2Activity::class.java)) } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private fun initAdapter() { | ||||
|         mMainAdapter = MainAdapter(null) | ||||
|         rv.layoutManager = LinearLayoutManager(this) | ||||
|         rv.adapter = mMainAdapter | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 相册选择 | ||||
|      */ | ||||
|     private fun selectImgFromD() { | ||||
|         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(0x01);//结果回调onActivityResult code } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 扫描assets目录 | ||||
|      */ | ||||
|     private fun assetsListSc() { | ||||
|         mMainAdapter.setNewData(null) | ||||
|         Toast.makeText(this, "请稍等...", Toast.LENGTH_LONG).show() | ||||
|         Thread(Runnable { | ||||
|             resources.assets.list("img")?.forEach { | ||||
|                 val bitmap = BitmapFactory.decodeStream(resources.assets.open("img/$it")) | ||||
|                 val nsfwScore = bitmap.getNsfwScore() | ||||
|                 addDataToAdapter( | ||||
|                     MyNsfwBean( | ||||
|                         nsfwScore.sfw, | ||||
|                         nsfwScore.nsfw, | ||||
|                         "img/$it", | ||||
|                         bitmap | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|         }).start() | ||||
|     } | ||||
| 
 | ||||
|     private fun addDataToAdapter(mMyNsfwBean: MyNsfwBean) { | ||||
|         runOnUiThread { mMainAdapter.addData(mMyNsfwBean) } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 检测权限 | ||||
|      */ | ||||
|     private fun checkPermissions() { | ||||
|         if (ContextCompat.checkSelfPermission( | ||||
|                 this, | ||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE | ||||
|             ) != PackageManager.PERMISSION_GRANTED | ||||
|         ) { | ||||
|             ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         super.onActivityResult(requestCode, resultCode, data) | ||||
|         if (requestCode == 0x01 && resultCode == RESULT_OK) { | ||||
|             selectList = PictureSelector.obtainMultipleResult(data) | ||||
|             selectList?.let { | ||||
|                 mMainAdapter.setNewData(null) | ||||
|                 Toast.makeText(this, "请稍等...", Toast.LENGTH_LONG).show() | ||||
|             } | ||||
|             Thread(Runnable { | ||||
|                 selectList?.forEach { | ||||
|                     val file = File(it.path) | ||||
|                     val nsfwScore = file.getNsfwScore() | ||||
|                     addDataToAdapter( | ||||
|                         MyNsfwBean( | ||||
|                             nsfwScore.sfw, | ||||
|                             nsfwScore.nsfw, | ||||
|                             it.path, | ||||
|                             BitmapFactory.decodeStream(file.inputStream()) | ||||
|                         ) | ||||
|                     ) | ||||
|                 } | ||||
|             }).start() | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,34 +1,34 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:aapt="http://schemas.android.com/aapt" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="108" | ||||
|     android:viewportHeight="108"> | ||||
|         xmlns:aapt="http://schemas.android.com/aapt" | ||||
|         android:width="108dp" | ||||
|         android:height="108dp" | ||||
|         android:viewportWidth="108" | ||||
|         android:viewportHeight="108"> | ||||
|     <path | ||||
|         android:fillType="evenOdd" | ||||
|         android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" | ||||
|         android:strokeWidth="1" | ||||
|         android:strokeColor="#00000000"> | ||||
|             android:fillType="evenOdd" | ||||
|             android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" | ||||
|             android:strokeWidth="1" | ||||
|             android:strokeColor="#00000000"> | ||||
|         <aapt:attr name="android:fillColor"> | ||||
|             <gradient | ||||
|                 android:endX="78.5885" | ||||
|                 android:endY="90.9159" | ||||
|                 android:startX="48.7653" | ||||
|                 android:startY="61.0927" | ||||
|                 android:type="linear"> | ||||
|                     android:endX="78.5885" | ||||
|                     android:endY="90.9159" | ||||
|                     android:startX="48.7653" | ||||
|                     android:startY="61.0927" | ||||
|                     android:type="linear"> | ||||
|                 <item | ||||
|                     android:color="#44000000" | ||||
|                     android:offset="0.0" /> | ||||
|                         android:color="#44000000" | ||||
|                         android:offset="0.0"/> | ||||
|                 <item | ||||
|                     android:color="#00000000" | ||||
|                     android:offset="1.0" /> | ||||
|                         android:color="#00000000" | ||||
|                         android:offset="1.0"/> | ||||
|             </gradient> | ||||
|         </aapt:attr> | ||||
|     </path> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFF" | ||||
|         android:fillType="nonZero" | ||||
|         android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" | ||||
|         android:strokeWidth="1" | ||||
|         android:strokeColor="#00000000" /> | ||||
|             android:fillColor="#FFFFFF" | ||||
|             android:fillType="nonZero" | ||||
|             android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" | ||||
|             android:strokeWidth="1" | ||||
|             android:strokeColor="#00000000"/> | ||||
| </vector> | ||||
|  | ||||
| @ -1,170 +1,170 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="108" | ||||
|     android:viewportHeight="108"> | ||||
|     <path | ||||
|         android:fillColor="#008577" | ||||
|         android:pathData="M0,0h108v108h-108z" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M9,0L9,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,0L19,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,0L29,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,0L39,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,0L49,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,0L59,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,0L69,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,0L79,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M89,0L89,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M99,0L99,108" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,9L108,9" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,19L108,19" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,29L108,29" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,39L108,39" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,49L108,49" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,59L108,59" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,69L108,69" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,79L108,79" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,89L108,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,99L108,99" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,29L89,29" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,39L89,39" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,49L89,49" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,59L89,59" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,69L89,69" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,79L89,79" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,19L29,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,19L39,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,19L49,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,19L59,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,19L69,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,19L79,89" | ||||
|         android:strokeWidth="0.8" | ||||
|         android:strokeColor="#33FFFFFF" /> | ||||
|         android:width="108dp" | ||||
|         android:height="108dp" | ||||
|         android:viewportWidth="108" | ||||
|         android:viewportHeight="108"> | ||||
|     <path | ||||
|             android:fillColor="#008577" | ||||
|             android:pathData="M0,0h108v108h-108z"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M9,0L9,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,0L19,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M29,0L29,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M39,0L39,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M49,0L49,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M59,0L59,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M69,0L69,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M79,0L79,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M89,0L89,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M99,0L99,108" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,9L108,9" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,19L108,19" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,29L108,29" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,39L108,39" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,49L108,49" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,59L108,59" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,69L108,69" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,79L108,79" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,89L108,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M0,99L108,99" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,29L89,29" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,39L89,39" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,49L89,49" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,59L89,59" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,69L89,69" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M19,79L89,79" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M29,19L29,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M39,19L39,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M49,19L49,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M59,19L59,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M69,19L69,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
|     <path | ||||
|             android:fillColor="#00000000" | ||||
|             android:pathData="M79,19L79,89" | ||||
|             android:strokeWidth="0.8" | ||||
|             android:strokeColor="#33FFFFFF"/> | ||||
| </vector> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
|     <background android:drawable="@drawable/ic_launcher_background"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
| @ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background" /> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground" /> | ||||
|     <background android:drawable="@drawable/ic_launcher_background"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
| @ -1,26 +0,0 @@ | ||||
| package com.zwy.nsfw; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.test.InstrumentationRegistry; | ||||
| import android.support.test.runner.AndroidJUnit4; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| /** | ||||
|  * Instrumented test, which will execute on an Android device. | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| public class ExampleInstrumentedTest { | ||||
|     @Test | ||||
|     public void useAppContext() { | ||||
|         // Context of the app under test.
 | ||||
|         Context appContext = InstrumentationRegistry.getTargetContext(); | ||||
| 
 | ||||
|         assertEquals("com.zwy.nsfw.test", appContext.getPackageName()); | ||||
|     } | ||||
| } | ||||
| @ -1,2 +0,0 @@ | ||||
| <manifest package="com.zwy.nsfw" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
| </manifest> | ||||
| @ -1,42 +0,0 @@ | ||||
| package com.zwy.nsfw.api | ||||
| 
 | ||||
| import android.graphics.Bitmap | ||||
| import com.zwy.nsfw.core.Classifier | ||||
| import com.zwy.nsfw.core.NSFWConfig | ||||
| import com.zwy.nsfw.core.NsfwBean | ||||
| 
 | ||||
| object NSFWHelper { | ||||
| 
 | ||||
|     private lateinit var classifier: Classifier | ||||
| 
 | ||||
|     private var isInit = false | ||||
| 
 | ||||
|     @Synchronized | ||||
|     fun init(nsfwConfig: NSFWConfig): NSFWHelper { | ||||
|         classifier = Classifier.create(nsfwConfig.assetMannager, nsfwConfig.userGPU, 10) | ||||
|         isInit = true | ||||
|         return this | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 同步扫描图片(建议使用者自己放在线程中处理) | ||||
|      */ | ||||
|     @Synchronized | ||||
|     fun scanBitmap(bitmap: Bitmap): NsfwBean { | ||||
|         if (!isInit) { | ||||
|             throw ExceptionInInitializerError("Initialize the scanned object 'NSFWHelper.init(nsfwConfig: NSFWConfig)' by calling the function first.") | ||||
|         } | ||||
|         return classifier.run(bitmap) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * 不会销毁该单利和配置的参数,建议页面处理完成后调用该代码对扫描器进行关闭,关闭后下次扫描不需要再次调用init方法 | ||||
|      */ | ||||
|     fun destroyFactory() { | ||||
|         classifier.close() | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| @ -1,197 +0,0 @@ | ||||
| package com.zwy.nsfw.core | ||||
| 
 | ||||
| import android.content.res.AssetManager | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.Color | ||||
| import android.os.SystemClock | ||||
| import android.util.Log | ||||
| import org.tensorflow.lite.Interpreter | ||||
| import org.tensorflow.lite.gpu.GpuDelegate | ||||
| import java.io.FileInputStream | ||||
| import java.lang.Math.max | ||||
| import java.nio.ByteBuffer | ||||
| import java.nio.ByteOrder | ||||
| import java.nio.MappedByteBuffer | ||||
| import java.nio.channels.FileChannel | ||||
| 
 | ||||
| class Classifier | ||||
| private constructor(assetManager: AssetManager, isGPU: Boolean?, numThreads: Int) { | ||||
| 
 | ||||
|     /** | ||||
|      * tensor input img size | ||||
|      */ | ||||
|     private val INPUT_WIDTH = 224 | ||||
|     private val INPUT_HEIGHT = 224 | ||||
| 
 | ||||
|     /** | ||||
|      * BytesPerChannel | ||||
|      */ | ||||
|     private val BYTES_PER_CHANNEL_NUM = 4 | ||||
| 
 | ||||
|     /** | ||||
|      * Preallocated buffers for storing image data in. | ||||
|      */ | ||||
|     private val intValues = IntArray(INPUT_WIDTH * INPUT_HEIGHT) | ||||
|     /** | ||||
|      * The loaded TensorFlow Lite model. | ||||
|      */ | ||||
|     private var tfliteModel: MappedByteBuffer? = null | ||||
| 
 | ||||
|     /** | ||||
|      * Optional GPU delegate for accleration. | ||||
|      */ | ||||
|     private var gpuDelegate: GpuDelegate? = null | ||||
| 
 | ||||
|     /** | ||||
|      * An instance of the driver class to run model inference with Tensorflow Lite. | ||||
|      */ | ||||
|     private var tflite: Interpreter? = null | ||||
| 
 | ||||
|     /** | ||||
|      * A ByteBuffer to hold image data, to be feed into Tensorflow Lite as inputs. | ||||
|      */ | ||||
|     private val imgData: ByteBuffer? | ||||
| 
 | ||||
|     init { | ||||
|         tfliteModel = loadModelFile(assetManager) | ||||
|         val tfliteOptions = Interpreter.Options() | ||||
|         if (isGPU == true) { | ||||
|             gpuDelegate = GpuDelegate() | ||||
|             tfliteOptions.addDelegate(gpuDelegate) | ||||
|         } | ||||
|         tfliteOptions.setNumThreads(numThreads) | ||||
|         tflite = Interpreter(tfliteModel!!, tfliteOptions) | ||||
| 
 | ||||
|         val tensor = tflite!!.getInputTensor(tflite!!.getInputIndex("input")) | ||||
|         val stringBuilder = (" \n" | ||||
|                 + "dataType : " + | ||||
|                 tensor.dataType() + | ||||
|                 "\n" + | ||||
|                 "numBytes : " + | ||||
|                 tensor.numBytes() + | ||||
|                 "\n" + | ||||
|                 "numDimensions : " + | ||||
|                 tensor.numDimensions() + | ||||
|                 "\n" + | ||||
|                 "numElements : " + | ||||
|                 tensor.numElements() + | ||||
|                 "\n" + | ||||
|                 "shape : " + | ||||
|                 tensor.shape().size) | ||||
|         Log.d(TAG, stringBuilder) | ||||
| 
 | ||||
|         imgData = ByteBuffer.allocateDirect( | ||||
|             DIM_BATCH_SIZE | ||||
|                     * INPUT_WIDTH | ||||
|                     * INPUT_HEIGHT | ||||
|                     * DIM_PIXEL_SIZE | ||||
|                     * BYTES_PER_CHANNEL_NUM | ||||
|         ) | ||||
| 
 | ||||
|         imgData!!.order(ByteOrder.LITTLE_ENDIAN) | ||||
|         Log.d(TAG, "Tensorflow Lite Image Classifier Initialization Success.") | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Memory-map the model file in Assets. | ||||
|      */ | ||||
|     private fun loadModelFile(assetManager: AssetManager): MappedByteBuffer { | ||||
|         val fileDescriptor = assetManager.openFd("nsfw.tflite") | ||||
|         val inputStream = FileInputStream(fileDescriptor.fileDescriptor) | ||||
|         val fileChannel = inputStream.channel | ||||
|         val startOffset = fileDescriptor.startOffset | ||||
|         val declaredLength = fileDescriptor.declaredLength | ||||
|         return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Writes Image data into a `ByteBuffer`. | ||||
|      */ | ||||
|     private fun convertBitmapToByteBuffer(bitmap_: Bitmap) { | ||||
|         if (imgData == null || bitmap_ == null) { | ||||
|             return | ||||
|         } | ||||
|         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 | ||||
|         bitmap_.getPixels(intValues, 0, INPUT_WIDTH, h_off, w_off, INPUT_WIDTH, INPUT_HEIGHT) | ||||
|         // Convert the image to floating point. | ||||
|         val startTime = SystemClock.uptimeMillis() | ||||
|         for (color in intValues) { | ||||
|             val r1 = Color.red(color) | ||||
|             val g1 = Color.green(color) | ||||
|             val b1 = Color.blue(color) | ||||
| 
 | ||||
|             val rr1 = r1 - 123 | ||||
|             val gg1 = g1 - 117 | ||||
|             val bb1 = b1 - 104 | ||||
| 
 | ||||
|             imgData.putFloat(bb1.toFloat()) | ||||
|             imgData.putFloat(gg1.toFloat()) | ||||
|             imgData.putFloat(rr1.toFloat()) | ||||
|         } | ||||
|         val endTime = SystemClock.uptimeMillis() | ||||
|         Log.d(TAG, "Timecost to put values into ByteBuffer: " + (endTime - startTime) + "ms") | ||||
|     } | ||||
| 
 | ||||
|     fun run(bitmap: Bitmap): NsfwBean { | ||||
| 
 | ||||
|         val bitmap_256 = Bitmap.createScaledBitmap(bitmap, 256, 256, true) | ||||
| 
 | ||||
|         //Writes image data into byteBuffer | ||||
|         convertBitmapToByteBuffer(bitmap_256) | ||||
| 
 | ||||
|         val startTime = SystemClock.uptimeMillis() | ||||
|         // out | ||||
|         val outArray = Array(1) { FloatArray(2) } | ||||
| 
 | ||||
|         tflite!!.run(imgData, outArray) | ||||
| 
 | ||||
|         val endTime = SystemClock.uptimeMillis() | ||||
| 
 | ||||
|         Log.d(TAG, "SFW score :" + outArray[0][0] + ",NSFW score :" + outArray[0][1]) | ||||
|         Log.d(TAG, "Timecost to run model inference: " + (endTime - startTime) + "ms") | ||||
|         return NsfwBean(outArray[0][0], outArray[0][1]) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Closes the interpreter and model to release resources. | ||||
|      */ | ||||
|     fun close() { | ||||
|         if (tflite != null) { | ||||
|             tflite!!.close() | ||||
|             tflite = null | ||||
|             Log.d(TAG, "Tensorflow Lite Image Classifier close.") | ||||
|         } | ||||
|         if (gpuDelegate != null) { | ||||
|             gpuDelegate!!.close() | ||||
|             Log.d(TAG, "Tensorflow Lite Image gpuDelegate close.") | ||||
|             gpuDelegate = null | ||||
|         } | ||||
|         tfliteModel = null | ||||
|         Log.d(TAG, "Tensorflow Lite destroyed.") | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
| 
 | ||||
|         val TAG = "open_nsfw_android" | ||||
|         /** | ||||
|          * Dimensions of inputs. | ||||
|          */ | ||||
|         private val DIM_BATCH_SIZE = 1 | ||||
| 
 | ||||
|         private val DIM_PIXEL_SIZE = 3 | ||||
| 
 | ||||
|         fun create(assetManager: AssetManager, isAddGpuDelegate: Boolean?, numThreads: Int): Classifier { | ||||
|             return Classifier(assetManager, isAddGpuDelegate!!, numThreads) | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| package com.zwy.nsfw.core | ||||
| 
 | ||||
| import android.content.res.AssetManager | ||||
| 
 | ||||
| /** | ||||
|  * 配置扫描参数 | ||||
|  * [assetMannager]资源管理器,用于读取lib下的tfLite文件 | ||||
|  * [userGPU]是否开启GPU加速 默认关闭,部分手机开启后会有奔溃 | ||||
|  */ | ||||
| data class NSFWConfig(val assetMannager: AssetManager, val userGPU: Boolean = false) | ||||
| @ -1,3 +0,0 @@ | ||||
| package com.zwy.nsfw.core | ||||
| 
 | ||||
| class NsfwBean(val sfw: Float, val nsfw: Float) | ||||
| @ -1,28 +0,0 @@ | ||||
| package com.zwy.nsfw.kotlin | ||||
| 
 | ||||
| import android.content.res.AssetManager | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.BitmapFactory | ||||
| import com.zwy.nsfw.api.NSFWHelper | ||||
| import com.zwy.nsfw.core.NSFWConfig | ||||
| import com.zwy.nsfw.core.NsfwBean | ||||
| import java.io.File | ||||
| 
 | ||||
| 
 | ||||
| fun Bitmap.getNsfwScore(mAssetManager: AssetManager): NsfwBean { | ||||
|     val nsfwBean = NSFWHelper.init(NSFWConfig(mAssetManager)).scanBitmap(this) | ||||
|     NSFWHelper.destroyFactory() | ||||
|     return nsfwBean | ||||
| } | ||||
| 
 | ||||
| fun File.getNsfwScore(mAssetManager: AssetManager): NsfwBean { | ||||
|     val bitmap = try { | ||||
|         BitmapFactory.decodeFile(this.path) | ||||
|     } catch (e: Exception) { | ||||
|         return NsfwBean(0f, 0f) | ||||
|     } | ||||
| 
 | ||||
|     val nsfwBean = NSFWHelper.init(NSFWConfig(mAssetManager)).scanBitmap(bitmap) | ||||
|     NSFWHelper.destroyFactory() | ||||
|     return nsfwBean | ||||
| } | ||||
| @ -1,3 +0,0 @@ | ||||
| <resources> | ||||
|     <string name="app_name">nsfw</string> | ||||
| </resources> | ||||
| @ -1,17 +0,0 @@ | ||||
| package com.zwy.nsfw; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| /** | ||||
|  * Example local unit test, which will execute on the development machine (host). | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| public class ExampleUnitTest { | ||||
|     @Test | ||||
|     public void addition_isCorrect() { | ||||
|         assertEquals(4, 2 + 2); | ||||
|     } | ||||
| } | ||||
| @ -1 +1 @@ | ||||
| include ':app', ':nsfw' | ||||
| include ':app', ':OpenNSFW' | ||||
|  | ||||
					Loading…
					
					
				
		Reference in new issue