diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0d739125c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 60c2ad866..030a155ae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,18 +24,18 @@ apply plugin: 'com.google.firebase.crashlytics' //#endif android { - namespace 'me.zhanghai.android.files' + namespace = 'me.zhanghai.android.files' buildToolsVersion = '37.0.0' compileSdk = 36 - ndkVersion '28.1.13356709' + ndkVersion = '28.1.13356709' defaultConfig { applicationId 'me.zhanghai.android.files' - minSdk 23 + minSdk = 23 // Not supporting foreground service timeout yet. //noinspection OldTargetApi - targetSdk 34 - versionCode 39 - versionName '1.7.4' + targetSdk = 34 + versionCode = 39 + versionName = '1.7.4' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' buildConfigField 'String', 'FILE_PROVIDIER_AUTHORITY', 'APPLICATION_ID + ".file_provider"' resValue 'string', 'app_provider_authority', applicationId + '.app_provider' @@ -56,9 +56,9 @@ android { generateLocaleConfig true } compileOptions { - coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + coreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } externalNativeBuild { cmake { @@ -68,24 +68,24 @@ android { lint { // For "Invalid package reference in library; not included in Android: javax.security.sasl. // Referenced from org.apache.mina.proxy.ProxyAuthException." - warning 'InvalidPackage', 'MissingTranslation' + warning += ['InvalidPackage', 'MissingTranslation'] } buildTypes { release { - minifyEnabled true - shrinkResources true + minifyEnabled = true + shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.release + signingConfig = signingConfigs.release //#ifdef NONFREE firebaseCrashlytics { - nativeSymbolUploadEnabled true + nativeSymbolUploadEnabled = true } //#endif } } packagingOptions { jniLibs { - useLegacyPackaging true + useLegacyPackaging = true } resources { excludes += [ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f4b7b77d5..b339727da 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ ~ TODO: Remove this attribute once Samsung respects the default value for it. --> ("__epaper_back_button") != null) return + + val size = activity.dpToDimensionPixelSize(40) + val margin = activity.dpToDimensionPixelSize(8) + + val backButton = AppCompatImageButton(activity).apply { + setImageResource(androidx.appcompat.R.drawable.abc_ic_ab_back_material) + contentDescription = "Back" + isFocusable = true + isClickable = true + tag = "__epaper_back_button" + // Make background transparent to fit e-paper + background = null + } + + val lp = FrameLayout.LayoutParams(size, size, Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM).apply { + bottomMargin = margin + } + + // Add to decor view (top-most). Wrap in FrameLayout if needed. + if (decor is FrameLayout) { + decor.addView(backButton, lp) + } else { + val container = FrameLayout(activity) + container.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + // move children into container + while (decor.childCount > 0) { + val child = decor.getChildAt(0) + decor.removeViewAt(0) + container.addView(child) + } + decor.addView(container) + container.addView(backButton, lp) + } + + if (activity is LifecycleOwner) { + Settings.EPAPER_BACK_BUTTON.observe(activity) { enabled -> + backButton.visibility = if (enabled) View.VISIBLE else View.GONE + } + } else { + backButton.visibility = if (Settings.EPAPER_BACK_BUTTON.valueCompat) View.VISIBLE else View.GONE + } + + backButton.setOnClickListener { + try { + if (activity is androidx.activity.ComponentActivity) { + activity.onBackPressedDispatcher.onBackPressed() + } else { + @Suppress("DEPRECATION") + activity.onBackPressed() + } + } catch (e: Exception) { + activity.finish() + } + } + } catch (ignored: Exception) { + } + } + + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) { + try { + val decor = activity.window.decorView as ViewGroup + val v = decor.findViewWithTag("__epaper_back_button") + if (v != null) { + (v.parent as? ViewGroup)?.removeView(v) + } + } catch (ignored: Exception) {} + } + }) + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt index 90ca73813..c4df607fb 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt @@ -7,9 +7,15 @@ package me.zhanghai.android.files.coil import coil.request.ImageRequest import coil.transition.CrossfadeTransition +import me.zhanghai.android.files.util.AnimationConfig fun ImageRequest.Builder.fadeIn(durationMillis: Int): ImageRequest.Builder = apply { placeholder(android.R.color.transparent) - transitionFactory(CrossfadeTransition.Factory(durationMillis, true)) + val animDuration = AnimationConfig.getAnimDuration(durationMillis) + if (animDuration > 0) { + transitionFactory(CrossfadeTransition.Factory(animDuration, true)) + } else { + transitionFactory(CrossfadeTransition.Factory(0, false)) + } } diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt index 76fbe2285..a89534109 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt @@ -42,6 +42,7 @@ import me.zhanghai.android.files.provider.common.isEncrypted import me.zhanghai.android.files.util.ParcelableArgs import me.zhanghai.android.files.util.ParcelableState import me.zhanghai.android.files.util.RemoteCallback +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.finish import me.zhanghai.android.files.util.getArgs @@ -107,7 +108,7 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { val visible = !binding.nameLayout.isVisible binding.showNameArrowImage.animate() .rotation(if (visible) 90f else 0f) - .setDuration(shortAnimTime.toLong()) + .setDuration(AnimationConfig.getAnimDuration(shortAnimTime).toLong()) .setInterpolator(FastOutSlowInInterpolator()) .start() binding.nameLayout.isVisible = visible diff --git a/app/src/main/java/me/zhanghai/android/files/settings/Settings.kt b/app/src/main/java/me/zhanghai/android/files/settings/Settings.kt index d2a8e2035..708197aca 100644 --- a/app/src/main/java/me/zhanghai/android/files/settings/Settings.kt +++ b/app/src/main/java/me/zhanghai/android/files/settings/Settings.kt @@ -126,6 +126,11 @@ object Settings { R.string.pref_key_file_list_animation, R.bool.pref_default_value_file_list_animation ) + val EPAPER_BACK_BUTTON: SettingLiveData = + BooleanSettingLiveData( + R.string.pref_key_epaper_back_button, R.bool.pref_default_value_epaper_back_button + ) + val FILE_NAME_ELLIPSIZE: SettingLiveData = EnumSettingLiveData( R.string.pref_key_file_name_ellipsize, R.string.pref_default_value_file_name_ellipsize, diff --git a/app/src/main/java/me/zhanghai/android/files/ui/AnimatedListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/ui/AnimatedListAdapter.kt index 1b2117fca..514c54e02 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/AnimatedListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/AnimatedListAdapter.kt @@ -10,6 +10,7 @@ import android.os.Looper import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import me.zhanghai.android.files.R +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.getAnimation abstract class AnimatedListAdapter( @@ -98,7 +99,7 @@ abstract class AnimatedListAdapter( } protected open val isAnimationEnabled: Boolean - get() = true + get() = AnimationConfig.shouldAnimate() companion object { private const val ANIMATION_STAGGER_MILLIS = 20 diff --git a/app/src/main/java/me/zhanghai/android/files/ui/AppBarLayoutExpandHackListener.kt b/app/src/main/java/me/zhanghai/android/files/ui/AppBarLayoutExpandHackListener.kt index e3b3ddf12..550a94cc1 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/AppBarLayoutExpandHackListener.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/AppBarLayoutExpandHackListener.kt @@ -8,12 +8,13 @@ package me.zhanghai.android.files.ui import android.view.animation.AnimationUtils import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout +import me.zhanghai.android.files.util.AnimationConfig class AppBarLayoutExpandHackListener( private val recyclerView: RecyclerView ) : AppBarLayout.OnOffsetChangedListener { private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis() - + MAX_OFFSET_ANIMATION_DURATION) + + AnimationConfig.getAnimDuration(MAX_OFFSET_ANIMATION_DURATION.toLong())) private var lastVerticalOffset: Int? = null diff --git a/app/src/main/java/me/zhanghai/android/files/ui/CheckableItemBackground.kt b/app/src/main/java/me/zhanghai/android/files/ui/CheckableItemBackground.kt index 455d2f9a6..f9eb98758 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/CheckableItemBackground.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/CheckableItemBackground.kt @@ -13,6 +13,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import androidx.annotation.Dimension import androidx.appcompat.graphics.drawable.AnimatedStateListDrawableCompat +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.asColor import me.zhanghai.android.files.util.dpToDimension import me.zhanghai.android.files.util.dpToDimensionPixelOffset @@ -34,7 +35,7 @@ object CheckableItemBackground { context: Context ): Drawable = AnimatedStateListDrawableCompat().apply { - val shortAnimTime = context.shortAnimTime + val shortAnimTime = AnimationConfig.getAnimDuration(context.shortAnimTime) setEnterFadeDuration(shortAnimTime) setExitFadeDuration(shortAnimTime) val checkedDrawable = GradientDrawable().apply { diff --git a/app/src/main/java/me/zhanghai/android/files/ui/CrossfadeSubtitleToolbar.kt b/app/src/main/java/me/zhanghai/android/files/ui/CrossfadeSubtitleToolbar.kt index 84beed46e..db6dd28ed 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/CrossfadeSubtitleToolbar.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/CrossfadeSubtitleToolbar.kt @@ -17,11 +17,12 @@ import android.widget.TextView import androidx.annotation.AttrRes import androidx.appcompat.widget.Toolbar import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.shortAnimTime class CrossfadeSubtitleToolbar : Toolbar { private val subtitleAnimator = ObjectAnimator.ofFloat(null, View.ALPHA, 1f, 0f, 1f).apply { - duration = (2 * context.shortAnimTime).toLong() + duration = AnimationConfig.getAnimDuration(2 * context.shortAnimTime).toLong() interpolator = FastOutSlowInInterpolator() val listener = AnimatorListener() addUpdateListener(listener) diff --git a/app/src/main/java/me/zhanghai/android/files/ui/ThemedSpeedDialView.kt b/app/src/main/java/me/zhanghai/android/files/ui/ThemedSpeedDialView.kt index d08bfd425..5a8aee423 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/ThemedSpeedDialView.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/ThemedSpeedDialView.kt @@ -33,6 +33,7 @@ import me.zhanghai.android.files.compat.createCompat import me.zhanghai.android.files.compat.drawableCompat import me.zhanghai.android.files.compat.foregroundCompat import me.zhanghai.android.files.compat.setTextAppearanceCompat +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.ParcelableState import me.zhanghai.android.files.util.asColor import me.zhanghai.android.files.util.dpToDimensionPixelSize @@ -128,7 +129,7 @@ class ThemedSpeedDialView : SpeedDialView { mainFab.drawable, DRAWABLE_PROPERTY_LEVEL, if (isOpen) 10000 else 0 ) ) - duration = context.shortAnimTime.toLong() + duration = AnimationConfig.getAnimDuration(context.shortAnimTime).toLong() interpolator = FastOutSlowInInterpolator() } diff --git a/app/src/main/java/me/zhanghai/android/files/util/AnimationConfig.kt b/app/src/main/java/me/zhanghai/android/files/util/AnimationConfig.kt new file mode 100644 index 000000000..a28c9c68e --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/util/AnimationConfig.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.util + + import me.zhanghai.android.files.settings.Settings + +/** + * Centralized configuration for animations throughout the app. + * Set [ANIMATIONS_ENABLED] to false to disable all transition animations, + * useful for e-paper devices where animations cause visual artifacts. + */ +object AnimationConfig { + /** + * Enable or disable all animations in the app. + * Set to false for e-paper device compatibility. + */ + val ANIMATIONS_ENABLED: Boolean + get() = Settings.FILE_LIST_ANIMATION.valueCompat + + /** + * Get animation duration adjusted based on [ANIMATIONS_ENABLED] flag. + * @param durationMs The requested animation duration in milliseconds + * @return 0ms if animations are disabled, otherwise the requested duration + */ + fun getAnimDuration(durationMs: Int): Int = + if (ANIMATIONS_ENABLED) durationMs else 0 + + /** + * Get animation duration with zero if animations are disabled. + * Convenience function for cases where duration is already retrieved. + * @param durationMs The requested animation duration in milliseconds + * @return 0ms if animations are disabled, otherwise the requested duration + */ + fun getAnimDuration(durationMs: Long): Long = + if (ANIMATIONS_ENABLED) durationMs else 0L + + /** + * Check if animations should be applied. + * @return true if animations are enabled, false otherwise + */ + fun shouldAnimate(): Boolean = ANIMATIONS_ENABLED +} diff --git a/app/src/main/java/me/zhanghai/android/files/util/ViewExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/ViewExtensions.kt index 405e1e83a..e8288ae3f 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/ViewExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/ViewExtensions.kt @@ -112,7 +112,7 @@ suspend fun View.fadeIn(force: Boolean = false) { if (!(isLaidOut || force) || (isVisible && alpha == 1f)) { duration = 0 } else { - duration = context.shortAnimTime.toLong() + duration = AnimationConfig.getAnimDuration(context.shortAnimTime).toLong() interpolator = context.getInterpolator(android.R.interpolator.fast_out_slow_in) } start() @@ -130,7 +130,7 @@ suspend fun View.fadeOut(force: Boolean = false, gone: Boolean = false) { if (!(isLaidOut || force) || (!isVisible || alpha == 0f)) { duration = 0 } else { - duration = context.shortAnimTime.toLong() + duration = AnimationConfig.getAnimDuration(context.shortAnimTime).toLong() interpolator = context.getInterpolator(android.R.interpolator.fast_out_linear_in) } start() @@ -170,7 +170,7 @@ suspend fun View.slideIn(gravity: Int, force: Boolean = false) { if (!(isLaidOut || force)) { duration = 0 } else { - duration = context.shortAnimTime.toLong() + duration = AnimationConfig.getAnimDuration(context.shortAnimTime).toLong() interpolator = context.getInterpolator(android.R.interpolator.fast_out_slow_in) } start() @@ -194,7 +194,7 @@ suspend fun View.slideOut(gravity: Int, force: Boolean = false, gone: Boolean = if (!(isLaidOut || force)) { duration = 0 } else { - duration = context.shortAnimTime.toLong() + duration = AnimationConfig.getAnimDuration(context.shortAnimTime).toLong() interpolator = context.getInterpolator(android.R.interpolator.fast_out_linear_in) } start() diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt index cb082f449..305eb1082 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt @@ -28,6 +28,7 @@ import me.zhanghai.android.files.databinding.ImageViewerFragmentBinding import me.zhanghai.android.files.file.fileProviderUri import me.zhanghai.android.files.provider.common.delete import me.zhanghai.android.files.ui.DepthPageTransformer +import me.zhanghai.android.files.util.AnimationConfig import me.zhanghai.android.files.util.ParcelableArgs import me.zhanghai.android.files.util.ParcelableListParceler import me.zhanghai.android.files.util.ParcelableState @@ -95,7 +96,7 @@ class ImageViewerFragment : Fragment(), ConfirmDeleteDialogFragment.Listener { binding.appBarLayout.animate() .alpha(if (visible) 1f else 0f) .translationY(if (visible) 0f else -binding.appBarLayout.bottom.toFloat()) - .setDuration(mediumAnimTime.toLong()) + .setDuration(AnimationConfig.getAnimDuration(mediumAnimTime).toLong()) .setInterpolator(FastOutSlowInInterpolator()) .start() } @@ -110,7 +111,9 @@ class ImageViewerFragment : Fragment(), ConfirmDeleteDialogFragment.Listener { adapter = this@ImageViewerFragment.adapter // ViewPager saves its position and will restore it later. setCurrentItem(args.position, false) - setPageTransformer(DepthPageTransformer) + if (AnimationConfig.shouldAnimate()) { + setPageTransformer(DepthPageTransformer) + } registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { updateTitle() diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c067c70d4..64c93494d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -600,6 +600,7 @@ 黑色夜间模式 文件列表动画 + 底部返回按钮 显示长文件名 省略开头 diff --git a/app/src/main/res/values/donottranslate_prefs.xml b/app/src/main/res/values/donottranslate_prefs.xml index 5edcd3425..499a3f0a4 100644 --- a/app/src/main/res/values/donottranslate_prefs.xml +++ b/app/src/main/res/values/donottranslate_prefs.xml @@ -51,6 +51,8 @@ false key_file_list_animation true + key_epaper_back_button + true key_file_name_ellipsize 1 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 964179529..1a3baf9fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ No application found to handle this action + Copied to clipboard @string/preference_copied Open settings Shortcut created @@ -748,6 +749,7 @@ Black night mode File list animation + Bottom back button Display long file name Ellipsize the beginning diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 8cdac819a..ddc728576 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -46,6 +46,11 @@ android:title="@string/settings_file_list_animation_title" android:defaultValue="@bool/pref_default_value_file_list_animation" /> + +