• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.example.android.vdmdemo.client;
18 
19 import android.annotation.SuppressLint;
20 import android.content.Intent;
21 import android.graphics.Rect;
22 import android.graphics.SurfaceTexture;
23 import android.util.Log;
24 import android.view.Display;
25 import android.view.InputDevice;
26 import android.view.LayoutInflater;
27 import android.view.MotionEvent;
28 import android.view.Surface;
29 import android.view.TextureView;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.TextView;
33 
34 import androidx.activity.result.ActivityResult;
35 import androidx.activity.result.ActivityResultLauncher;
36 import androidx.annotation.NonNull;
37 import androidx.recyclerview.widget.RecyclerView;
38 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
39 
40 import com.example.android.vdmdemo.client.DisplayAdapter.DisplayHolder;
41 import com.example.android.vdmdemo.common.RemoteEventProto.DisplayRotation;
42 import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType;
43 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent;
44 import com.example.android.vdmdemo.common.RemoteIo;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.Objects;
50 import java.util.concurrent.atomic.AtomicInteger;
51 import java.util.function.Consumer;
52 
53 final class DisplayAdapter extends RecyclerView.Adapter<DisplayHolder> {
54     private static final String TAG = "VdmClient";
55 
56     private static final AtomicInteger sNextDisplayIndex = new AtomicInteger(1);
57 
58     // Simple list of all active displays.
59     private final List<RemoteDisplay> mDisplayRepository =
60             Collections.synchronizedList(new ArrayList<>());
61 
62     private final RemoteIo mRemoteIo;
63     private final ClientView mRecyclerView;
64     private final InputManager mInputManager;
65     private ActivityResultLauncher<Intent> mFullscreenLauncher;
66 
DisplayAdapter(ClientView recyclerView, RemoteIo remoteIo, InputManager inputManager)67     DisplayAdapter(ClientView recyclerView, RemoteIo remoteIo, InputManager inputManager) {
68         mRecyclerView = recyclerView;
69         mRemoteIo = remoteIo;
70         mInputManager = inputManager;
71         setHasStableIds(true);
72     }
73 
setFullscreenLauncher(ActivityResultLauncher<Intent> launcher)74     void setFullscreenLauncher(ActivityResultLauncher<Intent> launcher) {
75         mFullscreenLauncher = launcher;
76     }
77 
onFullscreenActivityResult(ActivityResult result)78     void onFullscreenActivityResult(ActivityResult result) {
79         Intent data = result.getData();
80         if (data == null) {
81             return;
82         }
83         int displayId =
84                 data.getIntExtra(ImmersiveActivity.EXTRA_DISPLAY_ID, Display.INVALID_DISPLAY);
85         if (result.getResultCode() == ImmersiveActivity.RESULT_CLOSE) {
86             removeDisplay(displayId);
87         } else if (result.getResultCode() == ImmersiveActivity.RESULT_MINIMIZE) {
88             int requestedRotation =
89                     data.getIntExtra(ImmersiveActivity.EXTRA_REQUESTED_ROTATION, 0);
90             rotateDisplay(displayId, requestedRotation);
91         }
92     }
93 
addDisplay(boolean homeSupported, boolean rotationSupported)94     void addDisplay(boolean homeSupported, boolean rotationSupported) {
95         Log.i(TAG, "Adding display " + sNextDisplayIndex);
96         mDisplayRepository.add(
97                 new RemoteDisplay(sNextDisplayIndex.getAndIncrement(), homeSupported,
98                         rotationSupported));
99         notifyItemInserted(mDisplayRepository.size() - 1);
100     }
101 
removeDisplay(int displayId)102     void removeDisplay(int displayId) {
103         Log.i(TAG, "Removing display " + displayId);
104         for (int i = 0; i < mDisplayRepository.size(); ++i) {
105             if (displayId == mDisplayRepository.get(i).getDisplayId()) {
106                 mDisplayRepository.remove(i);
107                 notifyItemRemoved(i);
108                 break;
109             }
110         }
111     }
112 
rotateDisplay(int displayId, int rotationDegrees)113     void rotateDisplay(int displayId, int rotationDegrees) {
114         DisplayHolder holder = getDisplayHolder(displayId);
115         if (holder != null) {
116             holder.rotateDisplay(rotationDegrees, /* resize= */ false);
117         }
118     }
119 
processDisplayChange(RemoteEvent event)120     void processDisplayChange(RemoteEvent event) {
121         DisplayHolder holder = getDisplayHolder(event.getDisplayId());
122         if (holder != null) {
123             holder.setDisplayTitle(event.getDisplayChangeEvent().getTitle());
124         }
125     }
126 
clearDisplays()127     void clearDisplays() {
128         int size = mDisplayRepository.size();
129         if (size > 0) {
130             Log.i(TAG, "Clearing all displays");
131             mDisplayRepository.clear();
132             notifyItemRangeRemoved(0, size);
133         }
134     }
135 
pauseAllDisplays()136     void pauseAllDisplays() {
137         Log.i(TAG, "Pausing all displays");
138         forAllDisplays(DisplayHolder::pause);
139     }
140 
resumeAllDisplays()141     void resumeAllDisplays() {
142         Log.i(TAG, "Resuming all displays");
143         forAllDisplays(DisplayHolder::resume);
144     }
145 
forAllDisplays(Consumer<DisplayHolder> consumer)146     private void forAllDisplays(Consumer<DisplayHolder> consumer) {
147         for (int i = 0; i < mDisplayRepository.size(); ++i) {
148             DisplayHolder holder =
149                     (DisplayHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
150             if (holder != null) {
151                 consumer.accept(holder);
152             }
153         }
154     }
155 
getDisplayHolder(int displayId)156     private DisplayHolder getDisplayHolder(int displayId) {
157         for (int i = 0; i < mDisplayRepository.size(); ++i) {
158             if (displayId == mDisplayRepository.get(i).getDisplayId()) {
159                 return (DisplayHolder) mRecyclerView.findViewHolderForAdapterPosition(i);
160             }
161         }
162         return null;
163     }
164 
165     @NonNull
166     @Override
onCreateViewHolder(ViewGroup parent, int viewType)167     public DisplayHolder onCreateViewHolder(ViewGroup parent, int viewType) {
168         // Disable recycling so layout changes are not present in new displays.
169         mRecyclerView.getRecycledViewPool().setMaxRecycledViews(viewType, 0);
170         View view =
171                 LayoutInflater.from(parent.getContext())
172                         .inflate(R.layout.display_fragment, parent, false);
173         return new DisplayHolder(view);
174     }
175 
176     @Override
onBindViewHolder(DisplayHolder holder, int position)177     public void onBindViewHolder(DisplayHolder holder, int position) {
178         holder.onBind(position);
179     }
180 
181     @Override
onViewRecycled(DisplayHolder holder)182     public void onViewRecycled(DisplayHolder holder) {
183         holder.close();
184     }
185 
186     @Override
getItemId(int position)187     public long getItemId(int position) {
188         return mDisplayRepository.get(position).getDisplayId();
189     }
190 
191     @Override
getItemCount()192     public int getItemCount() {
193         return mDisplayRepository.size();
194     }
195 
196     public class DisplayHolder extends ViewHolder {
197         private DisplayController mDisplayController = null;
198         private InputManager.FocusListener mFocusListener = null;
199         private Surface mSurface = null;
200         private TextureView mTextureView = null;
201         private TextView mDisplayTitle = null;
202         private View mRotateButton = null;
203         private int mDisplayId = 0;
204         private RemoteDisplay mRemoteDisplay = null;
205 
DisplayHolder(View view)206         DisplayHolder(View view) {
207             super(view);
208         }
209 
rotateDisplay(int rotationDegrees, boolean resize)210         void rotateDisplay(int rotationDegrees, boolean resize) {
211             if (mTextureView.getRotation() == rotationDegrees) {
212                 return;
213             }
214             Log.i(TAG, "Rotating display " + mDisplayId + " to " + rotationDegrees);
215             mRotateButton.setEnabled(rotationDegrees == 0 || resize
216                     || mRemoteDisplay.isRotationSupported());
217 
218             // Make sure the rotation is visible.
219             View strut = itemView.requireViewById(R.id.strut);
220             ViewGroup.LayoutParams layoutParams = strut.getLayoutParams();
221             layoutParams.width = Math.max(mTextureView.getWidth(), mTextureView.getHeight());
222             strut.setLayoutParams(layoutParams);
223             final int postRotationWidth = (resize || rotationDegrees % 180 != 0)
224                     ? mTextureView.getHeight() : mTextureView.getWidth();
225 
226             mTextureView
227                     .animate()
228                     .rotation(rotationDegrees)
229                     .setDuration(420)
230                     .withEndAction(
231                             () -> {
232                                 if (resize) {
233                                     resizeDisplay(
234                                             new Rect(
235                                                     0,
236                                                     0,
237                                                     mTextureView.getHeight(),
238                                                     mTextureView.getWidth()));
239                                 }
240                                 layoutParams.width = postRotationWidth;
241                                 strut.setLayoutParams(layoutParams);
242                             })
243                     .start();
244         }
245 
resizeDisplay(Rect newBounds)246         private void resizeDisplay(Rect newBounds) {
247             Log.i(TAG, "Resizing display " + mDisplayId + " to " + newBounds);
248             mDisplayController.setSurface(mSurface, newBounds.width(), newBounds.height());
249 
250             ViewGroup.LayoutParams layoutParams = mTextureView.getLayoutParams();
251             layoutParams.width = newBounds.width();
252             layoutParams.height = newBounds.height();
253             mTextureView.setLayoutParams(layoutParams);
254         }
255 
setDisplayTitle(String title)256         private void setDisplayTitle(String title) {
257             mDisplayTitle.setText(
258                     itemView.getContext().getString(R.string.display_title, mDisplayId, title));
259         }
260 
close()261         void close() {
262             if (mDisplayController != null) {
263                 Log.i(TAG, "Closing DisplayHolder for display " + mDisplayId);
264                 mInputManager.removeFocusListener(mFocusListener);
265                 mInputManager.removeFocusableDisplay(mDisplayId);
266                 mDisplayController.close();
267                 mDisplayController = null;
268             }
269         }
270 
pause()271         void pause() {
272             mDisplayController.pause();
273         }
274 
resume()275         void resume() {
276             mDisplayController.setSurface(
277                     mSurface, mTextureView.getWidth(), mTextureView.getHeight());
278         }
279 
280         @SuppressLint("ClickableViewAccessibility")
onBind(int position)281         void onBind(int position) {
282             mRemoteDisplay = mDisplayRepository.get(position);
283             mDisplayId = mRemoteDisplay.getDisplayId();
284             Log.v(TAG, "Binding DisplayHolder for display " + mDisplayId + " to position "
285                     + position);
286 
287             mDisplayTitle = itemView.requireViewById(R.id.display_title);
288             mTextureView = itemView.requireViewById(R.id.remote_display_view);
289             final View displayHeader = itemView.requireViewById(R.id.display_header);
290 
291             mFocusListener =
292                     focusedDisplayId -> {
293                         if (focusedDisplayId == mDisplayId && mDisplayRepository.size() > 1) {
294                             displayHeader.setBackgroundResource(R.drawable.focus_frame);
295                         } else {
296                             displayHeader.setBackground(null);
297                         }
298                     };
299             mInputManager.addFocusListener(mFocusListener);
300 
301             mDisplayController = new DisplayController(mDisplayId, mRemoteIo);
302             Log.v(TAG, "Creating new DisplayController for display " + mDisplayId);
303 
304             setDisplayTitle("");
305 
306             View closeButton = itemView.requireViewById(R.id.display_close);
307             closeButton.setOnClickListener(
308                     v -> ((DisplayAdapter) Objects.requireNonNull(getBindingAdapter()))
309                             .removeDisplay(mDisplayId));
310 
311             View backButton = itemView.requireViewById(R.id.display_back);
312             backButton.setOnClickListener(v -> mInputManager.sendBack(mDisplayId));
313 
314             View homeButton = itemView.requireViewById(R.id.display_home);
315             if (mRemoteDisplay.isHomeSupported()) {
316                 homeButton.setVisibility(View.VISIBLE);
317                 homeButton.setOnClickListener(v -> mInputManager.sendHome(mDisplayId));
318             } else {
319                 homeButton.setVisibility(View.GONE);
320             }
321 
322             mRotateButton = itemView.requireViewById(R.id.display_rotate);
323             mRotateButton.setOnClickListener(v -> {
324                 mInputManager.setFocusedDisplayId(mDisplayId);
325                 if (mRemoteDisplay.isRotationSupported()) {
326                     mRemoteIo.sendMessage(RemoteEvent.newBuilder()
327                             .setDisplayId(mDisplayId)
328                             .setDisplayRotation(DisplayRotation.newBuilder())
329                             .build());
330                 } else {
331                     // This rotation is simply resizing the display with width with height swapped.
332                     mDisplayController.setSurface(
333                             mSurface,
334                             /* width= */ mTextureView.getHeight(),
335                             /* height= */ mTextureView.getWidth());
336                     rotateDisplay(mTextureView.getWidth() > mTextureView.getHeight() ? 90 : -90,
337                             true);
338                 }
339             });
340 
341             View resizeButton = itemView.requireViewById(R.id.display_resize);
342             resizeButton.setOnTouchListener((v, event) -> {
343                 if (event.getAction() != MotionEvent.ACTION_DOWN) {
344                     return false;
345                 }
346                 mInputManager.setFocusedDisplayId(mDisplayId);
347                 int maxSize = itemView.getHeight() - displayHeader.getHeight()
348                         - itemView.getPaddingTop() - itemView.getPaddingBottom();
349                 mRecyclerView.startResizing(
350                         mTextureView, event, maxSize, DisplayHolder.this::resizeDisplay);
351                 return true;
352             });
353 
354             View fullscreenButton = itemView.requireViewById(R.id.display_fullscreen);
355             fullscreenButton.setOnClickListener(v -> {
356                 mInputManager.setFocusedDisplayId(mDisplayId);
357                 Intent intent = new Intent(v.getContext(), ImmersiveActivity.class);
358                 intent.putExtra(ImmersiveActivity.EXTRA_DISPLAY_ID, mDisplayId);
359                 mFullscreenLauncher.launch(intent);
360             });
361 
362             mTextureView.setOnTouchListener(
363                     (v, event) -> {
364                         if (event.getDevice().supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) {
365                             mTextureView.getParent().requestDisallowInterceptTouchEvent(true);
366                             mInputManager.sendInputEvent(
367                                     InputDeviceType.DEVICE_TYPE_TOUCHSCREEN, event, mDisplayId);
368                         }
369                         return true;
370                     });
371             mTextureView.setSurfaceTextureListener(
372                     new TextureView.SurfaceTextureListener() {
373                         @Override
374                         public void onSurfaceTextureUpdated(@NonNull SurfaceTexture texture) {}
375 
376                         @Override
377                         public void onSurfaceTextureAvailable(
378                                 @NonNull SurfaceTexture texture, int width, int height) {
379                             Log.v(TAG, "Setting surface for display " + mDisplayId);
380                             mInputManager.addFocusableDisplay(mDisplayId);
381                             mSurface = new Surface(texture);
382                             mDisplayController.setSurface(mSurface, width, height);
383                         }
384 
385                         @Override
386                         public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture texture) {
387                             Log.v(TAG, "onSurfaceTextureDestroyed for display " + mDisplayId);
388                             if (mDisplayController != null) {
389                                 mDisplayController.pause();
390                             }
391                             return true;
392                         }
393 
394                         @Override
395                         public void onSurfaceTextureSizeChanged(
396                                 @NonNull SurfaceTexture texture, int width, int height) {
397                             Log.v(TAG, "onSurfaceTextureSizeChanged for display " + mDisplayId);
398                             mTextureView.setRotation(0);
399                             mRotateButton.setEnabled(true);
400                         }
401                     });
402             mTextureView.setOnGenericMotionListener(
403                     (v, event) -> {
404                         if (event.getDevice() == null
405                                 || !event.getDevice().supportsSource(InputDevice.SOURCE_MOUSE)) {
406                             return false;
407                         }
408                         mInputManager.sendInputEvent(
409                                 InputDeviceType.DEVICE_TYPE_MOUSE, event, mDisplayId);
410                         return true;
411                     });
412         }
413     }
414 
415     private static class RemoteDisplay {
416         // Local ID, not corresponding to the displayId of the relevant Display on the host device.
417         private final int mDisplayId;
418         private final boolean mHomeSupported;
419         private final boolean mRotationSupported;
420 
RemoteDisplay(int displayId, boolean homeSupported, boolean rotationSupported)421         RemoteDisplay(int displayId, boolean homeSupported, boolean rotationSupported) {
422             mDisplayId = displayId;
423             mHomeSupported = homeSupported;
424             mRotationSupported = rotationSupported;
425         }
426 
getDisplayId()427         int getDisplayId() {
428             return mDisplayId;
429         }
430 
isHomeSupported()431         boolean isHomeSupported() {
432             return mHomeSupported;
433         }
434 
isRotationSupported()435         boolean isRotationSupported() {
436             return mRotationSupported;
437         }
438     }
439 }
440