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 17 package com.android.sdksandboxclient; 18 19 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_DISPLAY_ID; 20 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HEIGHT_IN_PIXELS; 21 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_HOST_TOKEN; 22 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_SURFACE_PACKAGE; 23 import static android.app.sdksandbox.SdkSandboxManager.EXTRA_WIDTH_IN_PIXELS; 24 import static android.util.Log.DEBUG; 25 import static android.util.Log.ERROR; 26 import static android.util.Log.INFO; 27 import static android.util.Log.VERBOSE; 28 import static android.util.Log.WARN; 29 30 import android.annotation.NonNull; 31 import android.app.Activity; 32 import android.app.AlertDialog; 33 import android.app.sdksandbox.LoadSdkException; 34 import android.app.sdksandbox.RequestSurfacePackageException; 35 import android.app.sdksandbox.SandboxedSdk; 36 import android.app.sdksandbox.SdkSandboxManager; 37 import android.app.sdksandbox.interfaces.IActivityStarter; 38 import android.app.sdksandbox.interfaces.ISdkApi; 39 import android.content.SharedPreferences; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.IBinder; 43 import android.os.Looper; 44 import android.os.OutcomeReceiver; 45 import android.os.RemoteException; 46 import android.os.StrictMode; 47 import android.preference.PreferenceManager; 48 import android.text.InputType; 49 import android.util.Log; 50 import android.view.SurfaceControlViewHost.SurfacePackage; 51 import android.view.SurfaceView; 52 import android.view.View; 53 import android.widget.Button; 54 import android.widget.EditText; 55 import android.widget.LinearLayout; 56 import android.widget.Toast; 57 58 import com.android.modules.utils.BackgroundThread; 59 import com.android.modules.utils.build.SdkLevel; 60 61 import java.util.Set; 62 63 public class MainActivity extends Activity { 64 // TODO(b/253202014): Add toggle button 65 private static final Boolean IS_WEBVIEW_TESTING_ENABLED = false; 66 private static final String SDK_NAME = 67 IS_WEBVIEW_TESTING_ENABLED 68 ? "com.android.sdksandboxcode_webview" 69 : "com.android.sdksandboxcode"; 70 private static final String MEDIATEE_SDK_NAME = "com.android.sdksandboxcode_mediatee"; 71 private static final String TAG = "SdkSandboxClientMainActivity"; 72 73 private static final String VIEW_TYPE_KEY = "view-type"; 74 private static final String VIDEO_VIEW_VALUE = "video-view"; 75 private static final String VIDEO_URL_KEY = "video-url"; 76 77 private static final Handler sHandler = new Handler(Looper.getMainLooper()); 78 private static final String EXTRA_SDK_SDK_ENABLED_KEY = "sdkSdkCommEnabled"; 79 80 private static String sVideoUrl; 81 82 private boolean mSdksLoaded = false; 83 private boolean mSdkSdkCommEnabled = false; 84 private SdkSandboxManager mSdkSandboxManager; 85 86 private Button mLoadButton; 87 private Button mRenderButton; 88 private Button mCreateFileButton; 89 private Button mPlayVideoButton; 90 private Button mSyncKeysButton; 91 private Button mSdkSdkCommButton; 92 private Button mStartActivity; 93 94 private SurfaceView mRenderedView; 95 96 private SandboxedSdk mSandboxedSdk; 97 98 @Override onCreate(Bundle savedInstanceState)99 public void onCreate(Bundle savedInstanceState) { 100 enableStrictMode(); 101 super.onCreate(savedInstanceState); 102 setContentView(R.layout.activity_main); 103 mSdkSandboxManager = getApplicationContext().getSystemService(SdkSandboxManager.class); 104 Bundle extras = getIntent().getExtras(); 105 if (extras != null) { 106 sVideoUrl = extras.getString(VIDEO_URL_KEY); 107 } 108 109 mRenderedView = findViewById(R.id.rendered_view); 110 mRenderedView.setZOrderOnTop(true); 111 mRenderedView.setVisibility(View.INVISIBLE); 112 113 mLoadButton = findViewById(R.id.load_code_button); 114 mRenderButton = findViewById(R.id.request_surface_button); 115 mCreateFileButton = findViewById(R.id.create_file_button); 116 mPlayVideoButton = findViewById(R.id.play_video_button); 117 mSyncKeysButton = findViewById(R.id.sync_keys_button); 118 mSdkSdkCommButton = findViewById(R.id.enable_sdk_sdk_button); 119 mStartActivity = findViewById(R.id.start_activity); 120 121 registerLoadSdkProviderButton(); 122 registerLoadSurfacePackageButton(); 123 registerCreateFileButton(); 124 registerPlayVideoButton(); 125 registerSyncKeysButton(); 126 registerSdkSdkButton(); 127 registerStartActivityButton(); 128 } 129 registerLoadSdkProviderButton()130 private void registerLoadSdkProviderButton() { 131 mLoadButton.setOnClickListener( 132 v -> { 133 if (mSdksLoaded) { 134 resetStateForLoadSdkButton(); 135 return; 136 } 137 // Register for sandbox death event. 138 mSdkSandboxManager.addSdkSandboxProcessDeathCallback( 139 Runnable::run, () -> toastAndLog(ERROR, "Sdk Sandbox process died")); 140 141 Bundle params = new Bundle(); 142 OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver = 143 new OutcomeReceiver<SandboxedSdk, LoadSdkException>() { 144 @Override 145 public void onResult(SandboxedSdk sandboxedSdk) { 146 mSdksLoaded = true; 147 mSandboxedSdk = sandboxedSdk; 148 toastAndLog(INFO, "First SDK Loaded successfully!"); 149 } 150 151 @Override 152 public void onError(LoadSdkException error) { 153 toastAndLog(ERROR, "Failed to load first SDK: %s", error); 154 } 155 }; 156 OutcomeReceiver<SandboxedSdk, LoadSdkException> mediateeReceiver = 157 new OutcomeReceiver<SandboxedSdk, LoadSdkException>() { 158 @Override 159 public void onResult(SandboxedSdk sandboxedSdk) { 160 toastAndLog(INFO, "All SDKs Loaded successfully!"); 161 mLoadButton.setText("Unload SDKs"); 162 } 163 164 @Override 165 public void onError(LoadSdkException error) { 166 toastAndLog(ERROR, "Failed to load all SDKs: %s", error); 167 resetStateForLoadSdkButton(); 168 } 169 }; 170 Log.i(TAG, "Loading SDKs " + SDK_NAME + " and " + MEDIATEE_SDK_NAME); 171 mSdkSandboxManager.loadSdk(SDK_NAME, params, Runnable::run, receiver); 172 mSdkSandboxManager.loadSdk( 173 MEDIATEE_SDK_NAME, params, Runnable::run, mediateeReceiver); 174 }); 175 } 176 resetStateForLoadSdkButton()177 private void resetStateForLoadSdkButton() { 178 Log.i(TAG, "Unloading SDKs " + SDK_NAME + " and " + MEDIATEE_SDK_NAME); 179 mSdkSandboxManager.unloadSdk(SDK_NAME); 180 mSdkSandboxManager.unloadSdk(MEDIATEE_SDK_NAME); 181 mLoadButton.setText("Load SDKs"); 182 mSdksLoaded = false; 183 } 184 registerLoadSurfacePackageButton()185 private void registerLoadSurfacePackageButton() { 186 OutcomeReceiver<Bundle, RequestSurfacePackageException> receiver = 187 new RequestSurfacePackageReceiver(); 188 mRenderButton.setOnClickListener( 189 v -> { 190 if (mSdksLoaded) { 191 sHandler.post( 192 () -> { 193 mSdkSandboxManager.requestSurfacePackage( 194 SDK_NAME, 195 getRequestSurfacePackageParams(), 196 Runnable::run, 197 receiver); 198 }); 199 } else { 200 toastAndLog(WARN, "Sdk is not loaded"); 201 } 202 }); 203 } 204 registerCreateFileButton()205 private void registerCreateFileButton() { 206 mCreateFileButton.setOnClickListener( 207 v -> { 208 if (!mSdksLoaded) { 209 toastAndLog(WARN, "Sdk is not loaded"); 210 return; 211 } 212 AlertDialog.Builder builder = new AlertDialog.Builder(this); 213 builder.setTitle("Set size in MB (1-100)"); 214 final EditText input = new EditText(this); 215 input.setInputType(InputType.TYPE_CLASS_NUMBER); 216 builder.setView(input); 217 builder.setPositiveButton( 218 "Create", 219 (dialog, which) -> { 220 final int sizeInMb = Integer.parseInt(input.getText().toString()); 221 if (sizeInMb <= 0 || sizeInMb > 100) { 222 toastAndLog(WARN, "Please provide a value between 1 and 100"); 223 return; 224 } 225 IBinder binder = mSandboxedSdk.getInterface(); 226 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 227 228 BackgroundThread.getExecutor() 229 .execute( 230 () -> { 231 try { 232 String response = 233 sdkApi.createFile(sizeInMb); 234 toastAndLog(INFO, response); 235 } catch (Exception e) { 236 toastAndLog( 237 e, 238 "Failed to create file with %d Mb", 239 sizeInMb); 240 } 241 }); 242 }); 243 builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 244 builder.show(); 245 }); 246 } 247 registerPlayVideoButton()248 private void registerPlayVideoButton() { 249 if (sVideoUrl == null) { 250 mPlayVideoButton.setVisibility(View.GONE); 251 return; 252 } 253 254 OutcomeReceiver<Bundle, RequestSurfacePackageException> receiver = 255 new RequestSurfacePackageReceiver(); 256 mPlayVideoButton.setOnClickListener( 257 v -> { 258 if (mSdksLoaded) { 259 sHandler.post( 260 () -> { 261 Bundle params = getRequestSurfacePackageParams(); 262 params.putString(VIEW_TYPE_KEY, VIDEO_VIEW_VALUE); 263 params.putString(VIDEO_URL_KEY, sVideoUrl); 264 mSdkSandboxManager.requestSurfacePackage( 265 SDK_NAME, params, Runnable::run, receiver); 266 }); 267 } else { 268 toastAndLog(WARN, "Sdk is not loaded"); 269 } 270 }); 271 } 272 registerSdkSdkButton()273 private void registerSdkSdkButton() { 274 mSdkSdkCommButton.setOnClickListener( 275 v -> { 276 mSdkSdkCommEnabled = !mSdkSdkCommEnabled; 277 if (mSdkSdkCommEnabled) { 278 mSdkSdkCommButton.setText("Disable SDK SDK comm"); 279 toastAndLog(INFO, "Sdk Sdk Comm Enabled"); 280 } else { 281 mSdkSdkCommButton.setText("Enable SDK SDK comm"); 282 toastAndLog(INFO, "Sdk Sdk Comm Disabled"); 283 } 284 }); 285 } 286 registerSyncKeysButton()287 private void registerSyncKeysButton() { 288 mSyncKeysButton.setOnClickListener( 289 v -> { 290 if (!mSdksLoaded) { 291 toastAndLog(WARN, "Sdk is not loaded"); 292 return; 293 } 294 295 final AlertDialog.Builder alert = new AlertDialog.Builder(this); 296 297 alert.setTitle("Set the key and value to sync"); 298 LinearLayout linearLayout = new LinearLayout(this); 299 linearLayout.setOrientation(1); // 1 is for vertical orientation 300 final EditText inputKey = new EditText(this); 301 inputKey.setText("key"); 302 final EditText inputValue = new EditText(this); 303 inputValue.setText("value"); 304 linearLayout.addView(inputKey); 305 linearLayout.addView(inputValue); 306 alert.setView(linearLayout); 307 308 alert.setPositiveButton( 309 "Sync", 310 (dialog, which) -> { 311 onSyncKeyPressed(inputKey, inputValue); 312 }); 313 alert.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 314 alert.show(); 315 }); 316 } 317 onSyncKeyPressed(EditText inputKey, EditText inputValue)318 private void onSyncKeyPressed(EditText inputKey, EditText inputValue) { 319 BackgroundThread.getHandler() 320 .post( 321 () -> { 322 final SharedPreferences pref = 323 PreferenceManager.getDefaultSharedPreferences( 324 getApplicationContext()); 325 String keyToSync = inputKey.getText().toString(); 326 String valueToSync = inputValue.getText().toString(); 327 pref.edit().putString(keyToSync, valueToSync).commit(); 328 mSdkSandboxManager.addSyncedSharedPreferencesKeys(Set.of(keyToSync)); 329 IBinder binder = mSandboxedSdk.getInterface(); 330 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 331 try { 332 // Allow some time for data to sync 333 Thread.sleep(1000); 334 String syncedKeysValue = 335 sdkApi.getSyncedSharedPreferencesString(keyToSync); 336 if (syncedKeysValue.equals(valueToSync)) { 337 toastAndLog( 338 INFO, 339 "Key was synced successfully\n" 340 + "Key is : %s Value is : %s", 341 keyToSync, 342 syncedKeysValue); 343 } else { 344 toastAndLog(WARN, "Key was not synced"); 345 } 346 } catch (Exception e) { 347 toastAndLog(e, "Failed to sync keys (%s)", keyToSync); 348 } 349 }); 350 } 351 registerStartActivityButton()352 private void registerStartActivityButton() { 353 mStartActivity.setOnClickListener( 354 v -> { 355 if (!mSdksLoaded) { 356 toastAndLog(WARN, "Sdk is not loaded"); 357 return; 358 } 359 if (!SdkLevel.isAtLeastU()) { 360 toastAndLog(WARN, "Device should have Android U or above!"); 361 return; 362 } 363 IBinder binder = mSandboxedSdk.getInterface(); 364 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 365 ActivityStarter starter = new ActivityStarter(this, mSdkSandboxManager); 366 try { 367 sdkApi.startActivity(starter); 368 toastAndLog(INFO, "Started activity %s", starter); 369 370 } catch (RemoteException e) { 371 toastAndLog(e, "Failed to startActivity (%s)", starter); 372 } 373 }); 374 } 375 getRequestSurfacePackageParams()376 private Bundle getRequestSurfacePackageParams() { 377 Bundle params = new Bundle(); 378 params.putInt(EXTRA_WIDTH_IN_PIXELS, mRenderedView.getWidth()); 379 params.putInt(EXTRA_HEIGHT_IN_PIXELS, mRenderedView.getHeight()); 380 params.putInt(EXTRA_DISPLAY_ID, getDisplay().getDisplayId()); 381 params.putBinder(EXTRA_HOST_TOKEN, mRenderedView.getHostToken()); 382 params.putBoolean(EXTRA_SDK_SDK_ENABLED_KEY, mSdkSdkCommEnabled); 383 return params; 384 } 385 toastAndLog(int logLevel, String fmt, Object... args)386 private void toastAndLog(int logLevel, String fmt, Object... args) { 387 String message = String.format(fmt, args); 388 switch (logLevel) { 389 case DEBUG: 390 Log.d(TAG, message); 391 break; 392 case ERROR: 393 Log.e(TAG, message); 394 break; 395 case INFO: 396 Log.i(TAG, message); 397 break; 398 case VERBOSE: 399 Log.v(TAG, message); 400 break; 401 case WARN: 402 Log.w(TAG, message); 403 break; 404 default: 405 Log.w(TAG, "Invalid log level " + logLevel + " for message: " + message); 406 } 407 makeToast(message); 408 } 409 toastAndLog(Exception e, String fmt, Object... args)410 private void toastAndLog(Exception e, String fmt, Object... args) { 411 String message = String.format(fmt, args); 412 Log.e(TAG, message, e); 413 makeToast(message); 414 } 415 makeToast(CharSequence message)416 private void makeToast(CharSequence message) { 417 runOnUiThread(() -> Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show()); 418 } 419 420 private class RequestSurfacePackageReceiver 421 implements OutcomeReceiver<Bundle, RequestSurfacePackageException> { 422 423 @Override onResult(Bundle result)424 public void onResult(Bundle result) { 425 sHandler.post( 426 () -> { 427 SurfacePackage surfacePackage = 428 result.getParcelable(EXTRA_SURFACE_PACKAGE, SurfacePackage.class); 429 mRenderedView.setChildSurfacePackage(surfacePackage); 430 mRenderedView.setVisibility(View.VISIBLE); 431 }); 432 toastAndLog(INFO, "Rendered surface view"); 433 } 434 435 @Override onError(@onNull RequestSurfacePackageException error)436 public void onError(@NonNull RequestSurfacePackageException error) { 437 toastAndLog(ERROR, "Failed: %s", error.getMessage()); 438 } 439 } 440 441 private static final class ActivityStarter extends IActivityStarter.Stub { 442 private final Activity mActivity; 443 private final SdkSandboxManager mSdkSandboxManager; 444 ActivityStarter(Activity activity, SdkSandboxManager manager)445 ActivityStarter(Activity activity, SdkSandboxManager manager) { 446 this.mActivity = activity; 447 this.mSdkSandboxManager = manager; 448 } 449 450 @Override startActivity(IBinder token)451 public void startActivity(IBinder token) throws RemoteException { 452 mSdkSandboxManager.startSdkSandboxActivity(mActivity, token); 453 } 454 455 @Override toString()456 public String toString() { 457 return mActivity.getComponentName().flattenToShortString(); 458 } 459 } 460 enableStrictMode()461 private void enableStrictMode() { 462 StrictMode.setThreadPolicy( 463 new StrictMode.ThreadPolicy.Builder() 464 .detectAll() 465 .penaltyLog() 466 .penaltyDeath() 467 .build()); 468 StrictMode.setVmPolicy( 469 new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().penaltyDeath().build()); 470 } 471 } 472