commit bdb31debc8a5ce4ab00a2c706b55780fbfac1ce9 Author: litingting Date: Wed Sep 10 10:43:40 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..f927f5b --- /dev/null +++ b/app/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/ux/video/file/filerecovery/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/ux/video/file/filerecovery/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..a944e2c --- /dev/null +++ b/app/src/androidTest/java/com/ux/video/file/filerecovery/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4a9c69d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/fonts/PingFang Bold_0.ttf b/app/src/main/assets/fonts/PingFang Bold_0.ttf new file mode 100644 index 0000000..accaf1f Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Bold_0.ttf differ diff --git a/app/src/main/assets/fonts/PingFang Regular_0.ttf b/app/src/main/assets/fonts/PingFang Regular_0.ttf new file mode 100644 index 0000000..8790adb Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Regular_0.ttf differ diff --git a/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt new file mode 100644 index 0000000..4196ef7 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseActivity.kt @@ -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 : 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() {} +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/base/BaseAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseAdapter.kt new file mode 100644 index 0000000..93c1374 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/base/BaseAdapter.kt @@ -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( + protected val mContext: Context +) : RecyclerView.Adapter>() { + + protected val data: MutableList = mutableListOf() + + + + fun addData(items: List?) { + items?.let { + val start = data.size + data.addAll(it) + notifyItemRangeInserted(start, it.size) + } + } + + fun setData(items: List?) { + data.clear() + items?.let { data.addAll(it) } + notifyDataSetChanged() + } + + + override fun getItemCount(): Int = data.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VHolder { + return VHolder(getViewBinding(parent)) + } + + override fun onBindViewHolder(holder: VHolder, position: Int) { + bindItem(holder, data[position]) + } + + + protected abstract fun getViewBinding(parent: ViewGroup): T + protected abstract fun bindItem(holder: VHolder, item: K) + + + class VHolder(val vb: V) : RecyclerView.ViewHolder(vb.root) +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultActivity.kt new file mode 100644 index 0000000..3ea21dd --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultActivity.kt @@ -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() { + + + 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) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultAdapter.kt new file mode 100644 index 0000000..9128a53 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/documents/DocumentsScanResultAdapter.kt @@ -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(mContext) { + override fun getViewBinding(parent: ViewGroup): DocumentsScanResultAdapterBinding = + DocumentsScanResultAdapterBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + override fun bindItem( + holder: VHolder, + item: ResultPhotos + ) { + + holder.vb.run { + item.run { + tvDirName.text = dirName + tvFileCount.text = allFiles.size.toString() + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/main/FullScreenDialogFragment.kt b/app/src/main/java/com/ux/video/file/filerecovery/main/FullScreenDialogFragment.kt new file mode 100644 index 0000000..9d7964d --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/main/FullScreenDialogFragment.kt @@ -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) + } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt new file mode 100644 index 0000000..bc5389e --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/main/MainActivity.kt @@ -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() { + + + //是否正确引导用户打开所有文件管理权限 + 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() + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt new file mode 100644 index 0000000..b1b0095 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/main/ScanSelectTypeActivity.kt @@ -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() { + + 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() + private var deletedType by Delegates.notNull() + 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) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt new file mode 100644 index 0000000..e0dfbb5 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateAdapter.kt @@ -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>, 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, + item: Pair> + ) { + 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 + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt new file mode 100644 index 0000000..9dfe517 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoDisplayDateChildAdapter.kt @@ -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(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, + 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target, + isFirstResource: Boolean + ): Boolean { + ScanManager.showLog( + "加载图片", + "-------path = ${path} file=${targetFile}" + ) + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + + return false + } + + }) + .into(imageThumbnail) + + } + } + } + + private fun updateSetList(boolean: Boolean, path: String) { + ScanRepository.instance.toggleSelection(boolean,path) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt new file mode 100644 index 0000000..a5ea72b --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/PhotoSortingActivity.kt @@ -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() { + + 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() // 保存选中状态 + + + //默认倒序排序 + private var sortReverse = true + + //筛选时间,默认全部-1 + private var filterDate = FILTER_DATE_ALL + + //筛选大小,默认全部-1 + private var filterSize = FILTER_SIZE_ALL + + + private lateinit var sortBySizeBigToSmall: List + private lateinit var sortBySizeSmallToBig: List + private lateinit var sortByDayReverse: List>> + private lateinit var sortedByPositive: List>> + + override fun inflateBinding(inflater: LayoutInflater): ActivityPhotoSortingBinding = + ActivityPhotoSortingBinding.inflate(inflater) + + override fun initData() { + super.initData() + + val list: ArrayList? = + 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 { + 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) + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotos.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotos.kt new file mode 100644 index 0000000..001e03e --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotos.kt @@ -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 +): Parcelable diff --git a/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt new file mode 100644 index 0000000..8cdc0ba --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/photo/ResultPhotosFiles.kt @@ -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) } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt new file mode 100644 index 0000000..37201d5 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultAdapter.kt @@ -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) -> Unit +) : + BaseAdapter(mContext) { + override fun getViewBinding(parent: ViewGroup): ScanResultAdapterBinding = + ScanResultAdapterBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + override fun bindItem( + holder: VHolder, + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt new file mode 100644 index 0000000..6dcb1fc --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanResultDisplayActivity.kt @@ -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() { + 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? = + 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) +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt new file mode 100644 index 0000000..963100c --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/result/ScanningActivity.kt @@ -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() { + + 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}" + ) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt new file mode 100644 index 0000000..af59f33 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/Common.kt @@ -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): List>> { + + + val grouped = list.groupBy { + dateFormat.format(Date(it.lastModified)) + } + val parentData: List>> = grouped + .map { it.key to it.value } + .sortedByDescending { dateFormat.parse(it.first)?.time ?: 0L } + return parentData + } + + /** + * 按照日期排序, 时间最早的排前面 + * + */ + fun getSortByDayOldToNew(list: List>>) = + list.sortedBy { dateFormat.parse(it.first)?.time ?: 0L } + + /** + * 按照文件大小排序,将最大的排前面 + */ + fun getSortBySizeBigToSmall(list: ArrayList) = list.sortedByDescending { + it.size + } + + /** + * 按照文件大小排序,将最大的排前面 + */ + fun getSortBySizeSmallToBig(list: ArrayList) = 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>>,months: Int + ): List>> { + 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,months: Int): List { + 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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt new file mode 100644 index 0000000..7334915 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/CustomTextView.kt @@ -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 + } + } + } + } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt new file mode 100644 index 0000000..ef6e30d --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ExtendFunctions.kt @@ -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 Intent.getParcelableArrayListExtraCompat(key: String): ArrayList? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableArrayListExtra(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableArrayListExtra(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.filterWithinMonthsList(months: Int): List { + 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.filterBySizeList( + minSize: Long, + maxSize: Long + ): List { + if (minSize == -1L) return this + return this.filter { it.size in minSize..maxSize } + } + + /** + * 按时间筛选:最近 N 个月 + */ + fun List>>.filterWithinMonths(months: Int): List>> { + 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>>.filterBySize( + minSize: Long, + maxSize: Long + ): List>> { + 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 + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/GridSpacingItemDecoration.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/GridSpacingItemDecoration.kt new file mode 100644 index 0000000..6ce22e0 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/GridSpacingItemDecoration.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt new file mode 100644 index 0000000..57f5747 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanManager.kt @@ -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 = flow { + + val result = mutableMapOf>() + 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 = flow { + + val result = mutableMapOf>() + 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): 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, + 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, + 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) + } + } + + } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt new file mode 100644 index 0000000..5fa5695 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanRepository.kt @@ -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>() + val photoResults: LiveData> get() = _photoResults + + private val _videoResults = MutableLiveData>() + val videoResults: LiveData> get() = _videoResults + + + //----------查看指定目录里面的文件 + + fun setPhotoResults(data: List) { + _photoResults.value = data + } + + fun setVideoResults(data: List) { + _videoResults.value = data + } + + private val _selectedFlow = MutableStateFlow>(emptySet()) + val selectedFlow: StateFlow> = _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() } + } +} diff --git a/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanState.kt b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanState.kt new file mode 100644 index 0000000..2d792b7 --- /dev/null +++ b/app/src/main/java/com/ux/video/file/filerecovery/utils/ScanState.kt @@ -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) : ScanState() +} \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_progress_drawable.xml b/app/src/main/res/drawable/circle_progress_drawable.xml new file mode 100644 index 0000000..c295c8d --- /dev/null +++ b/app/src/main/res/drawable/circle_progress_drawable.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_audio.png b/app/src/main/res/drawable/ic_audio.png new file mode 100644 index 0000000..ea0a745 Binary files /dev/null and b/app/src/main/res/drawable/ic_audio.png differ diff --git a/app/src/main/res/drawable/ic_contact.png b/app/src/main/res/drawable/ic_contact.png new file mode 100644 index 0000000..734dd19 Binary files /dev/null and b/app/src/main/res/drawable/ic_contact.png differ diff --git a/app/src/main/res/drawable/ic_document.png b/app/src/main/res/drawable/ic_document.png new file mode 100644 index 0000000..7c9b51c Binary files /dev/null and b/app/src/main/res/drawable/ic_document.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_recovered.png b/app/src/main/res/drawable/ic_recovered.png new file mode 100644 index 0000000..861e6b4 Binary files /dev/null and b/app/src/main/res/drawable/ic_recovered.png differ diff --git a/app/src/main/res/drawable/ic_scan_deleted_file.png b/app/src/main/res/drawable/ic_scan_deleted_file.png new file mode 100644 index 0000000..c91cdc7 Binary files /dev/null and b/app/src/main/res/drawable/ic_scan_deleted_file.png differ diff --git a/app/src/main/res/drawable/ic_scan_file.png b/app/src/main/res/drawable/ic_scan_file.png new file mode 100644 index 0000000..52d993b Binary files /dev/null and b/app/src/main/res/drawable/ic_scan_file.png differ diff --git a/app/src/main/res/drawable/ic_setting.png b/app/src/main/res/drawable/ic_setting.png new file mode 100644 index 0000000..663ac6a Binary files /dev/null and b/app/src/main/res/drawable/ic_setting.png differ diff --git a/app/src/main/res/drawable/ic_wchat.png b/app/src/main/res/drawable/ic_wchat.png new file mode 100644 index 0000000..cbdca9e Binary files /dev/null and b/app/src/main/res/drawable/ic_wchat.png differ diff --git a/app/src/main/res/drawable/icon_back.png b/app/src/main/res/drawable/icon_back.png new file mode 100644 index 0000000..0ecc07c Binary files /dev/null and b/app/src/main/res/drawable/icon_back.png differ diff --git a/app/src/main/res/drawable/icon_selected.xml b/app/src/main/res/drawable/icon_selected.xml new file mode 100644 index 0000000..d175613 --- /dev/null +++ b/app/src/main/res/drawable/icon_selected.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_unselected.xml b/app/src/main/res/drawable/icon_unselected.xml new file mode 100644 index 0000000..729986b --- /dev/null +++ b/app/src/main/res/drawable/icon_unselected.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/im_video.png b/app/src/main/res/drawable/im_video.png new file mode 100644 index 0000000..32b89b6 Binary files /dev/null and b/app/src/main/res/drawable/im_video.png differ diff --git a/app/src/main/res/drawable/main_type_bg.xml b/app/src/main/res/drawable/main_type_bg.xml new file mode 100644 index 0000000..1ad57c2 --- /dev/null +++ b/app/src/main/res/drawable/main_type_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/photo_size_bg.xml b/app/src/main/res/drawable/photo_size_bg.xml new file mode 100644 index 0000000..aecf70b --- /dev/null +++ b/app/src/main/res/drawable/photo_size_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/scan_select_bg.xml b/app/src/main/res/drawable/scan_select_bg.xml new file mode 100644 index 0000000..6ea1d53 --- /dev/null +++ b/app/src/main/res/drawable/scan_select_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_icon_select.xml b/app/src/main/res/drawable/selector_icon_select.xml new file mode 100644 index 0000000..c3dd063 --- /dev/null +++ b/app/src/main/res/drawable/selector_icon_select.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_documents_scan_result.xml b/app/src/main/res/layout/activity_documents_scan_result.xml new file mode 100644 index 0000000..2c0b52f --- /dev/null +++ b/app/src/main/res/layout/activity_documents_scan_result.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cf93e47 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,298 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +