package com.artemis.fixture; import android.app.Activity; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.SeekBar; import android.widget.TextView; import org.json.JSONArray; import org.json.JSONObject; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; /** Disposable, dependency-free native controls and observable touch-event fixture. */ public final class MainActivity extends Activity { private TextView status; private TextView touchStatus; private EditText editor; private SeekBar range; private TouchSurface touch; private int clicks; private int longClicks; private int imeEnters; private int statusVersion; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle("ARTEMIS native actions fixture"); ScrollView scroll = new ScrollView(this); scroll.setContentDescription("fixture.scroll"); scroll.setFillViewport(true); LinearLayout body = new LinearLayout(this); body.setOrientation(LinearLayout.VERTICAL); body.setPadding(dp(12), dp(12), dp(12), dp(24)); scroll.addView(body); body.addView(label("ARTEMIS native controls", "fixture.title")); status = label("status:ready", "fixture.status"); body.addView(status); range = new SeekBar(this); range.setContentDescription("fixture.range"); range.setMax(100); range.setProgress(25); body.addView(range, new LinearLayout.LayoutParams(-1, dp(52))); range.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { updateStatus("range:" + progress); } @Override public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) { writeState(); } }); Button click = button("Native click", "fixture.click"); click.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { clicks++; updateStatus("click:" + clicks); } }); body.addView(click); Button longClick = button("Native long click", "fixture.long_click"); longClick.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { longClicks++; updateStatus("long_click:" + longClicks); return true; } }); body.addView(longClick); Button delayed = button("Change status after 700 ms", "fixture.delayed"); delayed.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { updateStatus("status:waiting"); status.postDelayed(new Runnable() { @Override public void run() { statusVersion++; updateStatus("status:changed:" + statusVersion); } }, 700L); } }); body.addView(delayed); editor = new EditText(this) { @Override protected void onSelectionChanged(int start, int end) { super.onSelectionChanged(start, end); writeState(); } }; editor.setContentDescription("fixture.editor"); editor.setInputType(InputType.TYPE_CLASS_TEXT); editor.setSingleLine(true); editor.setImeOptions(EditorInfo.IME_ACTION_DONE); editor.setText("A\uD83D\uDE00B中文"); editor.setSelection(editor.length()); editor.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView view, int actionId, android.view.KeyEvent event) { imeEnters++; updateStatus("ime_enter:" + imeEnters); return true; } }); editor.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence value, int start, int count, int after) {} @Override public void onTextChanged(CharSequence value, int start, int before, int count) {} @Override public void afterTextChanged(Editable value) { writeState(); } }); body.addView(editor, new LinearLayout.LayoutParams(-1, dp(52))); touchStatus = label("touch:ready", "fixture.touch_status"); body.addView(touchStatus); touch = new TouchSurface(); body.addView(touch, new LinearLayout.LayoutParams(-1, dp(210))); Button reset = button("Reset touch statistics", "fixture.touch_reset"); reset.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { touch.reset(); writeState(); } }); body.addView(reset); // Native ScrollView actions must traverse multiple viewports to reach this target. for (int i = 0; i < 45; i++) { TextView row = label("Native list row " + i, "fixture.row." + i); row.setMinHeight(dp(64)); body.addView(row); } Button bottom = button("Bottom target", "fixture.bottom"); bottom.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { updateStatus("bottom:clicked"); } }); body.addView(bottom); setContentView(scroll); writeState(); } private int dp(int value) { return Math.round(value * getResources().getDisplayMetrics().density); } private TextView label(String text, String description) { TextView result = new TextView(this); result.setText(text); result.setTextSize(16); result.setTextColor(Color.BLACK); result.setPadding(0, dp(4), 0, dp(4)); result.setContentDescription(description); return result; } private Button button(String text, String description) { Button result = new Button(this); result.setText(text); result.setContentDescription(description); return result; } private void updateStatus(String text) { status.setText(text); writeState(); } /** Read explicitly with: adb shell run-as com.artemis.fixture cat files/native_actions_state.json */ private void writeState() { if (editor == null || range == null || touch == null) return; try { JSONObject state = new JSONObject().put("click_count", clicks).put("long_click_count", longClicks) .put("ime_enter_count", imeEnters).put("status", status.getText().toString()) .put("progress", range.getProgress()).put("text", editor.getText().toString()) .put("selection_start", editor.getSelectionStart()).put("selection_end", editor.getSelectionEnd()) .put("touch", touch.state(true)); try (FileOutputStream stream = openFileOutput("native_actions_state.json", MODE_PRIVATE)) { stream.write(state.toString().getBytes(StandardCharsets.UTF_8)); } } catch (Exception ignored) {} } private final class TouchSurface extends View { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private JSONArray events = new JSONArray(); private int downs; private int ups; private int cancels; private int moves; private int maxPointers; private int pointerDowns; private int pointerUps; private double initialDistance; private double initialAngle; private double scale = 1.0; private double rotation; TouchSurface() { super(MainActivity.this); setContentDescription("fixture.touch"); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); setFocusable(true); setClickable(true); setBackgroundColor(Color.rgb(208, 239, 247)); paint.setColor(Color.rgb(15, 45, 60)); paint.setTextSize(dp(16)); } void reset() { events = new JSONArray(); downs = ups = cancels = moves = maxPointers = pointerDowns = pointerUps = 0; initialDistance = initialAngle = rotation = 0; scale = 1; publish(); } JSONObject state(boolean includeEvents) throws Exception { JSONObject result = new JSONObject().put("down_count", downs).put("up_count", ups) .put("cancel_count", cancels).put("move_count", moves).put("max_pointer_count", maxPointers) .put("pointer_down_count", pointerDowns).put("pointer_up_count", pointerUps) .put("scale", scale).put("rotation_degrees", rotation); if (includeEvents) result.put("events", events); return result; } private void publish() { try { touchStatus.setText(state(false).toString()); } catch (Exception ignored) {} invalidate(); writeState(); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { downs++; initialDistance = 0; scale = 1; rotation = 0; getParent().requestDisallowInterceptTouchEvent(true); } else if (action == MotionEvent.ACTION_UP) { ups++; getParent().requestDisallowInterceptTouchEvent(false); } else if (action == MotionEvent.ACTION_CANCEL) { cancels++; getParent().requestDisallowInterceptTouchEvent(false); } else if (action == MotionEvent.ACTION_MOVE) { moves++; } else if (action == MotionEvent.ACTION_POINTER_DOWN) { pointerDowns++; } else if (action == MotionEvent.ACTION_POINTER_UP) { pointerUps++; } int count = event.getPointerCount(); maxPointers = Math.max(maxPointers, count); if (count >= 2) { double dx = event.getX(1) - event.getX(0); double dy = event.getY(1) - event.getY(0); double distance = Math.hypot(dx, dy); double angle = Math.toDegrees(Math.atan2(dy, dx)); if (initialDistance == 0 || action == MotionEvent.ACTION_POINTER_DOWN) { initialDistance = Math.max(distance, 0.001); initialAngle = angle; } scale = distance / initialDistance; rotation = angle - initialAngle; while (rotation > 180) rotation -= 360; while (rotation < -180) rotation += 360; } try { JSONArray pointers = new JSONArray(); for (int i = 0; i < count; i++) { pointers.put(new JSONObject().put("id", event.getPointerId(i)) .put("x", event.getX(i)).put("y", event.getY(i))); } if (events.length() >= 64) events.remove(0); events.put(new JSONObject().put("action", action).put("time_ms", event.getEventTime()) .put("pointer_count", count).put("pointers", pointers)); } catch (Exception ignored) {} publish(); return true; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText("Touch fixture: pinch, rotate, continuous stroke", dp(8), dp(30), paint); canvas.drawText("Down " + downs + " / up " + ups + " / moves " + moves, dp(8), dp(60), paint); canvas.drawText("Pointers " + maxPointers + " / scale " + String.format(java.util.Locale.ROOT, "%.3f", scale), dp(8), dp(90), paint); canvas.drawText("Rotation " + String.format(java.util.Locale.ROOT, "%.1f", rotation), dp(8), dp(120), paint); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName("com.artemis.fixture.TouchSurface"); } } }