• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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