Commit d1022b52 authored by wanglei's avatar wanglei

...

parent 7d986ea7
...@@ -2,14 +2,8 @@ package com.base.datarecovery.activity ...@@ -2,14 +2,8 @@ package com.base.datarecovery.activity
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.Log
import androidx.activity.addCallback 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.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
......
...@@ -8,12 +8,10 @@ import androidx.core.view.updatePadding ...@@ -8,12 +8,10 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.base.datarecovery.activity.repeat.RepeatActivity import com.base.datarecovery.activity.repeat.RepeatActivity
import com.base.datarecovery.activity.screenshot.ScreenShotActivity import com.base.datarecovery.activity.screenshot.ScreenShotActivity
import com.base.datarecovery.bean.MediaBean
import com.base.datarecovery.databinding.ActivityPhotoManagerBinding import com.base.datarecovery.databinding.ActivityPhotoManagerBinding
import com.base.datarecovery.help.BaseActivity import com.base.datarecovery.help.BaseActivity
import com.base.datarecovery.help.FileHelp.getDirFiles import com.base.datarecovery.help.FileHelp.getDirFiles
import com.base.datarecovery.help.KotlinExt.toFormatSize 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.checkStorePermission
import com.base.datarecovery.help.PermissionHelp.requestStorePermission import com.base.datarecovery.help.PermissionHelp.requestStorePermission
import com.base.datarecovery.utils.BarUtils import com.base.datarecovery.utils.BarUtils
...@@ -29,14 +27,12 @@ class PhotoManagerActivity : BaseActivity<ActivityPhotoManagerBinding>() { ...@@ -29,14 +27,12 @@ class PhotoManagerActivity : BaseActivity<ActivityPhotoManagerBinding>() {
ActivityPhotoManagerBinding.inflate(layoutInflater) ActivityPhotoManagerBinding.inflate(layoutInflater)
} }
override fun initView() { override fun initView() {
BarUtils.setStatusBarLightMode(this, true) BarUtils.setStatusBarLightMode(this, true)
BarUtils.setStatusBarColor(this, Color.WHITE) BarUtils.setStatusBarColor(this, Color.WHITE)
binding.root.updatePadding(top = BarUtils.getStatusBarHeight()) binding.root.updatePadding(top = BarUtils.getStatusBarHeight())
if (checkStorePermission()) { if (checkStorePermission()) {
initDataSize() initDataSize()
} else { } 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.", 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>() { ...@@ -75,9 +71,8 @@ class PhotoManagerActivity : BaseActivity<ActivityPhotoManagerBinding>() {
getDirFiles(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)).filter { isImage(it) } getDirFiles(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)).filter { isImage(it) }
.sumOf { it.length() }.toFormatSize() .sumOf { it.length() }.toFormatSize()
val list = arrayListOf<MediaBean>() RepeatActivity.beanList.addAll(RepeatActivity.getSimilarList(this@PhotoManagerActivity))
getImageMedia(list) val photoSize = RepeatActivity.beanList.sumOf { it.beans.sumOf { s -> File(s.path).length() } }.toFormatSize()
val photoSize = (list.map { File(it.path) }.sumOf { it.length() } / 2).toFormatSize()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
binding.tvScreenshotSize.text = dcimSize binding.tvScreenshotSize.text = dcimSize
......
package com.base.datarecovery.activity.repeat package com.base.datarecovery.activity.repeat
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.view.View import android.view.View
import androidx.activity.addCallback import androidx.activity.addCallback
...@@ -7,10 +8,7 @@ import androidx.core.view.updatePadding ...@@ -7,10 +8,7 @@ import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.base.datarecovery.adapter.MediaAdapter import com.base.datarecovery.adapter.MediaAdapter
import com.base.datarecovery.ads.AdmobMaxHelper 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.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.MediaBean
import com.base.datarecovery.bean.MediaTimeBean import com.base.datarecovery.bean.MediaTimeBean
import com.base.datarecovery.databinding.ActivityRepeatBinding import com.base.datarecovery.databinding.ActivityRepeatBinding
...@@ -22,6 +20,7 @@ import com.base.datarecovery.help.MediaStoreHelp.getImageMedia ...@@ -22,6 +20,7 @@ import com.base.datarecovery.help.MediaStoreHelp.getImageMedia
import com.base.datarecovery.help.PermissionHelp.checkStorePermission import com.base.datarecovery.help.PermissionHelp.checkStorePermission
import com.base.datarecovery.help.PermissionHelp.requestStorePermission import com.base.datarecovery.help.PermissionHelp.requestStorePermission
import com.base.datarecovery.utils.BarUtils 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.showDeletePermanentlyDialog
import com.base.datarecovery.view.DialogViews.showExitFunctionDialog import com.base.datarecovery.view.DialogViews.showExitFunctionDialog
import com.base.datarecovery.view.DialogViews.showGerPermission import com.base.datarecovery.view.DialogViews.showGerPermission
...@@ -52,6 +51,12 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() { ...@@ -52,6 +51,12 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() {
} }
} }
binding.rv.adapter = mediaAdapter 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()) { if (checkStorePermission()) {
initData() initData()
...@@ -118,22 +123,18 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() { ...@@ -118,22 +123,18 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() {
} }
private fun initData() { private fun initData() {
if (beanList.isNotEmpty()) {
return
}
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val list = arrayListOf<MediaBean>()
getImageMedia(list)
val hashMap = HashMap<String, ArrayList<MediaBean>>() val beanList = getSimilarList(this@RepeatActivity)
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) }
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.tvScanning.visibility = View.GONE
binding.flScanning.visibility = View.GONE
mediaAdapter.setData(beanList) mediaAdapter.setData(beanList)
if (beanList.sumOf { it.beans.size } > 6 || ConfigHelper.mustShowNativeAd) { if (beanList.sumOf { it.beans.size } > 6 || ConfigHelper.mustShowNativeAd) {
AdmobMaxHelper.admobMaxShowNativeAd(this@RepeatActivity, binding.flAd) AdmobMaxHelper.admobMaxShowNativeAd(this@RepeatActivity, binding.flAd)
...@@ -143,4 +144,26 @@ class RepeatActivity : BaseActivity<ActivityRepeatBinding>() { ...@@ -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 ...@@ -7,7 +7,6 @@ import com.applovin.mediation.MaxError
import com.applovin.mediation.ads.MaxAppOpenAd import com.applovin.mediation.ads.MaxAppOpenAd
import com.base.datarecovery.MyApplication.Companion.isInterOpenShowing import com.base.datarecovery.MyApplication.Companion.isInterOpenShowing
import com.base.datarecovery.ads.AdDisplayUtils import com.base.datarecovery.ads.AdDisplayUtils
import com.base.datarecovery.ads.AdmobMaxHelper
import com.base.datarecovery.ads.AdmobMaxHelper.isAdInit import com.base.datarecovery.ads.AdmobMaxHelper.isAdInit
import com.base.datarecovery.ads.admob.AdmobCommonUtils import com.base.datarecovery.ads.admob.AdmobCommonUtils
import com.base.datarecovery.help.ConfigHelper 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 @@ ...@@ -131,11 +131,32 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ProgressBar <LinearLayout
android:id="@+id/progress_bar" android:id="@+id/fl_scanning"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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> </FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout <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