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