Add support to render app on multiple displays

BUG: 241152647

Test: Manual Testing
Change-Id: I912f164b99c34e9b04d73cf37d0a97a19a1afc40
diff --git a/tests/TouchLatency/app/build.gradle b/tests/TouchLatency/app/build.gradle
index 04a8788..f5ae6f4 100644
--- a/tests/TouchLatency/app/build.gradle
+++ b/tests/TouchLatency/app/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 28
+    compileSdkVersion 33
     buildToolsVersion '28.0.3'
 
     defaultConfig {
         applicationId "com.prefabulated.touchlatency"
         minSdkVersion 28
-        targetSdkVersion 28
+        targetSdkVersion 33
         versionCode 1
         versionName "1.0"
     }
diff --git a/tests/TouchLatency/app/src/main/AndroidManifest.xml b/tests/TouchLatency/app/src/main/AndroidManifest.xml
index 9894736..25bb5d9 100644
--- a/tests/TouchLatency/app/src/main/AndroidManifest.xml
+++ b/tests/TouchLatency/app/src/main/AndroidManifest.xml
@@ -20,16 +20,22 @@
     <application android:allowBackup="true"
          android:icon="@mipmap/ic_launcher"
          android:label="@string/app_name"
-         android:theme="@style/AppTheme">
+         android:theme="@style/AppTheme"
+        android:resizeableActivity="true" >
         <activity android:name=".TouchLatencyActivity"
              android:label="@string/app_name"
              android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+
+        <activity android:name=".TouchLatencyActivityPresentation"
+            android:label="@string/app_name"
+            android:parentActivityName=".TouchLatencyActivity"
+            android:exported="true">
+        </activity>
     </application>
 
 </manifest>
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
index a2842b6..6ab3b3e 100644
--- a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivity.java
@@ -17,218 +17,40 @@
 package com.prefabulated.touchlatency;
 
 import android.app.Activity;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
