Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Add image keyboard support on Android #27763

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
54a9657
Android GIF/Image Insertion
zlshames Jul 11, 2021
5f0582e
Remove debug logs
tneotia Jul 28, 2021
d2ab746
Add test for commitContent
tneotia Jul 29, 2021
983c07c
[flutter_releases] Flutter beta 2.5.0-5.3.pre Engine Cherrypicks (#28…
christopherfujino Sep 2, 2021
89ca066
[flutter_releases] Flutter Stable 2.2.3 Engine Cherrypicks Pt 2 (#27087)
christopherfujino Jun 30, 2021
b4c40dd
Full GIF/Image Insertion
zlshames Jul 11, 2021
17e900e
Update commitContent slightly
tneotia Nov 6, 2021
e414b03
Fix build
tneotia Nov 10, 2021
805f6f8
Fix build pt2
tneotia Nov 10, 2021
9123435
Fix build pt3
tneotia Nov 10, 2021
26d13e0
remove undefined function to test
ryanawhelan Jan 9, 2022
58c5299
update format to match dart standard for Java
ryanawhelan Jan 9, 2022
9d0eb17
Merge pull request #2 from ryanawhelan/fix_build
zlshames Jan 10, 2022
bbb3286
Adds contentCommitMimeTypes support
zlshames Jan 15, 2022
fa50825
Formatting fixes
zlshames Jan 15, 2022
0c1dbbd
Merge branch 'master' of github.com:BlueBubblesApp/engine into zach/c…
zlshames Jan 15, 2022
a933522
Merge pull request #3 from BlueBubblesApp/zach/commit-mimes
zlshames Jan 15, 2022
3cead60
2.8.1 content commit merges & comment fixes
zlshames Jan 28, 2022
4f735ff
Java refactor
zlshames Jan 28, 2022
76f5faf
Merge pull request #5 from BlueBubblesApp/zach/gif-insertion-pr
zlshames Jan 28, 2022
5d6d517
Adds additional parameter to TextInputChannel.Configuration
zlshames Jan 28, 2022
6004596
Merge pull request #6 from BlueBubblesApp/zach/gif-insertion-pr
zlshames Jan 28, 2022
0224531
Fix build failures
tneotia Apr 13, 2022
f1d7ab0
Merge remote-tracking branch 'flutter/master' into tanay/gif-insertio…
tneotia Apr 13, 2022
41fe8f0
Remove duplicate function
tneotia Apr 13, 2022
4105ebc
Fix missing imports
tneotia Apr 13, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
Expand Down Expand Up @@ -317,6 +318,14 @@ 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));
}

/** Instructs Flutter to execute an "unspecified" action. */
public void unspecifiedAction(int inputClientId) {
Log.v(TAG, "Sending 'unspecified' message.");
Expand Down Expand Up @@ -452,6 +461,19 @@ public static Configuration fromJson(@NonNull JSONObject json)
}
}
final Integer inputAction = inputActionFromTextInputAction(inputActionName);

// Build list of content commit mime types from the passed in JSON list
List<String> contentList = new ArrayList<String>();
JSONArray contentCommitMimeTypes =
json.isNull("contentCommitMimeTypes")
? null
: json.getJSONArray("contentCommitMimeTypes");
if (contentCommitMimeTypes != null) {
for (int i = 0; i < contentCommitMimeTypes.length(); i++) {
contentList.add(contentCommitMimeTypes.optString(i));
}
}

return new Configuration(
json.optBoolean("obscureText"),
json.optBoolean("autocorrect", true),
Expand All @@ -463,6 +485,7 @@ public static Configuration fromJson(@NonNull JSONObject json)
inputAction,
json.isNull("actionLabel") ? null : json.getString("actionLabel"),
json.isNull("autofill") ? null : Autofill.fromJson(json.getJSONObject("autofill")),
contentList.toArray(new String[contentList.size()]),
fields);
}

