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.content.res.Configuration; 22 import android.graphics.SurfaceTexture; 23 import android.os.Bundle; 24 import android.util.Log; 25 import android.view.Display; 26 import android.view.InputDevice; 27 import android.view.KeyEvent; 28 import android.view.Surface; 29 import android.view.TextureView; 30 import android.view.inputmethod.InputMethodManager; 31 32 import androidx.activity.OnBackPressedCallback; 33 import androidx.annotation.NonNull; 34 import androidx.appcompat.app.AppCompatActivity; 35 import androidx.core.view.WindowCompat; 36 import androidx.core.view.WindowInsetsCompat; 37 import androidx.core.view.WindowInsetsControllerCompat; 38 39 import com.example.android.vdmdemo.common.ConnectionManager; 40 import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType; 41 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; 42 import com.example.android.vdmdemo.common.RemoteIo; 43 44 import dagger.hilt.android.AndroidEntryPoint; 45 46 import java.util.function.Consumer; 47 48 import javax.inject.Inject; 49 50 /** 51 * VDM Client activity, showing apps running on a host device and sending input back to the host. 52 */ 53 @AndroidEntryPoint(AppCompatActivity.class) 54 public class ImmersiveActivity extends Hilt_ImmersiveActivity { 55 56 private static final String TAG = "VdmClientImmersiveActivity"; 57 58 static final String EXTRA_DISPLAY_ID = "displayId"; 59 static final String EXTRA_REQUESTED_ROTATION = "requestedRotation"; 60 61 static final int RESULT_MINIMIZE = 1; 62 static final int RESULT_CLOSE = 2; 63 64 // Approximately, see 65 // https://developer.android.com/reference/android/util/DisplayMetrics#density 66 private static final float DIP_TO_DPI = 160f; 67 68 @Inject ConnectionManager mConnectionManager; 69 @Inject RemoteIo mRemoteIo; 70 @Inject VirtualSensorController mSensorController; 71 @Inject AudioPlayer mAudioPlayer; 72 @Inject InputManager mInputManager; 73 74 private int mDisplayId = Display.INVALID_DISPLAY; 75 private DisplayController mDisplayController; 76 private Surface mSurface; 77 private InputMethodManager mInputMethodManager; 78 79 private int mPortraitWidth; 80 private int mPortraitHeight; 81 private int mRequestedRotation = 0; 82 83 private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent; 84 85 private final Consumer<ConnectionManager.ConnectionStatus> mConnectionCallback = 86 (status) -> { 87 if (status.state == ConnectionManager.ConnectionStatus.State.DISCONNECTED) { 88 finish(/* minimize= */ false); 89 } 90 }; 91 92 @Override 93 @SuppressLint("ClickableViewAccessibility") onCreate(Bundle savedInstanceState)94 public void onCreate(Bundle savedInstanceState) { 95 super.onCreate(savedInstanceState); 96 97 setContentView(R.layout.activity_immersive); 98 99 WindowInsetsControllerCompat windowInsetsController = 100 WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); 101 windowInsetsController.setSystemBarsBehavior( 102 WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 103 windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()); 104 105 mInputMethodManager = getSystemService(InputMethodManager.class); 106 107 mDisplayId = getIntent().getIntExtra(EXTRA_DISPLAY_ID, Display.INVALID_DISPLAY); 108 109 OnBackPressedCallback callback = 110 new OnBackPressedCallback(true) { 111 @Override 112 public void handleOnBackPressed() { 113 mInputManager.sendBack(mDisplayId); 114 } 115 }; 116 getOnBackPressedDispatcher().addCallback(this, callback); 117 118 mDisplayController = new DisplayController(mDisplayId, mRemoteIo); 119 mDisplayController.setDpi((int) (getResources().getDisplayMetrics().density * DIP_TO_DPI)); 120 121 TextureView textureView = requireViewById(R.id.immersive_surface_view); 122 textureView.setOnTouchListener( 123 (v, event) -> { 124 if (event.getDevice().supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) { 125 textureView.getParent().requestDisallowInterceptTouchEvent(true); 126 mInputManager.sendInputEvent( 127 InputDeviceType.DEVICE_TYPE_TOUCHSCREEN, event, mDisplayId); 128 } 129 return true; 130 }); 131 textureView.setSurfaceTextureListener( 132 new TextureView.SurfaceTextureListener() { 133 @Override 134 public void onSurfaceTextureUpdated(@NonNull SurfaceTexture texture) {} 135 136 @Override 137 public void onSurfaceTextureAvailable( 138 @NonNull SurfaceTexture texture, int width, int height) { 139 Log.v(TAG, "Setting surface for immersive display " + mDisplayId); 140 mSurface = new Surface(texture); 141 mPortraitWidth = Math.min(width, height); 142 mPortraitHeight = Math.max(width, height); 143 mDisplayController.setSurface(mSurface, width, height); 144 } 145 146 @Override 147 public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture texture) { 148 Log.v(TAG, "onSurfaceTextureDestroyed for immersive display " + mDisplayId); 149 return true; 150 } 151 152 @Override 153 public void onSurfaceTextureSizeChanged( 154 @NonNull SurfaceTexture texture, int width, int height) {} 155 }); 156 textureView.setOnGenericMotionListener( 157 (v, event) -> { 158 if (event.getDevice() == null 159 || !event.getDevice().supportsSource(InputDevice.SOURCE_MOUSE)) { 160 return false; 161 } 162 mInputManager.sendInputEvent( 163 InputDeviceType.DEVICE_TYPE_MOUSE, event, mDisplayId); 164 return true; 165 }); 166 } 167 168 @Override onStart()169 public void onStart() { 170 super.onStart(); 171 mConnectionManager.addConnectionCallback(mConnectionCallback); 172 mRemoteIo.addMessageConsumer(mAudioPlayer); 173 mRemoteIo.addMessageConsumer(mRemoteEventConsumer); 174 } 175 176 @Override onStop()177 public void onStop() { 178 super.onStop(); 179 mConnectionManager.removeConnectionCallback(mConnectionCallback); 180 mRemoteIo.removeMessageConsumer(mAudioPlayer); 181 mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); 182 } 183 184 @Override onDestroy()185 protected void onDestroy() { 186 super.onDestroy(); 187 mSensorController.close(); 188 } 189 processRemoteEvent(RemoteEvent event)190 private void processRemoteEvent(RemoteEvent event) { 191 if (event.hasStopStreaming()) { 192 finish(/* minimize= */ false); 193 } else if (event.hasDisplayRotation()) { 194 mRequestedRotation = event.getDisplayRotation().getRotationDegrees(); 195 } else if (event.hasKeyboardVisibilityEvent()) { 196 if (event.getKeyboardVisibilityEvent().getVisible()) { 197 mInputMethodManager.showSoftInput(getWindow().getDecorView(), 0); 198 } else { 199 mInputMethodManager.hideSoftInputFromWindow( 200 getWindow().getDecorView().getWindowToken(), 0); 201 } 202 } 203 } 204 205 @Override dispatchKeyEvent(KeyEvent event)206 public boolean dispatchKeyEvent(KeyEvent event) { 207 switch (event.getKeyCode()) { 208 case KeyEvent.KEYCODE_VOLUME_UP -> mInputManager.sendHome(mDisplayId); 209 case KeyEvent.KEYCODE_VOLUME_DOWN -> finish(/* minimize= */ true); 210 case KeyEvent.KEYCODE_BACK -> { 211 return super.dispatchKeyEvent(event); 212 } 213 default -> mInputManager.sendInputEvent( 214 InputDeviceType.DEVICE_TYPE_KEYBOARD, event, mDisplayId); 215 } 216 return true; 217 } 218 219 @Override onConfigurationChanged(@onNull Configuration config)220 public void onConfigurationChanged(@NonNull Configuration config) { 221 super.onConfigurationChanged(config); 222 if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 223 Log.d(TAG, "Switching landscape"); 224 mDisplayController.setSurface( 225 mSurface, /* width= */ mPortraitHeight, /* height= */ mPortraitWidth); 226 } else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) { 227 Log.d(TAG, "Switching to portrait"); 228 mDisplayController.setSurface(mSurface, mPortraitWidth, mPortraitHeight); 229 } 230 } 231 finish(boolean minimize)232 private void finish(boolean minimize) { 233 if (minimize) { 234 mDisplayController.pause(); 235 } else { 236 mDisplayController.close(); 237 } 238 Intent result = new Intent(); 239 result.putExtra(EXTRA_DISPLAY_ID, mDisplayId); 240 result.putExtra(EXTRA_REQUESTED_ROTATION, mRequestedRotation); 241 setResult(minimize ? RESULT_MINIMIZE : RESULT_CLOSE, result); 242 finish(); 243 } 244 } 245