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