+import android.app.ActivityOptions;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.Trace;
-import android.util.AttributeSet;
-import android.util.Log;
 import android.view.Display;
 import android.view.Display.Mode;
 import android.view.Menu;
 import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
 
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-
-class TouchLatencyView extends View implements View.OnTouchListener {
-    private static final String LOG_TAG = "TouchLatency";
-    private static final int BACKGROUND_COLOR = 0xFF400080;
-    private static final int INNER_RADIUS = 70;
-    private static final int BALL_DIAMETER = 200;
-    private static final int SEC_TO_NANOS = 1000000000;
-    private static final float FPS_UPDATE_THRESHOLD = 20;
-    private static final long BALL_VELOCITY = 420;
-
-    public TouchLatencyView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        Trace.beginSection("TouchLatencyView constructor");
-        setOnTouchListener(this);
-        setWillNotDraw(false);
-        mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mBluePaint.setColor(0xFF0000FF);
-        mBluePaint.setStyle(Paint.Style.FILL);
-        mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mGreenPaint.setColor(0xFF00FF00);
-        mGreenPaint.setStyle(Paint.Style.FILL);
-        mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mYellowPaint.setColor(0xFFFFFF00);
-        mYellowPaint.setStyle(Paint.Style.FILL);
-        mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mRedPaint.setColor(0xFFFF0000);
-        mRedPaint.setStyle(Paint.Style.FILL);
-        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mTextPaint.setColor(0xFFFFFFFF);
-        mTextPaint.setTextSize(100);
-        mTextPaint.setTextAlign(Align.RIGHT);
-
-        mTouching = false;
-
-        mLastDrawNano = 0;
-        mFps = 0;
-        mLastFpsUpdate = 0;
-        mFrameCount = 0;
-
-        mDf = new DecimalFormat("fps: #.##");
-        mDf.setRoundingMode(RoundingMode.HALF_UP);
-
-        Trace.endSection();
-    }
-
-    @Override
-    public boolean onTouch(View view, MotionEvent event) {
-        Trace.beginSection("TouchLatencyView onTouch");
-        int action = event.getActionMasked();
-        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
-            mTouching = true;
-            invalidate();
-
-            mTouchX = event.getX();
-            mTouchY = event.getY();
-        } else if (action == MotionEvent.ACTION_UP) {
-            mTouching = false;
-            invalidate();
-        }
-        Trace.endSection();
-        return true;
-    }
-
-    private void drawTouch(Canvas canvas) {
-        Trace.beginSection("TouchLatencyView drawTouch");
-
-        try {
-            if (!mTouching) {
-                Log.d(LOG_TAG, "Filling background");
-                canvas.drawColor(BACKGROUND_COLOR);
-                return;
-            }
-
-            float deltaX = (mTouchX - mLastDrawnX);
-            float deltaY = (mTouchY - mLastDrawnY);
-            float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f;
-
-            mLastDrawnX = mTouchX;
-            mLastDrawnY = mTouchY;
-
-            canvas.drawColor(BACKGROUND_COLOR);
-            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint);
-            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint);
-            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint);
-            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint);
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    private Paint getBallColor() {
-        if (mFps > 75)
-            return mGreenPaint;
-        else if (mFps > 45)
-            return mYellowPaint;
-        else
-            return mRedPaint;
-    }
-
-    private void drawBall(Canvas canvas) {
-        Trace.beginSection("TouchLatencyView drawBall");
-        int width = canvas.getWidth();
-        int height = canvas.getHeight();
-        float fps = 0f;
-
-        long t = System.nanoTime();
-        long tDiff = t - mLastDrawNano;
-        mLastDrawNano = t;
-        mFrameCount++;
-
-        if (tDiff < SEC_TO_NANOS) {
-            fps = 1f * SEC_TO_NANOS / tDiff;
-        }
-
-        long fDiff = t - mLastFpsUpdate;
-        if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) {
-            mFps = fps;
-            mLastFpsUpdate = t;
-            mFrameCount = 0;
-        } else if (fDiff > SEC_TO_NANOS) {
-            mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff;
-            mLastFpsUpdate = t;
-            mFrameCount = 0;
-        }
-
-        final long pos = t * BALL_VELOCITY / SEC_TO_NANOS;
-        final long xMax = width - BALL_DIAMETER;
-        final long yMax = height - BALL_DIAMETER;
-        long xOffset = pos % xMax;
-        long yOffset = pos % yMax;
-
-        float left, right, top, bottom;
-
-        if (((pos / xMax) & 1) == 0) {
-            left = xMax - xOffset;
-        } else {
-            left = xOffset;
-        }
-        right = left + BALL_DIAMETER;
-
-        if (((pos / yMax) & 1) == 0) {
-            top = yMax - yOffset;
-        } else {
-            top = yOffset;
-        }
-        bottom = top + BALL_DIAMETER;
-
-        // Draw the ball
-        canvas.drawColor(BACKGROUND_COLOR);
-        canvas.drawOval(left, top, right, bottom, getBallColor());
-        canvas.drawText(mDf.format(mFps), width, 100, mTextPaint);
-
-        invalidate();
-        Trace.endSection();
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        Trace.beginSection("TouchLatencyView onDraw");
-        if (mMode == 0) {
-            drawTouch(canvas);
-        } else {
-            drawBall(canvas);
-        }
-        Trace.endSection();
-    }
-
-    public void changeMode(MenuItem item) {
-        Trace.beginSection("TouchLatencyView changeMode");
-        final int NUM_MODES = 2;
-        final String modes[] = {"Touch", "Ball"};
-        mMode = (mMode + 1) % NUM_MODES;
-        invalidate();
-        item.setTitle(modes[mMode]);
-        Trace.endSection();
-    }
-
-    private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint;
-    private int mMode;
-
-    private boolean mTouching;
-    private float mTouchX, mTouchY;
-    private float mLastDrawnX, mLastDrawnY;
-
-    private long mLastDrawNano, mLastFpsUpdate, mFrameCount;
-    private float mFps;
-    private DecimalFormat mDf;
-}
-
 public class TouchLatencyActivity extends Activity {
     private Mode mDisplayModes[];
     private int mCurrentModeIndex;
+    private DisplayManager mDisplayManager;
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayManager.DisplayListener() {
+        @Override
+        public void onDisplayAdded(int i) {
+            invalidateOptionsMenu();
+        }
+
+        @Override
+        public void onDisplayRemoved(int i) {
+            invalidateOptionsMenu();
+        }
+
+        @Override
+        public void onDisplayChanged(int i) {
+            invalidateOptionsMenu();
+        }
+    };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -236,9 +58,9 @@
 
         Trace.beginSection("TouchLatencyActivity onCreate");
         setContentView(R.layout.activity_touch_latency);
-
         mTouchView = findViewById(R.id.canvasView);
 
+        configureDisplayListener();
         WindowManager wm = getWindowManager();
         Display display = wm.getDefaultDisplay();
         mDisplayModes = display.getSupportedModes();
@@ -250,11 +72,9 @@
                 break;
             }
         }
