1 /*
2  * Copyright (C) 2021 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.android.microdroid.demo;
18 
19 import android.app.Application;
20 import android.os.Bundle;
21 import android.os.IBinder;
22 import android.os.RemoteException;
23 import android.system.virtualmachine.VirtualMachine;
24 import android.system.virtualmachine.VirtualMachineCallback;
25 import android.system.virtualmachine.VirtualMachineConfig;
26 import android.system.virtualmachine.VirtualMachineException;
27 import android.system.virtualmachine.VirtualMachineManager;
28 import android.util.Log;
29 import android.view.View;
30 import android.widget.Button;
31 import android.widget.CheckBox;
32 import android.widget.ScrollView;
33 import android.widget.TextView;
34 
35 import androidx.appcompat.app.AppCompatActivity;
36 import androidx.lifecycle.AndroidViewModel;
37 import androidx.lifecycle.LiveData;
38 import androidx.lifecycle.MutableLiveData;
39 import androidx.lifecycle.ViewModelProvider;
40 
41 import com.android.microdroid.testservice.ITestService;
42 
43 import java.io.BufferedReader;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.io.InputStreamReader;
47 import java.util.concurrent.ExecutorService;
48 import java.util.concurrent.Executors;
49 
50 /**
51  * This app is to demonstrate the use of APIs in the android.system.virtualmachine library.
52  * Currently, this app starts a virtual machine running Microdroid and shows the console output from
53  * the virtual machine to the UI.
54  */
55 public class MainActivity extends AppCompatActivity {
56     private static final String TAG = "MicrodroidDemo";
57 
58     @Override
onCreate(Bundle savedInstanceState)59     protected void onCreate(Bundle savedInstanceState) {
60         super.onCreate(savedInstanceState);
61         setContentView(R.layout.activity_main);
62         Button runStopButton = findViewById(R.id.runStopButton);
63         TextView consoleView = findViewById(R.id.consoleOutput);
64         TextView logView = findViewById(R.id.logOutput);
65         TextView payloadView = findViewById(R.id.payloadOutput);
66         ScrollView scrollConsoleView = findViewById(R.id.scrollConsoleOutput);
67         ScrollView scrollLogView = findViewById(R.id.scrollLogOutput);
68 
69         VirtualMachineModel model = new ViewModelProvider(this).get(VirtualMachineModel.class);
70 
71         // When the button is clicked, run or stop the VM
72         runStopButton.setOnClickListener(
73                 v -> {
74                     Integer status = model.getStatus().getValue();
75                     if (status != null && status == VirtualMachine.STATUS_RUNNING) {
76                         model.stop();
77                     } else {
78                         CheckBox debugModeCheckBox = findViewById(R.id.debugMode);
79                         CheckBox protectedModeCheckBox = findViewById(R.id.protectedMode);
80                         final boolean debug = debugModeCheckBox.isChecked();
81                         final boolean protectedVm = protectedModeCheckBox.isChecked();
82                         model.run(debug, protectedVm);
83                     }
84                 });
85 
86         // When the VM status is updated, change the label of the button
87         model.getStatus()
88                 .observeForever(
89                         status -> {
90                             if (status == VirtualMachine.STATUS_RUNNING) {
91                                 runStopButton.setText("Stop");
92                                 // Clear the outputs from the previous run
93                                 consoleView.setText("");
94                                 logView.setText("");
95                                 payloadView.setText("");
96                             } else {
97                                 runStopButton.setText("Run");
98                             }
99                         });
100 
101         // When the console, log, or payload output is updated, append the new line to the
102         // corresponding text view.
103         model.getConsoleOutput()
104                 .observeForever(
105                         line -> {
106                             consoleView.append(line + "\n");
107                             scrollConsoleView.fullScroll(View.FOCUS_DOWN);
108                         });
109         model.getLogOutput()
110                 .observeForever(
111                         line -> {
112                             logView.append(line + "\n");
113                             scrollLogView.fullScroll(View.FOCUS_DOWN);
114                         });
115         model.getPayloadOutput()
116                 .observeForever(
117                         line -> payloadView.append(line + "\n"));
118     }
119 
120     /** Reads data from an input stream and posts it to the output data */
121     static class Reader implements Runnable {
122         private final String mName;
123         private final MutableLiveData<String> mOutput;
124         private final InputStream mStream;
125 
Reader(String name, MutableLiveData<String> output, InputStream stream)126         Reader(String name, MutableLiveData<String> output, InputStream stream) {
127             mName = name;
128             mOutput = output;
129             mStream = stream;
130         }
131 
132         @Override
run()133         public void run() {
134             try {
135                 BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
136                 String line;
137                 while ((line = reader.readLine()) != null && !Thread.interrupted()) {
138                     mOutput.postValue(line);
139                 }
140             } catch (IOException e) {
141                 Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
142             }
143         }
144     }
145 
146     /** Models a virtual machine and outputs from it. */
147     public static class VirtualMachineModel extends AndroidViewModel {
148         private static final String VM_NAME = "demo_vm";
149         private VirtualMachine mVirtualMachine;
150         private final MutableLiveData<String> mConsoleOutput = new MutableLiveData<>();
151         private final MutableLiveData<String> mLogOutput = new MutableLiveData<>();
152         private final MutableLiveData<String> mPayloadOutput = new MutableLiveData<>();
153         private final MutableLiveData<Integer> mStatus = new MutableLiveData<>();
154         private ExecutorService mExecutorService;
155 
VirtualMachineModel(Application app)156         public VirtualMachineModel(Application app) {
157             super(app);
158             mStatus.setValue(VirtualMachine.STATUS_DELETED);
159         }
160 
161         /** Runs a VM */
run(boolean debug, boolean protectedVm)162         public void run(boolean debug, boolean protectedVm) {
163             // Create a VM and run it.
164             mExecutorService = Executors.newFixedThreadPool(4);
165 
166             VirtualMachineCallback callback =
167                     new VirtualMachineCallback() {
168                         // store reference to ExecutorService to avoid race condition
169                         private final ExecutorService mService = mExecutorService;
170 
171                         @Override
172                         public void onPayloadStarted(VirtualMachine vm) {}
173 
174                         @Override
175                         public void onPayloadReady(VirtualMachine vm) {
176                             // This check doesn't 100% prevent race condition or UI hang.
177                             // However, it's fine for demo.
178                             if (mService.isShutdown()) {
179                                 return;
180                             }
181                             mPayloadOutput.postValue("(Payload is ready. Testing VM service...)");
182 
183                             mService.execute(() -> testVmService(vm));
184                         }
185 
186                         private void testVmService(VirtualMachine vm) {
187                             IBinder binder;
188                             try {
189                                 binder = vm.connectToVsockServer(ITestService.PORT);
190                             } catch (Exception e) {
191                                 if (!Thread.interrupted()) {
192                                     mPayloadOutput.postValue(
193                                             String.format(
194                                                     "(VM service connection failed: %s)",
195                                                     e.getMessage()));
196                                 }
197                                 return;
198                             }
199 
200                             try {
201                                 ITestService testService = ITestService.Stub.asInterface(binder);
202                                 int ret = testService.addInteger(123, 456);
203                                 mPayloadOutput.postValue(
204                                         String.format(
205                                                 "(VM payload service: %d + %d = %d)",
206                                                 123, 456, ret));
207                             } catch (RemoteException e) {
208                                 mPayloadOutput.postValue(
209                                         String.format(
210                                                 "(Exception while testing VM's binder service:"
211                                                         + " %s)",
212                                                 e.getMessage()));
213                             }
214                         }
215 
216                         @Override
217                         public void onPayloadFinished(VirtualMachine vm, int exitCode) {
218                             // This check doesn't 100% prevent race condition, but is fine for demo.
219                             if (!mService.isShutdown()) {
220                                 mPayloadOutput.postValue(
221                                         String.format(
222                                                 "(Payload finished. exit code: %d)", exitCode));
223                             }
224                         }
225 
226                         @Override
227                         public void onError(VirtualMachine vm, int errorCode, String message) {
228                             // This check doesn't 100% prevent race condition, but is fine for demo.
229                             if (!mService.isShutdown()) {
230                                 mPayloadOutput.postValue(
231                                         String.format(
232                                                 "(Error occurred. code: %d, message: %s)",
233                                                 errorCode, message));
234                             }
235                         }
236 
237                         @Override
238                         public void onStopped(VirtualMachine vm, int reason) {
239                             mService.shutdownNow();
240                             mStatus.postValue(VirtualMachine.STATUS_STOPPED);
241                         }
242                     };
243 
244             try {
245                 VirtualMachineConfig.Builder builder =
246                         new VirtualMachineConfig.Builder(getApplication());
247                 builder.setPayloadBinaryName("MicrodroidTestNativeLib.so");
248                 builder.setProtectedVm(protectedVm);
249 
250                 if (debug) {
251                     builder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
252                     builder.setVmOutputCaptured(true);
253                 }
254                 VirtualMachineConfig config = builder.build();
255                 VirtualMachineManager vmm =
256                         getApplication().getSystemService(VirtualMachineManager.class);
257                 mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
258                 try {
259                     mVirtualMachine.setConfig(config);
260                 } catch (VirtualMachineException e) {
261                     vmm.delete(VM_NAME);
262                     mVirtualMachine = vmm.create(VM_NAME, config);
263                 }
264                 mVirtualMachine.run();
265                 mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
266                 mStatus.postValue(mVirtualMachine.getStatus());
267 
268                 if (debug) {
269                     InputStream console = mVirtualMachine.getConsoleOutput();
270                     InputStream log = mVirtualMachine.getLogOutput();
271                     mExecutorService.execute(new Reader("console", mConsoleOutput, console));
272                     mExecutorService.execute(new Reader("log", mLogOutput, log));
273                 }
274             } catch (VirtualMachineException e) {
275                 throw new RuntimeException(e);
276             }
277         }
278 
279         /** Stops the running VM */
stop()280         public void stop() {
281             try {
282                 mVirtualMachine.stop();
283             } catch (VirtualMachineException e) {
284                 // Consume
285             }
286             mVirtualMachine = null;
287             mExecutorService.shutdownNow();
288             mStatus.postValue(VirtualMachine.STATUS_STOPPED);
289         }
290 
291         /** Returns the console output from the VM */
getConsoleOutput()292         public LiveData<String> getConsoleOutput() {
293             return mConsoleOutput;
294         }
295 
296         /** Returns the log output from the VM */
getLogOutput()297         public LiveData<String> getLogOutput() {
298             return mLogOutput;
299         }
300 
301         /** Returns the payload output from the VM */
getPayloadOutput()302         public LiveData<String> getPayloadOutput() {
303             return mPayloadOutput;
304         }
305 
306         /** Returns the status of the VM */
getStatus()307         public LiveData<Integer> getStatus() {
308             return mStatus;
309         }
310     }
311 }
312