media: handle upload on android

This commit is contained in:
Fernando López Guevara
2025-07-27 16:10:47 -03:00
committed by William Casarin
parent 31ee64827a
commit 6ee2b28e70
14 changed files with 469 additions and 419 deletions

View File

@@ -1,48 +0,0 @@
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();
}
}

View File

@@ -1,35 +0,0 @@
/*
* 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);
}

View File

@@ -1,174 +0,0 @@
/*
* 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);
}
}
}

View File

@@ -1,13 +1,18 @@
package com.damus.notedeck;
import android.content.ClipData;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
@@ -15,52 +20,23 @@ import androidx.core.view.WindowInsetsControllerCompat;
import com.google.androidgamesdk.GameActivity;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
public class MainActivity extends GameActivity {
static {
System.loadLibrary("notedeck_chrome");
}
static final int REQUEST_CODE_PICK_FILE = 420;
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.
super.onCreate(savedInstanceState);
private native void nativeOnFilePickedFailed(String uri, String e);
private native void nativeOnFilePickedWithContent(Object[] uri_info, byte[] content);
setupInsets();
//setupFullscreen()
//keyboardHelper = new KeyboardHeightHelper(this);
}
private void setupFullscreen() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
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();
}
private View getContent() {
return getWindow().getDecorView().findViewById(android.R.id.content);
public void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
}
private void setupInsets() {
@@ -92,35 +68,171 @@ public class MainActivity extends GameActivity {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
}
/*
@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
public boolean onTouchEvent(MotionEvent event) {
// Offset the location so it fits the view with margins caused by insets.
private void processSelectedFile(Uri uri) {
try {
nativeOnFilePickedWithContent(this.getUriInfo(uri), readUriContent(uri));
} catch (Exception e) {
Log.e("MainActivity", "Error processing file: " + uri.toString(), e);
int[] location = new int[2];
findViewById(android.R.id.content).getLocationOnScreen(location);
event.offsetLocation(-location[0], -location[1]);
return super.onTouchEvent(event);
nativeOnFilePickedFailed(uri.toString(), e.toString());
}
}
private Object[] getUriInfo(Uri uri) throws Exception {
if (!uri.getScheme().equals("content")) {
throw new Exception("uri should start with content://");
}
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
while (cursor.moveToNext()) {
Object[] info = new Object[3];
int col_idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
info[0] = cursor.getString(col_idx);
col_idx = cursor.getColumnIndex(OpenableColumns.SIZE);
info[1] = cursor.getLong(col_idx);
col_idx = cursor.getColumnIndex("mime_type");
info[2] = cursor.getString(col_idx);
return info;
}
return null;
}
private byte[] readUriContent(Uri uri) {
InputStream inputStream = null;
ByteArrayOutputStream buffer = null;
try {
inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) {
Log.e("MainActivity", "Could not open input stream for URI: " + uri);
return null;
}
buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192]; // 8KB buffer
int bytesRead;
while ((bytesRead = inputStream.read(data)) != -1) {
buffer.write(data, 0, bytesRead);
}
byte[] result = buffer.toByteArray();
Log.d("MainActivity", "Successfully read " + result.length + " bytes");
return result;
} catch (IOException e) {
Log.e("MainActivity", "IOException while reading URI: " + uri, e);
return null;
} catch (SecurityException e) {
Log.e("MainActivity", "SecurityException while reading URI: " + uri, e);
return null;
} finally {
// Close streams
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e("MainActivity", "Error closing input stream", e);
}
}
if (buffer != null) {
try {
buffer.close();
} catch (IOException e) {
Log.e("MainActivity", "Error closing buffer", e);
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Shrink view so it does not get covered by insets.
setupInsets();
//setupFullscreen()
super.onCreate(savedInstanceState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK) {
if (data == null) return;
if (data.getClipData() != null) {
// Multiple files selected
ClipData clipData = data.getClipData();
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri uri = clipData.getItemAt(i).getUri();
processSelectedFile(uri);
}
} else if (data.getData() != null) {
// Single file selected
Uri uri = data.getData();
processSelectedFile(uri);
}
}
}
private void setupFullscreen() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller =
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
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();
}
private View getContent() {
return getWindow().getDecorView().findViewById(android.R.id.content);
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Offset the location so it fits the view with margins caused by insets.
int[] location = new int[2];
findViewById(android.R.id.content).getLocationOnScreen(location);
event.offsetLocation(-location[0], -location[1]);
return super.onTouchEvent(event);
}
}