Skip to content

Commit

Permalink
Reland No.2 "Hardware Keyboard: Android" (#33686)
Browse files Browse the repository at this point in the history
* Original code

* Changes

* Fix and test

Better formatted tests
  • Loading branch information
dkwingsmt authored May 31, 2022
1 parent 7a981c8 commit c74a738
Show file tree
Hide file tree
Showing 16 changed files with 3,166 additions and 150 deletions.
3 changes: 3 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,10 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyData.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardMap.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/key.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class KeyData {

String? _escapeCharacter() {
if (character == null) {
return character ?? '<none>';
return '<none>';
}
switch (character!) {
case '\n':
Expand Down
3 changes: 3 additions & 0 deletions lib/ui/window/key_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ enum class KeyEventType : int64_t {
// a different way in KeyDataPacket.
//
// This structure is unpacked by hooks.dart.
//
// Changes to this struct must also be made to
// io/flutter/embedding/android/KeyData.java.
struct alignas(8) KeyData {
// Timestamp in microseconds from an arbitrary and consistent start point
uint64_t timestamp;
Expand Down
3 changes: 3 additions & 0 deletions lib/ui/window/key_data_packet.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
namespace flutter {

// A byte stream representing a key event, to be sent to the framework.
//
// Changes to the marshalling format here must also be made to
// io/flutter/embedding/android/KeyData.java.
class KeyDataPacket {
public:
// Build the key data packet by providing information.
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ android_java_sources = [
"io/flutter/embedding/android/FlutterTextureView.java",
"io/flutter/embedding/android/FlutterView.java",
"io/flutter/embedding/android/KeyChannelResponder.java",
"io/flutter/embedding/android/KeyData.java",
"io/flutter/embedding/android/KeyEmbedderResponder.java",
"io/flutter/embedding/android/KeyboardManager.java",
"io/flutter/embedding/android/KeyboardMap.java",
"io/flutter/embedding/android/MotionEventTracker.java",
"io/flutter/embedding/android/RenderMode.java",
"io/flutter/embedding/android/SplashScreen.java",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package io.flutter.embedding.android;

import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
Expand All @@ -19,66 +18,15 @@ public class KeyChannelResponder implements KeyboardManager.Responder {
private static final String TAG = "KeyChannelResponder";

@NonNull private final KeyEventChannel keyEventChannel;
private int combiningCharacter;

@NonNull
private final KeyboardManager.CharacterCombiner characterCombiner =
new KeyboardManager.CharacterCombiner();

public KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) {
this.keyEventChannel = keyEventChannel;
}

/**
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
* Unicode combining character and returns the combination of these characters if a combination
* exists.
*
* <p>This method mutates {@link #combiningCharacter} over time to combine characters.
*
* <p>One of the following things happens in this method:
*
* <ul>
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is not a combining character, then {@code newCharacterCodePoint} is returned.
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is a combining character, then {@code newCharacterCodePoint} is saved as the {@link
* #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* also a combining character, then the {@code newCharacterCodePoint} is combined with the
* existing {@link #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* not a combining character, then the {@link #combiningCharacter} is applied to the regular
* {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link
* #combiningCharacter} is cleared.
* </ul>
*
* <p>The following reference explains the concept of a "combining character":
* https://en.wikipedia.org/wiki/Combining_character
*/
Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) {
char complexCharacter = (char) newCharacterCodePoint;
boolean isNewCodePointACombiningCharacter =
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
if (isNewCodePointACombiningCharacter) {
// If a combining character was entered before, combine this one with that one.
int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
if (combiningCharacter != 0) {
combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint);
} else {
combiningCharacter = plainCodePoint;
}
} else {
// The new character is a regular character. Apply combiningCharacter to it, if
// it exists.
if (combiningCharacter != 0) {
int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
if (combinedChar > 0) {
complexCharacter = (char) combinedChar;
}
combiningCharacter = 0;
}
}

return complexCharacter;
}

@Override
public void handleEvent(
@NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) {
Expand All @@ -92,7 +40,7 @@ public void handleEvent(
}

final Character complexCharacter =
applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
characterCombiner.applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);

Expand Down
140 changes: 140 additions & 0 deletions shell/platform/android/io/flutter/embedding/android/KeyData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.embedding.android;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
* The resulting Flutter key events generated by {@link KeyEmbedderResponder}, and are sent through
* the messenger after being marshalled with {@link #toBytes()}.
*
* <p>This class is the Java adaption of {@code KeyData} and {@code KeyDataPacket} in the C engine.
* Changes made to either side must also be made to the other.
*
* <p>Each {@link KeyData} corresponds to a {@code ui.KeyData} in the framework.
*/
public class KeyData {
private static final String TAG = "KeyData";

/**
* The channel that key data should be sent through.
*
* <p>Must be kept in sync with kFlutterKeyDataChannel in embedder.cc
*/
public static final String CHANNEL = "flutter/keydata";

// The number of fields except for `character`.
private static final int FIELD_COUNT = 5;
private static final int BYTES_PER_FIELD = 8;

/** The action type of the key data. */
public enum Type {
kDown(0),
kUp(1),
kRepeat(2);

private long value;

private Type(long value) {
this.value = value;
}

public long getValue() {
return value;
}

static Type fromLong(long value) {
switch ((int) value) {
case 0:
return kDown;
case 1:
return kUp;
case 2:
return kRepeat;
default:
throw new AssertionError("Unexpected Type value");
}
}
}

/** Creates an empty {@link KeyData}. */
public KeyData() {}

/**
* Unmarshal fields from a buffer.
*
* <p>For the binary format, see {@code lib/ui/window/key_data_packet.h}.
*/
public KeyData(@NonNull ByteBuffer buffer) {
final long charSize = buffer.getLong();
this.timestamp = buffer.getLong();
this.type = Type.fromLong(buffer.getLong());
this.physicalKey = buffer.getLong();
this.logicalKey = buffer.getLong();
this.synthesized = buffer.getLong() != 0;

if (buffer.remaining() != charSize) {
throw new AssertionError(
String.format(
"Unexpected char length: charSize is %d while buffer has position %d, capacity %d, limit %d",
charSize, buffer.position(), buffer.capacity(), buffer.limit()));
}
this.character = null;
if (charSize != 0) {
final byte[] strBytes = new byte[(int) charSize];
buffer.get(strBytes, 0, (int) charSize);
try {
this.character = new String(strBytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertionError("UTF-8 unsupported");
}
}
}

long timestamp;
Type type;
long physicalKey;
long logicalKey;
boolean synthesized;

/** The character of this key data encoded in UTF-8. */
@Nullable String character;

/**
* Marshal the key data to a new byte buffer.
*
* <p>For the binary format, see {@code lib/ui/window/key_data_packet.h}.
*
* @return the marshalled bytes.
*/
ByteBuffer toBytes() {
byte[] charBytes;
try {
charBytes = character == null ? null : character.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new AssertionError("UTF-8 not supported");
}
final int charSize = charBytes == null ? 0 : charBytes.length;
final ByteBuffer packet =
ByteBuffer.allocateDirect((1 + FIELD_COUNT) * BYTES_PER_FIELD + charSize);
packet.order(ByteOrder.LITTLE_ENDIAN);

packet.putLong(charSize);
packet.putLong(timestamp);
packet.putLong(type.getValue());
packet.putLong(physicalKey);
packet.putLong(logicalKey);
packet.putLong(synthesized ? 1L : 0L);
if (charBytes != null) {
packet.put(charBytes);
}

return packet;
}
}
Loading

0 comments on commit c74a738

Please sign in to comment.