Minimal test IME with Stylus HW support

Minimal IME with Handwriting support that can draw ink trail and commit
random text after each session.

Test: Manually
      1. m -j HandwritingIme
      2. adb install out/../HandwritingIme.apk
      3. Simulate stylus with  I7723be718354872509a9e85beda10acdbe25dd48
      4. and enable the property in #3 with
         adb shell setprop <property> true
      5. Open any app with editor and swipe on EditText.
      6. HW ink window with ink will appear.
Bug: 217957587

Change-Id: I53808ec261bd4364797b93d57cf6a8ddfd615390
This commit is contained in:
Taran Singh
2022-02-01 18:36:49 +00:00
parent 070f7b67e8
commit 8f2ae0b7b1
5 changed files with 365 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
// Copyright (C) 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_test {
name: "HandwritingIme",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
certificate: "platform",
platform_apis: true,
static_libs: [
"androidx.core_core",
"androidx.appcompat_appcompat",
"com.google.android.material_material",
],
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (018C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.test.handwritingime">
<application android:label="Handwriting IME">
<service android:name=".HandwritingIme"
android:process=":HandwritingIme"
android:label="Handwriting IME"
android:permission="android.permission.BIND_INPUT_METHOD"
android:exported="true">
<intent-filter>
<action android:name="android.view.InputMethod"/>
</intent-filter>
<meta-data android:name="android.view.im"
android:resource="@xml/ime"/>
</service>
</application>
</manifest>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- Configuration info for an input method -->
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:supportsStylusHandwriting="true"/>

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.test.handwritingime;
import android.annotation.Nullable;
import android.inputmethodservice.InputMethodService;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.FrameLayout;
import android.widget.Toast;
import java.util.Random;
public class HandwritingIme extends InputMethodService {
public static final int HEIGHT_DP = 100;
private Window mInkWindow;
private InkView mInk;
static final String TAG = "HandwritingIme";
interface HandwritingFinisher {
void finish();
}
interface StylusListener {
void onStylusEvent(MotionEvent me);
}
final class StylusConsumer implements StylusListener {
@Override
public void onStylusEvent(MotionEvent me) {
HandwritingIme.this.onStylusEvent(me);
}
}
final class HandwritingFinisherImpl implements HandwritingFinisher {
HandwritingFinisherImpl() {}
@Override
public void finish() {
finishStylusHandwriting();
Log.d(TAG, "HandwritingIme called finishStylusHandwriting() ");
}
}
private void onStylusEvent(@Nullable MotionEvent event) {
// TODO Hookup recognizer here
if (event.getAction() == MotionEvent.ACTION_UP) {
sendKeyChar((char) (56 + new Random().nextInt(66)));
}
}
@Override
public View onCreateInputView() {
Log.d(TAG, "onCreateInputView");
final ViewGroup view = new FrameLayout(this);
final View inner = new View(this);
final float density = getResources().getDisplayMetrics().density;
final int height = (int) (HEIGHT_DP * density);
view.setPadding(0, 0, 0, 0);
view.addView(inner, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, height));
inner.setBackgroundColor(0xff0110fe); // blue
return view;
}
public void onPrepareStylusHandwriting() {
Log.d(TAG, "onPrepareStylusHandwriting ");
if (mInk == null) {
mInk = new InkView(this, new HandwritingFinisherImpl(), new StylusConsumer());
}
}
@Override
public boolean onStartStylusHandwriting() {
Log.d(TAG, "onStartStylusHandwriting ");
Toast.makeText(this, "START HW", Toast.LENGTH_SHORT).show();
mInkWindow = getStylusHandwritingWindow();
mInkWindow.setContentView(mInk, mInk.getLayoutParams());
return true;
}
@Override
public void onFinishStylusHandwriting() {
Log.d(TAG, "onFinishStylusHandwriting ");
Toast.makeText(this, "Finish HW", Toast.LENGTH_SHORT).show();
// Free-up
((ViewGroup) mInk.getParent()).removeView(mInk);
mInk = null;
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.test.handwritingime;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
class InkView extends View {
private static final long FINISH_TIMEOUT = 2500;
private final HandwritingIme.HandwritingFinisher mHwCanceller;
private final HandwritingIme.StylusConsumer mConsumer;
private Paint mPaint;
private Path mPath;
private float mX, mY;
private static final float STYLUS_MOVE_TOLERANCE = 1;
private Runnable mFinishRunnable;
InkView(Context context, HandwritingIme.HandwritingFinisher hwController,
HandwritingIme.StylusConsumer consumer) {
super(context);
mHwCanceller = hwController;
mConsumer = consumer;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(14);
mPath = new Path();
WindowManager wm = context.getSystemService(WindowManager.class);
WindowMetrics metrics = wm.getCurrentWindowMetrics();
Insets insets = metrics.getWindowInsets()
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
setLayoutParams(new ViewGroup.LayoutParams(
metrics.getBounds().width() - insets.left - insets.right,
metrics.getBounds().height() - insets.top - insets.bottom));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
canvas.drawARGB(20, 255, 50, 50);
}
private void stylusStart(float x, float y) {
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void stylusMove(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (mPath.isEmpty()) {
stylusStart(x, y);
}
if (dx >= STYLUS_MOVE_TOLERANCE || dy >= STYLUS_MOVE_TOLERANCE) {
mPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
}
}
private void stylusFinish() {
mPath.lineTo(mX, mY);
// TODO: support offscreen? e.g. mCanvas.drawPath(mPath, mPaint);
mPath.reset();
mX = 0;
mY = 0;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
mConsumer.onStylusEvent(event);
android.util.Log.w(HandwritingIme.TAG, "INK touch onStylusEvent " + event);
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
cancelTimer();
stylusStart(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
stylusMove(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
scheduleTimer();
break;
}
return true;
}
return false;
}
private void cancelTimer() {
if (mFinishRunnable != null) {
if (getHandler() != null) {
getHandler().removeCallbacks(mFinishRunnable);
}
mFinishRunnable = null;
}
if (getHandler() != null) {
getHandler().removeCallbacksAndMessages(null);
}
}
private void scheduleTimer() {
cancelTimer();
if (getHandler() != null) {
postDelayed(getFinishRunnable(), FINISH_TIMEOUT);
}
}
private Runnable getFinishRunnable() {
mFinishRunnable = () -> {
android.util.Log.e(HandwritingIme.TAG, "Hw view timer finishHandwriting ");
mHwCanceller.finish();
stylusFinish();
mPath.reset();
invalidate();
};
return mFinishRunnable;
}
}