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.bluetooth.BluetoothAdapter; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.content.pm.PackageManager; 24 import android.os.Build; 25 import android.os.Bundle; 26 import android.view.KeyEvent; 27 import android.view.Menu; 28 import android.view.MenuInflater; 29 import android.view.MenuItem; 30 import android.view.View; 31 import android.view.WindowManager; 32 import android.view.inputmethod.InputMethodManager; 33 34 import androidx.activity.result.ActivityResultLauncher; 35 import androidx.activity.result.contract.ActivityResultContracts; 36 import androidx.activity.result.contract.ActivityResultContracts.RequestPermission; 37 import androidx.appcompat.app.AppCompatActivity; 38 import androidx.appcompat.widget.Toolbar; 39 import androidx.core.content.ContextCompat; 40 import androidx.preference.PreferenceManager; 41 import androidx.recyclerview.widget.LinearLayoutManager; 42 43 import com.example.android.vdmdemo.common.ConnectionManager; 44 import com.example.android.vdmdemo.common.DpadFragment; 45 import com.example.android.vdmdemo.common.EdgeToEdgeUtils; 46 import com.example.android.vdmdemo.common.NavTouchpadFragment; 47 import com.example.android.vdmdemo.common.RemoteEventProto.DeviceCapabilities; 48 import com.example.android.vdmdemo.common.RemoteEventProto.DeviceState; 49 import com.example.android.vdmdemo.common.RemoteEventProto.InputDeviceType; 50 import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; 51 import com.example.android.vdmdemo.common.RemoteIo; 52 import com.example.android.vdmdemo.common.RotaryFragment; 53 54 import dagger.hilt.android.AndroidEntryPoint; 55 56 import java.util.function.Consumer; 57 58 import javax.inject.Inject; 59 60 /** 61 * VDM Client activity, showing apps running on a host device and sending input back to the host. 62 */ 63 @AndroidEntryPoint(AppCompatActivity.class) 64 public class MainActivity extends Hilt_MainActivity { 65 private static final String TAG = "VdmClient"; 66 67 @Inject RemoteIo mRemoteIo; 68 @Inject ConnectionManager mConnectionManager; 69 @Inject InputManager mInputManager; 70 @Inject VirtualSensorController mSensorController; 71 72 @Inject VirtualCameraController mVirtualCameraController; 73 @Inject AudioPlayer mAudioPlayer; 74 @Inject AudioRecorder mAudioRecorder; 75 76 private boolean mPowerOn = false; 77 78 private final Consumer<RemoteEvent> mRemoteEventConsumer = this::processRemoteEvent; 79 private DisplayAdapter mDisplayAdapter; 80 private InputMethodManager mInputMethodManager; 81 82 private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener = 83 this::onPreferencesChanged; 84 85 private final ActivityResultLauncher<String> mRequestPermissionLauncher = 86 registerForActivityResult(new RequestPermission(), isGranted -> { 87 if (isGranted) { 88 mRemoteIo.addMessageConsumer(mAudioRecorder); 89 } else { 90 mRemoteIo.removeMessageConsumer(mAudioRecorder); 91 } 92 }); 93 94 private final Consumer<ConnectionManager.ConnectionStatus> mConnectionCallback = 95 (status) -> { 96 if (status.state == ConnectionManager.ConnectionStatus.State.CONNECTED) { 97 boolean supportsAudioOutput = 98 MainActivity.this.getPackageManager().hasSystemFeature( 99 PackageManager.FEATURE_AUDIO_OUTPUT); 100 boolean supportsAudioInput = hasRecordAudioPermission(MainActivity.this); 101 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 102 .setDeviceCapabilities(DeviceCapabilities.newBuilder() 103 .setDeviceName(Build.MODEL) 104 .setBluetoothDeviceName( 105 BluetoothAdapter.getDefaultAdapter().getName()) 106 .addAllSensorCapabilities( 107 mSensorController.getSensorCapabilities()) 108 .addAllCameraCapabilities( 109 mVirtualCameraController.getCameraCapabilities()) 110 .setSupportsAudioOutput(supportsAudioOutput) 111 .setSupportsAudioInput(supportsAudioInput)) 112 .build()); 113 } else { 114 if (mDisplayAdapter != null) { 115 runOnUiThread(mDisplayAdapter::clearDisplays); 116 } 117 } 118 }; 119 120 @Override onCreate(Bundle savedInstanceState)121 public void onCreate(Bundle savedInstanceState) { 122 super.onCreate(savedInstanceState); 123 124 setContentView(R.layout.activity_main); 125 Toolbar toolbar = requireViewById(R.id.main_tool_bar); 126 setSupportActionBar(toolbar); 127 EdgeToEdgeUtils.applyTopInsets(toolbar); 128 129 ClientView displaysView = requireViewById(R.id.displays); 130 displaysView.setLayoutManager( 131 new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); 132 displaysView.setItemAnimator(null); 133 mDisplayAdapter = new DisplayAdapter(displaysView, mRemoteIo, mInputManager); 134 displaysView.setAdapter(mDisplayAdapter); 135 136 ActivityResultLauncher<Intent> fullscreenLauncher = registerForActivityResult( 137 new ActivityResultContracts.StartActivityForResult(), 138 mDisplayAdapter::onFullscreenActivityResult); 139 mDisplayAdapter.setFullscreenLauncher(fullscreenLauncher); 140 141 mInputMethodManager = getSystemService(InputMethodManager.class); 142 143 DpadFragment dpadFragment = 144 (DpadFragment) getSupportFragmentManager().findFragmentById( 145 R.id.dpad_fragment_container); 146 dpadFragment.setInputEventListener((event) -> 147 mInputManager.sendInputEventToFocusedDisplay( 148 InputDeviceType.DEVICE_TYPE_DPAD, event)); 149 NavTouchpadFragment navTouchpadFragment = 150 (NavTouchpadFragment) getSupportFragmentManager().findFragmentById( 151 R.id.nav_touchpad_fragment_container); 152 navTouchpadFragment.setInputEventListener((event) -> 153 mInputManager.sendInputEventToFocusedDisplay( 154 InputDeviceType.DEVICE_TYPE_NAVIGATION_TOUCHPAD, event)); 155 RotaryFragment rotaryFragment = 156 (RotaryFragment) getSupportFragmentManager().findFragmentById( 157 R.id.rotary_fragment_container); 158 rotaryFragment.setInputEventListener((event) -> 159 mInputManager.sendInputEventToFocusedDisplay( 160 InputDeviceType.DEVICE_TYPE_ROTARY_ENCODER, event)); 161 162 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 163 sharedPreferences.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener); 164 165 mConnectionManager.startClientSession( 166 sharedPreferences.getString( 167 getString(R.string.pref_network_channel), String.valueOf(0))); 168 } 169 170 @Override onStart()171 public void onStart() { 172 super.onStart(); 173 mConnectionManager.addConnectionCallback(mConnectionCallback); 174 mRemoteIo.addMessageConsumer(mAudioPlayer); 175 mRemoteIo.addMessageConsumer(mRemoteEventConsumer); 176 } 177 178 @Override onResume()179 public void onResume() { 180 super.onResume(); 181 mDisplayAdapter.resumeAllDisplays(); 182 183 if (hasRecordAudioPermission(this)) { 184 mRemoteIo.addMessageConsumer(mAudioRecorder); 185 } else { 186 mRequestPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO); 187 } 188 } 189 190 @Override onPause()191 public void onPause() { 192 super.onPause(); 193 mDisplayAdapter.pauseAllDisplays(); 194 mAudioRecorder.stop(); 195 mRemoteIo.removeMessageConsumer(mAudioRecorder); 196 } 197 198 @Override onStop()199 public void onStop() { 200 super.onStop(); 201 mConnectionManager.removeConnectionCallback(mConnectionCallback); 202 mRemoteIo.removeMessageConsumer(mRemoteEventConsumer); 203 mRemoteIo.removeMessageConsumer(mAudioPlayer); 204 } 205 206 @Override onDestroy()207 protected void onDestroy() { 208 super.onDestroy(); 209 mDisplayAdapter.clearDisplays(); 210 mConnectionManager.disconnect(); 211 mSensorController.close(); 212 } 213 214 @Override dispatchKeyEvent(KeyEvent event)215 public boolean dispatchKeyEvent(KeyEvent event) { 216 return mInputManager.sendInputEventToFocusedDisplay( 217 InputDeviceType.DEVICE_TYPE_KEYBOARD, event) 218 || super.dispatchKeyEvent(event); 219 } 220 221 @Override onCreateOptionsMenu(Menu menu)222 public boolean onCreateOptionsMenu(Menu menu) { 223 MenuInflater inflater = getMenuInflater(); 224 inflater.inflate(R.menu.options, menu); 225 return true; 226 } 227 228 @Override onOptionsItemSelected(MenuItem item)229 public boolean onOptionsItemSelected(MenuItem item) { 230 switch (item.getItemId()) { 231 case R.id.input -> toggleInputVisibility(); 232 case R.id.power -> togglePowerState(); 233 case R.id.settings -> startActivity(new Intent(this, SettingsActivity.class)); 234 default -> { 235 return super.onOptionsItemSelected(item); 236 } 237 } 238 return true; 239 } 240 onPreferencesChanged(SharedPreferences sharedPreferences, String key)241 private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) { 242 if (key.equals(getString(R.string.pref_network_channel))) { 243 mConnectionManager.disconnect(); 244 mConnectionManager.startClientSession( 245 sharedPreferences.getString( 246 getString(R.string.pref_network_channel), String.valueOf(0))); 247 } 248 } 249 processRemoteEvent(RemoteEvent event)250 private void processRemoteEvent(RemoteEvent event) { 251 if (event.hasStartStreaming()) { 252 runOnUiThread( 253 () -> mDisplayAdapter.addDisplay(event.getStartStreaming().getHomeEnabled(), 254 event.getStartStreaming().getRotationSupported())); 255 } else if (event.hasStopStreaming()) { 256 runOnUiThread(() -> mDisplayAdapter.removeDisplay(event.getDisplayId())); 257 } else if (event.hasDisplayRotation()) { 258 runOnUiThread(() -> mDisplayAdapter.rotateDisplay( 259 event.getDisplayId(), event.getDisplayRotation().getRotationDegrees())); 260 } else if (event.hasDisplayChangeEvent()) { 261 runOnUiThread(() -> mDisplayAdapter.processDisplayChange(event)); 262 } else if (event.hasKeyboardVisibilityEvent()) { 263 if (event.getKeyboardVisibilityEvent().getVisible()) { 264 mInputMethodManager.showSoftInput(getWindow().getDecorView(), 0); 265 } else { 266 mInputMethodManager.hideSoftInputFromWindow( 267 getWindow().getDecorView().getWindowToken(), 0); 268 } 269 } else if (event.hasDeviceState()) { 270 mPowerOn = event.getDeviceState().getPowerOn(); 271 } else if (event.hasBrightnessEvent()) { 272 runOnUiThread(() -> setBrightness(event.getBrightnessEvent().getBrightness())); 273 } else if (event.hasRequestBluetoothDiscoverable()) { 274 if (BluetoothAdapter.getDefaultAdapter().getScanMode() 275 != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { 276 startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)); 277 } 278 } 279 } 280 toggleInputVisibility()281 private void toggleInputVisibility() { 282 View dpad = requireViewById(R.id.dpad_fragment_container); 283 int visibility = dpad.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE; 284 dpad.setVisibility(visibility); 285 requireViewById(R.id.nav_touchpad_fragment_container).setVisibility(visibility); 286 requireViewById(R.id.rotary_fragment_container).setVisibility(visibility); 287 } 288 togglePowerState()289 private void togglePowerState() { 290 mPowerOn = !mPowerOn; 291 mRemoteIo.sendMessage(RemoteEvent.newBuilder() 292 .setDeviceState(DeviceState.newBuilder().setPowerOn(mPowerOn)) 293 .build()); 294 295 } 296 setBrightness(float brightness)297 private void setBrightness(float brightness) { 298 WindowManager.LayoutParams layout = getWindow().getAttributes(); 299 layout.screenBrightness = brightness; 300 getWindow().setAttributes(layout); 301 } 302 hasRecordAudioPermission(Context context)303 private static boolean hasRecordAudioPermission(Context context) { 304 return ContextCompat.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO) 305 == PackageManager.PERMISSION_GRANTED; 306 } 307 } 308