1 /** 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media.cujcommon.cts; 18 19 import android.app.Instrumentation; 20 import android.os.Looper; 21 import android.os.SystemClock; 22 import android.util.DisplayMetrics; 23 import android.util.Log; 24 import android.view.MotionEvent; 25 import android.view.MotionEvent.PointerCoords; 26 import android.view.MotionEvent.PointerProperties; 27 import android.view.ScaleGestureDetector; 28 29 import androidx.annotation.NonNull; 30 import androidx.media3.common.Player; 31 32 import java.time.Duration; 33 34 public class PinchToZoomTestPlayerListener extends PlayerListener { 35 36 private static final String TAG = PinchToZoomTestPlayerListener.class.getSimpleName(); 37 private static final Duration ZOOM_IN_DURATION = Duration.ofSeconds(4); 38 private static final int PINCH_STEP_COUNT = 10; 39 private static final float SPAN_GAP = 50.0f; 40 private static final float LEFT_MARGIN_WIDTH_FACTOR = 0.1f; 41 private static final float RIGHT_MARGIN_WIDTH_FACTOR = 0.9f; 42 43 private int mXStart; 44 private int mYStart; 45 private int mWidth; 46 private int mHeight; 47 private float mStepSize; 48 PinchToZoomTestPlayerListener(Duration sendMessagePosition)49 public PinchToZoomTestPlayerListener(Duration sendMessagePosition) { 50 super(); 51 this.mSendMessagePosition = sendMessagePosition; 52 } 53 54 /** 55 * Return a new pointer of the display. 56 * 57 * @param x x coordinate of the pointer 58 * @param y y coordinate of the pointer 59 */ getDisplayPointer(float x, float y)60 PointerCoords getDisplayPointer(float x, float y) { 61 PointerCoords pointerCoords = new PointerCoords(); 62 pointerCoords.x = x; 63 pointerCoords.y = y; 64 pointerCoords.pressure = 1; 65 pointerCoords.size = 1; 66 return pointerCoords; 67 } 68 69 @Override getTestType()70 public TestType getTestType() { 71 return TestType.PINCH_TO_ZOOM_TEST; 72 } 73 74 @Override onEventsPlaybackStateChanged(@onNull Player player)75 public void onEventsPlaybackStateChanged(@NonNull Player player) { 76 if (mExpectedTotalTime == 0 && player.getPlaybackState() == Player.STATE_READY) { 77 // At the first media transition player is not ready. So, add duration of 78 // first clip when player is ready 79 mExpectedTotalTime += player.getDuration(); 80 // Register scale gesture detector 81 mActivity.mScaleGestureDetector = new ScaleGestureDetector(mActivity, 82 new ScaleGestureListener(mActivity.mExoplayerView)); 83 // Adjust the touch input region. 84 setInputRegionSize(); 85 } 86 } 87 88 @Override onEventsMediaItemTransition(@onNull Player player)89 public void onEventsMediaItemTransition(@NonNull Player player) { 90 mActivity.mPlayer.createMessage((messageType, payload) -> { 91 // Programmatically pinch and zoom in 92 pinchAndZoom(true /* zoomIn */); 93 }).setLooper(Looper.getMainLooper()).setPosition(mSendMessagePosition.toMillis()) 94 .setDeleteAfterDelivery(true) 95 .send(); 96 mActivity.mPlayer.createMessage((messageType, payload) -> { 97 // Programmatically pinch and zoom out 98 pinchAndZoom(false /* zoomOut */); 99 }).setLooper(Looper.getMainLooper()) 100 .setPosition(mSendMessagePosition.plus(ZOOM_IN_DURATION).toMillis()) 101 .setDeleteAfterDelivery(true) 102 .send(); 103 } 104 105 /** Adjusts the touchable region size, based on the main activity's display metrics. */ setInputRegionSize()106 private void setInputRegionSize() { 107 int[] loc = new int[2]; 108 mActivity.getWindow().getDecorView().getRootView().getLocationOnScreen(loc); 109 mXStart = loc[0]; 110 mYStart = loc[1]; 111 DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics(); 112 mWidth = displayMetrics.widthPixels; 113 mHeight = displayMetrics.heightPixels; 114 mStepSize = (RIGHT_MARGIN_WIDTH_FACTOR * mWidth - LEFT_MARGIN_WIDTH_FACTOR * mWidth 115 - 2 * SPAN_GAP) / (2 * PINCH_STEP_COUNT); 116 Log.i(TAG, "Set the touchable region: x = " + mXStart + ", y = " + mYStart 117 + ", width=" + mWidth + ", height=" + mHeight + ", stepSize=" + mStepSize); 118 } 119 120 /** 121 * Create a new MotionEvent, filling in all of the basic values that define the motion. Then, 122 * dispatch a pointer event into a window owned by the instrumented application. 123 * 124 * @param inst An instance of {@link Instrumentation} for sending pointer event. 125 * @param action The kind of action being performed. 126 * @param pointerCount The number of pointers that will be in this event. 127 * @param pointerProperties An array of <em>pointerCount</em> values providing a 128 * {@link PointerProperties} property object for each pointer, which must 129 * include the pointer identifier. 130 * @param pointerCoords An array of <em>pointerCount</em> values providing a 131 * {@link PointerCoords} coordinate object for each pointer. 132 */ obtainAndSendPointerEvent(Instrumentation inst, int action, int pointerCount, PointerProperties[] pointerProperties, PointerCoords[] pointerCoords)133 void obtainAndSendPointerEvent(Instrumentation inst, int action, int pointerCount, 134 PointerProperties[] pointerProperties, PointerCoords[] pointerCoords) { 135 MotionEvent pointerMotionEvent = MotionEvent.obtain(SystemClock.uptimeMillis() /* downTime */, 136 SystemClock.uptimeMillis() /* eventTime */, action, pointerCount, pointerProperties, 137 pointerCoords, 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */, 138 1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */, 0 /* source */, 139 mActivity.getDisplayId(), 0 /* flags */); 140 inst.sendPointerSync(pointerMotionEvent); 141 } 142 143 /** 144 * Return array of two PointerCoords. 145 * 146 * @param isZoomIn True for zoom in. 147 */ getPointerCoords(boolean isZoomIn)148 PointerCoords[] getPointerCoords(boolean isZoomIn) { 149 PointerCoords leftPointerStartCoords; 150 PointerCoords rightPointerStartCoords; 151 float midDisplayHeight = mYStart + mHeight / 2.0f; 152 if (isZoomIn) { 153 float midDisplayWidth = mXStart + mWidth / 2.0f; 154 // During zoom in, start pinching from middle of the display towards the end. 155 leftPointerStartCoords = getDisplayPointer(midDisplayWidth - SPAN_GAP, midDisplayHeight); 156 rightPointerStartCoords = getDisplayPointer(midDisplayWidth + SPAN_GAP, midDisplayHeight); 157 } else { 158 // During zoom out, start pinching from end of the display towards the middle. 159 leftPointerStartCoords = getDisplayPointer(mXStart + LEFT_MARGIN_WIDTH_FACTOR * mWidth, 160 midDisplayHeight); 161 rightPointerStartCoords = getDisplayPointer(mXStart + RIGHT_MARGIN_WIDTH_FACTOR * mWidth, 162 midDisplayHeight); 163 } 164 return new PointerCoords[]{leftPointerStartCoords, rightPointerStartCoords}; 165 } 166 167 /** 168 * Return array of two PointerProperties. 169 */ getPointerProperties()170 PointerProperties[] getPointerProperties() { 171 PointerProperties defaultPointerProperties = new PointerProperties(); 172 defaultPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER; 173 PointerProperties leftPointerProperties = new PointerProperties(defaultPointerProperties); 174 leftPointerProperties.id = 0; 175 PointerProperties rightPointerProperties = new PointerProperties(defaultPointerProperties); 176 rightPointerProperties.id = 1; 177 return new PointerProperties[]{leftPointerProperties, rightPointerProperties}; 178 } 179 180 /** 181 * Simulate pinch gesture to zoom in and zoom out. 182 * 183 * @param isZoomIn True for zoom in. 184 */ pinchAndZoom(boolean isZoomIn)185 private void pinchAndZoom(boolean isZoomIn) { 186 new Thread(() -> { 187 try { 188 PointerCoords[] pointerCoords = getPointerCoords(isZoomIn); 189 PointerProperties[] pointerProperties = getPointerProperties(); 190 191 Instrumentation inst = new Instrumentation(); 192 // Pinch In 193 obtainAndSendPointerEvent(inst, MotionEvent.ACTION_DOWN, 1 /* pointerCount*/, 194 pointerProperties, pointerCoords); 195 obtainAndSendPointerEvent(inst, MotionEvent.ACTION_POINTER_DOWN + (pointerProperties[1].id 196 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2 /* pointerCount */, pointerProperties, 197 pointerCoords); 198 199 for (int i = 0; i < PINCH_STEP_COUNT; i++) { 200 if (isZoomIn) { 201 pointerCoords[0].x -= mStepSize; 202 pointerCoords[1].x += mStepSize; 203 } else { 204 pointerCoords[0].x += mStepSize; 205 pointerCoords[1].x -= mStepSize; 206 } 207 obtainAndSendPointerEvent(inst, MotionEvent.ACTION_MOVE, 2 /* pointerCount */, 208 pointerProperties, pointerCoords); 209 } 210 211 // Pinch Out 212 obtainAndSendPointerEvent(inst, MotionEvent.ACTION_POINTER_UP + (pointerProperties[1].id 213 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2 /* pointerCount */, pointerProperties, 214 pointerCoords); 215 obtainAndSendPointerEvent(inst, MotionEvent.ACTION_UP, 1 /* pointerCount */, 216 pointerProperties, pointerCoords); 217 } catch (Exception e) { 218 throw new RuntimeException(e); 219 } 220 }).start(); 221 } 222 } 223