-
         Trace.endSection();
     }
 
-
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         Trace.beginSection("TouchLatencyActivity onCreateOptionsMenu");
@@ -265,17 +85,26 @@
             Mode currentMode = getWindowManager().getDefaultDisplay().getMode();
             updateDisplayMode(menuItem, currentMode);
         }
+        updateMultiDisplayMenu(menu.findItem(R.id.multi_display));
         Trace.endSection();
         return true;
     }
 
-
     private void updateDisplayMode(MenuItem menuItem, Mode displayMode) {
         int fps = (int) displayMode.getRefreshRate();
         menuItem.setTitle(fps + "hz");
         menuItem.setVisible(true);
     }
 
+    private void updateMultiDisplayMenu(MenuItem item) {
+        item.setVisible(mDisplayManager.getDisplays().length > 1);
+    }
+
+    private void configureDisplayListener() {
+        mDisplayManager = getSystemService(DisplayManager.class);
+        mDisplayManager.registerDisplayListener(mDisplayListener, new Handler());
+    }
+
     public void changeDisplayMode(MenuItem item) {
         Window w = getWindow();
         WindowManager.LayoutParams params = w.getAttributes();
@@ -299,6 +128,19 @@
         mCurrentModeIndex = modeIndex;
     }
 
+    private void changeMultipleDisplays() {
+        Intent intent = new Intent(this, TouchLatencyActivityPresentation.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK);
+        ActivityOptions options = ActivityOptions.makeBasic();
+        for (int i = 1; i < mDisplayManager.getDisplays().length; ++i) {
+            // We assume the first display is already displaying the TouchLatencyActivity
+            int displayId = mDisplayManager.getDisplays()[i].getDisplayId();
+            options.setLaunchDisplayId(displayId);
+            intent.putExtra(TouchLatencyActivityPresentation.DISPLAY_ID, displayId);
+            startActivity(intent, options.toBundle());
+        }
+    }
+
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
@@ -309,15 +151,32 @@
         int id = item.getItemId();
 
         //noinspection SimplifiableIfStatement
