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 { PASSED, FAILED, RUNNING }; 72 protected TestStatus mTestStatus = TestStatus.RUNNING; 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 Handler handler = new Handler(Looper.getMainLooper()); 103 handler.postDelayed( 104 sendKeyEventRunnable, DELAYED_RUNNABLE_TIME); 105 mStatusView.setText("Running test..."); 106 mTimeScreenTurnedOff = SystemClock.elapsedRealtime(); 107 // Notify user its safe to turn screen back on after 5s + fudge factor 108 handler.postDelayed(playNotificationRunnable, TIME_SCREEN_OFF + 500); 109 } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { 110 if (SystemClock.elapsedRealtime() - mTimeScreenTurnedOff < TIME_SCREEN_OFF) { 111 mStatusView.setText("ERROR: Turned on screen too early"); 112 getPassButton().setEnabled(false); 113 mTestStatus = TestStatus.FAILED; 114 } 115 } 116 } 117 118 }; 119 120 protected final ServiceConnection mConnection = new ServiceConnection() { 121 122 @Override 123 public void onServiceConnected(ComponentName name, IBinder binder) { 124 mService = IProjectionService.Stub.asInterface(binder); 125 new Handler().post(new Runnable() { 126 127 @Override 128 public void run() { 129 Log.i(TAG, "onServiceConnected thread " + Thread.currentThread()); 130 try { 131 mService.startRendering(mReader.getSurface(), WIDTH, HEIGHT, DENSITY, 132 ProjectionPresentationType.OFFSCREEN.ordinal()); 133 } catch (RemoteException e) { 134 Log.e(TAG, "Failed to execute startRendering", e); 135 } 136 137 IntentFilter filter = new IntentFilter(); 138 filter.addAction(Intent.ACTION_SCREEN_OFF); 139 filter.addAction(Intent.ACTION_SCREEN_ON); 140 141 registerReceiver(mReceiver, filter); 142 mStatusView.setText("Please turn off your screen and turn it back on after " + 143 "5 seconds. A sound will be played when it is safe to turn the " + 144 "screen back on"); 145 } 146 147 }); 148 149 } 150 151 @Override 152 public void onServiceDisconnected(ComponentName name) { 153 mService = null; 154 } 155 156 }; 157 158 159 160 @Override 161 protected void onCreate(Bundle savedInstanceState) { 162 super.onCreate(savedInstanceState); 163 164 View view = getLayoutInflater().inflate(R.layout.poa_main, null); 165 mStatusView = (TextView) view.findViewById(R.id.poa_status_text); 166 mStatusView.setText("Waiting for service to bind..."); 167 168 setContentView(view); 169 170 setInfoResources(R.string.poa_test, R.string.poa_info, -1); 171 setPassFailButtonClickListeners(); 172 mReader = ImageReader.newInstance(WIDTH, HEIGHT, PixelFormat.RGBA_8888, 2); 173 mReader.setOnImageAvailableListener(this, null); 174 bindService(new Intent(this, ProjectionService.class), mConnection, 175 Context.BIND_AUTO_CREATE); 176 177 getPassButton().setEnabled(false); 178 } 179 180 @Override 181 protected void onDestroy() { 182 super.onDestroy(); 183 unregisterReceiver(mReceiver); 184 try { 185 mService.stopRendering(); 186 } catch (RemoteException e) { 187 Log.e(TAG, "Failed to execute stopRendering", e); 188 } 189 if (mConnection != null) { 190 unbindService(mConnection); 191 } 192 mReader.close(); 193 } 194 195 @Override 196 protected void onPause() { 197 super.onPause(); 198 if (mTestStatus == TestStatus.FAILED) { 199 setTestResultAndFinish(false); 200 } 201 } 202 203 @Override 204 public void onImageAvailable(ImageReader reader) { 205 Log.i(TAG, "onImageAvailable: " + reader); 206 207 if (mTimeKeyEventSent != 0 208 && mTestStatus == TestStatus.RUNNING 209 && mTimeKeyEventSent + RENDERER_DELAY_THRESHOLD < SystemClock.elapsedRealtime()) { 210 mTestStatus = TestStatus.FAILED; 211 mStatusView.setText("Failed: took too long to render"); 212 } 213 214 Image image = reader.acquireLatestImage(); 215 216 // No new images available 217 if (image == null) { 218 Log.w(TAG, "onImageAvailable called but no image!"); 219 return; 220 } 221 222 if (mTestStatus == TestStatus.RUNNING) { 223 int ret = scanImage(image); 224 if (ret == -1) { 225 mStatusView.setText("Failed: saw unexpected color"); 226 getPassButton().setEnabled(false); 227 mTestStatus = TestStatus.FAILED; 228 } else if (ret != mPreviousColor && ret == Color.BLUE) { 229 mStatusView.setText("Success: virtual display rendered expected color"); 230 getPassButton().setEnabled(true); 231 mTestStatus = TestStatus.PASSED; 232 } 233 } 234 image.close(); 235 } 236 237 // modified from the VirtualDisplay Cts test 238 /** 239 * Gets the color of the image and ensures all the pixels are the same color 240 * @param image input image 241 * @return The color of the image, or -1 for failure 242 */ 243 private int scanImage(Image image) { 244 final Image.Plane plane = image.getPlanes()[0]; 245 final ByteBuffer buffer = plane.getBuffer(); 246 final int width = image.getWidth(); 247 final int height = image.getHeight(); 248 final int pixelStride = plane.getPixelStride(); 249 final int rowStride = plane.getRowStride(); 250 final int rowPadding = rowStride - pixelStride * width; 251 252 Log.d(TAG, "- Scanning image: width=" + width + ", height=" + height 253 + ", pixelStride=" + pixelStride + ", rowStride=" + rowStride); 254 255 int offset = 0; 256 int blackPixels = 0; 257 int bluePixels = 0; 258 int otherPixels = 0; 259 for (int y = 0; y < height; y++) { 260 for (int x = 0; x < width; x++) { 261 int pixel = 0; 262 pixel |= (buffer.get(offset) & 0xff) << 16; // R 263 pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G 264 pixel |= (buffer.get(offset + 2) & 0xff); // B 265 pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A 266 if (pixel == Color.BLACK || pixel == 0) { 267 blackPixels += 1; 268 } else if (pixel == Color.BLUE) { 269 bluePixels += 1; 270 } else { 271 otherPixels += 1; 272 if (otherPixels < 10) { 273 Log.d(TAG, "- Found unexpected color: " + Integer.toHexString(pixel)); 274 } 275 } 276 offset += pixelStride; 277 } 278 offset += rowPadding; 279 } 280 281 // Return a color if it represents all of the pixels. 282 Log.d(TAG, "- Pixels: " + blackPixels + " black, " 283 + bluePixels + " blue, " 284 + otherPixels + " other"); 285 if (blackPixels == width * height) { 286 return Color.BLACK; 287 } else if (bluePixels == width * height) { 288 return Color.BLUE; 289 } else { 290 return -1; 291 } 292 } 293 } 294