Expand Down Expand Up @@ -619,6 +642,7 @@ public Autofill(
@Nullable public final Integer inputAction;
@Nullable public final String actionLabel;
@Nullable public final Autofill autofill;
@Nullable public final String[] contentCommitMimeTypes;
@Nullable public final Configuration[] fields;

public Configuration(
Expand All @@ -632,6 +656,7 @@ public Configuration(
@Nullable Integer inputAction,
@Nullable String actionLabel,
@Nullable Autofill autofill,
@Nullable String[] contentCommitMimeTypes,
@Nullable Configuration[] fields) {
this.obscureText = obscureText;
this.autocorrect = autocorrect;
Expand All @@ -643,6 +668,7 @@ public Configuration(
this.inputAction = inputAction;
this.actionLabel = actionLabel;
this.autofill = autofill;
this.contentCommitMimeTypes = contentCommitMimeTypes;
this.fields = fields;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

package io.flutter.plugin.editing;

import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.DynamicLayout;
Expand All @@ -22,11 +24,20 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.RequiresApi;
import androidx.core.os.BuildCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import io.flutter.Log;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

class InputConnectionAdaptor extends BaseInputConnection
implements ListenableEditingState.EditingStateWatcher {
Expand Down Expand Up @@ -473,6 +484,69 @@ public boolean performEditorAction(int actionCode) {
return true;
}

@Override
@TargetApi(25)
@RequiresApi(25)
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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 (contentCommitted maybe?). Though, it seems that adjacent API methods already have an imperative pattern, so maybe now is not the time to change that.

// Ensure permission is granted.
if (Build.VERSION.SDK_INT >= 25
&& (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this request show a dialog for permissions? If so, I would recommend keeping all UI-related behavior out of this class. Consider making it the clients responsibility to obtain the appropriate permissions before running the code that requires the permissions. You can throw an exception if that permission isn't granted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not appear to show a dialog for any permissions. I think its more of asking the system to have temporary access to the committed files, if the app isn't given access already.

https://developer.android.com/reference/android/view/inputmethod/InputContentInfo#requestPermission()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried running the code without the permissions to see what happens? If no dialog is shown, then who are you "requesting permission" from? Typically a "request" is something that can be denied, so this seems strange.

} catch (Exception e) {
return false;
}
} else {
return false;
}

if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
inputContentInfo.requestPermission();

final Uri uri = inputContentInfo.getContentUri();
final String mimeType = inputContentInfo.getDescription().getMimeType(0);
Context context = mFlutterView.getContext();
Boolean retval = false;

try {
final InputStream is = context.getContentResolver().openInputStream(uri);
final byte[] data = this.readStreamFully(is, 64 * 1024);

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) {
} catch (Exception ex) {
} finally {
inputContentInfo.releasePermission();
}
return retval;
}
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) {
}

return new byte[0];
}

// -------- Start: ListenableEditingState watcher implementation -------
@Override
public void didChangeEditingState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -339,6 +340,11 @@ public InputConnection createInputConnection(
}
outAttrs.imeOptions |= enterAction;

if (configuration.contentCommitMimeTypes != null) {
String[] imgTypeString = configuration.contentCommitMimeTypes;
EditorInfoCompat.setContentMimeTypes(outAttrs, imgTypeString);
}

InputConnectionAdaptor connection =
new InputConnectionAdaptor(
view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
Expand All @@ -31,6 +33,7 @@
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.InputMethodManager;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.lang.UProperty;
Expand Down Expand Up @@ -173,6 +176,44 @@ public void testPerformContextMenuAction_paste() {
assertTrue(editable.toString().startsWith(textToBePasted));
}

@Test
public void testCommitContent() throws JSONException {
View testView = new View(RuntimeEnvironment.application);
int client = 0;
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
adaptor.commitContent(
new InputContentInfo(
Uri.parse("content://mock/uri/test/commitContent"),
new ClipDescription("commitContent test", new String[] {"image/png"})),
0,
null);

ArgumentCaptor<String> channelCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<ByteBuffer> bufferCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
verify(dartExecutor, times(1)).send(channelCaptor.capture(), bufferCaptor.capture(), isNull());
assertEquals("flutter/textinput", channelCaptor.getValue());
verifyMethodCall(
bufferCaptor.getValue(),
"TextInputClient.performAction",
new String[] {
"0",
"TextInputAction.commitContent",
"{\"data\":[],\"mimeType\":\"image\\/png\",\"uri\":\"content:\\/\\/mock\\/uri\\/test\\/commitContent\"}"
});
}

@Test
public void testPerformPrivateCommand_dataIsNull() throws JSONException {
View testView = new View(RuntimeEnvironment.application);
Expand Down
Loading