1 /*
2  * Copyright (C) 2014 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 package android.uirendering.cts.testinfrastructure;
17 
18 import static org.junit.Assert.assertNotNull;
19 import static org.junit.Assert.fail;
20 
21 import android.app.Activity;
22 import android.content.pm.ActivityInfo;
23 import android.content.res.Configuration;
24 import android.graphics.Point;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Message;
28 import androidx.annotation.Nullable;
29 import android.uirendering.cts.R;
30 import android.util.Log;
31 import android.util.Pair;
32 import android.view.FrameMetrics;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.ViewStub;
36 import android.view.ViewTreeObserver;
37 import android.view.Window;
38 
39 import java.util.ArrayList;
40 import java.util.Iterator;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.TimeUnit;
43 
44 /**
45  * A generic activity that uses a view specified by the user.
46  */
47 public class DrawActivity extends Activity {
48     static final String EXTRA_WIDE_COLOR_GAMUT = "DrawActivity.WIDE_COLOR_GAMUT";
49 
50     private final static long TIME_OUT_MS = 10000;
51     private final Object mLock = new Object();
52     private ActivityTestBase.TestPositionInfo mPositionInfo;
53 
54     private Handler mHandler;
55     private View mView;
56     private View mViewWrapper;
57     private boolean mOnTv;
58     private DrawMonitor mDrawMonitor;
59 
onCreate(Bundle bundle)60     public void onCreate(Bundle bundle){
61         super.onCreate(bundle);
62         getWindow().getDecorView().setSystemUiVisibility(
63                 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
64         if (getIntent().getBooleanExtra(EXTRA_WIDE_COLOR_GAMUT, false)) {
65             getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT);
66         }
67         mHandler = new RenderSpecHandler();
68         int uiMode = getResources().getConfiguration().uiMode;
69         mOnTv = (uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION;
70         mDrawMonitor = new DrawMonitor(getWindow());
71     }
72 
getOnTv()73     public boolean getOnTv() {
74         return mOnTv;
75     }
76 
enqueueRenderSpecAndWait(int layoutId, CanvasClient canvasClient, @Nullable ViewInitializer viewInitializer, boolean useHardware, boolean usePicture)77     public ActivityTestBase.TestPositionInfo enqueueRenderSpecAndWait(int layoutId,
78             CanvasClient canvasClient, @Nullable ViewInitializer viewInitializer,
79             boolean useHardware, boolean usePicture) {
80         ((RenderSpecHandler) mHandler).setViewInitializer(viewInitializer);
81         int arg2 = (useHardware ? View.LAYER_TYPE_NONE : View.LAYER_TYPE_SOFTWARE);
82         synchronized (mLock) {
83             if (canvasClient != null) {
84                 mHandler.obtainMessage(RenderSpecHandler.CANVAS_MSG, usePicture ? 1 : 0,
85                         arg2, canvasClient).sendToTarget();
86             } else {
87                 mHandler.obtainMessage(RenderSpecHandler.LAYOUT_MSG, layoutId, arg2).sendToTarget();
88             }
89 
90             try {
91                 mLock.wait(TIME_OUT_MS);
92             } catch (InterruptedException e) {
93                 e.printStackTrace();
94             }
95         }
96         assertNotNull("Timeout waiting for draw", mPositionInfo);
97         return mPositionInfo;
98     }
99 
reset()100     public void reset() {
101         CountDownLatch fence = new CountDownLatch(1);
102         mHandler.obtainMessage(RenderSpecHandler.RESET_MSG, fence).sendToTarget();
103         try {
104             if (!fence.await(10, TimeUnit.SECONDS)) {
105                 fail("Timeout exception");
106             }
107         } catch (InterruptedException ex) {
108             fail(ex.getMessage());
109         }
110     }
111 
112     private ViewInitializer mViewInitializer;
113 
notifyOnDrawCompleted()114     private void notifyOnDrawCompleted() {
115         DrawCounterListener onDrawListener = new DrawCounterListener();
116         mView.getViewTreeObserver().addOnDrawListener(onDrawListener);
117         mView.invalidate();
118     }
119 
120     private class RenderSpecHandler extends Handler {
121         public static final int RESET_MSG = 0;
122         public static final int LAYOUT_MSG = 1;
123         public static final int CANVAS_MSG = 2;
124 
125 
setViewInitializer(ViewInitializer viewInitializer)126         public void setViewInitializer(ViewInitializer viewInitializer) {
127             mViewInitializer = viewInitializer;
128         }
129 
handleMessage(Message message)130         public void handleMessage(Message message) {
131             Log.d("UiRendering", "message of type " + message.what);
132             if (message.what == RESET_MSG) {
133                 ((ViewGroup)findViewById(android.R.id.content)).removeAllViews();
134                 ((CountDownLatch)message.obj).countDown();
135                 return;
136             }
137             setContentView(R.layout.test_container);
138             ViewStub stub = (ViewStub) findViewById(R.id.test_content_stub);
139             mViewWrapper = findViewById(R.id.test_content_wrapper);
140             switch (message.what) {
141                 case LAYOUT_MSG: {
142                     stub.setLayoutResource(message.arg1);
143                     mView = stub.inflate();
144                 } break;
145 
146                 case CANVAS_MSG: {
147                     stub.setLayoutResource(R.layout.test_content_canvasclientview);
148                     mView = stub.inflate();
149                     ((CanvasClientView) mView).setCanvasClient((CanvasClient) (message.obj));
150                     if (message.arg1 != 0) {
151                         ((CanvasClientView) mView).setUsePicture(true);
152                     }
153                 } break;
154             }
155 
156             if (mView == null) {
157                 throw new IllegalStateException("failed to inflate test content");
158             }
159 
160             if (mViewInitializer != null) {
161                 mViewInitializer.initializeView(mView);
162             }
163 
164             // set layer on wrapper parent of view, so view initializer
165             // can control layer type of View under test.
166             mViewWrapper.setLayerType(message.arg2, null);
167 
168             notifyOnDrawCompleted();
169         }
170     }
171 
172     @Override
onPause()173     protected void onPause() {
174         super.onPause();
175         if (mViewInitializer != null) {
176             mViewInitializer.teardownView();
177         }
178     }
179 
180     @Override
finish()181     public void finish() {
182         // Ignore
183     }
184 
185     /** Call this when all the tests that use this activity have completed.
186      * This will then clean up any internal state and finish the activity. */
allTestsFinished()187     public void allTestsFinished() {
188         super.finish();
189     }
190 
191     private class DrawCounterListener implements ViewTreeObserver.OnDrawListener {
192         private static final int DEBUG_REQUIRE_EXTRA_FRAMES = 1;
193         private int mDrawCount = 0;
194 
195         @Override
onDraw()196         public void onDraw() {
197             if (++mDrawCount <= DEBUG_REQUIRE_EXTRA_FRAMES) {
198                 mView.postInvalidate();
199                 return;
200             }
201 
202             long vsyncMillis = mView.getDrawingTime();
203 
204             mView.post(() -> mView.getViewTreeObserver().removeOnDrawListener(this));
205 
206             mDrawMonitor.notifyWhenDrawn(vsyncMillis, () -> {
207                 final int[] location = new int[2];
208                 mViewWrapper.getLocationInWindow(location);
209                 Point surfaceOffset = new Point(location[0], location[1]);
210                 mViewWrapper.getLocationOnScreen(location);
211                 Point screenOffset = new Point(location[0], location[1]);
212                 synchronized (mLock) {
213                     mPositionInfo = new ActivityTestBase.TestPositionInfo(
214                             surfaceOffset, screenOffset);
215                     mLock.notify();
216                 }
217             });
218         }
219     }
220 
221     private static class DrawMonitor {
222 
223         private ArrayList<Pair<Long, Runnable>> mListeners = new ArrayList<>();
224 
DrawMonitor(Window window)225         private DrawMonitor(Window window) {
226             window.addOnFrameMetricsAvailableListener(this::onFrameMetricsAvailable,
227                     new Handler());
228         }
229 
onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, @SuppressWarnings("unused") int dropCountSinceLastInvocation)230         private void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics,
231                 /* This isn't actually unused as it's necessary for the method handle */
232                 @SuppressWarnings("unused") int dropCountSinceLastInvocation) {
233             ArrayList<Runnable> toInvoke = null;
234             synchronized (mListeners) {
235                 if (mListeners.size() == 0) {
236                     return;
237                 }
238 
239                 long vsyncAtMillis = TimeUnit.NANOSECONDS.convert(frameMetrics
240                         .getMetric(FrameMetrics.VSYNC_TIMESTAMP), TimeUnit.MILLISECONDS);
241                 boolean isUiThreadDraw = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) > 0;
242 
243                 Iterator<Pair<Long, Runnable>> iter = mListeners.iterator();
244                 while (iter.hasNext()) {
245                     Pair<Long, Runnable> listener = iter.next();
246                     if ((listener.first == vsyncAtMillis && isUiThreadDraw)
247                             || (listener.first < vsyncAtMillis)) {
248                         if (toInvoke == null) {
249                             toInvoke = new ArrayList<>();
250                         }
251                         Log.d("UiRendering", "adding listener for vsync " + listener.first
252                                 + "; got vsync timestamp: " + vsyncAtMillis
253                                 + " with isUiThreadDraw " + isUiThreadDraw);
254                         toInvoke.add(listener.second);
255                         iter.remove();
256                     } else if (listener.first == vsyncAtMillis && !isUiThreadDraw) {
257                         Log.d("UiRendering",
258                                 "Ignoring draw that's not from the UI thread at vsync: "
259                                         + vsyncAtMillis);
260                     }
261                 }
262             }
263 
264             if (toInvoke != null && toInvoke.size() > 0) {
265                 for (Runnable run : toInvoke) {
266                     run.run();
267                 }
268             }
269 
270         }
271 
notifyWhenDrawn(long contentVsyncMillis, Runnable runWhenDrawn)272         public void notifyWhenDrawn(long contentVsyncMillis, Runnable runWhenDrawn) {
273             synchronized (mListeners) {
274                 mListeners.add(new Pair<>(contentVsyncMillis, runWhenDrawn));
275             }
276         }
277     }
278 }
279