1 /*
2  * Copyright (C) 2022 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.google.android.car.kitchensink.display;
18 
19 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
20 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
21 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
22 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.res.TypedArray;
26 import android.hardware.display.DisplayManager;
27 import android.hardware.display.VirtualDisplay;
28 import android.hardware.input.InputManager;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.util.AttributeSet;
32 import android.util.DisplayMetrics;
33 import android.util.Log;
34 import android.view.MotionEvent;
35 import android.view.Surface;
36 import android.view.SurfaceHolder;
37 import android.view.SurfaceView;
38 
39 import com.google.android.car.kitchensink.R;
40 
41 import java.io.PrintWriter;
42 import java.util.Objects;
43 import java.util.concurrent.CountDownLatch;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * Custom view that hosts a virtual display.
48  */
49 public final class VirtualDisplayView extends SurfaceView {
50 
51     private static final String TAG = VirtualDisplayView.class.getSimpleName();
52 
53     private static final boolean REALLY_VERBOSE_IS_FINE = false;
54 
55     private static final int WAIT_TIMEOUT_MS = 4_000;
56     private static final int NO_DISPLAY_ID = -42;
57 
58     private final Context mContext;
59     private final InputManager mInputManager;
60     private String mName = "LAYOUT XML, Y U NO HAVE A NAME?";
61 
62     private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() {
63 
64         @Override
65         public void surfaceCreated(SurfaceHolder holder) {
66             Log.d(TAG, "surfaceCreated(): holder=" + holder);
67         }
68 
69         @Override
70         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
71             Log.d(TAG, "surfaceChanged(): holder=" + holder + ", forma=:" + format
72                     + ", width=" + width + ", height=" + height);
73 
74             if (mSurface != null) {
75                 Log.d(TAG, "Releasing old surface (" + mSurface + ")");
76                 mSurface.release();
77             }
78 
79             mSurface = holder.getSurface();
80         }
81 
82         @Override
83         public void surfaceDestroyed(SurfaceHolder holder) {
84             Log.d(TAG, "surfaceDestroyed, holder: " + holder + ", detaching surface from"
85                     + " display, surface: " + holder.getSurface());
86             // Detaching surface is similar to turning off the display
87             mSurface = null;
88             if (mVirtualDisplay != null) {
89                 mVirtualDisplay.setSurface(null);
90             }
91         }
92     };
93 
94     @Nullable
95     private VirtualDisplay mVirtualDisplay;
96 
97     private int mDisplayId = NO_DISPLAY_ID;
98 
99     @Nullable
100     private Surface mSurface;
101 
102     @Nullable
103     private Handler mHandler;
104 
VirtualDisplayView(Context context)105     public VirtualDisplayView(Context context) {
106         this(context, null);
107     }
108 
VirtualDisplayView(Context context, @Nullable AttributeSet attrs)109     public VirtualDisplayView(Context context, @Nullable AttributeSet attrs) {
110         super(context, attrs);
111 
112         mContext = context;
113         String name = getName(context, attrs);
114         if (name != null) {
115             mName = name;
116         }
117         mInputManager = context.getSystemService(InputManager.class);
118         getHolder().addCallback(mSurfaceViewCallback);
119     }
120 
121     // NOTE: it might be needed to handle focus and a11y events as well, but for now we'll assume
122     // it's not needed and keep it simpler
123     @Override
onTouchEvent(MotionEvent event)124     public boolean onTouchEvent(MotionEvent event) {
125         if (REALLY_VERBOSE_IS_FINE) {
126             Log.v(TAG, "onTouchEvent(" + event + ")");
127         }
128 
129         if (mVirtualDisplay == null) {
130             return super.onTouchEvent(event);
131         }
132 
133         if (mDisplayId == NO_DISPLAY_ID) {
134             // Shouldn't happen, but it doesn't hurt to check...
135             Log.w(TAG, "onTouchEvent(): display id not set, calling super instead");
136             return super.onTouchEvent(event);
137         }
138 
139         // NOTE: it might be need to re-calculate the coordinates to offset the diplay, but
140         // pparently it's working without it
141         event.setDisplayId(mDisplayId);
142 
143         if (REALLY_VERBOSE_IS_FINE) {
144             Log.v(TAG, "re-dispatching event after changing display id to " + mDisplayId);
145         }
146         if (mInputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
147             return true;
148         }
149 
150         Log.w(TAG, "onTouchEvent(): not handled by display, calling super instead");
151         return super.onTouchEvent(event);
152     }
153 
154     /**
155      * Sets the name of the display
156      */
setName(String name)157     public void setName(String name) {
158         Objects.requireNonNull(name, "name cannot be null");
159         Log.v(TAG, "Changing name from " + mName + " to " + name);
160         mName = name;
161     }
162 
163     /**
164      * Gets the name of the display
165      */
getName()166     public String getName() {
167         return mName;
168     }
169 
170     /**
171      * Creates the virtual display and return its id.
172      */
createVirtualDisplay()173     public int createVirtualDisplay() {
174         if (mVirtualDisplay != null) {
175             throw new IllegalStateException("Display already exist: " + mVirtualDisplay);
176         }
177 
178         if (mSurface == null) {
179             throw new IllegalStateException("Surface not created yet (or released)");
180         }
181 
182         if (mHandler == null) {
183             HandlerThread handlerThread = new HandlerThread("VirtualDisplayHelperThread");
184             Log.i(TAG, "Starting " + handlerThread);
185             handlerThread.start();
186             mHandler = new Handler(handlerThread.getLooper());
187         }
188 
189         CountDownLatch latch = new CountDownLatch(1);
190 
191         DisplayManager.DisplayListener listener = new DisplayManager.DisplayListener() {
192             @Override
193             public void onDisplayAdded(int displayId) {
194                 Log.d(TAG, "onDisplayAdded(" + displayId + ")");
195                 latch.countDown();
196             }
197 
198             @Override
199             public void onDisplayRemoved(int displayId) {
200                 Log.v(TAG, "onDisplayRemoved(" + displayId + ")");
201             }
202 
203             @Override
204             public void onDisplayChanged(int displayId) {
205                 Log.v(TAG, "onDisplayChanged(" + displayId + ")");
206             }
207         };
208 
209         DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
210         DisplayMetrics metrics = new DisplayMetrics();
211         displayManager.getDisplay(android.view.Display.DEFAULT_DISPLAY).getRealMetrics(metrics);
212         Log.v(TAG, "Physical display size: " + metrics.widthPixels + " x " + metrics.heightPixels);
213         Log.v(TAG, "View size: " + getWidth() + " x " + getHeight());
214 
215         Log.v(TAG, "Registering listener " + listener);
216         displayManager.registerDisplayListener(listener, mHandler);
217 
218         int flags = VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
219                 | VIRTUAL_DISPLAY_FLAG_PUBLIC
220                 | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
221 
222         Log.d(TAG, "Creating display named '" + mName + "'");
223         mVirtualDisplay = displayManager.createVirtualDisplay(mName,
224                 getWidth(), getHeight(), (int) metrics.xdpi, mSurface, flags);
225         int displayId = mVirtualDisplay.getDisplay().getDisplayId();
226         Log.i(TAG, "Created display with id " + displayId);
227         boolean created = false;
228         try {
229             created = latch.await(WAIT_TIMEOUT_MS, TimeUnit.SECONDS);
230 
231         } catch (InterruptedException e) {
232             Log.e(TAG, "Interruped waiting for display callback", e);
233             Thread.currentThread().interrupt();
234         } finally {
235             Log.v(TAG, "Unregistering listener " + listener);
236             displayManager.unregisterDisplayListener(listener);
237         }
238         if (!created) {
239             throw new IllegalStateException("Timed out (up to " + WAIT_TIMEOUT_MS
240                     + "ms waiting for callback");
241         }
242         mDisplayId = displayId;
243         return displayId;
244     }
245 
246     /**
247      * Deletes the virtual display.
248      */
deleteVirtualDisplay()249     public void deleteVirtualDisplay() {
250         if (mVirtualDisplay == null) {
251             throw new IllegalStateException("Display doesn't exist");
252         }
253         releaseDisplay();
254     }
255 
256     /**
257      * Gets the virtual display.
258      */
259     @Nullable
getVirtualDisplay()260     public VirtualDisplay getVirtualDisplay() {
261         return mVirtualDisplay;
262     }
263 
264     /**
265      * Releases the internal resources.
266      */
release()267     public void release() {
268         releaseDisplay();
269 
270         if (mSurface != null) {
271             Log.d(TAG, "Releasing surface");
272             mSurface.release();
273             mSurface = null;
274         }
275     }
276 
277     /**
278      * Dumps its state.
279      */
dump(String prefix, PrintWriter writer)280     public void dump(String prefix, PrintWriter writer) {
281         writer.printf("%sName: %s\n", prefix, mName);
282         writer.printf("%sSurface: %s\n", prefix, mSurface);
283         writer.printf("%sVirtualDisplay: %s\n", prefix, mVirtualDisplay);
284         writer.printf("%sDisplayId: %d%s\n", prefix, mDisplayId,
285                 (mDisplayId == NO_DISPLAY_ID ? " (not set)" : ""));
286         writer.printf("%sHandler: %s\n", prefix, mHandler);
287         writer.printf("%sWait timeout: %dms\n", prefix, WAIT_TIMEOUT_MS);
288         writer.printf("%sREALLY_VERBOSE_IS_FINE: %b\n", prefix, REALLY_VERBOSE_IS_FINE);
289     }
290 
releaseDisplay()291     private void releaseDisplay() {
292         if (mVirtualDisplay != null) {
293             Log.i(TAG, "Releasing display id " + mDisplayId);
294             mVirtualDisplay.release();
295             mVirtualDisplay = null;
296             mDisplayId = NO_DISPLAY_ID;
297         }
298     }
299 
300     @Nullable
getName(Context context, AttributeSet attrs)301     static String getName(Context context, AttributeSet attrs) {
302         if (attrs == null) {
303             return null;
304         }
305         TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
306                 R.styleable.VirtualDisplayView, /* defStyleAttr= */ 0, /* defStyleRes= */ 0);
307         try {
308             return a.getString(R.styleable.VirtualDisplayView_name);
309         } finally {
310             a.recycle();
311         }
312     }
313 }
314