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