1 /* 2 * Copyright (C) 2022 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 package com.google.android.car.kitchensink.display; 17 18 import static com.google.android.car.kitchensink.KitchenSinkActivity.DUMP_ARG_CMD; 19 import static com.google.android.car.kitchensink.KitchenSinkActivity.DUMP_ARG_FRAGMENT; 20 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.util.Log; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.widget.AdapterView; 28 import android.widget.AdapterView.OnItemSelectedListener; 29 import android.widget.ArrayAdapter; 30 import android.widget.Button; 31 import android.widget.RelativeLayout; 32 import android.widget.Spinner; 33 34 import androidx.fragment.app.Fragment; 35 36 import com.google.android.car.kitchensink.KitchenSinkActivity; 37 import com.google.android.car.kitchensink.R; 38 39 import java.io.FileDescriptor; 40 import java.io.PrintWriter; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 44 /** 45 * Provides a virtual display that could be used by other apps. 46 * 47 * <p>Once the activity hosting this fragment is launched, it can be controlled using {@code adb}. 48 * Example: 49 * 50 * <pre><code> 51 * adb shell 'am start -n com.google.android.car.kitchensink/.KitchenSinkActivity --es select "virtual display"' 52 * adb shell 'dumpsys activity com.google.android.car.kitchensink/.KitchenSinkActivity fragment "virtual display" cmd create' 53 * </code></pre> 54 */ 55 public final class VirtualDisplayFragment extends Fragment { 56 57 private static final String TAG = VirtualDisplayFragment.class.getSimpleName(); 58 59 public static final String FRAGMENT_NAME = "virtual display"; 60 private static final String DISPLAY_BASE_NAME = "KitchenSinkDisplay-"; 61 62 private static final String CMD_HELP = "help"; 63 private static final String CMD_CREATE = "create"; 64 private static final String CMD_DELETE = "delete"; 65 private static final String CMD_MAXIMIZE = "maximize"; 66 private static final String CMD_MINIMIZE = "minimize"; 67 private static final String CMD_SET_NUMBER_DISPLAYS = "set-number-of-displays"; 68 69 private static final int MAX_NUMBER_DISPLAYS = 4; 70 71 private Spinner mNumberDisplaySpinner; 72 private RelativeLayout mDisplaysContainer; 73 74 private SelfManagedVirtualDisplayView[] mDisplays = 75 new SelfManagedVirtualDisplayView[MAX_NUMBER_DISPLAYS]; 76 77 private int mCurrentNumberOfDisplays; 78 79 // TODO(b/231499090): should not need those if we figure out how to automatically wrap the views 80 private int mDisplayWidth; 81 private int mDisplayHeight; 82 83 private Button mMaximizeButton; 84 private boolean mMaximized; 85 86 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)87 public View onCreateView(LayoutInflater inflater, ViewGroup container, 88 Bundle savedInstanceState) { 89 Log.v(TAG, "onCreateView(): mMaximizeButton=" + mMaximizeButton); 90 Context context = getContext(); 91 92 if (mMaximizeButton == null) { 93 mMaximizeButton = new Button(context); 94 mMaximizeButton.setText("Maximize"); 95 Log.v(TAG, "Created mMaximizeButton: " + mMaximizeButton); 96 mMaximizeButton.setOnClickListener((v) -> maximizeScreen()); 97 ((KitchenSinkActivity) getActivity()).addHeaderView(mMaximizeButton); 98 } 99 100 if (mNumberDisplaySpinner == null) { 101 ArrayList<String> spinnerValues = new ArrayList<String>(MAX_NUMBER_DISPLAYS); 102 103 for (int i = 0; i < MAX_NUMBER_DISPLAYS; i++) { 104 int displayNumber = i + 1; 105 spinnerValues.add(displayNumber == 1 ? "1 display" : displayNumber + " displays"); 106 } 107 ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<String>(context, 108 android.R.layout.simple_spinner_dropdown_item, spinnerValues); 109 110 mNumberDisplaySpinner = new Spinner(context); 111 mNumberDisplaySpinner.setAdapter(spinnerAdapter); 112 mNumberDisplaySpinner.setOnItemSelectedListener(new OnItemSelectedListener() { 113 114 @Override 115 public void onItemSelected(AdapterView<?> parent, View view, int position, 116 long id) { 117 updateNumberDisplays(position + 1, /* force= */ false); 118 } 119 120 @Override 121 public void onNothingSelected(AdapterView<?> parent) { 122 } 123 }); 124 125 Log.v(TAG, "Created mNumberDisplaySpinner: " + mNumberDisplaySpinner); 126 ((KitchenSinkActivity) getActivity()).addHeaderView(mNumberDisplaySpinner); 127 } 128 129 View view = inflater.inflate(R.layout.virtual_display, container, false); 130 131 mDisplaysContainer = view.findViewById(R.id.displays_container); 132 133 return view; 134 } 135 136 @Override onHiddenChanged(boolean hidden)137 public void onHiddenChanged(boolean hidden) { 138 Log.v(TAG, "onHiddenChanged(hidden=" + hidden + "): mMaximizeButton=" + mMaximizeButton); 139 if (mMaximizeButton == null) { 140 // NOTE: onHiddenChanged(false) is called before onCreateView(...) 141 Log.v(TAG, "Ignoring onHiddenChanged() call as fragment was not created yet"); 142 return; 143 } 144 boolean on = !hidden; 145 toggleView(mMaximizeButton, on); 146 toggleView(mNumberDisplaySpinner, on); 147 } 148 149 @Override onDestroyView()150 public void onDestroyView() { 151 onAllDisplays((index, display) -> display.release()); 152 153 super.onDestroyView(); 154 } 155 156 @Override dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)157 public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 158 Log.v(TAG, "dump(): " + Arrays.toString(args)); 159 160 if (args != null && args.length > 0 && args[0].equals(DUMP_ARG_CMD)) { 161 runCmd(writer, args); 162 return; 163 } 164 165 writer.printf("%sMaximized: %b\n", prefix, mMaximized); 166 dumpView(prefix, writer, mMaximizeButton, "Maximize screen button"); 167 168 writer.printf("%sCurrent number of displays: %d\n", prefix, mCurrentNumberOfDisplays); 169 170 writer.printf("%sDisplay resolution: %d x %d\n", prefix, mDisplayWidth, mDisplayHeight); 171 172 String prefix2 = prefix + " "; 173 174 onAllDisplays((index, display) -> { 175 writer.printf("%sDisplay #%d:\n", prefix, (index + 1)); 176 display.dump(prefix2, writer, args); 177 }); 178 179 } 180 181 /** 182 * Visits all instantiated displays, including the hidden ones. 183 */ onAllDisplays(DisplayVisitor visitor)184 private void onAllDisplays(DisplayVisitor visitor) { 185 onDisplays(MAX_NUMBER_DISPLAYS, visitor); 186 } 187 188 /** 189 * Visits the visible displays. 190 */ onVisibleDisplays(DisplayVisitor visitor)191 private void onVisibleDisplays(DisplayVisitor visitor) { 192 onDisplays(mCurrentNumberOfDisplays, visitor); 193 } 194 onDisplays(int upperLimit, DisplayVisitor visitor)195 private void onDisplays(int upperLimit, DisplayVisitor visitor) { 196 for (int i = 0; i < mDisplays.length && i < upperLimit; i++) { 197 SelfManagedVirtualDisplayView display = mDisplays[i]; 198 if (display == null) { 199 // All done! 200 return; 201 } 202 visitor.visit(i, display); 203 } 204 } 205 updateNumberDisplays(int numberDisplays, boolean force)206 private void updateNumberDisplays(int numberDisplays, boolean force) { 207 if (numberDisplays < 0 || numberDisplays > MAX_NUMBER_DISPLAYS) { 208 throw new IllegalArgumentException("Invalid number of displays: " + numberDisplays); 209 } 210 if (numberDisplays == mCurrentNumberOfDisplays && !force) { 211 Log.v(TAG, "updateNumberDisplays(): ignoring, already " + numberDisplays); 212 return; 213 } 214 Log.i(TAG, "updating number of displays from " + mCurrentNumberOfDisplays + " to " 215 + numberDisplays); 216 mCurrentNumberOfDisplays = numberDisplays; 217 218 // TODO(b/231499090): figure out how to use properly use WRAP_CONTENT without one of the 219 // displays taking the full view 220 mDisplayWidth = mDisplaysContainer.getRight(); 221 mDisplayHeight = mDisplaysContainer.getBottom(); 222 Log.v(TAG, "Full dimension: " + mDisplayWidth + "x" + mDisplayHeight); 223 switch (numberDisplays) { 224 case 3: 225 case 4: 226 mDisplayHeight /= 2; 227 // Fall through 228 case 2: 229 mDisplayWidth /= 2; 230 // Fall through 231 default: 232 // No op. 233 } 234 Log.v(TAG, "Display dimension: " + mDisplayWidth + "x" + mDisplayHeight); 235 236 for (int i = 0; i < MAX_NUMBER_DISPLAYS; i++) { 237 SelfManagedVirtualDisplayView display = mDisplays[i]; 238 239 if (i >= numberDisplays && display != null) { 240 Log.v(TAG, "Disabling display at index " + i); 241 toggleView(display, /* on= */ false); 242 continue; 243 } 244 245 boolean isNew = false; 246 if (display == null) { 247 int subject = i + 1; 248 String name = DISPLAY_BASE_NAME + subject; 249 Log.i(TAG, "Creating display " + name + " at index " + i 250 + " and RelativeLayout subject " + subject); 251 display = new SelfManagedVirtualDisplayView(getContext(), name); 252 display.setId(subject); 253 display.enableUserSwitching(); 254 mDisplays[i] = display; 255 isNew = true; 256 } else { 257 Log.v(TAG, "Updating dimensions of display at index " + i); 258 } 259 RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( 260 mDisplayWidth, mDisplayHeight); 261 switch (i) { 262 // Display 0 is the reference (Subject 1), it doesn't need any rule 263 case 1: // Subject 2 264 params.addRule(RelativeLayout.RIGHT_OF, /* subject= */ 1); 265 break; 266 case 2: // Subect 3 267 params.addRule(RelativeLayout.BELOW, /* subject= */ 1); 268 break; 269 case 3: // Subject 4 270 params.addRule(RelativeLayout.BELOW, /* subject= */ 2); 271 params.addRule(RelativeLayout.RIGHT_OF, /* subject= */ 3); 272 break; 273 default: 274 // No op. 275 } 276 display.setLayoutParams(params); 277 toggleView(display, /* on= */ true); 278 279 if (isNew) { 280 Log.v(TAG, "Adding display to container"); 281 mDisplaysContainer.addView(display); 282 } 283 } 284 } 285 runCmd(PrintWriter writer, String[] args)286 private void runCmd(PrintWriter writer, String[] args) { 287 if (args.length < 2) { 288 writer.println("missing command\n"); 289 return; 290 } 291 String cmd = args[1]; 292 switch (cmd) { 293 case CMD_HELP: 294 cmdShowHelp(writer); 295 break; 296 case CMD_CREATE: 297 cmdCreateDisplay(writer, args); 298 break; 299 case CMD_DELETE: 300 cmdDeleteDisplay(writer, args); 301 break; 302 case CMD_MAXIMIZE: 303 cmdMinimizeOrMaximizeScreen(writer, /* maximize= */ true); 304 break; 305 case CMD_MINIMIZE: 306 cmdMinimizeOrMaximizeScreen(writer, /* maximize= */ false); 307 break; 308 case CMD_SET_NUMBER_DISPLAYS: 309 cmdSetNumberOfDisplays(writer, args); 310 break; 311 312 default: 313 cmdShowHelp(writer); 314 writer.printf("Invalid cmd: %s\n", Arrays.toString(args)); 315 } 316 return; 317 } 318 cmdDeleteDisplay(PrintWriter writer, String[] args)319 private void cmdDeleteDisplay(PrintWriter writer, String[] args) { 320 // TODO(b/231499090): parse args to get display # 321 try { 322 mDisplays[0].deleteDisplay(); 323 printMessage(writer, "Deleted virtual display"); 324 } catch (Exception e) { 325 writer.printf("Failed: %s\n", e); 326 } 327 } 328 cmdCreateDisplay(PrintWriter writer, String[] args)329 private void cmdCreateDisplay(PrintWriter writer, String[] args) { 330 // TODO(b/231499090): parse args to get display # 331 try { 332 int displayId = mDisplays[0].createDisplay(); 333 printMessage(writer, "Created virtual display with id %d", displayId); 334 } catch (Exception e) { 335 writer.printf("Failed: %s\n", e); 336 } 337 } 338 cmdSetNumberOfDisplays(PrintWriter writer, String[] args)339 private void cmdSetNumberOfDisplays(PrintWriter writer, String[] args) { 340 // TODO(b/231499090): use helper to parse args 341 int number; 342 try { 343 number = Integer.parseInt(args[2]); 344 } catch (Exception e) { 345 writer.printf("Invalid args: %s\n", Arrays.toString(args)); 346 return; 347 } 348 if (number < 1 || number > MAX_NUMBER_DISPLAYS) { 349 writer.printf("Invalid number of display (%d) - must be between 1 and %d\n", 350 number, MAX_NUMBER_DISPLAYS); 351 return; 352 } 353 mNumberDisplaySpinner.setSelection(number - 1); 354 } 355 cmdShowHelp(PrintWriter writer)356 private void cmdShowHelp(PrintWriter writer) { 357 writer.println("Available commands:\n"); 358 showCommandHelp(writer, "Shows this help message.", CMD_HELP); 359 showCommandHelp(writer, "Maximizes the display view so it takes the whole screen.", 360 CMD_MAXIMIZE); 361 showCommandHelp(writer, "Minimizes the display view so the screen show the controls.", 362 CMD_MINIMIZE); 363 showCommandHelp(writer, "Creates the virtual display.", 364 CMD_CREATE); 365 showCommandHelp(writer, "Deletes the virtual display.", 366 CMD_DELETE); 367 showCommandHelp(writer, "Sets the number of virtual displays.", 368 CMD_SET_NUMBER_DISPLAYS, "<NUMBER>"); 369 } 370 showCommandHelp(PrintWriter writer, String description, String cmd, String... args)371 private void showCommandHelp(PrintWriter writer, String description, String cmd, 372 String... args) { 373 writer.printf("%s", cmd); 374 if (args != null) { 375 for (String arg : args) { 376 writer.printf(" %s", arg); 377 } 378 } 379 writer.println(":"); 380 writer.printf(" %s\n\n", description); 381 } 382 383 private interface DisplayVisitor { visit(int index, SelfManagedVirtualDisplayView display)384 void visit(int index, SelfManagedVirtualDisplayView display); 385 } 386 toggleView(View view, boolean on)387 private void toggleView(View view, boolean on) { 388 view.setEnabled(on); 389 view.setVisibility(on ? View.VISIBLE : View.GONE); 390 } 391 392 // TODO(b/231499090): use custom Writer interface to reuse logic for toast / writer below 393 maximizeScreen()394 private void maximizeScreen() { 395 if (mMaximized) { 396 ToastUtils.logAndToastError(getContext(), /* exception= */ null, "Already maximized"); 397 return; 398 } 399 String msg1 = "Maximizing display. To minimize, run:"; 400 String pkg = getContext().getPackageName(); 401 String activity = KitchenSinkActivity.class.getSimpleName(); 402 String msg2 = String.format("adb shell 'dumpsys activity %s/.%s %s \"%s\" %s %s'", 403 pkg, activity, DUMP_ARG_FRAGMENT, FRAGMENT_NAME, DUMP_ARG_CMD, CMD_MINIMIZE); 404 Context context = getContext(); 405 ToastUtils.logAndToastMessage(context, msg1); 406 ToastUtils.logAndToastMessage(context, msg2); 407 minimizeOrMaximizeViews(/* maximize= */ true); 408 } 409 cmdMinimizeOrMaximizeScreen(PrintWriter writer, boolean maximize)410 private void cmdMinimizeOrMaximizeScreen(PrintWriter writer, boolean maximize) { 411 if (maximize && mMaximized) { 412 printError(writer, /* exception= */ null, "Already maximized"); 413 return; 414 } 415 if (!maximize && !mMaximized) { 416 printError(writer, /* exception= */ null, "Already minimized"); 417 return; 418 } 419 String msg1; 420 if (maximize) { 421 msg1 = "Maximizing display. To minimize, run:"; 422 } else { 423 msg1 = "Minimizing display. To maximize, run:"; 424 } 425 String pkg = getContext().getPackageName(); 426 String activity = KitchenSinkActivity.class.getSimpleName(); 427 String msg2 = String.format("adb shell 'dumpsys activity %s/.%s %s \"%s\" %s %s'", 428 pkg, activity, DUMP_ARG_FRAGMENT, FRAGMENT_NAME, DUMP_ARG_CMD, 429 (maximize ? CMD_MINIMIZE : CMD_MAXIMIZE)); 430 printMessage(writer, msg1); 431 printMessage(writer, msg2); 432 minimizeOrMaximizeViews(maximize); 433 } 434 minimizeOrMaximizeViews(boolean maximize)435 private void minimizeOrMaximizeViews(boolean maximize) { 436 mMaximized = maximize; 437 boolean visible = !maximize; 438 439 // TODO(b/231499090): must call updateNumberDisplays() as the dimensions are manually 440 // calculated (hence the force argument) 441 updateNumberDisplays(mCurrentNumberOfDisplays, /* force= */ true); 442 443 onVisibleDisplays((index, display) -> display.setHeaderVisible(visible)); 444 445 ((KitchenSinkActivity) getActivity()).setHeaderVisibility(!maximize); 446 } 447 448 // TODO(b/231499090): move plumbing below to common code 449 dumpView(String prefix, PrintWriter writer, View view, String name)450 private void dumpView(String prefix, PrintWriter writer, View view, String name) { 451 writer.printf("%s%s: %s %s\n", prefix, name, 452 (view.isEnabled() ? "enabled" : "disabled"), 453 visibilityToString(view.getVisibility())); 454 } 455 printMessage(PrintWriter writer, String format, Object... args)456 protected void printMessage(PrintWriter writer, String format, Object... args) { 457 String message = String.format(format, args); 458 writer.printf("%s\n", message); 459 } 460 printError(PrintWriter writer, Exception exception, String format, Object... args)461 protected void printError(PrintWriter writer, Exception exception, 462 String format, Object... args) { 463 String message = String.format(format, args); 464 if (exception != null) { 465 writer.printf("%s: %s\n", message, exception); 466 } else { 467 writer.printf("%s\n", message); 468 } 469 } 470 visibilityToString(int visibility)471 public static String visibilityToString(int visibility) { 472 switch (visibility) { 473 case View.VISIBLE: 474 return "VISIBLE"; 475 case View.INVISIBLE: 476 return "INVISIBLE"; 477 case View.GONE: 478 return "GONE"; 479 default: 480 return "UNKNOWN-" + visibility; 481 } 482 } 483 } 484