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