-        if (id == R.id.action_settings) {
-            mTouchView.changeMode(item);
-        } else if (id == R.id.display_mode) {
-            changeDisplayMode(item);
+        switch (id) {
+            case R.id.action_settings: {
+                mTouchView.changeMode(item);
+                break;
+            }
+            case R.id.display_mode: {
+                changeDisplayMode(item);
+                break;
+            }
+            case R.id.multi_display: {
+                changeMultipleDisplays();
+                break;
+            }
         }
 
         Trace.endSection();
         return super.onOptionsItemSelected(item);
     }
 
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (mDisplayManager != null) {
+            mDisplayManager.unregisterDisplayListener(mDisplayListener);
+        }
+    }
+
     private TouchLatencyView mTouchView;
 }
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivityPresentation.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivityPresentation.java
new file mode 100644
index 0000000..2602e6b
--- /dev/null
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyActivityPresentation.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 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.prefabulated.touchlatency;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Trace;
+import android.view.Display;
+import android.view.Display.Mode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+public class TouchLatencyActivityPresentation extends Activity {
+    public static final String DISPLAY_ID = "DISPLAY_ID";
+    private Mode[] mDisplayModes;
+    private int mCurrentModeIndex;
+    private int mDisplayId;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (getIntent().hasExtra(DISPLAY_ID)) {
+            mDisplayId = (int) getIntent().getExtras().get(DISPLAY_ID);
+        }
+        Trace.beginSection(
+                "TouchLatencyActivityPresentation::DisplayId::" + mDisplayId + " onCreate");
+        setContentView(R.layout.activity_touch_latency);
+
+        mTouchView = findViewById(R.id.canvasView);
+
+        WindowManager wm = getWindowManager();
+        Display display = wm.getDefaultDisplay();
+        mDisplayModes = display.getSupportedModes();
+        Mode currentMode = getWindowManager().getDefaultDisplay().getMode();
+
+        for (int i = 0; i < mDisplayModes.length; i++) {
+            if (currentMode.getModeId() == mDisplayModes[i].getModeId()) {
+                mCurrentModeIndex = i;
+                break;
+            }
+        }
+        Trace.endSection();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        Trace.beginSection(
+                "TouchLatencyActivityPresentation::DisplayId:: "
+                        + mDisplayId + "  onCreateOptionsMenu");
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_touch_latency, menu);
+        if (mDisplayModes.length > 1) {
+            MenuItem menuItem = menu.findItem(R.id.display_mode);
+            Mode currentMode = getWindowManager().getDefaultDisplay().getMode();
+            updateDisplayMode(menuItem, currentMode);
+        }
+        Trace.endSection();
+        return true;
+    }
+
+    private void updateDisplayMode(MenuItem menuItem, Mode displayMode) {
+        int fps = (int) displayMode.getRefreshRate();
+        menuItem.setTitle(fps + "hz");
+        menuItem.setVisible(true);
+    }
+
+    public void changeDisplayMode(MenuItem item) {
+        Window w = getWindow();
+        WindowManager.LayoutParams params = w.getAttributes();
+
+        int modeIndex = (mCurrentModeIndex + 1) % mDisplayModes.length;
+        params.preferredDisplayModeId = mDisplayModes[modeIndex].getModeId();
+        w.setAttributes(params);
+
+        updateDisplayMode(item, mDisplayModes[modeIndex]);
+        mCurrentModeIndex = modeIndex;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        Trace.beginSection(
+                "TouchLatencyActivityPresentation::DisplayId::"
+                        + mDisplayId + "  onOptionsItemSelected");
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        switch (id) {
+            case R.id.action_settings: {
+                mTouchView.changeMode(item);
+                break;
+            }
+            case R.id.display_mode: {
+                changeDisplayMode(item);
+                break;
+            }
+        }
+
+        Trace.endSection();
+        return super.onOptionsItemSelected(item);
+    }
+
+    private TouchLatencyView mTouchView;
+}
diff --git a/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyView.java b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyView.java
new file mode 100644
index 0000000..0803e8e
--- /dev/null
+++ b/tests/TouchLatency/app/src/main/java/com/prefabulated/touchlatency/TouchLatencyView.java
@@ -0,0 +1,219 @@
+/*
+ * 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.prefabulated.touchlatency;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+
+class TouchLatencyView extends View implements View.OnTouchListener {
+    private static final String LOG_TAG = "TouchLatency";
+    private static final int BACKGROUND_COLOR = 0xFF400080;
+    private static final int INNER_RADIUS = 70;
+    private static final int BALL_DIAMETER = 200;
+    private static final int SEC_TO_NANOS = 1000000000;
+    private static final float FPS_UPDATE_THRESHOLD = 20;
+    private static final long BALL_VELOCITY = 420;
+
+    public TouchLatencyView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Trace.beginSection("TouchLatencyView constructor");
+        setOnTouchListener(this);
+        setWillNotDraw(false);
+        mBluePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mBluePaint.setColor(0xFF0000FF);
+        mBluePaint.setStyle(Paint.Style.FILL);
+        mGreenPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mGreenPaint.setColor(0xFF00FF00);
+        mGreenPaint.setStyle(Paint.Style.FILL);
+        mYellowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mYellowPaint.setColor(0xFFFFFF00);
+        mYellowPaint.setStyle(Paint.Style.FILL);
+        mRedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mRedPaint.setColor(0xFFFF0000);
+        mRedPaint.setStyle(Paint.Style.FILL);
+        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mTextPaint.setColor(0xFFFFFFFF);
+        mTextPaint.setTextSize(100);
+        mTextPaint.setTextAlign(Paint.Align.RIGHT);
+
+        mTouching = false;
+
+        mLastDrawNano = 0;
+        mFps = 0;
+        mLastFpsUpdate = 0;
+        mFrameCount = 0;
+
+        mDf = new DecimalFormat("fps: #.##");
+        mDf.setRoundingMode(RoundingMode.HALF_UP);
+
+        Trace.endSection();
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        Trace.beginSection("TouchLatencyView onTouch");
+        int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) {
+            mTouching = true;
+            invalidate();
+
+            mTouchX = event.getX();
+            mTouchY = event.getY();
+        } else if (action == MotionEvent.ACTION_UP) {
+            mTouching = false;
+            invalidate();
+        }
+        Trace.endSection();
+        return true;
+    }
+
+    private void drawTouch(Canvas canvas) {
+        Trace.beginSection("TouchLatencyView drawTouch");
+
+        try {
+            if (!mTouching) {
+                Log.d(LOG_TAG, "Filling background");
+                canvas.drawColor(BACKGROUND_COLOR);
+                return;
+            }
+
+            float deltaX = (mTouchX - mLastDrawnX);
+            float deltaY = (mTouchY - mLastDrawnY);
+            float scaleFactor = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY) * 1.5f;
+
+            mLastDrawnX = mTouchX;
+            mLastDrawnY = mTouchY;
+
+            canvas.drawColor(BACKGROUND_COLOR);
+            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 3 * scaleFactor, mRedPaint);
+            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + 2 * scaleFactor, mYellowPaint);
+            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS + scaleFactor, mGreenPaint);
+            canvas.drawCircle(mTouchX, mTouchY, INNER_RADIUS, mBluePaint);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    private Paint getBallColor() {
+        if (mFps > 75) {
+            return mGreenPaint;
+        } else if (mFps > 45) {
+            return mYellowPaint;
+        } else
+            return mRedPaint;
+    }
+
+    private void drawBall(Canvas canvas) {
+        Trace.beginSection("TouchLatencyView drawBall");
+        int width = canvas.getWidth();
+        int height = canvas.getHeight();
+        float fps = 0f;
+
+        long t = System.nanoTime();
+        long tDiff = t - mLastDrawNano;
+        mLastDrawNano = t;
+        mFrameCount++;
+
+        if (tDiff < SEC_TO_NANOS) {
+            fps = 1f * SEC_TO_NANOS / tDiff;
+        }
+
+        long fDiff = t - mLastFpsUpdate;
+        if (Math.abs(mFps - fps) > FPS_UPDATE_THRESHOLD) {
+            mFps = fps;
+            mLastFpsUpdate = t;
+            mFrameCount = 0;
+        } else if (fDiff > SEC_TO_NANOS) {
+            mFps = 1f * mFrameCount * SEC_TO_NANOS / fDiff;
+            mLastFpsUpdate = t;
+            mFrameCount = 0;
+        }
+
+        final long pos = t * BALL_VELOCITY / SEC_TO_NANOS;
+        final long xMax = width - BALL_DIAMETER;
+        final long yMax = height - BALL_DIAMETER;
+        long xOffset = pos % xMax;
+        long yOffset = pos % yMax;
+
+        float left, right, top, bottom;
+
+        if (((pos / xMax) & 1) == 0) {
+            left = xMax - xOffset;
+        } else {
+            left = xOffset;
+        }
+        right = left + BALL_DIAMETER;
+
+        if (((pos / yMax) & 1) == 0) {
+            top = yMax - yOffset;
+        } else {
+            top = yOffset;
+        }
+        bottom = top + BALL_DIAMETER;
+
+        // Draw the ball
+        canvas.drawColor(BACKGROUND_COLOR);
+        canvas.drawOval(left, top, right, bottom, getBallColor());
+        canvas.drawText(mDf.format(mFps), width, 100, mTextPaint);
+
+        invalidate();
+        Trace.endSection();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        Trace.beginSection("TouchLatencyView onDraw");
+        if (mMode == 0) {
+            drawTouch(canvas);
+        } else {
+            drawBall(canvas);
+        }
+        Trace.endSection();
+    }
+
+    public void changeMode(MenuItem item) {
+        Trace.beginSection("TouchLatencyView changeMode");
+        final int NUM_MODES = 2;
+        final String modes[] = {"Touch", "Ball"};
+        mMode = (mMode + 1) % NUM_MODES;
+        invalidate();
+        item.setTitle(modes[mMode]);
+        Trace.endSection();
+    }
+
+    private final Paint mBluePaint, mGreenPaint, mYellowPaint, mRedPaint, mTextPaint;
+    private int mMode;
+
+    private boolean mTouching;
+    private float mTouchX, mTouchY;
+    private float mLastDrawnX, mLastDrawnY;
+
+    private long mLastDrawNano, mLastFpsUpdate, mFrameCount;
+    private float mFps;
+    private DecimalFormat mDf;
+}
diff --git a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
index 52be919..abc7fd5 100644
--- a/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
+++ b/tests/TouchLatency/app/src/main/res/menu/menu_touch_latency.xml
@@ -25,4 +25,10 @@
         android:showAsAction="ifRoom"
         android:title="@string/display_mode"
         android:visible="false"/>
+
+    <item
+        android:id="@+id/multi_display"
+        android:showAsAction="ifRoom"
+        android:title="@string/multi_display"
+        android:visible="false"/>
 </menu>
diff --git a/tests/TouchLatency/app/src/main/res/values/strings.xml b/tests/TouchLatency/app/src/main/res/values/strings.xml
index 771992c..5ee86d8 100644
--- a/tests/TouchLatency/app/src/main/res/values/strings.xml
+++ b/tests/TouchLatency/app/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
 
     <string name="mode">Touch</string>
     <string name="display_mode">Mode</string>
+    <string name="multi_display">multi-display</string>
 </resources>
diff --git a/tests/TouchLatency/build.gradle b/tests/TouchLatency/build.gradle
index 03abe82..381e55e 100644
--- a/tests/TouchLatency/build.gradle
+++ b/tests/TouchLatency/build.gradle
@@ -6,7 +6,7 @@
         google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.2.1'
+        classpath 'com.android.tools.build:gradle:4.2.2'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/tests/TouchLatency/gradle/wrapper/gradle-wrapper.properties b/tests/TouchLatency/gradle/wrapper/gradle-wrapper.properties
index 2d80b69..4d9ca16 100644
--- a/tests/TouchLatency/gradle/wrapper/gradle-wrapper.properties
+++ b/tests/TouchLatency/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists