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 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 73 public boolean getOnTv() { 74 return mOnTv; 75 } 76 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 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 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 126 public void setViewInitializer(ViewInitializer viewInitializer) { 127 mViewInitializer = viewInitializer; 128 } 129 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 173 protected void onPause() { 174 super.onPause(); 175 if (mViewInitializer != null) { 176 mViewInitializer.teardownView(); 177 } 178 } 179 180 @Override 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. */ 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 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 225 private DrawMonitor(Window window) { 226 window.addOnFrameMetricsAvailableListener(this::onFrameMetricsAvailable, 227 new Handler()); 228 } 229 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 272 public void notifyWhenDrawn(long contentVsyncMillis, Runnable runWhenDrawn) { 273 synchronized (mListeners) { 274 mListeners.add(new Pair<>(contentVsyncMillis, runWhenDrawn)); 275 } 276 } 277 } 278 } 279