Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
30 changes: 15 additions & 15 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand All @@ -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 += [
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
~ TODO: Remove this attribute once Samsung respects the default value for it.
-->
<application
android:name="me.zhanghai.android.files.App"
android:allowBackup="true"
android:banner="@drawable/banner"
android:enableOnBackInvokedCallback="true"
Expand Down
106 changes: 106 additions & 0 deletions app/src/main/java/me/zhanghai/android/files/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Auto-added Application subclass to inject a visible back button into activities.
*/

package me.zhanghai.android.files

import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.widget.AppCompatImageButton
import me.zhanghai.android.files.settings.Settings
import me.zhanghai.android.files.util.dpToDimensionPixelSize
import me.zhanghai.android.files.util.valueCompat
import androidx.lifecycle.LifecycleOwner

class App : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
try {
val decor = activity.window.decorView as ViewGroup
// Avoid injecting twice
if (decor.findViewWithTag<View>("__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<View>("__epaper_back_button")
if (v != null) {
(v.parent as? ViewGroup)?.removeView(v)
}
} catch (ignored: Exception) {}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean> =
BooleanSettingLiveData(
R.string.pref_key_epaper_back_button, R.bool.pref_default_value_epaper_back_button
)

val FILE_NAME_ELLIPSIZE: SettingLiveData<TextUtils.TruncateAt> =
EnumSettingLiveData(
R.string.pref_key_file_name_ellipsize, R.string.pref_default_value_file_name_ellipsize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, VH : RecyclerView.ViewHolder>(
Expand Down Expand Up @@ -98,7 +99,7 @@ abstract class AnimatedListAdapter<T, VH : RecyclerView.ViewHolder>(
}

protected open val isAnimationEnabled: Boolean
get() = true
get() = AnimationConfig.shouldAnimate()

companion object {
private const val ANIMATION_STAGGER_MILLIS = 20
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
* 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
Loading