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:
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