Commit d1022b52 authored by wanglei's avatar wanglei

...

parent 7d986ea7
......@@ -2,14 +2,8 @@ package com.base.datarecovery.activity
import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.os.Environment
import android.util.Log
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
......
......@@ -8,12 +8,10 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.base.datarecovery.activity.repeat.RepeatActivity
import com.base.datarecovery.activity.screenshot.ScreenShotActivity
import com.base.datarecovery.bean.MediaBean
import com.base.datarecovery.databinding.ActivityPhotoManagerBinding
import com.base.datarecovery.help.BaseActivity
import com.base.datarecovery.help.FileHelp.getDirFiles
import com.base.datarecovery.help.KotlinExt.toFormatSize
import com.base.datarecovery.help.MediaStoreHelp.getImageMedia
import com.base.datarecovery.help.PermissionHelp.checkStorePermission
import com.base.datarecovery.help.PermissionHelp.requestStorePermission
import com.base.datarecovery.utils.BarUtils
......@@ -29,14 +27,12 @@ class PhotoManagerActivity : BaseActivity<ActivityPhotoManagerBinding>() {
ActivityPhotoManagerBinding.inflate(layoutInflater)
}
override fun initView() {
BarUtils.setStatusBarLightMode(this, true)
BarUtils.setStatusBarColor(this, Color.WHITE)
binding.root.updatePadding(top = BarUtils.getStatusBarHeight())
if (checkStorePermission()) {
initDataSize()
} else {
showGerPermission(desc = "This feature requires access to your storage to scan your files and clean up screenshots. We will not transmit your data to any third-party service. Please grant permission so that we can provide you with better service.",
......@@ -75,9 +71,8 @@ class PhotoManagerActivity : BaseActivity<ActivityPhotoManagerBinding>() {
getDirFiles(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)).filter { isImage(it) }
.sumOf { it.length() }.toFormatSize()
val list = arrayListOf<MediaBean>()
getImageMedia(list)
val photoSize = (list.map { File(it.path) }.sumOf { it.length() } / 2).toFormatSize()
RepeatActivity.beanList.addAll(RepeatActivity.getSimilarList(this@PhotoManagerActivity))
val photoSize = RepeatActivity.beanList.sumOf { it.beans.sumOf { s -> File(s.path).length() } }.toFormatSize()
launch(Dispatchers.Main) {
binding.tvScreenshotSize.text = dcimSize
......
package com.base.datarecovery.activity.repeat
import android.content.Context
import android.graphics.Color
import android.view.View
import androidx.activity.addCallback
......@@ -7,10 +8,7 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.base.datarecovery.adapter.MediaAdapter
import com.base.datarecovery.ads.AdmobMaxHelper
import com.base.datarecovery.ads.admob.AdmobInterstitialUtils
import com.base.datarecovery.ads.admob.AdmobInterstitialUtils.showInterAdSp
import com.base.datarecovery.ads.admob.AdmobNativeUtils
import com.base.datarecovery.ads.max.AdMaxInterstitialUtils
import com.base.datarecovery.bean.MediaBean
import com.base.datarecovery.bean.MediaTimeBean
import com.base.datarecovery.databinding.ActivityRepeatBinding
......@@ -22,6 +20,7 @@ import com.base.datarecovery.help.MediaStoreHelp.getImageMedia
import com.base.datarecovery.help.PermissionHelp.checkStorePermission
import com.base.datarecovery.help.PermissionHelp.requestStorePermission
import com.base.datarecovery.utils.BarUtils
import com.base.datarecovery.utils.SimilarHelper.calculateSimilar
import com.base.datarecovery.view.DialogViews.showDeletePermanentlyDialog
import com.base.datarecovery.view.DialogViews.showExitFunctionDialog
import com.base.datarecovery.view.DialogViews.showGerPermission
......@@ -52,6 +51,12 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() {
}
}
binding.rv.adapter = mediaAdapter
if (beanList.isNotEmpty()) {
binding.progressBar.visibility = View.GONE
binding.tvScanning.visibility = View.GONE
binding.flScanning.visibility = View.GONE
mediaAdapter.setData(beanList)
}
if (checkStorePermission()) {
initData()
......@@ -118,22 +123,18 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() {
}
private fun initData() {
if (beanList.isNotEmpty()) {
return
}
lifecycleScope.launch(Dispatchers.IO) {
val list = arrayListOf<MediaBean>()
getImageMedia(list)
val hashMap = HashMap<String, ArrayList<MediaBean>>()
list.forEach {
val time = it.time.toFormatTime()
if (hashMap[time] == null) {
hashMap[time] = arrayListOf()
}
hashMap[time]?.add(it)
}
val beanList = hashMap.map { MediaTimeBean(it.key, it.value) }
val beanList = getSimilarList(this@RepeatActivity)
launch(Dispatchers.Main) {
binding.progressBar.visibility = View.GONE
binding.tvScanning.visibility = View.GONE
binding.flScanning.visibility = View.GONE
mediaAdapter.setData(beanList)
if (beanList.sumOf { it.beans.size } > 6 || ConfigHelper.mustShowNativeAd) {
AdmobMaxHelper.admobMaxShowNativeAd(this@RepeatActivity, binding.flAd)
......@@ -143,4 +144,26 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() {
}
companion object {
var beanList: ArrayList<MediaTimeBean> = ArrayList()
fun getSimilarList(context: Context): List<MediaTimeBean> {
val list = arrayListOf<MediaBean>()
context.getImageMedia(list)
val similarList = calculateSimilar(list)
val beanList = similarList.map { map ->
val time = File(map.key).lastModified().toFormatTime()
MediaTimeBean(time = time, beans = map.value)
}
return beanList
}
}
override fun onDestroy() {
super.onDestroy()
beanList.clear()
}
}
\ No newline at end of file
......@@ -7,7 +7,6 @@ import com.applovin.mediation.MaxError
import com.applovin.mediation.ads.MaxAppOpenAd
import com.base.datarecovery.MyApplication.Companion.isInterOpenShowing
import com.base.datarecovery.ads.AdDisplayUtils
import com.base.datarecovery.ads.AdmobMaxHelper
import com.base.datarecovery.ads.AdmobMaxHelper.isAdInit
import com.base.datarecovery.ads.admob.AdmobCommonUtils
import com.base.datarecovery.help.ConfigHelper
......
package com.base.datarecovery.utils
import android.graphics.BitmapFactory
import android.provider.MediaStore.Audio.Radio
import com.base.datarecovery.bean.MediaBean
import com.base.datarecovery.utils.TestSimilar.testSimilar
//import com.base.filerecoveryrecyclebin.utils.OpencvImageHelp.opencvCompareSimilar
import java.io.File
import kotlin.random.Random
object SimilarHelper {
private val TAG = "SimilarHelper"
fun calculateSimilar(list: List<MediaBean>): HashMap<String, ArrayList<MediaBean>> {
val result = HashMap<String, ArrayList<MediaBean>>()
val eachArrayList = arrayListOf<MediaBean>().apply {
addAll(list)
}
val haveComperedList: ArrayList<String> = arrayListOf()
val iterator = list.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
LogEx.logDebug(TAG, "next item=$item")
LogEx.logDebug(TAG, "eachArrayList=${eachArrayList.size}")
if (!haveComperedList.contains(item.path)) {
val compareIterator = eachArrayList.iterator()
while (compareIterator.hasNext()) {
val compareItem = compareIterator.next()
// LogEx.logDebug(TAG, "compareItem=$compareItem")
if (item.path != compareItem.path) {
// val percent = opencvCompareSimilar(item.path, compareItem.path)
// val percent = similarPercent(File(item.path), File(compareItem.path))
val isSimilar = testSimilar(item.path, compareItem.path)
LogEx.logDebug(TAG, "isSimilar=$isSimilar")
if (isSimilar) {
if (result[item.path] == null) {
LogEx.logDebug(TAG, "item=$item")
result[item.path] = arrayListOf()
}
if (result[item.path]?.isEmpty() == true) {
result[item.path]?.add(item)
}
result[item.path]?.add(compareItem)
haveComperedList.add(compareItem.path)
compareIterator.remove()
}
} else {
compareIterator.remove()
}
}
}
}
return result
}
// private fun similarPercent(srcFile: File, compareFile: File): Int {
// var similarityScore: Double = 0.00
// try {
// val bitmap1 = BitmapFactory.decodeFile(srcFile.absolutePath)
// val bitmap2 = BitmapFactory.decodeFile(compareFile.absolutePath)
//
// if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) {
// return 0 // 如果尺寸不同,则直接返回0
// }
//
// var totalDifference = 0L
// for (x in 0 until bitmap1.width) {
// for (y in 0 until bitmap1.height) {
// val pixel1 = bitmap1.getPixel(x, y)
// val pixel2 = bitmap2.getPixel(x, y)
// val redDifference = (pixel1 shr 16 and 0xff) - (pixel2 shr 16 and 0xff)
// val greenDifference = (pixel1 shr 8 and 0xff) - (pixel2 shr 8 and 0xff)
// val blueDifference = (pixel1 and 0xff) - (pixel2 and 0xff)
// totalDifference += (redDifference * redDifference + greenDifference * greenDifference + blueDifference * blueDifference)
// }
// }
//
// // 计算相似度得分
// val maxDifference = bitmap1.width * bitmap1.height * 3 * 255 * 255
// similarityScore = (1 - totalDifference.toDouble() / maxDifference) * 100
//
// } catch (e: Exception) {
// e.printStackTrace()
// }
//
// // 将相似度得分四舍五入到整数
// return similarityScore.toInt().coerceIn(0, 100)
// }
// fun similarPercent(srcFile: File, compareFile: File): Int {
// val bitmap1 = BitmapFactory.decodeFile(srcFile.absolutePath)
// val bitmap2 = BitmapFactory.decodeFile(compareFile.absolutePath)
//
// if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) {
// return 0 // 如果尺寸不同,则直接返回0
// }
//
// val width = bitmap1.width
// val height = bitmap1.height
// val pixels1 = IntArray(width * height)
// val pixels2 = IntArray(width * height)
//
// bitmap1.getPixels(pixels1, 0, width, 0, 0, width, height)
// bitmap2.getPixels(pixels2, 0, width, 0, 0, width, height)
//
// var totalDifference = 0L
// for (i in pixels1.indices) {
// val pixel1 = pixels1[i]
// val pixel2 = pixels2[i]
// if (pixel1 != pixel2) {
// val redDifference = (pixel1 shr 16 and 0xff) - (pixel2 shr 16 and 0xff)
// val greenDifference = (pixel1 shr 8 and 0xff) - (pixel2 shr 8 and 0xff)
// val blueDifference = (pixel1 and 0xff) - (pixel2 and 0xff)
// totalDifference += (redDifference * redDifference + greenDifference * greenDifference + blueDifference * blueDifference)
// }
// }
//
// val maxDifference = width * height * 3 * 255 * 255
// val similarityScore = (1 - totalDifference.toDouble() / maxDifference) * 100
//
// return similarityScore.toInt().coerceIn(0, 100)
// }
}
\ No newline at end of file
package com.base.datarecovery.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import kotlin.math.sqrt
object TestSimilar {
private val TAG = "TestSimilar"
fun testSimilar(srcPath: String, comparePath: String): Boolean {
//低内存加载
val srcLowBitmap: Bitmap? = loadBitmapWithLowMemory(srcPath)
val compareLowBitmap: Bitmap? = loadBitmapWithLowMemory(comparePath)
if (srcLowBitmap == null || compareLowBitmap == null) {
return false
}
try {
//缩放
val srcScaledBitmap = Bitmap.createScaledBitmap(srcLowBitmap, 16, 16, true)
val compareScaledBitmap = Bitmap.createScaledBitmap(compareLowBitmap, 16, 16, true)
LogEx.logDebug(TAG, "finish to Scaled with=${srcScaledBitmap.width} height=${srcScaledBitmap.height}")
//转数组
val srcBitmapArray = bitmapToFeatureIntArray(srcScaledBitmap)
val compareBitmapArray = bitmapToFeatureIntArray(compareScaledBitmap)
srcBitmapArray.forEach {
LogEx.logDebug(TAG, "finish to IntArray $it")
}
//dct转换
val srcDctArray = dct16x16(srcBitmapArray)
val compareDctArray = dct16x16(compareBitmapArray)
LogEx.logDebug(TAG, "finish to DctArray")
//感知hash转换
val srcHash = generatePerceptualHash(srcDctArray)
val compareHash = generatePerceptualHash(compareDctArray)
LogEx.logDebug(TAG, "srcHash=$srcHash compareHash=$compareHash")
//对比
return isImagesSimilar(srcHash, compareHash, 5)
} catch (e: Exception) {
LogEx.logDebug(TAG, "$e")
} finally {
srcLowBitmap.recycle()
srcLowBitmap.recycle()
}
return false
}
private fun loadBitmapWithLowMemory(filePath: String, reqWidth: Int = 16, reqHeight: Int = 16): Bitmap? {
// 设置BitmapFactory.Options的inJustDecodeBounds为true
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, this)
inJustDecodeBounds = false
// 计算inSampleSize值
// inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
// 使用RGB_565配置以减少内存使用
inPreferredConfig = Bitmap.Config.RGB_565
}
// 使用计算得到的options加载Bitmap
return BitmapFactory.decodeFile(filePath)
}
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// 原始图像的宽度和高度
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
// 计算出需要的inSampleSize值
while (height / inSampleSize > reqHeight || width / inSampleSize > reqWidth) {
inSampleSize *= 2
}
return inSampleSize
}
private fun isImagesSimilar(hash1: String, hash2: String, threshold: Int): Boolean {
return calculateHammingDistance(hash1, hash2) <= threshold
}
private fun calculateHammingDistance(hash1: String, hash2: String): Int {
var distance = 0
for (i in hash1.indices) {
if (hash1[i] != hash2[i]) distance++
}
return distance
}
fun bitmapToFeatureIntArray(bitmap: Bitmap): IntArray {
// 创建一个整型数组来存储特征,这里我们假设每个像素一个特征值
val featureSize = bitmap.width * bitmap.height
val featureIntArray = IntArray(featureSize)
// 遍历Bitmap的每个像素
var index = 0
for (x in 0 until bitmap.width) {
for (y in 0 until bitmap.height) {
val pixel = bitmap.getPixel(x, y)
// 计算灰度值
val grayValue = getGrayscaleValue(pixel)
// 存储灰度值到整型数组中
featureIntArray[index++] = grayValue
}
}
return featureIntArray
}
// 辅助函数,用于计算给定像素的灰度值
fun getGrayscaleValue(color: Int): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
// 使用加权和来计算灰度值
return ((0.299 * red + 0.587 * green + 0.114 * blue).toInt() shl 24) or (color and 0x00FFFFFF)
}
fun dct16x16(block: IntArray): DoubleArray {
val N = 16 // 变换的尺寸
val scaledBlock = DoubleArray(N * N)
for (u in 0 until N) {
for (v in 0 until N) {
var sum = 0.0
for (x in 0 until N) {
for (y in 0 until N) {
// 应用DCT变换公式
sum += block[x * N + y] *
Math.cos((2.0 * x + 1.0) * u * Math.PI / (2.0 * N)) *
Math.cos((2.0 * y + 1.0) * v * Math.PI / (2.0 * N))
}
}
// 标准化因子,当u或v为0时,使用1/sqrt(N),否则使用1/N
val norm = if (u == 0 && v == 0) (1.0 / sqrt(N.toDouble())) else (1.0 / N)
scaledBlock[u * N + v] = sum * norm
}
}
return scaledBlock
}
private fun generatePerceptualHash(dctCoefficients: DoubleArray): String {
// 1. 截断DCT系数,假设dctCoefficients已经是16x16的DCT系数
// 保留8x8的系数块,这里简化处理,只取前64个系数
val truncatedDctCoefficients = dctCoefficients.take(64)
// 2. 量化DCT系数,将系数映射到0-1范围,然后四舍五入到最近的整数
val quantizedCoefficients = truncatedDctCoefficients.map {
((it + 128) / 255 * 0.5).coerceIn(0.0, 1.0).toInt()
}
// 3. 生成哈希值,将量化后的系数转换为二进制字符串
val hashBuilder = StringBuilder()
quantizedCoefficients.forEach {
// 将每个量化系数转换为二进制表示,并添加到哈希构建器中
hashBuilder.append(it.toString(2))
}
// 返回最终的哈希值字符串
return hashBuilder.toString()
}
}
\ No newline at end of file
......@@ -131,11 +131,32 @@
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ProgressBar
android:id="@+id/progress_bar"
<LinearLayout
android:id="@+id/fl_scanning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:layout_gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
<TextView
android:id="@+id/tv_scanning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:text="Scanning for similar photos, please wait..."
android:textColor="@color/black"
android:textSize="17sp"
tools:ignore="HardcodedText" />
</LinearLayout>
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment