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 public class PinchToZoomTestPlayerListener extends PlayerListener {
33 
34   private static final String TAG = PinchToZoomTestPlayerListener.class.getSimpleName();
35   private static final int ZOOM_IN_DURATION_MS = 4000;
36   private static final int PINCH_STEP_COUNT = 10;
37   private static final float SPAN_GAP = 50.0f;
38   private static final float LEFT_MARGIN_WIDTH_FACTOR = 0.1f;
39   private static final float RIGHT_MARGIN_WIDTH_FACTOR = 0.9f;
40 
41   private int mWidth;
42   private int mHeight;
43   private float mStepSize;
44 
PinchToZoomTestPlayerListener(long sendMessagePosition)45   public PinchToZoomTestPlayerListener(long sendMessagePosition) {
46     super();
47     this.mSendMessagePosition = sendMessagePosition;
48   }
49 
50   /**
51    * Return a new pointer of the display.
52    *
53    * @param x x coordinate of the pointer
54    * @param y y coordinate of the pointer
55    */
getDisplayPointer(float x, float y)56   PointerCoords getDisplayPointer(float x, float y) {
57     PointerCoords pointerCoords = new PointerCoords();
58     pointerCoords.x = x;
59     pointerCoords.y = y;
60     pointerCoords.pressure = 1;
61     pointerCoords.size = 1;
62     return pointerCoords;
63   }
64 
65   @Override
getTestType()66   public TestType getTestType() {
67     return TestType.PINCH_TO_ZOOM_TEST;
68   }
69 
70   @Override
onEventsPlaybackStateChanged(@onNull Player player)71   public void onEventsPlaybackStateChanged(@NonNull Player player) {
72     if (player.getPlaybackState() == Player.STATE_READY) {
73       // At the first media transition player is not ready. So, add duration of
74       // first clip when player is ready
75       mExpectedTotalTime += player.getDuration();
76       // Register scale gesture detector
77       mActivity.mScaleGestureDetector = new ScaleGestureDetector(mActivity,
78           new ScaleGestureListener(mActivity.mExoplayerView));
79       // Adjust the touch input region.
80       setInputRegionSize();
81     }
82   }
83 
84   @Override
onEventsMediaItemTransition(@onNull Player player)85   public void onEventsMediaItemTransition(@NonNull Player player) {
86     mActivity.mPlayer.createMessage((messageType, payload) -> {
87           // Programmatically pinch and zoom in
88           pinchAndZoom(true /* zoomIn */);
89         }).setLooper(Looper.getMainLooper()).setPosition(mSendMessagePosition)
90         .setDeleteAfterDelivery(true)
91         .send();
92     mActivity.mPlayer.createMessage((messageType, payload) -> {
93           // Programmatically pinch and zoom out
94           pinchAndZoom(false /* zoomOut */);
95         }).setLooper(Looper.getMainLooper())
96         .setPosition(mSendMessagePosition + ZOOM_IN_DURATION_MS)
97         .setDeleteAfterDelivery(true)
98         .send();
99   }
100 
101   /** Adjusts the touchable region size, based on the main activity's display metrics. */
setInputRegionSize()102   private void setInputRegionSize() {
103     DisplayMetrics displayMetrics = mActivity.getResources().getDisplayMetrics();
104     mWidth = displayMetrics.widthPixels;
105     mHeight = displayMetrics.heightPixels;
106     mStepSize = (RIGHT_MARGIN_WIDTH_FACTOR * mWidth - LEFT_MARGIN_WIDTH_FACTOR * mWidth
107             - 2 * SPAN_GAP) / (2 * PINCH_STEP_COUNT);
108     Log.i(TAG, "Set the touchable region size: width=" + mWidth + ", height=" + mHeight
109             + ", stepSize=" + mStepSize);
110   }
111 
112   /**
113    * Create a new MotionEvent, filling in all of the basic values that define the motion. Then,
114    * dispatch a pointer event into a window owned by the instrumented application.
115    *
116    * @param inst              An instance of {@link Instrumentation} for sending pointer event.
117    * @param action            The kind of action being performed.
118    * @param pointerCount      The number of pointers that will be in this event.
119    * @param pointerProperties An array of <em>pointerCount</em> values providing a
120    *                          {@link PointerProperties} property object for each pointer, which must
121    *                          include the pointer identifier.
122    * @param pointerCoords     An array of <em>pointerCount</em> values providing a
123    *                          {@link PointerCoords} coordinate object for each pointer.
124    */
obtainAndSendPointerEvent(Instrumentation inst, int action, int pointerCount, PointerProperties[] pointerProperties, PointerCoords[] pointerCoords)125   void obtainAndSendPointerEvent(Instrumentation inst, int action, int pointerCount,
126       PointerProperties[] pointerProperties, PointerCoords[] pointerCoords) {
127     MotionEvent pointerMotionEvent = MotionEvent.obtain(SystemClock.uptimeMillis() /* downTime */,
128         SystemClock.uptimeMillis() /* eventTime */, action, pointerCount, pointerProperties,
129         pointerCoords, 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */,
130         1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */, 0 /* source */,
131         mActivity.getDisplayId(), 0 /* flags */);
132     inst.sendPointerSync(pointerMotionEvent);
133   }
134 
135   /**
136    * Return array of two PointerCoords.
137    *
138    * @param isZoomIn  True for zoom in.
139    */
getPointerCoords(boolean isZoomIn)140   PointerCoords[] getPointerCoords(boolean isZoomIn) {
141     PointerCoords leftPointerStartCoords;
142     PointerCoords rightPointerStartCoords;
143     float midDisplayHeight = mHeight / 2.0f;
144     if (isZoomIn) {
145       float midDisplayWidth = mWidth / 2.0f;
146       // During zoom in, start pinching from middle of the display towards the end.
147       leftPointerStartCoords = getDisplayPointer(midDisplayWidth - SPAN_GAP, midDisplayHeight);
148       rightPointerStartCoords = getDisplayPointer(midDisplayWidth + SPAN_GAP, midDisplayHeight);
149     } else {
150       // During zoom out, start pinching from end of the display towards the middle.
151       leftPointerStartCoords = getDisplayPointer(LEFT_MARGIN_WIDTH_FACTOR * mWidth,
152           midDisplayHeight);
153       rightPointerStartCoords = getDisplayPointer(RIGHT_MARGIN_WIDTH_FACTOR * mWidth,
154           midDisplayHeight);
155     }
156     return new PointerCoords[]{leftPointerStartCoords, rightPointerStartCoords};
157   }
158 
159   /**
160    * Return array of two PointerProperties.
161    */
getPointerProperties()162   PointerProperties[] getPointerProperties() {
163     PointerProperties defaultPointerProperties = new PointerProperties();
164     defaultPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
165     PointerProperties leftPointerProperties = new PointerProperties(defaultPointerProperties);
166     leftPointerProperties.id = 0;
167     PointerProperties rightPointerProperties = new PointerProperties(defaultPointerProperties);
168     rightPointerProperties.id = 1;
169     return new PointerProperties[]{leftPointerProperties, rightPointerProperties};
170   }
171 
172   /**
173    * Simulate pinch gesture to zoom in and zoom out.
174    *
175    * @param isZoomIn  True for zoom in.
176    */
pinchAndZoom(boolean isZoomIn)177   private void pinchAndZoom(boolean isZoomIn) {
178     new Thread(() -> {
179       try {
180         PointerCoords[] pointerCoords = getPointerCoords(isZoomIn);
181         PointerProperties[] pointerProperties = getPointerProperties();
182 
183         Instrumentation inst = new Instrumentation();
184         // Pinch In
185         obtainAndSendPointerEvent(inst, MotionEvent.ACTION_DOWN, 1 /* pointerCount*/,
186             pointerProperties, pointerCoords);
187         obtainAndSendPointerEvent(inst, MotionEvent.ACTION_POINTER_DOWN + (pointerProperties[1].id
188                 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2 /* pointerCount */, pointerProperties,
189             pointerCoords);
190 
191         for (int i = 0; i < PINCH_STEP_COUNT; i++) {
192           if (isZoomIn) {
193             pointerCoords[0].x -= mStepSize;
194             pointerCoords[1].x += mStepSize;
195           } else {
196             pointerCoords[0].x += mStepSize;
197             pointerCoords[1].x -= mStepSize;
198           }
199           obtainAndSendPointerEvent(inst, MotionEvent.ACTION_MOVE, 2 /* pointerCount */,
200               pointerProperties, pointerCoords);
201         }
202 
203         // Pinch Out
204         obtainAndSendPointerEvent(inst, MotionEvent.ACTION_POINTER_UP + (pointerProperties[1].id
205                 << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2 /* pointerCount */, pointerProperties,
206             pointerCoords);
207         obtainAndSendPointerEvent(inst, MotionEvent.ACTION_UP, 1 /* pointerCount */,
208             pointerProperties, pointerCoords);
209       } catch (Exception e) {
210         throw new RuntimeException(e);
211       }
212     }).start();
213   }
214 }
215