diff options
18 files changed, 482 insertions, 0 deletions
diff --git a/tests/MotionPrediction/Android.bp b/tests/MotionPrediction/Android.bp index 6cda8f050987..b4a435909953 100644 --- a/tests/MotionPrediction/Android.bp +++ b/tests/MotionPrediction/Android.bp @@ -26,5 +26,8 @@ package { android_app { name: "MotionPrediction", srcs: ["**/*.kt"], + kotlincflags: [ + "-Werror", + ], sdk_version: "current", } diff --git a/tests/MultiDeviceInput/Android.bp b/tests/MultiDeviceInput/Android.bp new file mode 100644 index 000000000000..3c80873168b4 --- /dev/null +++ b/tests/MultiDeviceInput/Android.bp @@ -0,0 +1,33 @@ +// +// Copyright 2023 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_app { + name: "MultiDeviceInput", + srcs: ["**/*.kt"], + kotlincflags: [ + "-Werror", + ], + sdk_version: "current", +} diff --git a/tests/MultiDeviceInput/AndroidManifest.xml b/tests/MultiDeviceInput/AndroidManifest.xml new file mode 100644 index 000000000000..ed8cadb9519b --- /dev/null +++ b/tests/MultiDeviceInput/AndroidManifest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2023 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="test.multideviceinput"> + + <application android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/AppTheme"> + <activity android:name=".MainActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/tests/MultiDeviceInput/OWNERS b/tests/MultiDeviceInput/OWNERS new file mode 100644 index 000000000000..c88bfe97cab9 --- /dev/null +++ b/tests/MultiDeviceInput/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/INPUT_OWNERS diff --git a/tests/MultiDeviceInput/README.md b/tests/MultiDeviceInput/README.md new file mode 100644 index 000000000000..5fcdeda6e5d7 --- /dev/null +++ b/tests/MultiDeviceInput/README.md @@ -0,0 +1,19 @@ +# MultiDeviceInput test app # + +This demo app is for manual testing of the multi-device input feature. +It creates two windows - one on the left and one on the right. You can use different input devices +in these windows. + +## Installation ## +Install this using: +``` +APP=MultiDeviceInput; m $APP && adb install $ANDROID_PRODUCT_OUT/system/app/$APP/$APP.apk +``` + +## Features ## + +* Touch in one window, use stylus in another window, at the same time +* Visualize hovering stylus +* Pinch zoom in one window to affect the line thickness in another window +* Check whether stylus rejects touch in the same window +* (in the future) Check stylus and touch operation in the same window diff --git a/tests/MultiDeviceInput/res/layout/activity_main.xml b/tests/MultiDeviceInput/res/layout/activity_main.xml new file mode 100644 index 000000000000..a6a6f891a034 --- /dev/null +++ b/tests/MultiDeviceInput/res/layout/activity_main.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2023 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + tools:context="test.multideviceinput.MainActivity"> + +</LinearLayout> diff --git a/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..cde69bcccec6 --- /dev/null +++ b/tests/MultiDeviceInput/res/mipmap-hdpi/ic_launcher.png diff --git a/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..c133a0cbd379 --- /dev/null +++ b/tests/MultiDeviceInput/res/mipmap-mdpi/ic_launcher.png diff --git a/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..bfa42f0e7b91 --- /dev/null +++ b/tests/MultiDeviceInput/res/mipmap-xhdpi/ic_launcher.png diff --git a/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..324e72cdd748 --- /dev/null +++ b/tests/MultiDeviceInput/res/mipmap-xxhdpi/ic_launcher.png diff --git a/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png b/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..aee44e138434 --- /dev/null +++ b/tests/MultiDeviceInput/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/tests/MultiDeviceInput/res/values-w820dp/dimens.xml b/tests/MultiDeviceInput/res/values-w820dp/dimens.xml new file mode 100644 index 000000000000..b14a560efd72 --- /dev/null +++ b/tests/MultiDeviceInput/res/values-w820dp/dimens.xml @@ -0,0 +1,20 @@ +<!-- Copyright 2023 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. +--> +<resources> + <!-- Example customization of dimensions originally defined in res/values/dimens.xml + (such as screen margins) for screens with more than 820dp of available width. This + would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). --> + <dimen name="activity_horizontal_margin">64dp</dimen> +</resources> diff --git a/tests/MultiDeviceInput/res/values/colors.xml b/tests/MultiDeviceInput/res/values/colors.xml new file mode 100644 index 000000000000..c37df9f7b428 --- /dev/null +++ b/tests/MultiDeviceInput/res/values/colors.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2023 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. +--> +<resources> + <color name="colorPrimary">#3F51B5</color> + <color name="colorPrimaryDark">#303F9F</color> + <color name="colorAccent">#FF4081</color> +</resources> diff --git a/tests/MultiDeviceInput/res/values/dimens.xml b/tests/MultiDeviceInput/res/values/dimens.xml new file mode 100644 index 000000000000..bdb8ede1c913 --- /dev/null +++ b/tests/MultiDeviceInput/res/values/dimens.xml @@ -0,0 +1,19 @@ +<!-- Copyright 2023 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. +--> +<resources> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> +</resources> diff --git a/tests/MultiDeviceInput/res/values/strings.xml b/tests/MultiDeviceInput/res/values/strings.xml new file mode 100644 index 000000000000..3827c344f87f --- /dev/null +++ b/tests/MultiDeviceInput/res/values/strings.xml @@ -0,0 +1,17 @@ +<!-- Copyright 2023 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. +--> +<resources> + <string name="app_name">Simultaneous touch and stylus</string> +</resources> diff --git a/tests/MultiDeviceInput/res/values/styles.xml b/tests/MultiDeviceInput/res/values/styles.xml new file mode 100644 index 000000000000..a563e7e09706 --- /dev/null +++ b/tests/MultiDeviceInput/res/values/styles.xml @@ -0,0 +1,23 @@ +<!-- Copyright 2023 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. +--> +<resources> + <!-- Base application theme. --> + <style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar"> + <!-- Customize your theme here. --> + <item name="android:colorPrimary">@color/colorPrimary</item> + <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item> + <item name="android:colorAccent">@color/colorAccent</item> + </style> +</resources> diff --git a/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt b/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt new file mode 100644 index 000000000000..b5bd9ca746aa --- /dev/null +++ b/tests/MultiDeviceInput/src/test/multideviceinput/DrawingView.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2023 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 test.multideviceinput + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import android.view.InputDevice.SOURCE_STYLUS +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_HOVER_EXIT +import android.view.MotionEvent.ACTION_UP +import android.view.ScaleGestureDetector +import android.view.View + +import java.util.Vector + +private fun drawLine(canvas: Canvas, from: MotionEvent, to: MotionEvent, paint: Paint) { + // Correct implementation here would require us to build a set of pointers and then iterate + // through them. Instead, we are taking a few shortcuts and ignore some of the events, which + // causes occasional gaps in the drawings. + if (from.pointerCount != to.pointerCount) { + return + } + // Now, 'from' is guaranteed to have as many pointers as the 'to' event. It doesn't + // necessarily mean they are the same pointers, though. + for (p in 0..<from.pointerCount) { + val x0 = from.getX(p) + val y0 = from.getY(p) + if (to.getPointerId(p) == from.getPointerId(p)) { + // This only works when the i-th pointer in "to" is the same pointer + // as the i-th pointer in "from"`. It's not guaranteed by the input APIs, + // but it works in practice. + val x1 = to.getX(p) + val y1 = to.getY(p) + // Ignoring historical data here for simplicity + canvas.drawLine(x0, y0, x1, y1, paint) + } + } +} + +private fun drawCircle(canvas: Canvas, event: MotionEvent, paint: Paint, radius: Float) { + val x = event.getX() + val y = event.getY() + canvas.drawCircle(x, y, radius, paint) +} + +/** + * Draw the current stroke + */ +class DrawingView : View { + private val TAG = "DrawingView" + + private var myState: SharedScaledPointerSize? = null + private var otherState: SharedScaledPointerSize? = null + + constructor( + context: Context, + myState: SharedScaledPointerSize, + otherState: SharedScaledPointerSize + ) : super(context) { + this.myState = myState + this.otherState = otherState + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + val touchEvents = mutableMapOf<Int, Vector<Pair<MotionEvent, Paint>>>() + val hoverEvents = mutableMapOf<Int, MotionEvent>() + + val scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + + override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean { + return true + } + + override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean { + val scaleFactor = scaleGestureDetector.scaleFactor + when (otherState?.state) { + PointerState.DOWN -> { + otherState?.lineSize = (otherState?.lineSize ?: 5f) * scaleFactor + } + PointerState.HOVER -> { + otherState?.circleSize = (otherState?.circleSize ?: 20f) * scaleFactor + } + else -> {} + } + return true + } + } + private val scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener, null) + + private var touchPaint = Paint() + private var stylusPaint = Paint() + + private fun init() { + touchPaint.color = Color.RED + touchPaint.setStrokeWidth(5f) + stylusPaint.color = Color.YELLOW + stylusPaint.setStrokeWidth(5f) + + setOnHoverListener { _, event -> processHoverEvent(event); true } + } + + private fun processTouchEvent(event: MotionEvent) { + scaleGestureDetector.onTouchEvent(event) + if (event.actionMasked == ACTION_DOWN) { + touchEvents.remove(event.deviceId) + myState?.state = PointerState.DOWN + } else if (event.actionMasked == ACTION_UP) { + myState?.state = PointerState.NONE + } + var vec = touchEvents.getOrPut(event.deviceId) { Vector<Pair<MotionEvent, Paint>>() } + + val paint = if (event.isFromSource(SOURCE_STYLUS)) { + val size = myState?.lineSize ?: 5f + stylusPaint.setStrokeWidth(size) + Paint(stylusPaint) + } else { + val size = myState?.lineSize ?: 5f + touchPaint.setStrokeWidth(size) + Paint(touchPaint) + } + vec.add(Pair(MotionEvent.obtain(event), paint)) + invalidate() + } + + private fun processHoverEvent(event: MotionEvent) { + hoverEvents.remove(event.deviceId) + if (event.getActionMasked() != ACTION_HOVER_EXIT) { + hoverEvents.put(event.deviceId, MotionEvent.obtain(event)) + myState?.state = PointerState.HOVER + } else { + myState?.state = PointerState.NONE + } + invalidate() + } + + public override fun onTouchEvent(event: MotionEvent): Boolean { + processTouchEvent(event) + return true + } + + public override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Draw touch and stylus MotionEvents + for ((_, vec) in touchEvents ) { + for (i in 1 until vec.size) { + drawLine(canvas, vec[i - 1].first, vec[i].first, vec[i].second) + } + } + // Draw hovers + for ((_, event) in hoverEvents ) { + if (event.isFromSource(SOURCE_STYLUS)) { + val size = myState?.circleSize ?: 20f + drawCircle(canvas, event, stylusPaint, size) + } else { + val size = myState?.circleSize ?: 20f + drawCircle(canvas, event, touchPaint, size) + } + } + } +} diff --git a/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt b/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt new file mode 100644 index 000000000000..911208579d4f --- /dev/null +++ b/tests/MultiDeviceInput/src/test/multideviceinput/MainActivity.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 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 test.multideviceinput + +import android.app.Activity +import android.graphics.Color +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets.Type +import android.view.WindowManager + + +enum class PointerState { + DOWN, // One or more pointer(s) down, lines are being drawn + HOVER, // Pointer is hovering + NONE, // Nothing is touching or hovering +} + +data class SharedScaledPointerSize( + var lineSize: Float, + var circleSize: Float, + var state: PointerState +) + +class MainActivity : Activity() { + val TAG = "MultiDeviceInput" + private val leftState = SharedScaledPointerSize(5f, 20f, PointerState.NONE) + private val rightState = SharedScaledPointerSize(5f, 20f, PointerState.NONE) + private lateinit var left: View + private lateinit var right: View + + override fun onResume() { + super.onResume() + + val wm = getSystemService(WindowManager::class.java) + val wmlp = WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION) + wmlp.flags = (wmlp.flags or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + + val windowMetrics = windowManager.currentWindowMetrics + val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(Type.systemBars()) + val width = windowMetrics.bounds.width() - insets.left - insets.right + val height = windowMetrics.bounds.height() - insets.top - insets.bottom + + wmlp.width = width * 24 / 50 + wmlp.height = height * 35 / 50 + + val vglp = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + + wmlp.setTitle("Left -- " + getPackageName()) + wmlp.gravity = Gravity.CENTER_VERTICAL or Gravity.START + left = DrawingView(this, leftState, rightState) + left.setBackgroundColor(Color.LTGRAY) + left.setLayoutParams(vglp) + wm.addView(left, wmlp) + + wmlp.setTitle("Right -- " + getPackageName()) + wmlp.gravity = Gravity.CENTER_VERTICAL or Gravity.END + right = DrawingView(this, rightState, leftState) + right.setBackgroundColor(Color.LTGRAY) + right.setLayoutParams(vglp) + wm.addView(right, wmlp) + } +} |