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 17 package com.android.cts.verifier.projection.offscreen; 18 19 import android.content.BroadcastReceiver; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.ServiceConnection; 25 import android.graphics.Color; 26 import android.graphics.PixelFormat; 27 import android.media.Image; 28 import android.media.ImageReader; 29 import android.media.Ringtone; 30 import android.media.RingtoneManager; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.os.IBinder; 35 import android.os.Looper; 36 import android.os.RemoteException; 37 import android.os.SystemClock; 38 import android.os.Vibrator; 39 import android.util.DisplayMetrics; 40 import android.util.Log; 41 import android.view.KeyEvent; 42 import android.view.View; 43 import android.widget.TextView; 44 45 import com.android.cts.verifier.PassFailButtons; 46 import com.android.cts.verifier.R; 47 import com.android.cts.verifier.projection.IProjectionService; 48 import com.android.cts.verifier.projection.ProjectionPresentationType; 49 import com.android.cts.verifier.projection.ProjectionService; 50 51 import java.nio.ByteBuffer; 52 53 public class ProjectionOffscreenActivity extends PassFailButtons.Activity 54 implements ImageReader.OnImageAvailableListener { 55 private static String TAG = ProjectionOffscreenActivity.class.getSimpleName(); 56 private static final int WIDTH = 800; 57 private static final int HEIGHT = 480; 58 private static final int DENSITY = DisplayMetrics.DENSITY_MEDIUM; 59 private static final int TIME_SCREEN_OFF = 5000; // Time screen must remain off for test to run 60 private static final int DELAYED_RUNNABLE_TIME = 1000; // Time after screen turned off 61 // keyevent is sent 62 private static final int RENDERER_DELAY_THRESHOLD = 2000; // Time after keyevent sent that 63 // rendering must happen by 64 65 protected ImageReader mReader; 66 protected IProjectionService mService; 67 protected TextView mStatusView; 68 protected int mPreviousColor = Color.BLACK; 69 private long mTimeScreenTurnedOff = 0; 70 private long mTimeKeyEventSent = 0; 71 private enum TestStatus { CREATED, PASSED, FAILED, RUNNING }; 72 protected TestStatus mTestStatus = TestStatus.CREATED; 73 74 private final Runnable sendKeyEventRunnable = new Runnable() { 75 @Override 76 public void run() { 77 try { 78 mService.onKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN)); 79 mTimeKeyEventSent = SystemClock.elapsedRealtime(); 80 } catch (RemoteException e) { 81 Log.e(TAG, "Error running onKeyEvent", e); 82 } 83 } 84 }; 85 86 private final Runnable playNotificationRunnable = new Runnable() { 87 88 @Override 89 public void run() { 90 Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 91 Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), notification); 92 r.play(); 93 ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(1000); 94 } 95 }; 96 97 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 98 99 @Override 100 public void onReceive(Context context, Intent intent) { 101 if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { 102 mTestStatus = TestStatus.RUNNING; 103 Handler handler = new Handler(Looper.getMainLooper()); 104 handler.postDelayed( 105 sendKeyEventRunnable, DELAYED_RUNNABLE_TIME); 106 mStatusView.setText("Running test..."); 107 mTimeScreenTurnedOff = SystemClock.elapsedRealtime(); 108 // Notify user its safe to turn screen back on after 5s + fudge factor 109 handler.postDelayed(playNotificationRunnable, TIME_SCREEN_OFF + 500); 110 } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { 111 if (SystemClock.elapsedRealtime() - mTimeScreenTurnedOff < TIME_SCREEN_OFF) { 112 mStatusView.setText("ERROR: Turned on screen too early"); 113 getPassButton().setEnabled(false); 114 mTestStatus = TestStatus.FAILED; 115 } else if (mTestStatus == TestStatus.RUNNING) { 116 mStatusView.setText("Failed: Image not rendered"); 117 mTestStatus = TestStatus.FAILED; 118 } 119 } 120 } 121 122 }; 123 124 protected final ServiceConnection mConnection = new ServiceConnection() { 125 126 @Override 127 public void onServiceConnected(ComponentName name, IBinder binder) { 128 mService = IProjectionService.Stub.asInterface(binder); 129 new Handler().post(new Runnable() { 130 131 @Override 132 public void run() { 133 Log.i(TAG, "onServiceConnected thread " + Thread.currentThread()); 134 try { 135 mService.startRendering(mReader.getSurface(), WIDTH, HEIGHT, DENSITY, 136 ProjectionPresentationType.OFFSCREEN.ordinal()); 137 } catch (RemoteException e) { 138 Log.e(TAG, "Failed to execute startRendering", e); 139 } 140 141 IntentFilter filter = new IntentFilter(); 142 filter.addAction(Intent.ACTION_SCREEN_OFF); 143 filter.addAction(Intent.ACTION_SCREEN_ON); 144 145 registerReceiver(mReceiver, filter); 146 mStatusView.setText("Please turn off your screen and turn it back on after " + 147 "5 seconds. A sound will be played when it is safe to turn the " + 148 "screen back on"); 149 } 150 151 }); 152 153 } 154 155 @Override 156 public void onServiceDisconnected(ComponentName name) { 157 mService = null; 158 } 159 160 }; 161 162 163 164 @Override onCreate(Bundle savedInstanceState)165 protected void onCreate(Bundle savedInstanceState) { 166 super.onCreate(savedInstanceState); 167 168 View view = getLayoutInflater().inflate(R.layout.poa_main, null); 169 mStatusView = (TextView) view.findViewById(R.id.poa_status_text); 170 mStatusView.setText("Waiting for service to bind..."); 171 172 setContentView(view); 173 174 setInfoResources(R.string.poa_test, R.string.poa_info, -1); 175 setPassFailButtonClickListeners(); 176 mReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2); 177 178 mReader.setOnImageAvailableListener(this, null); 179 bindService(new Intent(this, ProjectionService.class), mConnection, 180 Context.BIND_AUTO_CREATE); 181 182 getPassButton().setEnabled(false); 183 } 184 185 @Override onDestroy()186 protected void onDestroy() { 187 super.onDestroy(); 188 unregisterReceiver(mReceiver); 189 try { 190 mService.stopRendering(); 191 } catch (RemoteException e) { 192 Log.e(TAG, "Failed to execute stopRendering", e); 193 } 194 if (mConnection != null) { 195 unbindService(mConnection); 196 } 197 mReader.close(); 198 } 199 200 @Override onPause()201 protected void onPause() { 202 super.onPause(); 203 if (mTestStatus == TestStatus.FAILED) { 204 setTestResultAndFinish(false); 205 } 206 } 207 208 @Override onImageAvailable(ImageReader reader)209 public void onImageAvailable(ImageReader reader) { 210 Log.i(TAG, "onImageAvailable: " + reader); 211 if (mTestStatus == TestStatus.CREATED) { 212 Log.i(TAG, "onImageAvailable called before test started"); 213 // Drop latest image to keep buffer clear. 214 dropLatestImage(reader); 215 return; 216 } 217 218 if (mTimeKeyEventSent != 0 219 && mTestStatus == TestStatus.RUNNING 220 && mTimeKeyEventSent + RENDERER_DELAY_THRESHOLD < SystemClock.elapsedRealtime()) { 221 mTestStatus = TestStatus.FAILED; 222 mStatusView.setText("Failed: took too long to render"); 223 } 224 225 Image image = reader.acquireLatestImage(); 226 227 // No new images available 228 if (image == null) { 229 Log.w(TAG, "onImageAvailable called but no image!"); 230 return; 231 } 232 233 if (mTestStatus == TestStatus.RUNNING) { 234 int ret = scanImage(image); 235 if (ret == -1) { 236 mStatusView.setText("Failed: saw unexpected color"); 237 getPassButton().setEnabled(false); 238 mTestStatus = TestStatus.FAILED; 239 } else if (ret != mPreviousColor && ret == Color.BLUE) { 240 mStatusView.setText("Success: virtual display rendered expected color"); 241 getPassButton().setEnabled(true); 242 mTestStatus = TestStatus.PASSED; 243 } 244 } 245 image.close(); 246 } 247 248 // modified from the VirtualDisplay Cts test 249 /** 250 * Gets the color of the image and ensures all the pixels are the same color 251 * @param image input image 252 * @return The color of the image, or -1 for failure 253 */ scanImage(Image image)254 private int scanImage(Image image) { 255 final Image.Plane plane = image.getPlanes()[0]; 256 final ByteBuffer buffer = plane.getBuffer(); 257 final int width = image.getWidth(); 258 final int height = image.getHeight(); 259 final int pixelStride = plane.getPixelStride(); 260 final int rowStride = plane.getRowStride(); 261 final int rowPadding = rowStride - pixelStride * width; 262 263 Log.d(TAG, "- Scanning image: width=" + width + ", height=" + height 264 + ", pixelStride=" + pixelStride + ", rowStride=" + rowStride); 265 266 int offset = 0; 267 int blackPixels = 0; 268 int bluePixels = 0; 269 int otherPixels = 0; 270 for (int y = 0; y < height; y++) { 271 for (int x = 0; x < width; x++) { 272 int pixel = 0; 273 pixel |= (buffer.get(offset) & 0xff) << 16; // R 274 pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G 275 pixel |= (buffer.get(offset + 2) & 0xff); // B 276 pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A 277 if (pixel == Color.BLACK || pixel == 0) { 278 blackPixels += 1; 279 } else if (pixel == Color.BLUE) { 280 bluePixels += 1; 281 } else { 282 otherPixels += 1; 283 if (otherPixels < 10) { 284 Log.d(TAG, "- Found unexpected color: " + Integer.toHexString(pixel)); 285 } 286 } 287 offset += pixelStride; 288 } 289 offset += rowPadding; 290 } 291 292 // Return a color if it represents all of the pixels. 293 Log.d(TAG, "- Pixels: " + blackPixels + " black, " 294 + bluePixels + " blue, " 295 + otherPixels + " other"); 296 if (blackPixels == width * height) { 297 return Color.BLACK; 298 } else if (bluePixels == width * height) { 299 return Color.BLUE; 300 } else { 301 return -1; 302 } 303 } 304 dropLatestImage(ImageReader reader)305 private void dropLatestImage(ImageReader reader) { 306 Image image = reader.acquireLatestImage(); 307 if (image != null) { 308 image.close(); 309 } 310 } 311 } 312