From fba6272ce69de53725a6250345ad2c1a674e647e Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:13:53 -0700 Subject: [PATCH 1/7] Add Keyboard Show on Dialogs and Windows --- AGENTS.md | 6 ++++ .../ui/dialogs/DialogExtensions.kt | 33 +++++++++++++++++++ .../djangofiles/ui/files/FilesBottomSheet.kt | 7 ++-- .../djangofiles/ui/files/FilesFragment.kt | 7 ++-- .../djangofiles/ui/login/LoginFragment.kt | 9 +++++ .../ui/settings/SettingsFragment.kt | 2 ++ 6 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt diff --git a/AGENTS.md b/AGENTS.md index 109b0a3..330bf8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,12 @@ Android client for [Django Files Upload Server](https://github.com/django-files/ - `gradle/libs.versions.toml` - Library versions - `Taskfile.yml` - [task](https://github.com/go-task/task) commands +## Android + +minSdk = 26 +targetSdk = 36 +compileSdk = 37 + ## Commands ALWAYS use the `task *` commands diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt new file mode 100644 index 0000000..2488adc --- /dev/null +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt @@ -0,0 +1,33 @@ +package com.djangofiles.djangofiles.ui.dialogs + +import android.app.Dialog +import android.os.Build +import android.view.Window +import android.view.WindowInsets +import android.view.WindowManager + +/** + * Shows the soft keyboard for this dialog window. + * + * Copied from androidx.preference PreferenceDialogFragmentCompat.requestInputMethod() + * which is how EditTextPreference dialogs shows the keyboard when a dialog is shown. + * + * https://github.com/androidx/androidx/blob/androidx-main/preference/preference/src/main/java/androidx/preference/PreferenceDialogFragmentCompat.java + * + * AI NOTE: Call AFTER create() and BEFORE show() (like the library calls requestInputMethod + * in onCreateDialog). The focused editor and window flags must be in place before the + * dialog window gains focus or the keyboard will not show reliably. + */ +fun Dialog.showKeyboard() { + val window: Window = window ?: return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Same as androidx.preference Api30Impl.showIme(window) + window.decorView.windowInsetsController?.show(WindowInsets.Type.ime()) + } else { + // NOTE: SOFT_INPUT_ADJUST_PAN prevents shrinking the dialog + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE or + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + ) + } +} diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesBottomSheet.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesBottomSheet.kt index 9db4047..e1287f1 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesBottomSheet.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesBottomSheet.kt @@ -30,6 +30,7 @@ import com.djangofiles.djangofiles.ServerApi.FileEditRequest import com.djangofiles.djangofiles.copyToClipboard import com.djangofiles.djangofiles.databinding.FragmentFilesBottomBinding import com.djangofiles.djangofiles.db.AlbumDatabase +import com.djangofiles.djangofiles.ui.dialogs.showKeyboard import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -279,7 +280,7 @@ class FilesBottomSheet : BottomSheetDialogFragment() { layout.addView(input) input.setSelection(0, filePassword.length) - MaterialAlertDialogBuilder(requireContext(), R.style.AlertDialogTheme) + val dialog = MaterialAlertDialogBuilder(requireContext(), R.style.AlertDialogTheme) .setView(layout) .setTitle("Set Password") .setIcon(R.drawable.md_key_24) @@ -306,6 +307,8 @@ class FilesBottomSheet : BottomSheetDialogFragment() { } } } - .show() + .create() + dialog.showKeyboard() + dialog.show() } } diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesFragment.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesFragment.kt index 6b96d38..ed45158 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesFragment.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/files/FilesFragment.kt @@ -40,6 +40,7 @@ import com.djangofiles.djangofiles.databinding.FragmentFilesBinding import com.djangofiles.djangofiles.db.AlbumDao import com.djangofiles.djangofiles.db.AlbumDatabase import com.djangofiles.djangofiles.getUserAgent +import com.djangofiles.djangofiles.ui.dialogs.showKeyboard import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -708,7 +709,7 @@ fun Context.showExpireDialog( val preferences = PreferenceManager.getDefaultSharedPreferences(this) val savedUrl = preferences.getString("saved_url", "").toString() - MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme) + val dialog = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme) .setView(layout) .setTitle("Set Expiration") .setIcon(R.drawable.md_timer_24) @@ -731,7 +732,9 @@ fun Context.showExpireDialog( } } } - .show() + .create() + dialog.showKeyboard() + dialog.show() } suspend fun Context.getAlbums(savedUrl: String) { diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/login/LoginFragment.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/login/LoginFragment.kt index 96883ba..4472628 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/login/LoginFragment.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/login/LoginFragment.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.core.net.toUri import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment @@ -100,6 +101,14 @@ class LoginFragment : Fragment() { binding.hostnameText.requestFocus() binding.hostnameText.setSelection(binding.hostnameText.text.length) + // TODO: Validate this code and NOTE - This is separate from DialogExtensions.kt + // AI NOTE: requestFocus() only places the cursor. The IME must be shown explicitly + // via WindowInsetsController (recommended over InputMethodManager.showSoftInput(), + // which fails silently when the window does not have focus yet). Per docs, show() + // is guaranteed to be scheduled after the window is focused. + WindowCompat.getInsetsController(requireActivity().window, binding.hostnameText) + .show(WindowInsetsCompat.Type.ime()) + val loginFunction = View.OnClickListener { Log.d("loginFunction", "it: ${it.id}") val inputHost = binding.hostnameText.text.toString().trim() diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt index 95e4ae9..e9eb9b4 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt @@ -34,6 +34,7 @@ import com.djangofiles.djangofiles.api.FeedbackApi import com.djangofiles.djangofiles.db.Server import com.djangofiles.djangofiles.db.ServerDao import com.djangofiles.djangofiles.db.ServerDatabase +import com.djangofiles.djangofiles.ui.dialogs.showKeyboard import com.djangofiles.djangofiles.work.enqueueWorkRequest import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -441,6 +442,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } input.requestFocus() } + dialog.showKeyboard() dialog.show() } From 938d9b5aa208360c4fbed107b2edc7fd4a8d32ac Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 22 Aug 2026 13:28:29 -0700 Subject: [PATCH 2/7] Add Keyboard Show on Dialogs and Windows --- .../ui/dialogs/DialogExtensions.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt index 2488adc..9bfacd1 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt @@ -1,10 +1,13 @@ package com.djangofiles.djangofiles.ui.dialogs import android.app.Dialog +import android.content.Context import android.os.Build +import android.os.SystemClock import android.view.Window import android.view.WindowInsets import android.view.WindowManager +import android.view.inputmethod.InputMethodManager /** * Shows the soft keyboard for this dialog window. @@ -29,5 +32,37 @@ fun Dialog.showKeyboard() { WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN ) + + // TODO: Validate code below here - added to show keyboard in landscape in API <30 + // AI NOTE: Port of androidx.preference EditTextPreferenceDialogFragmentCompat + // scheduleShowSoftInputInner(): below Android R, imm.showSoftInput() is + // silently refused while the dialog window has not gained focus yet + // (async gap between show() and focus arriving), so retry every + // SHOW_RETRY_DELAY_MS until the system accepts the request or the + // SHOW_REQUEST_TIMEOUT_MS budget runs out - same values as the library. + val startMs = SystemClock.uptimeMillis() + + fun tryShow() { + val editor = window.currentFocus ?: window.decorView.findFocus() + if (editor != null) { + val imm = editor.context.getSystemService( + Context.INPUT_METHOD_SERVICE + ) as InputMethodManager + if (imm.showSoftInput(editor, 0)) { + return + } + } + if (SystemClock.uptimeMillis() - startMs < SHOW_REQUEST_TIMEOUT_MS) { + window.decorView.postDelayed({ tryShow() }, SHOW_RETRY_DELAY_MS) + } + } + + tryShow() } } + +// Same budget as androidx.preference (SHOW_REQUEST_TIMEOUT = 1000). +private const val SHOW_REQUEST_TIMEOUT_MS = 1000L + +// Same retry interval as androidx.preference (postDelayed(..., 50)). +private const val SHOW_RETRY_DELAY_MS = 50L From a981fbbeda2b4253ec507077cbeac81b58921ca2 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:12:55 -0700 Subject: [PATCH 3/7] Reduce Top Padding on Dialogs for API 30+ --- .../ui/dialogs/DialogExtensions.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt index 9bfacd1..cf57671 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt @@ -8,6 +8,9 @@ import android.view.Window import android.view.WindowInsets import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat /** * Shows the soft keyboard for this dialog window. @@ -24,9 +27,12 @@ import android.view.inputmethod.InputMethodManager fun Dialog.showKeyboard() { val window: Window = window ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + slideAboveIme() // Same as androidx.preference Api30Impl.showIme(window) window.decorView.windowInsetsController?.show(WindowInsets.Type.ime()) } else { + // AI NOTE: Below R, WindowInsetsCompat.Type.ime() carries no data, so fall back to the + // legacy system pan behavior there. // NOTE: SOFT_INPUT_ADJUST_PAN prevents shrinking the dialog window.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE or @@ -61,6 +67,63 @@ fun Dialog.showKeyboard() { } } +// AI NOTE: The dialog keeps its NATURAL SIZE and while the keyboard is open is TRANSLATED +// upward so its top edge pins right below the status bar, consuming the ENTIRE empty space +// between the dialog and the top of the screen. Any leftover room ends up between the +// dialog's bottom edge and the keyboard - it cannot disappear without resizing the dialog +// window frame, which squashes the AlertDialog into the leftover strip and makes it +// unreadable (SOFT_INPUT_ADJUST_RESIZE), and panning (SOFT_INPUT_ADJUST_PAN) only moves +// the window until the FOCUSED editor clears the top of the keyboard - ViewRootImpl +// scrollY = focusRect.top - visibleTop - which leaves dead space above the dialog while +// the bottom buttons stay covered. +// +// Mechanics: a Dialog has its own Window with its own softInputMode; the activity manifest +// setting never applies to it. ADJUST_NOTHING disables both built-in behaviors so nothing +// fights this manual translation, and setDecorFitsSystemWindows(false) lets the raw ime() +// insets through to the listener. Per AOSP InsetsState.processSource(), ime() insets are +// calculated relative to THIS window's frame, so ime.bottom on the dialog = exactly how +// many pixels of it the keyboard covers. +private fun Dialog.slideAboveIme() { + val window = window ?: return + val decor = window.decorView + WindowCompat.setDecorFitsSystemWindows(window, false) + window.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE or + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING + ) + ViewCompat.setOnApplyWindowInsetsListener(decor) { v, insets -> + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + val barsTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top + if (!imeVisible || imeBottom <= 0) { + if (v.translationY != 0f) v.translationY = 0f + return@setOnApplyWindowInsetsListener insets + } + // Post: getLocationOnScreen() can be stale mid-layout during inset dispatch. + // translationY is subtracted back out so repeated callbacks stay anchored to the + // window's untranslated position instead of drifting upward every callback. + v.post { + val location = IntArray(2) + v.getLocationOnScreen(location) + val baseTop = location[1] - v.translationY.toInt() + // Consume the ENTIRE gap above the dialog: pin its top edge right below the + // status bar while the keyboard is open. + val maxUp = (baseTop - barsTop).coerceAtLeast(0) + if (maxUp > 0) { + v.translationY = -maxUp.toFloat() + } else if (v.translationY != 0f) { + v.translationY = 0f + } + } + insets + } + // Re-evaluate when the dialog's own layout changes (e.g. the multiline feedback + // EditText grows between minLines and maxLines while typing). + decor.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ -> + ViewCompat.requestApplyInsets(view) + } +} + // Same budget as androidx.preference (SHOW_REQUEST_TIMEOUT = 1000). private const val SHOW_REQUEST_TIMEOUT_MS = 1000L From 94917bad7ffb269df4b9c5bd2b819fe5dc1eaeac Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:13:43 -0700 Subject: [PATCH 4/7] Update feedback dialog padding and focus --- .../djangofiles/djangofiles/ui/settings/SettingsFragment.kt | 6 ++++-- app/src/main/res/layout/dialog_feedback.xml | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt index e9eb9b4..5c27caf 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/settings/SettingsFragment.kt @@ -440,9 +440,11 @@ class SettingsFragment : PreferenceFragmentCompat() { input.error = "Feedback is Required" } } - input.requestFocus() + // NOTE: Since were not showing keyboard (below) do not the focus field? + // input.requestFocus() } - dialog.showKeyboard() + // NOTE: Keyboard can cover the bottom of dialog on small (1280px) devices + //dialog.showKeyboard() dialog.show() } diff --git a/app/src/main/res/layout/dialog_feedback.xml b/app/src/main/res/layout/dialog_feedback.xml index 4fbe679..9975654 100644 --- a/app/src/main/res/layout/dialog_feedback.xml +++ b/app/src/main/res/layout/dialog_feedback.xml @@ -13,8 +13,8 @@ From 2b85e38ee414aea4abed1bf554122b5dca7820e8 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:20:05 -0700 Subject: [PATCH 5/7] Remove AI added hift = imeBottom.coerceAtMost(maxUp) --- .../ui/dialogs/DialogExtensions.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt index cf57671..ac7d39f 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt @@ -67,15 +67,14 @@ fun Dialog.showKeyboard() { } } -// AI NOTE: The dialog keeps its NATURAL SIZE and while the keyboard is open is TRANSLATED -// upward so its top edge pins right below the status bar, consuming the ENTIRE empty space -// between the dialog and the top of the screen. Any leftover room ends up between the -// dialog's bottom edge and the keyboard - it cannot disappear without resizing the dialog -// window frame, which squashes the AlertDialog into the leftover strip and makes it -// unreadable (SOFT_INPUT_ADJUST_RESIZE), and panning (SOFT_INPUT_ADJUST_PAN) only moves -// the window until the FOCUSED editor clears the top of the keyboard - ViewRootImpl -// scrollY = focusRect.top - visibleTop - which leaves dead space above the dialog while -// the bottom buttons stay covered. +// AI NOTE: The dialog keeps its NATURAL SIZE and is TRANSLATED upward into the empty space +// between its top and the top of the screen, until either its bottom edge clears the +// keyboard or its top reaches just below the status bar. This fills the gap above instead +// of shrinking (SOFT_INPUT_ADJUST_RESIZE squashes the whole AlertDialog window frame into +// the leftover strip and makes it unreadable) and instead of panning +// (SOFT_INPUT_ADJUST_PAN only moves the window until the FOCUSED editor clears the top of +// the keyboard - ViewRootImpl scrollY = focusRect.top - visibleTop - which leaves dead +// space above the dialog while the bottom buttons stay covered). // // Mechanics: a Dialog has its own Window with its own softInputMode; the activity manifest // setting never applies to it. ADJUST_NOTHING disables both built-in behaviors so nothing @@ -106,11 +105,11 @@ private fun Dialog.slideAboveIme() { val location = IntArray(2) v.getLocationOnScreen(location) val baseTop = location[1] - v.translationY.toInt() - // Consume the ENTIRE gap above the dialog: pin its top edge right below the - // status bar while the keyboard is open. + // Max distance the dialog can move up: down to just below the status bar. val maxUp = (baseTop - barsTop).coerceAtLeast(0) - if (maxUp > 0) { - v.translationY = -maxUp.toFloat() + val shift = imeBottom.coerceAtMost(maxUp) + if (shift > 0) { + v.translationY = -shift.toFloat() } else if (v.translationY != 0f) { v.translationY = 0f } From bdd3033f570a18ac5a96a367f9eb3486e7b86998 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:53:51 -0700 Subject: [PATCH 6/7] Add NOTE --- .../com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt index ac7d39f..60511b0 100644 --- a/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt +++ b/app/src/main/java/com/djangofiles/djangofiles/ui/dialogs/DialogExtensions.kt @@ -23,6 +23,9 @@ import androidx.core.view.WindowInsetsCompat * AI NOTE: Call AFTER create() and BEFORE show() (like the library calls requestInputMethod * in onCreateDialog). The focused editor and window flags must be in place before the * dialog window gains focus or the keyboard will not show reliably. + * + * IMPORTANT: This should probably be reverted to the simplified version: + * https://github.com/cssnr/zipline-android/blob/master/app/src/main/java/org/cssnr/zipline/ui/dialogs/DialogExtensions.kt */ fun Dialog.showKeyboard() { val window: Window = window ?: return From 04acf3272fb8e6c440c528b24a6ba81b1d359f8d Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:58:47 -0700 Subject: [PATCH 7/7] AGENTS.md --- AGENTS.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 330bf8d..34757b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,9 +8,12 @@ Android client for [Django Files Upload Server](https://github.com/django-files/ ## Android -minSdk = 26 -targetSdk = 36 -compileSdk = 37 +- applicationId = "com.djangofiles.djangofiles" Release +- applicationId = "com.djangofiles.djangofiles.dev" Debug + +- minSdk = 26 +- targetSdk = 36 +- compileSdk = 37 ## Commands