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
onCreate(Bundle savedInstanceState)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
onDestroy()181     protected void onDestroy() {
182         super.onDestroy();
183         unregisterReceiver(mReceiver);
184         mReader.close();
185     }
186 
187     @Override
onPause()188     protected void onPause() {
189         super.onPause();
190         if (mTestStatus == TestStatus.FAILED) {
191             setTestResultAndFinish(false);
192         }
193     }
194 
195     @Override
onImageAvailable(ImageReader reader)196     public void onImageAvailable(ImageReader reader) {
197         Log.i(TAG, "onImageAvailable: " + reader);
198 
199         if (mTimeKeyEventSent != 0
200                 && mTestStatus == TestStatus.RUNNING
201                 && mTimeKeyEventSent + RENDERER_DELAY_THRESHOLD < SystemClock.elapsedRealtime()) {
202             mTestStatus = TestStatus.FAILED;
203             mStatusView.setText("Failed: took too long to render");
204         }
205 
206         Image image = reader.acquireLatestImage();
207 
208         // No new images available
209         if (image == null) {
210             Log.w(TAG, "onImageAvailable called but no image!");
211             return;
212         }
213 
214         if (mTestStatus == TestStatus.RUNNING) {
215             int ret = scanImage(image);
216             if (ret == -1) {
217                 mStatusView.setText("Failed: saw unexpected color");
218                 getPassButton().setEnabled(false);
219                 mTestStatus = TestStatus.FAILED;
220             } else if (ret != mPreviousColor && ret == Color.BLUE) {
221                 mStatusView.setText("Success: virtual display rendered expected color");
222                 getPassButton().setEnabled(true);
223                 mTestStatus = TestStatus.PASSED;
224             }
225         }
226         image.close();
227     }
228 
229     // modified from the VirtualDisplay Cts test
230     /**
231      * Gets the color of the image and ensures all the pixels are the same color
232      * @param image input image
233      * @return The color of the image, or -1 for failure
234      */
scanImage(Image image)235     private int scanImage(Image image) {
236         final Image.Plane plane = image.getPlanes()[0];
237         final ByteBuffer buffer = plane.getBuffer();
238         final int width = image.getWidth();
239         final int height = image.getHeight();
240         final int pixelStride = plane.getPixelStride();
241         final int rowStride = plane.getRowStride();
242         final int rowPadding = rowStride - pixelStride * width;
243 
244         Log.d(TAG, "- Scanning image: width=" + width + ", height=" + height
245                 + ", pixelStride=" + pixelStride + ", rowStride=" + rowStride);
246 
247         int offset = 0;
248         int blackPixels = 0;
249         int bluePixels = 0;
250         int otherPixels = 0;
251         for (int y = 0; y < height; y++) {
252             for (int x = 0; x < width; x++) {
253                 int pixel = 0;
254                 pixel |= (buffer.get(offset) & 0xff) << 16;     // R
255                 pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
256                 pixel |= (buffer.get(offset + 2) & 0xff);       // B
257                 pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
258                 if (pixel == Color.BLACK || pixel == 0) {
259                     blackPixels += 1;
260                 } else if (pixel == Color.BLUE) {
261                     bluePixels += 1;
262                 } else {
263                     otherPixels += 1;
264                     if (otherPixels < 10) {
265                         Log.d(TAG, "- Found unexpected color: " + Integer.toHexString(pixel));
266                     }
267                 }
268                 offset += pixelStride;
269             }
270             offset += rowPadding;
271         }
272 
273         // Return a color if it represents all of the pixels.
274         Log.d(TAG, "- Pixels: " + blackPixels + " black, "
275                 + bluePixels + " blue, "
276                 + otherPixels + " other");
277         if (blackPixels == width * height) {
278             return Color.BLACK;
279         } else if (bluePixels == width * height) {
280             return Color.BLUE;
281         } else {
282             return -1;
283         }
284     }
285 }
286