notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit bd852331206bc36916fb060168e184ecb33ff65a
parent 09ad354d240dc78e9c1a0dd7299921f8d328f23d
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 11 Mar 2025 10:09:39 -0700

android: capture current keyboard height

expose a new virtual_keyboard_height function under notedeck::platform::android

which gets the current height of the virtual keyboard. We can use this
to tranlate the view out of the way

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 1+
MCargo.toml | 1+
Mcrates/notedeck/Cargo.toml | 4++++
Mcrates/notedeck/src/lib.rs | 1+
Acrates/notedeck/src/platform/android.rs | 26++++++++++++++++++++++++++
Acrates/notedeck/src/platform/mod.rs | 2++
Acrates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightHelper.java | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightObserver.java | 35+++++++++++++++++++++++++++++++++++
Acrates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightProvider.java | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
10 files changed, 348 insertions(+), 20 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2783,6 +2783,7 @@ dependencies = [ "enostr", "hex", "image", + "jni", "mime_guess", "nostrdb", "poll-promise", diff --git a/Cargo.toml b/Cargo.toml @@ -61,6 +61,7 @@ sha2 = "0.10.8" bincode = "1.3.3" mime_guess = "2.0.5" pretty_assertions = "1.4.1" +jni = "0.21.1" [profile.small] inherits = 'release' diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml @@ -6,6 +6,7 @@ description = "The APIs and data structures used by notedeck apps" [dependencies] nostrdb = { workspace = true } +jni = { workspace = true } url = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } @@ -35,5 +36,8 @@ tempfile = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] security-framework = { workspace = true } +[target.'cfg(target_os = "android")'.dependencies] +jni = { workspace = true } + [features] profiling = ["puffin", "puffin_egui"] diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -11,6 +11,7 @@ mod muted; pub mod note; mod notecache; mod persist; +pub mod platform; pub mod relay_debug; pub mod relayspec; mod result; diff --git a/crates/notedeck/src/platform/android.rs b/crates/notedeck/src/platform/android.rs @@ -0,0 +1,26 @@ +use std::sync::atomic::{AtomicI32, Ordering}; +use tracing::debug; + +// Thread-safe static global +static KEYBOARD_HEIGHT: AtomicI32 = AtomicI32::new(0); + +/// This function is called by our main notedeck android activity when the +/// keyboard height changes. You can use [`virtual_keyboard_height`] to access +/// this +#[no_mangle] +pub extern "C" fn Java_com_damus_notedeck_KeyboardHeightHelper_nativeKeyboardHeightChanged( + _env: jni::JNIEnv, + _class: jni::objects::JClass, + height: jni::sys::jint, +) { + debug!("updating virtual keyboard height {}", height); + + // Convert and store atomically + KEYBOARD_HEIGHT.store(height as i32, Ordering::SeqCst); +} + +/// Gets the current Android virtual keyboard height. Useful for transforming +/// the view +pub fn virtual_keyboard_height() -> i32 { + KEYBOARD_HEIGHT.load(Ordering::SeqCst) +} diff --git a/crates/notedeck/src/platform/mod.rs b/crates/notedeck/src/platform/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_os = "android")] +pub mod android; diff --git a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightHelper.java b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightHelper.java @@ -0,0 +1,48 @@ +package com.damus.notedeck; + +import android.app.Activity; +import android.content.res.Configuration; +import android.util.Log; +import android.view.View; + +public class KeyboardHeightHelper { + private static final String TAG = "KeyboardHeightHelper"; + private KeyboardHeightProvider keyboardHeightProvider; + private Activity activity; + + // Static JNI method not tied to any specific activity + private static native void nativeKeyboardHeightChanged(int height); + + public KeyboardHeightHelper(Activity activity) { + this.activity = activity; + keyboardHeightProvider = new KeyboardHeightProvider(activity); + + // Create observer implementation + KeyboardHeightObserver observer = (height, orientation) -> { + Log.d(TAG, "Keyboard height: " + height + "px, orientation: " + + (orientation == Configuration.ORIENTATION_PORTRAIT ? "portrait" : "landscape")); + + // Call the generic native method + nativeKeyboardHeightChanged(height); + }; + + // Set up the provider + keyboardHeightProvider.setKeyboardHeightObserver(observer); + } + + public void start() { + // Start the keyboard height provider after the view is ready + final View contentView = activity.findViewById(android.R.id.content); + contentView.post(() -> { + keyboardHeightProvider.start(); + }); + } + + public void stop() { + keyboardHeightProvider.setKeyboardHeightObserver(null); + } + + public void close() { + keyboardHeightProvider.close(); + } +} diff --git a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightObserver.java b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightObserver.java @@ -0,0 +1,35 @@ +/* + * This file is part of Siebe Projects samples. + * + * Siebe Projects samples is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Siebe Projects samples is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Lesser GNU General Public License for more details. + * + * You should have received a copy of the Lesser GNU General Public License + * along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.damus.notedeck; + +/** + * The observer that will be notified when the height of + * the keyboard has changed + */ +public interface KeyboardHeightObserver { + + /** + * Called when the keyboard height has changed, 0 means keyboard is closed, + * >= 1 means keyboard is opened. + * + * @param height The height of the keyboard in pixels + * @param orientation The orientation either: Configuration.ORIENTATION_PORTRAIT or + * Configuration.ORIENTATION_LANDSCAPE + */ + void onKeyboardHeightChanged(int height, int orientation); +} diff --git a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightProvider.java b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/KeyboardHeightProvider.java @@ -0,0 +1,174 @@ +/* + * This file is part of Siebe Projects samples. + * + * Siebe Projects samples is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Siebe Projects samples is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Lesser GNU General Public License for more details. + * + * You should have received a copy of the Lesser GNU General Public License + * along with Siebe Projects samples. If not, see <http://www.gnu.org/licenses/>. + */ + +package com.damus.notedeck; + +import android.app.Activity; + +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.Log; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.util.DisplayMetrics; + +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; + +import android.view.WindowManager.LayoutParams; + +import android.widget.PopupWindow; + + +/** + * The keyboard height provider, this class uses a PopupWindow + * to calculate the window height when the floating keyboard is opened and closed. + */ +public class KeyboardHeightProvider extends PopupWindow { + + /** The tag for logging purposes */ + private final static String TAG = "sample_KeyboardHeightProvider"; + + /** The keyboard height observer */ + private KeyboardHeightObserver observer; + + /** The cached landscape height of the keyboard */ + private int keyboardLandscapeHeight; + + /** The cached portrait height of the keyboard */ + private int keyboardPortraitHeight; + + /** The view that is used to calculate the keyboard height */ + private View popupView; + + /** The parent view */ + private View parentView; + + /** The root activity that uses this KeyboardHeightProvider */ + private Activity activity; + + /** + * Construct a new KeyboardHeightProvider + * + * @param activity The parent activity + */ + public KeyboardHeightProvider(Activity activity) { + super(activity); + this.activity = activity; + + //LayoutInflater inflator = (LayoutInflater) activity.getSystemService(Activity.LAYOUT_INFLATER_SERVICE); + //this.popupView = inflator.inflate(android.R.layout.popupwindow, null, false); + this.popupView = new View(activity); + setContentView(popupView); + + setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_RESIZE | LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); + setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); + + parentView = activity.findViewById(android.R.id.content); + + setWidth(0); + setHeight(LayoutParams.MATCH_PARENT); + + popupView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + if (popupView != null) { + handleOnGlobalLayout(); + } + } + }); + } + + /** + * Start the KeyboardHeightProvider, this must be called after the onResume of the Activity. + * PopupWindows are not allowed to be registered before the onResume has finished + * of the Activity. + */ + public void start() { + + if (!isShowing() && parentView.getWindowToken() != null) { + setBackgroundDrawable(new ColorDrawable(0)); + showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0); + } + } + + /** + * Close the keyboard height provider, + * this provider will not be used anymore. + */ + public void close() { + this.observer = null; + dismiss(); + } + + /** + * Set the keyboard height observer to this provider. The + * observer will be notified when the keyboard height has changed. + * For example when the keyboard is opened or closed. + * + * @param observer The observer to be added to this provider. + */ + public void setKeyboardHeightObserver(KeyboardHeightObserver observer) { + this.observer = observer; + } + + /** + * Popup window itself is as big as the window of the Activity. + * The keyboard can then be calculated by extracting the popup view bottom + * from the activity window height. + */ + private void handleOnGlobalLayout() { + + Point screenSize = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(screenSize); + + Rect rect = new Rect(); + popupView.getWindowVisibleDisplayFrame(rect); + + // REMIND, you may like to change this using the fullscreen size of the phone + // and also using the status bar and navigation bar heights of the phone to calculate + // the keyboard height. But this worked fine on a Nexus. + int orientation = getScreenOrientation(); + int keyboardHeight = screenSize.y - rect.bottom; + + if (keyboardHeight == 0) { + notifyKeyboardHeightChanged(0, orientation); + } + else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + this.keyboardPortraitHeight = keyboardHeight; + notifyKeyboardHeightChanged(keyboardPortraitHeight, orientation); + } + else { + this.keyboardLandscapeHeight = keyboardHeight; + notifyKeyboardHeightChanged(keyboardLandscapeHeight, orientation); + } + } + + private int getScreenOrientation() { + return activity.getResources().getConfiguration().orientation; + } + + private void notifyKeyboardHeightChanged(int height, int orientation) { + if (observer != null) { + observer.onKeyboardHeightChanged(height, orientation); + } + } +} diff --git a/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java b/crates/notedeck_chrome/android/app/src/main/java/com/damus/notedeck/MainActivity.java @@ -20,29 +20,23 @@ public class MainActivity extends GameActivity { System.loadLibrary("notedeck_chrome"); } + private native void nativeOnKeyboardHeightChanged(int height); + private KeyboardHeightHelper keyboardHelper; + @Override protected void onCreate(Bundle savedInstanceState) { // Shrink view so it does not get covered by insets. - View content = getWindow().getDecorView().findViewById(android.R.id.content); - ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> { - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); - - ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); - mlp.topMargin = insets.top; - mlp.leftMargin = insets.left; - mlp.bottomMargin = insets.bottom; - mlp.rightMargin = insets.right; - v.setLayoutParams(mlp); - - return WindowInsetsCompat.CONSUMED; - }); - - WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + setupInsets(); + //setupFullscreen() + keyboardHelper = new KeyboardHeightHelper(this); + + super.onCreate(savedInstanceState); + } - //WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + private void setupFullscreen() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - /* WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (controller != null) { @@ -51,13 +45,55 @@ public class MainActivity extends GameActivity { ); controller.hide(WindowInsetsCompat.Type.systemBars()); } - */ + //focus(getContent()) + } + + // not sure if this does anything + private void focus(View content) { content.setFocusable(true); content.setFocusableInTouchMode(true); content.requestFocus(); - - super.onCreate(savedInstanceState); + } + + private View getContent() { + return getWindow().getDecorView().findViewById(android.R.id.content); + } + + private void setupInsets() { + View content = getContent(); + ViewCompat.setOnApplyWindowInsetsListener(content, (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + + ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + mlp.topMargin = insets.top; + mlp.leftMargin = insets.left; + mlp.bottomMargin = insets.bottom; + mlp.rightMargin = insets.right; + v.setLayoutParams(mlp); + + return WindowInsetsCompat.CONSUMED; + }); + + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + } + + @Override + public void onResume() { + super.onResume(); + keyboardHelper.start(); + } + + @Override + public void onPause() { + super.onPause(); + keyboardHelper.stop(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + keyboardHelper.close(); } @Override