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