Skip to content
Merged
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
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ 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

- applicationId = "com.djangofiles.djangofiles" Release
- applicationId = "com.djangofiles.djangofiles.dev" Debug

- minSdk = 26
- targetSdk = 36
- compileSdk = 37

## Commands

ALWAYS use the `task *` commands
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat

/**
* 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.
*
* 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
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
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()
}
}

// 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
// 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()
// Max distance the dialog can move up: down to just below the status bar.
val maxUp = (baseTop - barsTop).coerceAtLeast(0)
val shift = imeBottom.coerceAtMost(maxUp)
if (shift > 0) {
v.translationY = -shift.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

// Same retry interval as androidx.preference (postDelayed(..., 50)).
private const val SHOW_RETRY_DELAY_MS = 50L
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -306,6 +307,8 @@ class FilesBottomSheet : BottomSheetDialogFragment() {
}
}
}
.show()
.create()
dialog.showKeyboard()
dialog.show()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -731,7 +732,9 @@ fun Context.showExpireDialog(
}
}
}
.show()
.create()
dialog.showKeyboard()
dialog.show()
}

suspend fun Context.getAlbums(savedUrl: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -439,8 +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()
}
// NOTE: Keyboard can cover the bottom of dialog on small (1280px) devices
//dialog.showKeyboard()
dialog.show()
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/layout/dialog_feedback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:paddingHorizontal="24dp"
android:paddingTop="12dp"
android:paddingHorizontal="12dp"
android:gravity="center_horizontal"
android:orientation="vertical">

Expand Down
Loading