-
Notifications
You must be signed in to change notification settings - Fork 6k
Add image keyboard support on Android #27763
Changes from 1 commit
54a9657
5f0582e
d2ab746
983c07c
89ca066
b4c40dd
17e900e
e414b03
805f6f8
9123435
26d13e0
58c5299
9d0eb17
bbb3286
fa50825
0c1dbbd
a933522
3cead60
4f735ff
76f5faf
5d6d517
6004596
0224531
f1d7ab0
41fe8f0
4105ebc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -285,6 +285,13 @@ public void previous(int inputClientId) { | |
"TextInputClient.performAction", Arrays.asList(inputClientId, "TextInputAction.previous")); | ||
} | ||
|
||
/** Instructs flutter to commit content back to the text channel */ | ||
public void commitContent(int inputClientId, Map<String, Object> content) { | ||
Log.v(TAG, "Sending 'commitContent' message."); | ||
channel.invokeMethod( | ||
"TextInputClient.performAction", Arrays.asList(inputClientId, "TextInputAction.commitContent", content)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyone have an opinion about the use of |
||
} | ||
|
||
/** Instructs Flutter to execute an "unspecified" action. */ | ||
public void unspecifiedAction(int inputClientId) { | ||
Log.v(TAG, "Sending 'unspecified' message."); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,12 @@ | |
|
||
package io.flutter.plugin.editing; | ||
|
||
import java.io.FileNotFoundException; | ||
import java.io.ByteArrayOutputStream; | ||
import java.io.InputStream; | ||
import java.util.Map; | ||
import java.util.HashMap; | ||
|
||
import android.content.ClipData; | ||
import android.content.ClipboardManager; | ||
import android.content.Context; | ||
|
@@ -23,6 +29,12 @@ | |
import android.view.inputmethod.ExtractedText; | ||
import android.view.inputmethod.ExtractedTextRequest; | ||
import android.view.inputmethod.InputMethodManager; | ||
import android.view.inputmethod.InputContentInfo; | ||
import android.net.Uri; | ||
|
||
import androidx.core.view.inputmethod.InputConnectionCompat; | ||
import androidx.core.os.BuildCompat; | ||
|
||
import io.flutter.Log; | ||
import io.flutter.embedding.android.KeyboardManager; | ||
import io.flutter.embedding.engine.FlutterJNI; | ||
|
@@ -473,6 +485,79 @@ public boolean performEditorAction(int actionCode) { | |
return true; | ||
} | ||
|
||
@Override | ||
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A good point was recently brought up by @matthew-carroll about how APIs like this should follow Flutter's declarative pattern instead of being imperative. So instead of telling the client to commit the content, informing the client that the user committed content ( |
||
Log.d("HackFlutterEngine", "Content Commit Invoked"); | ||
|
||
// Ensure permission is granted | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Period here. |
||
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BuildCompat.isAtLeastNMR1() is deprecated. You can use the equivalent: https://developer.android.com/reference/androidx/core/os/BuildCompat#isAtLeastNMR1() |
||
try { | ||
inputContentInfo.requestPermission(); | ||
Log.d("HackFlutterEngine", "Content Commit request permissions: PASS"); | ||
} catch (Exception e) { | ||
Log.d("HackFlutterEngine", "Content Commit reqest permissions: FAIL"); | ||
return false; | ||
} | ||
} | ||
|
||
if (inputContentInfo.getDescription().getMimeTypeCount() > 0) { | ||
inputContentInfo.requestPermission(); | ||
|
||
final Uri uri = inputContentInfo.getContentUri(); | ||
final String mimeType = inputContentInfo.getDescription().getMimeType(0); | ||
Log.d("HackFlutterEngine", "Content Commit received URI: " + uri + " (MIME: " + mimeType + ")"); | ||
Context context = mFlutterView.getContext(); | ||
Boolean retval = false; | ||
|
||
try { | ||
final InputStream is = context.getContentResolver().openInputStream(uri); | ||
final byte[] data = this.readStreamFully(is, 64 * 1024); | ||
Log.d("HackFlutterEngine", "Content Commit data length: " + data.length); | ||
|
||
final Map<String, Object> obj = new HashMap<>(); | ||
obj.put("mimeType", mimeType); | ||
obj.put("data", data); | ||
obj.put("uri", uri != null ? uri.toString() : null); | ||
|
||
// Commit the content to the text input channel | ||
textInputChannel.commitContent(mClient, obj); | ||
retval = true; | ||
} catch (FileNotFoundException ex) { | ||
Log.d("HackFlutterEngine", "Content Commit load file: FAIL (Not Found)"); | ||
} catch (Exception ex) { | ||
Log.d("HackFlutterEngine", "Content Commit load data: FAIL"); | ||
} finally { | ||
inputContentInfo.releasePermission(); | ||
} | ||
|
||
if (retval) { | ||
Log.d("HackFlutterEngine", "Content Commit Result: PASS"); | ||
} | ||
|
||
return retval; | ||
} | ||
|
||
// If it gets to this point, it failed | ||
Log.d("HackFlutterEngine", "Content Commit Result: FAIL"); | ||
return false; | ||
} | ||
|
||
public byte[] readStreamFully(InputStream is, int blocksize) { | ||
try { | ||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ||
|
||
byte[] buffer = new byte[blocksize]; | ||
while (true) { | ||
int len = is.read(buffer); | ||
if (len == -1) break; | ||
baos.write(buffer, 0, len); | ||
} | ||
return baos.toByteArray(); | ||
} catch (Exception e) {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same comment. Please add a single statement in the try block, and specify a concrete exception. |
||
|
||
return new byte[0]; | ||
} | ||
|
||
// -------- Start: ListenableEditingState watcher implementation ------- | ||
@Override | ||
public void didChangeEditingState( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ | |
import androidx.annotation.NonNull; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.VisibleForTesting; | ||
import androidx.core.view.inputmethod.EditorInfoCompat; | ||
import io.flutter.Log; | ||
import io.flutter.embedding.android.KeyboardManager; | ||
import io.flutter.embedding.engine.systemchannels.TextInputChannel; | ||
|
@@ -330,6 +331,17 @@ public InputConnection createInputConnection( | |
} | ||
outAttrs.imeOptions |= enterAction; | ||
|
||
String[] imgTypeString = new String[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Should this be declared outside the method as a constant? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This nit may be obsolete actually, see my comment below. |
||
"image/png", | ||
"image/bmp", | ||
"image/jpg", | ||
"image/tiff", | ||
"image/gif", | ||
"image/jpeg", | ||
"image/webp" | ||
}; | ||
EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that on Android, if try I use Gboard to insert a gif into a field that doesn't support images, it gives me a message saying, "Google does not support image insertion here". Is that message based on this mime type list? If so, maybe we need a way for Flutter developers to specify that a field does not support images. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also tried this on a Flutter app (before your changes) and I get the same message. We might want to include a way to support this original behavior. The first thing that comes to mind is adding a contentMimeTypes parameter to EditableText and passing it through to here, would that work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I know that message is only based on whether the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @justinmc so apparently the mime types do nothing in @zlshames's testing... how should we approach this then? Should we just remove this code entirely? I don't really know why it doesn't affect the content insertion, it seems to me that we have everything set up correctly. If we indeed haven't made any mistakes, imho the best option would be to let the dev handle their desired We could make the default behavior of |
||
|
||
InputConnectionAdaptor connection = | ||
new InputConnectionAdaptor( | ||
view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Period at the end of the sentence here.