init
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
||||
55
app/build.gradle.kts
Normal file
@ -0,0 +1,55 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
id ("kotlin-kapt")
|
||||
id ("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.ux.video.file.filerecovery"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.ux.video.file.filerecovery"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures{
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
implementation (libs.glide)
|
||||
kapt (libs.compiler)
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@ -0,0 +1,24 @@
|
||||
package com.ux.video.file.filerecovery
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.ux.video.file.filerecovery", appContext.packageName)
|
||||
}
|
||||
}
|
||||
53
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Android 10 及以下 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" /> <!-- Android 11+ -->
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
|
||||
tools:targetApi="31">
|
||||
<!-- android:theme="@style/Theme.FileRecovery"-->
|
||||
<activity
|
||||
android:name=".photo.PhotoSortingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".result.ScanResultDisplayActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".result.ScanningActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".main.ScanSelectTypeActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".documents.DocumentsScanResultActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".main.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/assets/fonts/PingFang Bold_0.ttf
Normal file
BIN
app/src/main/assets/fonts/PingFang Regular_0.ttf
Normal file
@ -0,0 +1,35 @@
|
||||
package com.ux.video.file.filerecovery.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.ux.video.file.filerecovery.R
|
||||
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: VB
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
binding = inflateBinding(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
initView()
|
||||
initData()
|
||||
}
|
||||
|
||||
protected abstract fun inflateBinding(inflater: LayoutInflater): VB
|
||||
|
||||
protected open fun initView() {}
|
||||
|
||||
protected open fun initData() {}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.ux.video.file.filerecovery.base
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlin.let
|
||||
|
||||
abstract class BaseAdapter<K, T : ViewBinding>(
|
||||
protected val mContext: Context
|
||||
) : RecyclerView.Adapter<BaseAdapter.VHolder<T>>() {
|
||||
|
||||
protected val data: MutableList<K> = mutableListOf()
|
||||
|
||||
|
||||
|
||||
fun addData(items: List<K>?) {
|
||||
items?.let {
|
||||
val start = data.size
|
||||
data.addAll(it)
|
||||
notifyItemRangeInserted(start, it.size)
|
||||
}
|
||||
}
|
||||
|
||||
fun setData(items: List<K>?) {
|
||||
data.clear()
|
||||
items?.let { data.addAll(it) }
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VHolder<T> {
|
||||
return VHolder(getViewBinding(parent))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VHolder<T>, position: Int) {
|
||||
bindItem(holder, data[position])
|
||||
}
|
||||
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup): T
|
||||
protected abstract fun bindItem(holder: VHolder<T>, item: K)
|
||||
|
||||
|
||||
class VHolder<V : ViewBinding>(val vb: V) : RecyclerView.ViewHolder(vb.root)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.ux.video.file.filerecovery.documents
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityDocumentsScanResultBinding
|
||||
import com.ux.video.file.filerecovery.utils.ScanRepository
|
||||
|
||||
class DocumentsScanResultActivity : BaseActivity<ActivityDocumentsScanResultBinding>() {
|
||||
|
||||
|
||||
private var resultAdapter: DocumentsScanResultAdapter? = null
|
||||
override fun inflateBinding(inflater: LayoutInflater): ActivityDocumentsScanResultBinding =
|
||||
ActivityDocumentsScanResultBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initView() {
|
||||
super.initView()
|
||||
resultAdapter = DocumentsScanResultAdapter(this@DocumentsScanResultActivity)
|
||||
binding.recyclerView.run {
|
||||
adapter = resultAdapter
|
||||
layoutManager = LinearLayoutManager(this@DocumentsScanResultActivity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
super.initData()
|
||||
ScanRepository.instance.photoResults.observe(this@DocumentsScanResultActivity) {
|
||||
resultAdapter?.setData(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.ux.video.file.filerecovery.documents
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.ux.video.file.filerecovery.base.BaseAdapter
|
||||
import com.ux.video.file.filerecovery.databinding.DocumentsScanResultAdapterBinding
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
|
||||
class DocumentsScanResultAdapter(mContext: Context) :
|
||||
BaseAdapter<ResultPhotos, DocumentsScanResultAdapterBinding>(mContext) {
|
||||
override fun getViewBinding(parent: ViewGroup): DocumentsScanResultAdapterBinding =
|
||||
DocumentsScanResultAdapterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
override fun bindItem(
|
||||
holder: VHolder<DocumentsScanResultAdapterBinding>,
|
||||
item: ResultPhotos
|
||||
) {
|
||||
|
||||
holder.vb.run {
|
||||
item.run {
|
||||
tvDirName.text = dirName
|
||||
tvFileCount.text = allFiles.size.toString()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.ux.video.file.filerecovery.main
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.ux.video.file.filerecovery.R
|
||||
|
||||
|
||||
class FullScreenDialogFragment : DialogFragment() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.FullScreenDialog)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.apply {
|
||||
setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) // 背景透明叠加
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_fullscreen, container, false)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package com.ux.video.file.filerecovery.main
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityMainBinding
|
||||
import com.ux.video.file.filerecovery.main.ScanSelectTypeActivity
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
|
||||
|
||||
//是否正确引导用户打开所有文件管理权限
|
||||
private var isRequestPermission = false
|
||||
private var currentGoType = ScanSelectTypeActivity.Companion.VALUE_PHOTO
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
var isAllOK = true
|
||||
permissions.entries.forEach { entry ->
|
||||
val permission = entry.key
|
||||
val granted = entry.value
|
||||
if (granted) {
|
||||
|
||||
} else {
|
||||
isAllOK = false
|
||||
}
|
||||
}
|
||||
ScanManager.showLog("权限", "====isAllOK=${isAllOK}")
|
||||
if (isAllOK) {
|
||||
startScan()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun inflateBinding(inflater: LayoutInflater) = ActivityMainBinding.inflate(inflater)
|
||||
|
||||
|
||||
override fun initView() {
|
||||
super.initView()
|
||||
onBackPressedDispatcher.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
ScanManager.showLog("权限", "====0000000")
|
||||
if (binding.layoutPermission.isVisible) {
|
||||
ScanManager.showLog("权限", "====111111")
|
||||
binding.layoutPermission.visibility = View.GONE
|
||||
} else {
|
||||
ScanManager.showLog("权限", "====222222222222")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
super.initData()
|
||||
binding.run {
|
||||
allow.setOnClickListener {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
||||
data = "package:${packageName}".toUri()
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
startActivity(intent)
|
||||
}
|
||||
isRequestPermission = true
|
||||
}
|
||||
layoutPhoto.setOnClickListener {
|
||||
currentGoType = ScanSelectTypeActivity.Companion.VALUE_PHOTO
|
||||
intentCheck()
|
||||
}
|
||||
layoutVideo.setOnClickListener {
|
||||
currentGoType = ScanSelectTypeActivity.Companion.VALUE_VIDEO
|
||||
intentCheck()
|
||||
}
|
||||
layoutAudio.setOnClickListener {
|
||||
currentGoType = ScanSelectTypeActivity.Companion.VALUE_AUDIO
|
||||
intentCheck()
|
||||
}
|
||||
layoutDocument.setOnClickListener {
|
||||
currentGoType = ScanSelectTypeActivity.Companion.VALUE_DOCUMENT
|
||||
intentCheck()
|
||||
}
|
||||
}
|
||||
binding.btnPermission.setOnClickListener {
|
||||
|
||||
|
||||
binding.layoutPermission.isVisible = true
|
||||
|
||||
}
|
||||
binding.btnScanAllPhoto.setOnClickListener {
|
||||
|
||||
startActivity(Intent(this@MainActivity, ScanSelectTypeActivity::class.java).apply {
|
||||
putExtra(
|
||||
ScanSelectTypeActivity.Companion.KEY_FILE_TYPE,
|
||||
ScanSelectTypeActivity.Companion.VALUE_PHOTO
|
||||
)
|
||||
})
|
||||
}
|
||||
binding.btnScanAllVideo.setOnClickListener {
|
||||
|
||||
startActivity(Intent(this@MainActivity, ScanSelectTypeActivity::class.java).apply {
|
||||
putExtra(
|
||||
ScanSelectTypeActivity.Companion.KEY_FILE_TYPE,
|
||||
ScanSelectTypeActivity.Companion.VALUE_VIDEO
|
||||
)
|
||||
})
|
||||
}
|
||||
binding.btnScanAllAudio.setOnClickListener {
|
||||
|
||||
startActivity(Intent(this@MainActivity, ScanSelectTypeActivity::class.java).apply {
|
||||
putExtra(
|
||||
ScanSelectTypeActivity.Companion.KEY_FILE_TYPE,
|
||||
ScanSelectTypeActivity.Companion.VALUE_AUDIO
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
binding.btnScanAllFile.setOnClickListener {
|
||||
// val results = mutableListOf<ResultPhotos>()
|
||||
val root = Environment.getExternalStorageDirectory()
|
||||
|
||||
// ScanManager.scanAllDocuments(root, results)
|
||||
// ScanRepository.instance.setResults(results)
|
||||
|
||||
startActivity(Intent(this@MainActivity, ScanSelectTypeActivity::class.java).apply {
|
||||
putExtra(
|
||||
ScanSelectTypeActivity.Companion.KEY_FILE_TYPE,
|
||||
ScanSelectTypeActivity.Companion.VALUE_DOCUMENT
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
// startActivity(Intent(this@MainActivity,DocumentsScanResultActivity::class.java))
|
||||
//
|
||||
// results.forEach { doc ->
|
||||
// ScanManager.showLog("FileScan", "目录: ${doc.dirName}, 文件数: ${doc.allFiles.size}")
|
||||
// doc.allFiles.forEach { file ->
|
||||
// ScanManager.showLog("FileScan", " -> 文件: ${file.targetFile.isHidden} ")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if(isRequestPermission&&hasAllFilesAccess(this)){
|
||||
isRequestPermission = false
|
||||
ScanManager.showLog("--", "-------onResume")
|
||||
startScan()
|
||||
binding.layoutPermission.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
private fun intentCheck() {
|
||||
if (!hasAllFilesAccess(this)) {
|
||||
requestPermission(this@MainActivity)
|
||||
} else {
|
||||
ScanManager.showLog("--", "-------权限已经授予")
|
||||
startScan()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件管理权限
|
||||
*/
|
||||
private fun hasAllFilesAccess(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
val read = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
val write = ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
read && write
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermission(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
binding.layoutPermission.visibility = View.VISIBLE
|
||||
} else {
|
||||
requestPermissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startScan() {
|
||||
startActivity(Intent(this@MainActivity, ScanSelectTypeActivity::class.java).apply {
|
||||
putExtra(ScanSelectTypeActivity.Companion.KEY_FILE_TYPE, currentGoType)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding.layoutPermission.isVisible = false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.ux.video.file.filerecovery.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import com.ux.video.file.filerecovery.R
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityScanSelectTypeBinding
|
||||
import com.ux.video.file.filerecovery.result.ScanningActivity
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class ScanSelectTypeActivity : BaseActivity<ActivityScanSelectTypeBinding>() {
|
||||
|
||||
companion object {
|
||||
val KEY_FILE_TYPE = "file_type"
|
||||
val VALUE_PHOTO = 0
|
||||
val VALUE_VIDEO = 1
|
||||
val VALUE_AUDIO = 2
|
||||
val VALUE_DOCUMENT = 3
|
||||
}
|
||||
|
||||
private var allType by Delegates.notNull<Int>()
|
||||
private var deletedType by Delegates.notNull<Int>()
|
||||
override fun inflateBinding(inflater: LayoutInflater): ActivityScanSelectTypeBinding =
|
||||
ActivityScanSelectTypeBinding.inflate(inflater)
|
||||
|
||||
|
||||
override fun initView() {
|
||||
super.initView()
|
||||
val type = intent.getIntExtra(KEY_FILE_TYPE, VALUE_PHOTO)
|
||||
setSelectType(type)
|
||||
|
||||
}
|
||||
override fun initData() {
|
||||
super.initData()
|
||||
|
||||
binding.scanAllFile.setOnClickListener {
|
||||
|
||||
startActivity(Intent(this@ScanSelectTypeActivity, ScanningActivity::class.java).apply {
|
||||
putExtra(ScanningActivity.Companion.KEY_SCAN_TYPE, allType)
|
||||
})
|
||||
|
||||
}
|
||||
binding.scanDeletedFile.setOnClickListener {
|
||||
startActivity(Intent(this@ScanSelectTypeActivity, ScanningActivity::class.java).apply {
|
||||
putExtra(ScanningActivity.Companion.KEY_SCAN_TYPE, deletedType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSelectType(fileType: Int) {
|
||||
when (fileType) {
|
||||
VALUE_PHOTO -> {
|
||||
allType = ScanningActivity.Companion.VALUE_SCAN_TYPE_photo
|
||||
deletedType = ScanningActivity.Companion.VALUE_SCAN_TYPE_deleted_photo
|
||||
binding.title.text = getString(R.string.photo_title)
|
||||
}
|
||||
|
||||
VALUE_VIDEO -> {
|
||||
allType = ScanningActivity.Companion.VALUE_SCAN_TYPE_video
|
||||
deletedType = ScanningActivity.Companion.VALUE_SCAN_TYPE_deleted_video
|
||||
binding.title.text = getString(R.string.video_title)
|
||||
}
|
||||
|
||||
VALUE_AUDIO -> {
|
||||
allType = ScanningActivity.Companion.VALUE_SCAN_TYPE_audio
|
||||
deletedType = ScanningActivity.Companion.VALUE_SCAN_TYPE_deleted_audio
|
||||
binding.title.text = getString(R.string.audio_title)
|
||||
}
|
||||
|
||||
VALUE_DOCUMENT -> {
|
||||
allType = ScanningActivity.Companion.VALUE_SCAN_TYPE_documents
|
||||
deletedType = ScanningActivity.Companion.VALUE_SCAN_TYPE_deleted_documents
|
||||
binding.title.text = getString(R.string.document_title)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.ux.video.file.filerecovery.photo
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.ux.video.file.filerecovery.base.BaseAdapter
|
||||
import com.ux.video.file.filerecovery.databinding.PhotoDisplayDateAdapterBinding
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.addItemDecorationOnce
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
|
||||
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
|
||||
|
||||
class PhotoDisplayDateAdapter(mContext: Context) :
|
||||
BaseAdapter<Pair<String, List<ResultPhotosFiles>>, PhotoDisplayDateAdapterBinding>(mContext) {
|
||||
|
||||
private var allSelected: Boolean? = null
|
||||
private var columns = 3
|
||||
override fun getViewBinding(parent: ViewGroup): PhotoDisplayDateAdapterBinding =
|
||||
PhotoDisplayDateAdapterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
fun setAllSelected(allSelect: Boolean) {
|
||||
allSelected = allSelect
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
fun setColumns(int: Int){
|
||||
columns = int
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
override fun bindItem(
|
||||
holder: VHolder<PhotoDisplayDateAdapterBinding>,
|
||||
item: Pair<String, List<ResultPhotosFiles>>
|
||||
) {
|
||||
holder.vb.run {
|
||||
val photoDisplayDateChildAdapter = PhotoDisplayDateChildAdapter(mContext)
|
||||
item.run {
|
||||
allSelected?.let {
|
||||
imSelectStatus.isSelected = it
|
||||
photoDisplayDateChildAdapter.setAllSelected(it)
|
||||
}
|
||||
imSelectStatus.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
photoDisplayDateChildAdapter.setAllSelected(it.isSelected)
|
||||
}
|
||||
val (date, files) = item
|
||||
textDate.text = date
|
||||
recyclerChild.apply {
|
||||
layoutManager = GridLayoutManager(context, columns)
|
||||
val gridSpacingItemDecoration =
|
||||
GridSpacingItemDecoration(4, 8.dpToPx(mContext), true)
|
||||
addItemDecorationOnce(gridSpacingItemDecoration)
|
||||
adapter = photoDisplayDateChildAdapter.apply { setData(files) }
|
||||
isNestedScrollingEnabled = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package com.ux.video.file.filerecovery.photo
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.ux.video.file.filerecovery.base.BaseAdapter
|
||||
import com.ux.video.file.filerecovery.databinding.PhotoDisplayDateChildAdapterBinding
|
||||
import com.ux.video.file.filerecovery.utils.Common
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager
|
||||
import com.ux.video.file.filerecovery.utils.ScanRepository
|
||||
import kotlin.collections.remove
|
||||
|
||||
class PhotoDisplayDateChildAdapter(mContext: Context) :
|
||||
BaseAdapter<ResultPhotosFiles, PhotoDisplayDateChildAdapterBinding>(mContext) {
|
||||
|
||||
private var allSelected: Boolean? = null
|
||||
override fun getViewBinding(parent: ViewGroup): PhotoDisplayDateChildAdapterBinding =
|
||||
PhotoDisplayDateChildAdapterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
|
||||
fun setAllSelected(allselect: Boolean) {
|
||||
allSelected = allselect
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun bindItem(
|
||||
holder: VHolder<PhotoDisplayDateChildAdapterBinding>,
|
||||
item: ResultPhotosFiles
|
||||
) {
|
||||
|
||||
holder.vb.run {
|
||||
item.run {
|
||||
imSelectStatus.isSelected = ScanRepository.instance.checkIsSelect(path.toString())
|
||||
allSelected?.let {
|
||||
imSelectStatus.isSelected = it
|
||||
//全选按钮手动触发,需要更新
|
||||
updateSetList(it, path.toString())
|
||||
}
|
||||
imSelectStatus.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
updateSetList(it.isSelected, path.toString())
|
||||
|
||||
}
|
||||
textSize.text = Common.formatFileSize(mContext, size)
|
||||
Glide.with(mContext)
|
||||
.load(targetFile)
|
||||
.apply(
|
||||
RequestOptions()
|
||||
.transform(CenterCrop(), RoundedCorners(15.dpToPx(mContext)))
|
||||
)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable?>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
ScanManager.showLog(
|
||||
"加载图片",
|
||||
"-------path = ${path} file=${targetFile}"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable?>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
.into(imageThumbnail)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSetList(boolean: Boolean, path: String) {
|
||||
ScanRepository.instance.toggleSelection(boolean,path)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,309 @@
|
||||
package com.ux.video.file.filerecovery.photo
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.ux.video.file.filerecovery.R
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityPhotoSortingBinding
|
||||
import com.ux.video.file.filerecovery.utils.Common
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.addItemDecorationOnce
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySize
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterBySizeList
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonths
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.filterWithinMonthsList
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.mbToBytes
|
||||
import com.ux.video.file.filerecovery.utils.GridSpacingItemDecoration
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager.copySelectedFilesAsync
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager.deleteFilesAsync
|
||||
import com.ux.video.file.filerecovery.utils.ScanRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PhotoSortingActivity : BaseActivity<ActivityPhotoSortingBinding>() {
|
||||
|
||||
companion object {
|
||||
//指定文件夹下的所有文件
|
||||
val KEY_PHOTO_FOLDER_FILE = "folder_file"
|
||||
|
||||
val FILTER_DATE_ALL = -1
|
||||
val FILTER_DATE_1 = 1
|
||||
val FILTER_DATE_6 = 6
|
||||
val FILTER_DATE_24 = 24
|
||||
|
||||
val FILTER_SIZE_ALL = -1
|
||||
val FILTER_SIZE_1 = 1
|
||||
val FILTER_SIZE_5 = 2
|
||||
val FILTER_SIZE_OVER_5 = 3
|
||||
}
|
||||
|
||||
private var columns = 3
|
||||
private var dateAdapter: PhotoDisplayDateAdapter? = null
|
||||
private var sizeAdapter: PhotoDisplayDateChildAdapter? = null
|
||||
val selectedSet = mutableSetOf<String>() // 保存选中状态
|
||||
|
||||
|
||||
//默认倒序排序
|
||||
private var sortReverse = true
|
||||
|
||||
//筛选时间,默认全部-1
|
||||
private var filterDate = FILTER_DATE_ALL
|
||||
|
||||
//筛选大小,默认全部-1
|
||||
private var filterSize = FILTER_SIZE_ALL
|
||||
|
||||
|
||||
private lateinit var sortBySizeBigToSmall: List<ResultPhotosFiles>
|
||||
private lateinit var sortBySizeSmallToBig: List<ResultPhotosFiles>
|
||||
private lateinit var sortByDayReverse: List<Pair<String, List<ResultPhotosFiles>>>
|
||||
private lateinit var sortedByPositive: List<Pair<String, List<ResultPhotosFiles>>>
|
||||
|
||||
override fun inflateBinding(inflater: LayoutInflater): ActivityPhotoSortingBinding =
|
||||
ActivityPhotoSortingBinding.inflate(inflater)
|
||||
|
||||
override fun initData() {
|
||||
super.initData()
|
||||
|
||||
val list: ArrayList<ResultPhotosFiles>? =
|
||||
intent.getParcelableArrayListExtraCompat(KEY_PHOTO_FOLDER_FILE)
|
||||
|
||||
list?.let {
|
||||
//倒序(最近的在前面)
|
||||
sortByDayReverse = Common.getSortByDayNewToOld(it)
|
||||
//正序(最远的在前面)
|
||||
sortedByPositive = Common.getSortByDayOldToNew(sortByDayReverse)
|
||||
sortBySizeBigToSmall = Common.getSortBySizeBigToSmall(it)
|
||||
sortBySizeSmallToBig = Common.getSortBySizeSmallToBig(it)
|
||||
|
||||
sizeAdapter = PhotoDisplayDateChildAdapter(this@PhotoSortingActivity)
|
||||
dateAdapter = PhotoDisplayDateAdapter(this@PhotoSortingActivity).apply {
|
||||
setData(sortByDayReverse)
|
||||
}
|
||||
setDateAdapter()
|
||||
setFilter()
|
||||
binding.run {
|
||||
|
||||
lifecycleScope.launch {
|
||||
ScanRepository.instance.selectedFlow.collect { newSet ->
|
||||
println("选中集合变化:$newSet")
|
||||
tvSelectCounts.text = newSet.size.toString()
|
||||
}
|
||||
}
|
||||
tvSelectCounts.setOnClickListener {
|
||||
lifecycleScope.copySelectedFilesAsync(
|
||||
selectedSet = ScanRepository.instance.selectedFlow.value,
|
||||
folder = Common.recoveryPhotoDir,
|
||||
onProgress = { currentCounts: Int, fileName: String, success: Boolean ->
|
||||
|
||||
ScanManager.showLog(
|
||||
"--------恢复图片 ",
|
||||
"----------${currentCounts} ${fileName}"
|
||||
)
|
||||
|
||||
}) { counts ->
|
||||
|
||||
ScanManager.showLog("--------恢复图片 ", "----------恢复完成 ${counts}")
|
||||
}
|
||||
|
||||
}
|
||||
btnDelete.setOnClickListener {
|
||||
lifecycleScope.deleteFilesAsync(
|
||||
selectedSet = ScanRepository.instance.selectedFlow.value,
|
||||
onProgress = { currentCounts: Int, path:String, success: Boolean ->
|
||||
ScanManager.showLog(
|
||||
"--------删除图片 ",
|
||||
"----------${currentCounts} ${path}"
|
||||
)
|
||||
|
||||
}) { counts ->
|
||||
|
||||
ScanManager.showLog("--------恢复图片 ", "----------恢复完成 ${counts}")
|
||||
}
|
||||
|
||||
}
|
||||
linear.setOnCheckedChangeListener { group, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.btn_date_old_to_new -> {
|
||||
setDateAdapter()
|
||||
dateAdapter?.setData(sortedByPositive)
|
||||
sortReverse = false
|
||||
}
|
||||
|
||||
R.id.btn_date_new_to_old -> {
|
||||
setDateAdapter()
|
||||
dateAdapter?.setData(sortByDayReverse)
|
||||
sortReverse = true
|
||||
}
|
||||
|
||||
R.id.btn_size_big_to_small -> {
|
||||
setSizeAdapter()
|
||||
sizeAdapter?.setData(sortBySizeBigToSmall)
|
||||
sortReverse = true
|
||||
}
|
||||
|
||||
R.id.btn_size_small_to_big -> {
|
||||
setSizeAdapter()
|
||||
sizeAdapter?.setData(sortBySizeSmallToBig)
|
||||
sortReverse = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
imSelectAll.setOnClickListener {
|
||||
imSelectAll.isSelected = !imSelectAll.isSelected
|
||||
sizeAdapter?.setAllSelected(imSelectAll.isSelected)
|
||||
dateAdapter?.setAllSelected(imSelectAll.isSelected)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setDateAdapter() {
|
||||
binding.recyclerView.run {
|
||||
adapter = dateAdapter?.apply { setColumns(columns) }
|
||||
layoutManager = LinearLayoutManager(this@PhotoSortingActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSizeAdapter() {
|
||||
binding.recyclerView.run {
|
||||
adapter = sizeAdapter
|
||||
layoutManager = GridLayoutManager(context, columns)
|
||||
val gridSpacingItemDecoration =
|
||||
GridSpacingItemDecoration(4, 8.dpToPx(this@PhotoSortingActivity), true)
|
||||
addItemDecorationOnce(gridSpacingItemDecoration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选
|
||||
*/
|
||||
private fun setFilter() {
|
||||
binding.linearDate.setOnCheckedChangeListener { group, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.btn_date_all -> {
|
||||
filterDate = FILTER_DATE_ALL
|
||||
}
|
||||
|
||||
R.id.btn_date_within_1 -> {
|
||||
filterDate = FILTER_DATE_1
|
||||
}
|
||||
|
||||
R.id.btn_date_customize -> {
|
||||
|
||||
}
|
||||
}
|
||||
startFilter()
|
||||
}
|
||||
binding.linearSize.setOnCheckedChangeListener { group, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.btn_size_all -> {
|
||||
filterSize = FILTER_SIZE_ALL
|
||||
}
|
||||
|
||||
R.id.btn_size_1m -> {
|
||||
filterSize = FILTER_SIZE_1
|
||||
}
|
||||
|
||||
R.id.btn_size_5m -> {
|
||||
filterSize = FILTER_SIZE_5
|
||||
}
|
||||
|
||||
R.id.btn_size_over_5m -> {
|
||||
filterSize = FILTER_SIZE_OVER_5
|
||||
}
|
||||
}
|
||||
startFilter()
|
||||
}
|
||||
|
||||
binding.rgLayout.setOnCheckedChangeListener { group, checkedId ->
|
||||
|
||||
when (checkedId) {
|
||||
R.id.rb_columns_2 -> {
|
||||
columns = 2
|
||||
}
|
||||
|
||||
R.id.rb_columns_3 -> {
|
||||
columns = 3
|
||||
}
|
||||
|
||||
R.id.rb_columns_4 -> {
|
||||
columns = 4
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
when (binding.recyclerView.adapter) {
|
||||
is PhotoDisplayDateAdapter -> {
|
||||
dateAdapter?.setColumns(columns)
|
||||
}
|
||||
|
||||
is PhotoDisplayDateChildAdapter -> {
|
||||
binding.recyclerView.layoutManager =
|
||||
GridLayoutManager(this@PhotoSortingActivity, columns)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行筛选结果
|
||||
*/
|
||||
private fun startFilter() {
|
||||
when (binding.recyclerView.adapter) {
|
||||
is PhotoDisplayDateAdapter -> {
|
||||
val list = if (sortReverse) sortByDayReverse else sortedByPositive
|
||||
val filterSizeCovert = filterSizeCovert(filterSize)
|
||||
list.filterWithinMonths(filterDate)
|
||||
.filterBySize(filterSizeCovert.first, filterSizeCovert.second).let {
|
||||
dateAdapter?.setData(it)
|
||||
ScanManager.showLog(
|
||||
"---",
|
||||
"---date-----${it.size} filterDate=${filterDate} first=${filterSizeCovert.first} second=${filterSizeCovert.second} dateAdapter=${dateAdapter}"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
is PhotoDisplayDateChildAdapter -> {
|
||||
val list = if (sortReverse) sortBySizeBigToSmall else sortBySizeSmallToBig
|
||||
val filterSizeCovert = filterSizeCovert(filterSize)
|
||||
list.filterWithinMonthsList(filterDate)
|
||||
.filterBySizeList(filterSizeCovert.first, filterSizeCovert.second).let {
|
||||
sizeAdapter?.setData(it)
|
||||
ScanManager.showLog(
|
||||
"---",
|
||||
"----size----${it.size} filterDate=${filterDate} first=${filterSizeCovert.first} second=${filterSizeCovert.second} sizeAdapter=${sizeAdapter}"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun filterSizeCovert(filterSize: Int): Pair<Long, Long> {
|
||||
return when (filterSize) {
|
||||
FILTER_SIZE_ALL -> Pair(-1L, -1L)
|
||||
FILTER_SIZE_1 -> Pair(0L, 1.mbToBytes())
|
||||
FILTER_SIZE_5 -> Pair(1L, 5.mbToBytes())
|
||||
FILTER_SIZE_OVER_5 -> Pair(5.mbToBytes(), Long.MAX_VALUE)
|
||||
else -> Pair(-1L, -1L)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.ux.video.file.filerecovery.photo
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class ResultPhotos(
|
||||
val dirName: String,
|
||||
val allFiles: ArrayList<ResultPhotosFiles>
|
||||
): Parcelable
|
||||
@ -0,0 +1,18 @@
|
||||
package com.ux.video.file.filerecovery.photo
|
||||
|
||||
import java.io.File
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ResultPhotosFiles(
|
||||
val name: String,
|
||||
val path: String?= null,
|
||||
val size: Long, // 字节
|
||||
val lastModified: Long, // 时间戳
|
||||
var isSelected: Boolean = false // 选中状态
|
||||
): Parcelable{
|
||||
val targetFile: File?
|
||||
get() = path?.let { File(it) }
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.ux.video.file.filerecovery.result
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.ux.video.file.filerecovery.base.BaseAdapter
|
||||
import com.ux.video.file.filerecovery.databinding.ScanResultAdapterBinding
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.dpToPx
|
||||
import java.io.File
|
||||
|
||||
class ScanResultAdapter(
|
||||
mContext: Context,
|
||||
var onClickItem: (allFiles: ArrayList<ResultPhotosFiles>) -> Unit
|
||||
) :
|
||||
BaseAdapter<ResultPhotos, ScanResultAdapterBinding>(mContext) {
|
||||
override fun getViewBinding(parent: ViewGroup): ScanResultAdapterBinding =
|
||||
ScanResultAdapterBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
override fun bindItem(
|
||||
holder: VHolder<ScanResultAdapterBinding>,
|
||||
item: ResultPhotos
|
||||
) {
|
||||
|
||||
holder.vb.run {
|
||||
val imageViews = listOf(im1, im2, im3)
|
||||
item.run {
|
||||
textDirNameCount.text = "$dirName(${allFiles.size})"
|
||||
val takeFiles = allFiles.take(3)
|
||||
imageViews.forEachIndexed { index, imageView ->
|
||||
if (index < takeFiles.size) {
|
||||
takeFiles[index].targetFile?.let {
|
||||
loadImageView(mContext, it, imageView)
|
||||
}
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
}
|
||||
imageView.setOnClickListener { onClickItem(allFiles) }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun loadImageView(context: Context, file: File, imageView: ImageView) {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.apply(
|
||||
RequestOptions()
|
||||
.transform(CenterCrop(), RoundedCorners(15.dpToPx(context)))
|
||||
)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.ux.video.file.filerecovery.result
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityScanResultDisplayBinding
|
||||
import com.ux.video.file.filerecovery.photo.PhotoSortingActivity
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
import com.ux.video.file.filerecovery.result.ScanningActivity
|
||||
import com.ux.video.file.filerecovery.utils.ExtendFunctions.getParcelableArrayListExtraCompat
|
||||
import com.ux.video.file.filerecovery.utils.ScanRepository
|
||||
|
||||
/**
|
||||
* 扫描结果汇总展示
|
||||
*/
|
||||
class ScanResultDisplayActivity : BaseActivity<ActivityScanResultDisplayBinding>() {
|
||||
private var scanResultAdapter: ScanResultAdapter? = null
|
||||
|
||||
companion object {
|
||||
val KEY_SCAN_RESULT = "scan_result"
|
||||
}
|
||||
|
||||
override fun inflateBinding(inflater: LayoutInflater): ActivityScanResultDisplayBinding =
|
||||
ActivityScanResultDisplayBinding.inflate(inflater)
|
||||
|
||||
override fun initView() {
|
||||
super.initView()
|
||||
val list: ArrayList<ResultPhotos>? =
|
||||
intent.getParcelableArrayListExtraCompat(KEY_SCAN_RESULT)
|
||||
scanResultAdapter = ScanResultAdapter(this@ScanResultDisplayActivity){ folderLists->
|
||||
startActivity(Intent(this@ScanResultDisplayActivity, PhotoSortingActivity::class.java).apply {
|
||||
putParcelableArrayListExtra(PhotoSortingActivity.KEY_PHOTO_FOLDER_FILE, folderLists)
|
||||
})
|
||||
}
|
||||
binding.recyclerResult.run {
|
||||
adapter = scanResultAdapter
|
||||
layoutManager = LinearLayoutManager(this@ScanResultDisplayActivity)
|
||||
}
|
||||
list?.let {
|
||||
binding.run {
|
||||
textDirCount.text = it.size.toString()
|
||||
val sumOf = it.sumOf { it.allFiles.size }
|
||||
textAllCounts.text = sumOf.toString()
|
||||
}
|
||||
scanResultAdapter?.setData(it)
|
||||
}
|
||||
|
||||
|
||||
// ScanRepository.instance.photoResults.observe(this@ScanResultDisplayActivity) {
|
||||
// binding.run {
|
||||
// textDirCount.text = it.size.toString()
|
||||
// val sumOf = it.sumOf { it.allFiles.size }
|
||||
// textAllCounts.text = sumOf.toString()
|
||||
// }
|
||||
// scanResultAdapter?.setData(it)
|
||||
// }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
package com.ux.video.file.filerecovery.result
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.view.LayoutInflater
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.ux.video.file.filerecovery.base.BaseActivity
|
||||
import com.ux.video.file.filerecovery.databinding.ActivityScanningBinding
|
||||
import com.ux.video.file.filerecovery.utils.ScanManager
|
||||
import com.ux.video.file.filerecovery.utils.ScanRepository
|
||||
import com.ux.video.file.filerecovery.utils.ScanState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ScanningActivity : BaseActivity<ActivityScanningBinding>() {
|
||||
|
||||
companion object {
|
||||
val KEY_SCAN_TYPE = "scan_type"
|
||||
val VALUE_SCAN_TYPE_photo = 0
|
||||
val VALUE_SCAN_TYPE_deleted_photo = 1
|
||||
val VALUE_SCAN_TYPE_video = 2
|
||||
val VALUE_SCAN_TYPE_deleted_video = 3
|
||||
val VALUE_SCAN_TYPE_audio = 4
|
||||
val VALUE_SCAN_TYPE_deleted_audio = 5
|
||||
val VALUE_SCAN_TYPE_documents = 6
|
||||
val VALUE_SCAN_TYPE_deleted_documents = 7
|
||||
}
|
||||
|
||||
private var scanType: Int = VALUE_SCAN_TYPE_photo
|
||||
override fun inflateBinding(inflater: LayoutInflater): ActivityScanningBinding =
|
||||
ActivityScanningBinding.inflate(inflater)
|
||||
|
||||
override fun initData() {
|
||||
super.initData()
|
||||
scanType = intent.getIntExtra(KEY_SCAN_TYPE, VALUE_SCAN_TYPE_photo)
|
||||
when (scanType) {
|
||||
VALUE_SCAN_TYPE_photo, VALUE_SCAN_TYPE_video, VALUE_SCAN_TYPE_audio, VALUE_SCAN_TYPE_documents -> scanAll()
|
||||
VALUE_SCAN_TYPE_deleted_photo, VALUE_SCAN_TYPE_deleted_video, VALUE_SCAN_TYPE_deleted_audio, VALUE_SCAN_TYPE_deleted_documents -> scanDeleted()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun scanAll() {
|
||||
val total = 800
|
||||
lifecycleScope.launch {
|
||||
val root = Environment.getExternalStorageDirectory()
|
||||
ScanManager.scanAllDocuments(root, type = scanType).flowOn(Dispatchers.IO).collect {
|
||||
when (it) {
|
||||
is ScanState.Progress -> {
|
||||
updateProgress(it)
|
||||
}
|
||||
|
||||
is ScanState.Complete -> {
|
||||
updateComplete(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanDeleted() {
|
||||
lifecycleScope.launch {
|
||||
|
||||
val root = Environment.getExternalStorageDirectory()
|
||||
ScanManager.scanHiddenPhotoAsync(root, type = scanType).flowOn(Dispatchers.IO).collect {
|
||||
when (it) {
|
||||
is ScanState.Progress -> {
|
||||
updateProgress(it)
|
||||
|
||||
}
|
||||
|
||||
is ScanState.Complete -> {
|
||||
updateComplete(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun updateProgress(scanState: ScanState.Progress){
|
||||
val total = 1000
|
||||
scanState.let {
|
||||
binding.run {
|
||||
val percent = (it.scannedCount * 100 / total).coerceAtMost(100)
|
||||
scanProgress.progress = percent
|
||||
tvScanCurrentFilePath.text = it.filePath
|
||||
tvScanCurrentCounts.text = it.scannedCount.toString()
|
||||
ScanManager.showLog("Scan", it.filePath)
|
||||
}
|
||||
ScanManager.showLog(
|
||||
"HiddenScan",
|
||||
"进度: ${it.scannedCount}"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
private fun updateComplete(scanState: ScanState.Complete){
|
||||
binding.scanProgress.progress = 100
|
||||
scanState.let {
|
||||
// when(scanType){
|
||||
// VALUE_SCAN_TYPE_photo,VALUE_SCAN_TYPE_deleted_photo->ScanRepository.instance.setPhotoResults(it.result)
|
||||
// VALUE_SCAN_TYPE_video,VALUE_SCAN_TYPE_deleted_video->ScanRepository.instance.setVideoResults(it.result)
|
||||
// VALUE_SCAN_TYPE_audio,VALUE_SCAN_TYPE_deleted_audio->ScanRepository.instance.setPhotoResults(it.result)
|
||||
// VALUE_SCAN_TYPE_documents,VALUE_SCAN_TYPE_deleted_documents->ScanRepository.instance.setPhotoResults(it.result)
|
||||
// }
|
||||
startActivity(Intent(this@ScanningActivity,ScanResultDisplayActivity::class.java).apply {
|
||||
putParcelableArrayListExtra(ScanResultDisplayActivity.KEY_SCAN_RESULT, it.result)
|
||||
})
|
||||
ScanManager.showLog(
|
||||
"HiddenScan",
|
||||
"完成: ${it.result.size}"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
107
app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.icu.util.Calendar
|
||||
import android.os.Environment
|
||||
import com.ux.video.file.filerecovery.R
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
object Common {
|
||||
val rootDir = Environment.getExternalStorageDirectory()
|
||||
val dateFormat = SimpleDateFormat("MMM dd yyyy", Locale.ENGLISH)
|
||||
val recoveryPhotoDir = "MyAllRecovery/Photo"
|
||||
|
||||
/**
|
||||
* 默认按照日期分类,将最新的排前面
|
||||
*/
|
||||
fun getSortByDayNewToOld(list: ArrayList<ResultPhotosFiles>): List<Pair<String, List<ResultPhotosFiles>>> {
|
||||
|
||||
|
||||
val grouped = list.groupBy {
|
||||
dateFormat.format(Date(it.lastModified))
|
||||
}
|
||||
val parentData: List<Pair<String, List<ResultPhotosFiles>>> = grouped
|
||||
.map { it.key to it.value }
|
||||
.sortedByDescending { dateFormat.parse(it.first)?.time ?: 0L }
|
||||
return parentData
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照日期排序, 时间最早的排前面
|
||||
*
|
||||
*/
|
||||
fun getSortByDayOldToNew(list: List<Pair<String, List<ResultPhotosFiles>>>) =
|
||||
list.sortedBy { dateFormat.parse(it.first)?.time ?: 0L }
|
||||
|
||||
/**
|
||||
* 按照文件大小排序,将最大的排前面
|
||||
*/
|
||||
fun getSortBySizeBigToSmall(list: ArrayList<ResultPhotosFiles>) = list.sortedByDescending {
|
||||
it.size
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照文件大小排序,将最大的排前面
|
||||
*/
|
||||
fun getSortBySizeSmallToBig(list: ArrayList<ResultPhotosFiles>) = list.sortedBy {
|
||||
it.size
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化文件大小显示
|
||||
*/
|
||||
fun formatFileSize(context: Context, size: Long): String {
|
||||
if (size < 1024) {
|
||||
return "$size B"
|
||||
}
|
||||
val kb = size / 1024.0
|
||||
if (kb < 1024) {
|
||||
return String.format(context.getString(R.string.size_kb), kb)
|
||||
}
|
||||
val mb = kb / 1024.0
|
||||
if (mb < 1024) {
|
||||
return String.format(context.getString(R.string.size_kb), mb)
|
||||
}
|
||||
val gb = mb / 1024.0
|
||||
return String.format(context.getString(R.string.size_gb), gb)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param months 筛选months月之内的数据
|
||||
*/
|
||||
fun filterWithinOneMonthByDay(
|
||||
grouped: List<Pair<String, List<ResultPhotosFiles>>>,months: Int
|
||||
): List<Pair<String, List<ResultPhotosFiles>>> {
|
||||
val today = Calendar.getInstance()
|
||||
val oneMonthAgo = Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -months) // 1 个月前
|
||||
}
|
||||
|
||||
return grouped.filter { (dayStr, _) ->
|
||||
val day = dateFormat.parse(dayStr)
|
||||
day != null && !day.before(oneMonthAgo.time) && !day.after(today.time)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param months 筛选months月之内的数据
|
||||
*/
|
||||
fun filterWithinOneMonth(list: List<ResultPhotosFiles>,months: Int): List<ResultPhotosFiles> {
|
||||
val today = Calendar.getInstance()
|
||||
val oneMonthAgo = Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -months)
|
||||
}
|
||||
|
||||
return list.filter {
|
||||
val cal = Calendar.getInstance().apply { timeInMillis = it.lastModified }
|
||||
!cal.before(oneMonthAgo) && !cal.after(today)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import com.ux.video.file.filerecovery.R
|
||||
|
||||
class CustomTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
companion object {
|
||||
private var regular: Typeface? = null
|
||||
private var bold: Typeface? = null
|
||||
private var italic: Typeface? = null
|
||||
}
|
||||
|
||||
init {
|
||||
attrs?.let {
|
||||
val typedArray = context.obtainStyledAttributes(it, R.styleable.CustomTextView)
|
||||
val type = typedArray.getInt(R.styleable.CustomTextView_fontType, 0)
|
||||
typedArray.recycle()
|
||||
when (type) {
|
||||
0 -> {
|
||||
if (regular == null) {
|
||||
regular = Typeface.createFromAsset(context.assets, "fonts/PingFang Regular_0.ttf")
|
||||
}
|
||||
typeface = regular
|
||||
}
|
||||
1 -> {
|
||||
if (bold == null) {
|
||||
bold = Typeface.createFromAsset(context.assets, "fonts/PingFang Bold_0.ttf")
|
||||
}
|
||||
typeface = bold
|
||||
}
|
||||
2 -> {
|
||||
if (italic == null) {
|
||||
italic = Typeface.createFromAsset(context.assets, "fonts/italic.ttf")
|
||||
}
|
||||
typeface = italic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.icu.util.Calendar
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.TypedValue
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
|
||||
|
||||
object ExtendFunctions {
|
||||
|
||||
|
||||
fun Int.dpToPx(context: Context): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
this.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
inline fun <reified T : Parcelable> Intent.getParcelableArrayListExtraCompat(key: String): ArrayList<T>? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getParcelableArrayListExtra(key, T::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableArrayListExtra<T>(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.addItemDecorationOnce(decoration: RecyclerView.ItemDecoration) {
|
||||
for (i in 0 until itemDecorationCount) {
|
||||
if (getItemDecorationAt(i)::class == decoration::class) {
|
||||
return // 已经有同类型,避免重复
|
||||
}
|
||||
}
|
||||
addItemDecoration(decoration)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 按时间筛选:最近 N 个月
|
||||
*/
|
||||
fun List<ResultPhotosFiles>.filterWithinMonthsList(months: Int): List<ResultPhotosFiles> {
|
||||
if (months == -1) return this
|
||||
val today = Calendar.getInstance()
|
||||
val monthsAgo = Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -months)
|
||||
}
|
||||
return this.filter {
|
||||
val cal = Calendar.getInstance().apply { timeInMillis = it.lastModified }
|
||||
!cal.before(monthsAgo) && !cal.after(today)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按文件大小筛选:区间 [minSize, maxSize]
|
||||
*/
|
||||
fun List<ResultPhotosFiles>.filterBySizeList(
|
||||
minSize: Long,
|
||||
maxSize: Long
|
||||
): List<ResultPhotosFiles> {
|
||||
if (minSize == -1L) return this
|
||||
return this.filter { it.size in minSize..maxSize }
|
||||
}
|
||||
|
||||
/**
|
||||
* 按时间筛选:最近 N 个月
|
||||
*/
|
||||
fun List<Pair<String, List<ResultPhotosFiles>>>.filterWithinMonths(months: Int): List<Pair<String, List<ResultPhotosFiles>>> {
|
||||
if (months == -1) return this
|
||||
val sdf = Common.dateFormat
|
||||
val today = Calendar.getInstance()
|
||||
val monthsAgo = Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, -months)
|
||||
}
|
||||
return this.filter { (dayStr, _) ->
|
||||
val day = sdf.parse(dayStr)
|
||||
day != null && !day.before(monthsAgo.time) && !day.after(today.time)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组数据:按大小筛选
|
||||
*/
|
||||
fun List<Pair<String, List<ResultPhotosFiles>>>.filterBySize(
|
||||
minSize: Long,
|
||||
maxSize: Long
|
||||
): List<Pair<String, List<ResultPhotosFiles>>> {
|
||||
if (minSize == -1L) return this
|
||||
return this.mapNotNull { (date, files) ->
|
||||
val filtered = files.filter { it.size in minSize..maxSize }
|
||||
if (filtered.isNotEmpty()) date to filtered else null
|
||||
}
|
||||
}
|
||||
fun Int.mbToBytes(): Long {
|
||||
return this * 1024L * 1024L
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class GridSpacingItemDecoration(
|
||||
private val spanCount: Int,
|
||||
private val spacing: Int,
|
||||
private val includeEdge: Boolean
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
|
||||
) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val column = position % spanCount
|
||||
|
||||
if (includeEdge) {
|
||||
outRect.left = spacing - column * spacing / spanCount
|
||||
outRect.right = (column + 1) * spacing / spanCount
|
||||
|
||||
if (position < spanCount) {
|
||||
outRect.top = spacing
|
||||
}
|
||||
outRect.bottom = spacing
|
||||
} else {
|
||||
outRect.left = column * spacing / spanCount
|
||||
outRect.right = spacing - (column + 1) * spacing / spanCount
|
||||
if (position >= spanCount) {
|
||||
outRect.top = spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,273 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotosFiles
|
||||
import com.ux.video.file.filerecovery.result.ScanningActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
object ScanManager {
|
||||
val IMAGE_FILE = listOf("jpg", "jpeg", "png", "gif")
|
||||
val AUDIO_FILE = listOf("mp3", "wav", "flac", "aac", "ogg", "m4a", "amr")
|
||||
val SHORT_AUDIO_FILE = listOf("mp3", "wav", "ogg")
|
||||
val VIDEO_FILE = listOf("webm", "mkv", "mp4")
|
||||
val DOCUMENT_FILE = listOf(
|
||||
"php",
|
||||
"txt",
|
||||
"zip",
|
||||
"stoutner",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"apk",
|
||||
"xapk"
|
||||
)
|
||||
|
||||
/**
|
||||
* 扫描所有文件
|
||||
*/
|
||||
fun scanAllDocuments(
|
||||
root: File, maxDepth: Int = 5,
|
||||
maxFiles: Int = 5000,
|
||||
type: Int
|
||||
): Flow<ScanState> = flow {
|
||||
|
||||
val result = mutableMapOf<String, MutableList<File>>()
|
||||
var fileCount = 0
|
||||
suspend fun scanDocuments(dir: File, depth: Int) {
|
||||
if (!dir.exists() || !dir.isDirectory) return
|
||||
if (depth > maxDepth || fileCount >= maxFiles) return
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
scanDocuments(file, depth + 1)
|
||||
} else {
|
||||
var fileCheckBoolean: Boolean = false
|
||||
when (type) {
|
||||
ScanningActivity.VALUE_SCAN_TYPE_photo -> {
|
||||
fileCheckBoolean = isFormatFile(file, IMAGE_FILE) && isValidImage(file)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_video -> {
|
||||
fileCheckBoolean = isFormatFile(file, VIDEO_FILE)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_audio -> {
|
||||
fileCheckBoolean = isFormatFile(file, AUDIO_FILE)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_documents -> {
|
||||
fileCheckBoolean = file.isFile && isFormatFile(file, DOCUMENT_FILE)
|
||||
}
|
||||
}
|
||||
if (fileCheckBoolean) {
|
||||
val dirName = file.parentFile?.name ?: "Unknown"
|
||||
|
||||
val list = result.getOrPut(dirName) { mutableListOf() }
|
||||
list.add(file)
|
||||
fileCount++
|
||||
|
||||
emit(ScanState.Progress(fileCount, file.absolutePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDocuments(root, depth = 0)
|
||||
val map = result.map { (dir, files) ->
|
||||
val resultPhotosFilesList = files.map { file ->
|
||||
ResultPhotosFiles(
|
||||
name = file.name,
|
||||
path = file.absolutePath,
|
||||
size = file.length(),
|
||||
lastModified = file.lastModified()
|
||||
)
|
||||
}
|
||||
ResultPhotos(dir, ArrayList(resultPhotosFilesList))
|
||||
}
|
||||
|
||||
emit(ScanState.Complete(ArrayList(map)))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 递归扫描隐藏目录下的有效图片(删除的图片)
|
||||
* @param maxDepth // 最大递归深度
|
||||
* @param maxFiles // 最大收集的文件数
|
||||
*/
|
||||
fun scanHiddenPhotoAsync(
|
||||
root: File, maxDepth: Int = 5,
|
||||
maxFiles: Int = 5000, type: Int
|
||||
): Flow<ScanState> = flow {
|
||||
|
||||
val result = mutableMapOf<String, MutableList<File>>()
|
||||
var fileCount = 0
|
||||
suspend fun scanDir(dir: File, depth: Int, insideHidden: Boolean = false) {
|
||||
if (!dir.exists() || !dir.isDirectory) return
|
||||
if (depth > maxDepth || fileCount >= maxFiles) return
|
||||
showLog("HiddenScan", "${dir.name} 111111")
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
val isHidden = file.name.startsWith(".")
|
||||
scanDir(file, depth + 1, insideHidden = insideHidden || isHidden)
|
||||
} else {
|
||||
if (insideHidden) {
|
||||
var fileCheckBoolean: Boolean = false
|
||||
when (type) {
|
||||
ScanningActivity.VALUE_SCAN_TYPE_deleted_photo -> {
|
||||
fileCheckBoolean =
|
||||
isFormatFile(file, IMAGE_FILE) && isValidImage(file)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_deleted_video -> {
|
||||
fileCheckBoolean = isFormatFile(file, VIDEO_FILE)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_deleted_audio -> {
|
||||
fileCheckBoolean = isFormatFile(file, AUDIO_FILE)
|
||||
}
|
||||
|
||||
ScanningActivity.VALUE_SCAN_TYPE_deleted_documents -> {
|
||||
fileCheckBoolean = file.isFile && isFormatFile(file, DOCUMENT_FILE)
|
||||
}
|
||||
}
|
||||
if (fileCheckBoolean) {
|
||||
val dirName = file.parentFile?.name ?: "Unknown"
|
||||
ScanManager.showLog("HiddenScan", "${dirName} 22222")
|
||||
val list = result.getOrPut(dirName) { mutableListOf() }
|
||||
list.add(file)
|
||||
fileCount++
|
||||
emit(ScanState.Progress(fileCount, file.absolutePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scanDir(root, depth = 0)
|
||||
|
||||
ScanManager.showLog("HiddenScan", " 3333")
|
||||
val map = result.map { (dir, files) ->
|
||||
val resultPhotosFilesList = files.map { file ->
|
||||
ResultPhotosFiles(
|
||||
name = file.name,
|
||||
path = file.absolutePath,
|
||||
size = file.length(),
|
||||
lastModified = file.lastModified()
|
||||
)
|
||||
}
|
||||
ResultPhotos(dir, ArrayList(resultPhotosFilesList))
|
||||
}
|
||||
emit(ScanState.Complete(ArrayList(map)))
|
||||
}
|
||||
|
||||
|
||||
private fun isFormatFile(file: File, types: List<String>): Boolean {
|
||||
val ext = file.extension.lowercase()
|
||||
return types.contains(ext)
|
||||
}
|
||||
|
||||
private fun isNumericDir(name: String): Boolean {
|
||||
return name.matches(Regex("^\\d+$"))
|
||||
}
|
||||
|
||||
fun showLog(tag: String, msg: String) {
|
||||
Log.d(tag, msg)
|
||||
}
|
||||
|
||||
fun isValidImage(file: File): Boolean {
|
||||
if (!file.exists() || file.length() <= 0) return false
|
||||
return try {
|
||||
BitmapFactory.decodeFile(file.absolutePath)?.let {
|
||||
it.width > 0 && it.height > 0
|
||||
} == true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 做批量恢复
|
||||
* @param folder "AllRecovery/Photo"
|
||||
*/
|
||||
fun CoroutineScope.copySelectedFilesAsync(
|
||||
selectedSet: Set<String>,
|
||||
rootDir: File = Common.rootDir,
|
||||
folder: String,
|
||||
onProgress: (currentCounts: Int, fileName: String, success: Boolean) -> Unit,
|
||||
onComplete: (currentCounts: Int) -> Unit
|
||||
) {
|
||||
launch(Dispatchers.IO) {
|
||||
val targetDir = File(rootDir, folder)
|
||||
if (!targetDir.exists()) targetDir.mkdirs()
|
||||
selectedSet.forEachIndexed { index, path ->
|
||||
val srcFile = File(path)
|
||||
if (srcFile.exists() && srcFile.isFile) {
|
||||
val destFile = File(targetDir, srcFile.name)
|
||||
var success = false
|
||||
try {
|
||||
srcFile.inputStream().use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
success = true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(index + 1, srcFile.name, success)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onComplete(selectedSet.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 做批量删除文件
|
||||
*
|
||||
*/
|
||||
fun CoroutineScope.deleteFilesAsync(
|
||||
selectedSet: Set<String>,
|
||||
onProgress: (currentCounts: Int, path: String, success: Boolean) -> Unit,
|
||||
onComplete: (currentCounts: Int) -> Unit
|
||||
) {
|
||||
launch(Dispatchers.IO) {
|
||||
var deletedCount = 0
|
||||
selectedSet.forEachIndexed { index, path ->
|
||||
try {
|
||||
val file = File(path)
|
||||
if (file.exists() && file.delete()) {
|
||||
deletedCount++
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onProgress(index + 1, path, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onProgress(index + 1, path, false)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onComplete(selectedSet.size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
|
||||
class ScanRepository private constructor() {
|
||||
|
||||
//---------扫描结果
|
||||
private val _photoResults = MutableLiveData<List<ResultPhotos>>()
|
||||
val photoResults: LiveData<List<ResultPhotos>> get() = _photoResults
|
||||
|
||||
private val _videoResults = MutableLiveData<List<ResultPhotos>>()
|
||||
val videoResults: LiveData<List<ResultPhotos>> get() = _videoResults
|
||||
|
||||
|
||||
//----------查看指定目录里面的文件
|
||||
|
||||
fun setPhotoResults(data: List<ResultPhotos>) {
|
||||
_photoResults.value = data
|
||||
}
|
||||
|
||||
fun setVideoResults(data: List<ResultPhotos>) {
|
||||
_videoResults.value = data
|
||||
}
|
||||
|
||||
private val _selectedFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||
val selectedFlow: StateFlow<Set<String>> = _selectedFlow
|
||||
|
||||
fun toggleSelection(boolean: Boolean, path: String) {
|
||||
val current = _selectedFlow.value.toMutableSet()
|
||||
if (boolean) {
|
||||
current.add(path)
|
||||
ScanManager.showLog("_------", "add selected ${path}")
|
||||
} else {
|
||||
current.remove(path)
|
||||
ScanManager.showLog("_------", "remove selected ${path}")
|
||||
}
|
||||
_selectedFlow.value = current
|
||||
}
|
||||
|
||||
fun checkIsSelect(path: String) : Boolean{
|
||||
val current = _selectedFlow.value.toMutableSet()
|
||||
return current.contains(path)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val instance by lazy { ScanRepository() }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.ux.video.file.filerecovery.utils
|
||||
|
||||
import com.ux.video.file.filerecovery.photo.ResultPhotos
|
||||
|
||||
|
||||
sealed class ScanState {
|
||||
data class Progress(val scannedCount: Int,val filePath: String) : ScanState()
|
||||
data class Complete(val result: ArrayList<ResultPhotos>) : ScanState()
|
||||
}
|
||||
25
app/src/main/res/drawable/circle_progress_drawable.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- 背景圆环 -->
|
||||
<item android:id="@android:id/background">
|
||||
<shape
|
||||
android:innerRadiusRatio="3"
|
||||
android:shape="ring"
|
||||
android:thicknessRatio="10"
|
||||
android:useLevel="false">
|
||||
<solid android:color="#DDDDDD" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- 前景进度圆环 -->
|
||||
<item android:id="@android:id/progress">
|
||||
<shape
|
||||
android:innerRadiusRatio="3"
|
||||
android:shape="ring"
|
||||
android:thicknessRatio="10"
|
||||
android:useLevel="true">
|
||||
<solid android:color="#3F51B5" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
app/src/main/res/drawable/ic_audio.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/drawable/ic_contact.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/drawable/ic_document.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +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="#3DDC84"
|
||||
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>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<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">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_recovered.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/drawable/ic_scan_deleted_file.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/src/main/res/drawable/ic_scan_file.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/src/main/res/drawable/ic_setting.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable/ic_wchat.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
app/src/main/res/drawable/icon_back.png
Normal file
|
After Width: | Height: | Size: 563 B |
9
app/src/main/res/drawable/icon_selected.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512.2,64.2c-247.4,0 -448,200.6 -448,448s200.6,448 448,448 448,-200.6 448,-448 -200.6,-448 -448,-448zM709,445.6L474.4,632.4c-14.7,11.7 -35.9,9.5 -48,-4.7 -0.5,-0.3 -1,-0.6 -1.4,-1L310.4,543c-15.5,-11.3 -18.8,-33 -7.5,-48.5s33,-18.8 48.5,-7.5l101.3,74.1 213.2,-169.7c15,-11.9 36.8,-9.5 48.7,5.5 11.8,14.9 9.4,36.8 -5.6,48.7z"
|
||||
android:fillColor="#13227a"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/icon_unselected.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M518,87.1c-235.3,0 -426,190.7 -426,426s190.7,426 426,426 426,-190.7 426,-426 -190.7,-426 -426,-426zM518,855.1c-188.9,0 -342.1,-153.2 -342.1,-342.1S329,170.9 518,170.9s342.1,153.2 342.1,342.1 -153.2,342.1 -342.1,342.1z"
|
||||
android:fillColor="#13227a"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/im_video.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
7
app/src/main/res/drawable/main_type_bg.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="20dp"/>
|
||||
<solid android:color="@color/white"/>
|
||||
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/photo_size_bg.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="12dp"/>
|
||||
<solid android:color="@color/color_photo_size_bg"/>
|
||||
|
||||
</shape>
|
||||
7
app/src/main/res/drawable/scan_select_bg.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="12dp"/>
|
||||
<solid android:color="@color/color_scan_select_bg"/>
|
||||
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/selector_icon_select.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/icon_selected" android:state_selected="true" />
|
||||
<item android:drawable="@drawable/icon_unselected" />
|
||||
</selector>
|
||||
17
app/src/main/res/layout/activity_documents_scan_result.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".documents.DocumentsScanResultActivity">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
298
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,298 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".main.MainActivity">
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="19dp"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="24sp"
|
||||
app:fontType="bold"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_setting"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_title" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_photo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="168dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:paddingTop="18dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/layout_video"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_title">
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:id="@+id/tv_photo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:text="@string/main_title_photo"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_photo"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/main_title_quick_recovery"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="14sp"
|
||||
app:fontType="regular" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_video"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="168dp"
|
||||
android:layout_marginStart="11dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:paddingTop="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/layout_photo"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_photo">
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:id="@+id/tv_video"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:text="@string/main_title_video"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:id="@+id/sub_video"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_video"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/main_title_video_recovery"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="14sp"
|
||||
app:fontType="regular" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/sub_video"
|
||||
android:layout_marginTop="12dp"
|
||||
android:src="@drawable/im_video" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_audio"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="18dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/layout_document"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_photo">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_audio" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/main_title_audio"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_document"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="11dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/layout_audio"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_audio">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_document" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/main_title_document"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_recovery"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="18dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/layout_wchat"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_audio">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_recovered" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/main_title_recovered"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_wchat"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="11dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/layout_recovery"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/layout_recovery">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_wchat" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/main_title_document"
|
||||
android:textColor="@color/main_sub_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_wchat">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_permission"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="管理所有文件的权限申请" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_scan_all_photo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="扫描所有图片" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_scan_all_video"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="扫描所有视频" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_scan_all_audio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="扫描所有音频" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_scan_all_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="扫描所有文件" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_permission"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="50dp"
|
||||
android:id="@+id/allow"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="Allow"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
169
app/src/main/res/layout/activity_photo_sorting.xml
Normal file
@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".photo.PhotoSortingActivity">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_select_counts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/app_name" />
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Delete"
|
||||
android:id="@+id/btn_delete"
|
||||
android:layout_toEndOf="@id/tv_select_counts"/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_select_counts"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_columns_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/columns_2" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_columns_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/columns_3" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_columns_4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/columns_4" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/linear_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/rg_layout"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="all size" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_1m"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0-1m" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_5m"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1-5m" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_over_5m"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="over 5m" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/linear_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/linear_size"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_date_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="all date" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_date_within_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="within 1 month" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_date_customize"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="customize" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/im_select_all"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_below="@id/linear_date"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/selector_icon_select" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@id/im_select_all"
|
||||
android:layout_alignBottom="@id/im_select_all"
|
||||
android:layout_toEndOf="@id/im_select_all"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/select_all" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/linear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/im_select_all"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_date_old_to_new"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="old" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_date_new_to_old"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="new" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_small_to_big"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="small" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/btn_size_big_to_small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="big" />
|
||||
</RadioGroup>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/linear" />
|
||||
|
||||
</RelativeLayout>
|
||||
47
app/src/main/res/layout/activity_scan_result_display.xml
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".result.ScanResultDisplayActivity">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_all_counts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="13" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_file_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_all_counts"
|
||||
android:text="photos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dir_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="75dp"
|
||||
android:layout_toEndOf="@id/text_all_counts"
|
||||
android:text="13" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dir"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_dir_count"
|
||||
android:layout_alignStart="@id/text_dir_count"
|
||||
android:text="Folders" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_dir"
|
||||
android:layout_marginTop="35dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
94
app/src/main/res/layout/activity_scan_select_type.xml
Normal file
@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".main.ScanSelectTypeActivity">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:background="@color/white"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="16sp"
|
||||
app:fontType="bold" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/color_bg"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/scan_all_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_scan_file" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/scan_all_file"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="20sp"
|
||||
app:fontType="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/scan_deleted_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/main_type_bg"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_scan_deleted_file" />
|
||||
|
||||
<com.ux.video.file.filerecovery.utils.CustomTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/scan_deleted_files"
|
||||
android:textColor="@color/main_title"
|
||||
android:textSize="20sp"
|
||||
app:fontType="bold" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
56
app/src/main/res/layout/activity_scanning.xml
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".result.ScanningActivity">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view_back"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/icon_back" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="45dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_toEndOf="@id/image_view_back"
|
||||
android:gravity="center"
|
||||
android:text="@string/app_name" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/scan_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="130dp"
|
||||
android:layout_height="130dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="260dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressDrawable="@drawable/circle_progress_drawable" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_scan_current_counts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/scan_progress"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="10 photos"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_scan_current_file_path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_scan_current_counts"
|
||||
android:layout_marginStart="25dp"
|
||||
android:layout_marginTop="25dp"
|
||||
android:text="10 photos"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
</RelativeLayout>
|
||||
22
app/src/main/res/layout/dialog_fullscreen.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="50dp"
|
||||
android:id="@+id/allow"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="Allow"/>
|
||||
|
||||
</RelativeLayout>
|
||||
18
app/src/main/res/layout/documents_scan_result_adapter.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_height="50dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/tv_dir_name"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/tv_file_count"/>
|
||||
|
||||
</RelativeLayout>
|
||||
31
app/src/main/res/layout/photo_display_date_adapter.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_date"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textSize="19sp"
|
||||
android:text="@string/app_name"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:padding="8dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:id="@+id/im_select_status"
|
||||
android:src="@drawable/selector_icon_select" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_child"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_date" />
|
||||
|
||||
</RelativeLayout>
|
||||
38
app/src/main/res/layout/photo_display_date_child_adapter.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/photo_size_bg">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="150kb"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="15sp" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/im_select_status"
|
||||
android:src="@drawable/selector_icon_select" />
|
||||
|
||||
</RelativeLayout>
|
||||
58
app/src/main/res/layout/scan_result_adapter.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="13dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_dir_name_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="26dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/im1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_below="@id/text_dir_name_count"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/im2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_dir_name_count" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/im2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_below="@id/text_dir_name_count"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
app:layout_constraintBottom_toBottomOf="@id/im1"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/im1"
|
||||
app:layout_constraintRight_toLeftOf="@id/im3"
|
||||
app:layout_constraintTop_toTopOf="@id/im1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/im3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_below="@id/text_dir_name_count"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
app:layout_constraintBottom_toBottomOf="@id/im1"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintLeft_toRightOf="@id/im2"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/im1" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?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" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?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" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
8
app/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.FileRecovery" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
<item name="android:windowBackground">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
10
app/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="CustomTextView">
|
||||
<attr name="fontType" format="enum">
|
||||
<enum name="regular" value="0"/>
|
||||
<enum name="bold" value="1"/>
|
||||
<enum name="italic" value="2"/>
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
11
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="color_scan_select_bg">#DDF6C1</color>
|
||||
<color name="color_photo_size_bg">#B1A6A6A6</color>
|
||||
|
||||
<color name="main_title">#000000</color>
|
||||
<color name="main_sub_title">#9696A2</color>
|
||||
<color name="color_bg">#F5F5FA</color>
|
||||
</resources>
|
||||
29
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<resources>
|
||||
<string name="app_name">File Recovery</string>
|
||||
<string name="scan_all_file">Scan all files</string>
|
||||
<string name="scan_deleted_files">Scan deleted files</string>
|
||||
<string name="select_all">Select all</string>
|
||||
|
||||
|
||||
<string name="size_kb">%.2f KB</string>
|
||||
<string name="size_mb">%.2f MB</string>
|
||||
<string name="size_gb">%.2f GB</string>
|
||||
|
||||
<string name="columns_2">2 columns</string>
|
||||
<string name="columns_3">3 columns</string>
|
||||
<string name="columns_4">4 columns</string>
|
||||
|
||||
<string name="main_title_photo">Photo</string>
|
||||
<string name="main_title_quick_recovery">Quick recovery</string>
|
||||
<string name="main_title_video">Video</string>
|
||||
<string name="main_title_video_recovery">Video recovery</string>
|
||||
<string name="main_title_audio">Audio</string>
|
||||
<string name="main_title_document">Document</string>
|
||||
<string name="main_title_recovered">Recovered</string>
|
||||
<string name="main_title_contacts_recovered">Contacts recovery</string>
|
||||
|
||||
<string name="photo_title">Photo recovery</string>
|
||||
<string name="video_title">Video recovery</string>
|
||||
<string name="audio_title">Audio recovery</string>
|
||||
<string name="document_title">Document recovery</string>
|
||||
</resources>
|
||||
11
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="FullScreenDialog" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
|
||||
</resources>
|
||||
9
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.FileRecovery" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
|
||||
<style name="Theme.FileRecovery" parent="Base.Theme.FileRecovery" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@ -0,0 +1,17 @@
|
||||
package com.ux.video.file.filerecovery
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
5
build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
}
|
||||
23
gradle.properties
Normal file
@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
29
gradle/libs.versions.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[versions]
|
||||
agp = "8.10.1"
|
||||
glide = "4.16.0"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.12.0"
|
||||
activity = "1.10.1"
|
||||
constraintlayout = "2.2.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
|
||||
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Tue Aug 26 15:34:03 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
gradlew
vendored
Normal file
@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
89
gradlew.bat
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
24
settings.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "FileRecovery"
|
||||
include(":app")
|
||||
|
||||