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