Skip to content

Commit

Permalink
Feat: Add checksums tab for file properties
Browse files Browse the repository at this point in the history
Fixes: #48
  • Loading branch information
zhanghai committed Apr 17, 2024
1 parent 62253bb commit 10a67dd
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
Expand All @@ -21,6 +22,7 @@ import me.zhanghai.android.files.filelist.name
import me.zhanghai.android.files.fileproperties.apk.FilePropertiesApkTabFragment
import me.zhanghai.android.files.fileproperties.audio.FilePropertiesAudioTabFragment
import me.zhanghai.android.files.fileproperties.basic.FilePropertiesBasicTabFragment
import me.zhanghai.android.files.fileproperties.checksum.FilePropertiesChecksumTabFragment
import me.zhanghai.android.files.fileproperties.image.FilePropertiesImageTabFragment
import me.zhanghai.android.files.fileproperties.permission.FilePropertiesPermissionTabFragment
import me.zhanghai.android.files.fileproperties.video.FilePropertiesVideoTabFragment
Expand Down Expand Up @@ -70,6 +72,15 @@ class FilePropertiesDialogFragment : AppCompatDialogFragment() {
to { FilePropertiesPermissionTabFragment() }
)
}
if (FilePropertiesChecksumTabFragment.isAvailable(args.file)) {
add(
R.string.file_properties_checksum to {
FilePropertiesChecksumTabFragment().putArgs(
FilePropertiesChecksumTabFragment.Args(args.file.path)
)
}
)
}
if (FilePropertiesImageTabFragment.isAvailable(args.file)) {
add(
R.string.file_properties_image to {
Expand Down Expand Up @@ -117,6 +128,14 @@ class FilePropertiesDialogFragment : AppCompatDialogFragment() {
binding.tabLayout.setupWithViewPager(binding.viewPager)
}

override fun onStart() {
super.onStart()

// AlertDialog (its AlertController) adds FLAG_ALT_FOCUSABLE_IM when the initial custom
// view doesn't have any view that returns true for onCheckIsTextEditor().
requireDialog().window!!.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}

companion object {
fun show(file: FileItem, fragment: Fragment) {
FilePropertiesDialogFragment().putArgs(Args(file)).show(fragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.get
import androidx.core.view.size
import androidx.core.view.forEach
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import me.zhanghai.android.files.databinding.FilePropertiesTabFragmentBinding
import me.zhanghai.android.files.databinding.FilePropertiesTabItemBinding
import me.zhanghai.android.files.util.Failure
Expand Down Expand Up @@ -68,29 +68,41 @@ abstract class FilePropertiesTabFragment : Fragment() {
}
}

protected class ViewBuilder(private val linearLayout: LinearLayout) {
private var itemCount = 0
protected class ViewBuilder(val linearLayout: LinearLayout) {
private val scrapViews = mutableMapOf<Class<out ViewBinding>, MutableList<ViewBinding>>()

init {
linearLayout.forEach { view ->
val binding = view.tag as ViewBinding
scrapViews.getOrPut(binding.javaClass) { mutableListOf() } += binding
}
linearLayout.removeAllViews()
}

@Suppress("UNCHECKED_CAST")
fun <T : ViewBinding> getScrapItemBinding(bindingClass: Class<T>): T? =
scrapViews[bindingClass]?.removeLastOrNull() as T?

fun addView(binding: ViewBinding) {
linearLayout.addView(binding.root)
}

fun addItemView(
hint: String,
text: String,
onClickListener: ((View) -> Unit)? = null
): TextView {
val itemBinding = if (itemCount < linearLayout.size) {
linearLayout[itemCount].tag as FilePropertiesTabItemBinding
} else {
FilePropertiesTabItemBinding.inflate(
linearLayout.context.layoutInflater, linearLayout, true
).also { it.root.tag = it }
}
val itemBinding =
getScrapItemBinding(FilePropertiesTabItemBinding::class.java)?.also { addView(it) }
?: FilePropertiesTabItemBinding.inflate(
linearLayout.context.layoutInflater, linearLayout, true
)
.also { it.root.tag = it }
itemBinding.textInputLayout.hint = hint
itemBinding.textInputLayout.setDropDown(onClickListener != null)
itemBinding.text.setText(text)
itemBinding.text.setTextIsSelectable(onClickListener == null)
itemBinding.text.setOnClickListener(
onClickListener?.let { View.OnClickListener(it) }
)
++itemCount
itemBinding.text.setOnClickListener(onClickListener?.let { View.OnClickListener(it) })
return itemBinding.text
}

Expand All @@ -101,9 +113,7 @@ abstract class FilePropertiesTabFragment : Fragment() {
): TextView = addItemView(linearLayout.context.getString(hintRes), text, onClickListener)

fun build() {
for (index in linearLayout.size - 1 downTo itemCount) {
linearLayout.removeViewAt(index)
}
scrapViews.clear()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.fileproperties.checksum

import androidx.annotation.StringRes
import me.zhanghai.android.files.R
import java.security.MessageDigest

class ChecksumInfo(val checksums: Map<Algorithm, String>) {
enum class Algorithm(@StringRes val nameRes: Int) {
CRC32(R.string.file_properties_checksum_crc32),
MD5(R.string.file_properties_checksum_md5),
SHA1(R.string.file_properties_checksum_sha_1),
SHA256(R.string.file_properties_checksum_sha_256),
SHA512(R.string.file_properties_checksum_sha_512);

fun createMessageDigest(): MessageDigest =
when (this) {
CRC32 -> Crc32MessageDigest()
MD5 -> MessageDigest.getInstance("MD5")
SHA1 -> MessageDigest.getInstance("SHA-1")
SHA256 -> MessageDigest.getInstance("SHA-256")
SHA512 -> MessageDigest.getInstance("SHA-512")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.fileproperties.checksum

import android.os.AsyncTask
import java8.nio.file.Path
import me.zhanghai.android.files.fileproperties.PathObserverLiveData
import me.zhanghai.android.files.provider.common.newInputStream
import me.zhanghai.android.files.util.Failure
import me.zhanghai.android.files.util.Loading
import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.Success
import me.zhanghai.android.files.util.toHexString
import me.zhanghai.android.files.util.valueCompat

class ChecksumInfoLiveData(path: Path) : PathObserverLiveData<Stateful<ChecksumInfo>>(path) {
init {
loadValue()
observe()
}

override fun loadValue() {
value = Loading(value?.value)
AsyncTask.THREAD_POOL_EXECUTOR.execute {
val value = try {
val messageDigests =
ChecksumInfo.Algorithm.entries.associateWith { it.createMessageDigest() }
path.newInputStream().use { inputStream ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
val readSize = inputStream.read(buffer)
if (readSize == -1) {
break
}
messageDigests.values.forEach { it.update(buffer, 0, readSize) }
}
}
val checksumInfo = ChecksumInfo(
messageDigests.mapValues { it.value.digest().toHexString() }
)
Success(checksumInfo)
} catch (e: Exception) {
Failure(valueCompat.value, e)
}
postValue(value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.fileproperties.checksum

import java.security.MessageDigest
import java.util.zip.CRC32

class Crc32MessageDigest : MessageDigest("CRC32") {
private val crc32 = CRC32()

override fun engineUpdate(input: Byte) {
crc32.update(input.toInt())
}

override fun engineUpdate(input: ByteArray, offset: Int, length: Int) {
crc32.update(input, offset, length)
}

override fun engineDigest(): ByteArray {
val value = crc32.value
crc32.reset()
return ByteArray(4).apply {
this[0] = (value ushr 24).toByte()
this[1] = (value ushr 16).toByte()
this[2] = (value ushr 8).toByte()
this[3] = value.toByte()
}
}

override fun engineReset() {
crc32.reset()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.fileproperties.checksum

import androidx.core.widget.doAfterTextChanged
import java8.nio.file.Path
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import me.zhanghai.android.files.R
import me.zhanghai.android.files.databinding.FilePropertiesChecksumCompareItemBinding
import me.zhanghai.android.files.file.FileItem
import me.zhanghai.android.files.fileproperties.FilePropertiesTabFragment
import me.zhanghai.android.files.util.ParcelableArgs
import me.zhanghai.android.files.util.ParcelableParceler
import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.args
import me.zhanghai.android.files.util.layoutInflater
import me.zhanghai.android.files.util.viewModels

class FilePropertiesChecksumTabFragment : FilePropertiesTabFragment() {
private val args by args<Args>()

private val viewModel by viewModels { { FilePropertiesChecksumTabViewModel(args.path) } }

override fun onResume() {
super.onResume()

viewModel.checksumInfoLiveData.observe(viewLifecycleOwner) { onChecksumInfoChanged(it) }
}

override fun refresh() {
viewModel.reload()
}

private fun onChecksumInfoChanged(stateful: Stateful<ChecksumInfo>) {
bindView(stateful) { checksumInfo ->
checksumInfo.checksums.forEach { addItemView(it.key.nameRes, it.value) }
addCompareEdit(checksumInfo)
}
}

private fun ViewBuilder.addCompareEdit(checksumInfo: ChecksumInfo) {
val binding = getScrapItemBinding(FilePropertiesChecksumCompareItemBinding::class.java)
?.also { addView(it) }
?: FilePropertiesChecksumCompareItemBinding.inflate(
linearLayout.context.layoutInflater, linearLayout, true
)
.also { it.root.tag = it }
binding.compareEdit.doAfterTextChanged { editable ->
val text = editable!!.toString().trim()
if (text.isEmpty()) {
binding.compareLayout.helperText = null
binding.compareLayout.error = null
return@doAfterTextChanged
}
val matchingAlgorithm = checksumInfo.checksums.firstNotNullOfOrNull {
if (it.value.equals(text, true)) it.key else null
}
if (matchingAlgorithm != null) {
binding.compareLayout.helperText =
getString(
R.string.file_properties_checksum_compare_match_format,
getString(matchingAlgorithm.nameRes)
)
return@doAfterTextChanged
}
val prefixMatchingAlgorithm = checksumInfo.checksums.firstNotNullOfOrNull {
if (it.value.startsWith(text, true)) it.key else null
}
if (prefixMatchingAlgorithm != null) {
binding.compareLayout.helperText =
getString(
R.string.file_properties_checksum_compare_prefix_match_format,
getString(prefixMatchingAlgorithm.nameRes)
)
return@doAfterTextChanged
}
binding.compareLayout.error =
getString(R.string.file_properties_checksum_compare_no_match)
}
}

companion object {
fun isAvailable(file: FileItem): Boolean = file.attributes.isRegularFile
}

@Parcelize
class Args(val path: @WriteWith<ParcelableParceler> Path) : ParcelableArgs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/

package me.zhanghai.android.files.fileproperties.checksum

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import java8.nio.file.Path
import me.zhanghai.android.files.util.Stateful

class FilePropertiesChecksumTabViewModel(path: Path) : ViewModel() {
private val _checksumInfoLiveData = ChecksumInfoLiveData(path)
val checksumInfoLiveData: LiveData<Stateful<ChecksumInfo>>
get() = _checksumInfoLiveData

fun reload() {
_checksumInfoLiveData.loadValue()
}

override fun onCleared() {
_checksumInfoLiveData.close()
}
}
25 changes: 25 additions & 0 deletions app/src/main/res/layout/file_properties_checksum_compare_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>

<!--
~ Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
~ All Rights Reserved.
-->

<com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/compareLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/file_properties_checksum_compare"
app:errorEnabled="true"
app:expandedHintEnabled="false"
app:placeholderText="@string/file_properties_checksum_compare_placeholder">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/compareEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="no"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>

0 comments on commit 10a67dd

Please sign in to comment.