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