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.AppOwnedSdkSandboxInterface; 34 import android.app.sdksandbox.LoadSdkException; 35 import android.app.sdksandbox.RequestSurfacePackageException; 36 import android.app.sdksandbox.SandboxedSdk; 37 import android.app.sdksandbox.SdkSandboxManager; 38 import android.app.sdksandbox.SdkSandboxManager.SdkSandboxProcessDeathCallback; 39 import android.app.sdksandbox.interfaces.IActivityStarter; 40 import android.app.sdksandbox.interfaces.ISdkApi; 41 import android.content.Context; 42 import android.content.Intent; 43 import android.content.SharedPreferences; 44 import android.content.pm.PackageInfo; 45 import android.content.pm.PackageManager; 46 import android.os.Bundle; 47 import android.os.Handler; 48 import android.os.IBinder; 49 import android.os.Looper; 50 import android.os.OutcomeReceiver; 51 import android.os.ParcelFileDescriptor; 52 import android.os.RemoteException; 53 import android.os.StrictMode; 54 import android.text.InputType; 55 import android.util.Log; 56 import android.view.SurfaceControlViewHost.SurfacePackage; 57 import android.view.SurfaceView; 58 import android.view.View; 59 import android.view.ViewGroup; 60 import android.widget.ArrayAdapter; 61 import android.widget.Button; 62 import android.widget.EditText; 63 import android.widget.ImageButton; 64 import android.widget.LinearLayout; 65 import android.widget.Spinner; 66 import android.widget.TextView; 67 68 import androidx.appcompat.app.AppCompatActivity; 69 import androidx.preference.PreferenceManager; 70 71 import com.android.modules.utils.BackgroundThread; 72 import com.android.modules.utils.build.SdkLevel; 73 74 import com.google.android.material.snackbar.BaseTransientBottomBar; 75 import com.google.android.material.snackbar.Snackbar; 76 77 import java.io.File; 78 import java.io.FileInputStream; 79 import java.io.FileOutputStream; 80 import java.nio.charset.StandardCharsets; 81 import java.util.LinkedList; 82 import java.util.Queue; 83 import java.util.Set; 84 import java.util.Stack; 85 import java.util.concurrent.Executor; 86 import java.util.concurrent.Executors; 87 88 public class MainActivity extends AppCompatActivity { 89 private static final String SDK_NAME = "com.android.sdksandboxcode"; 90 private static final String MEDIATEE_SDK_NAME = "com.android.sdksandboxcode_mediatee"; 91 private static final String TAG = "SdkSandboxClientMainActivity"; 92 93 private static final String VIEW_TYPE_KEY = "view-type"; 94 private static final String VIDEO_VIEW_VALUE = "video-view"; 95 private static final String VIDEO_URL_KEY = "video-url"; 96 private static final String VIEW_TYPE_INFLATED_VIEW = "view-type-inflated-view"; 97 private static final String VIEW_TYPE_WEBVIEW = "view-type-webview"; 98 private static final String VIEW_TYPE_AD_REFRESH = "view-type-ad-refresh"; 99 private static final String VIEW_TYPE_EDITTEXT = "view-type-edittext"; 100 101 private static final String ON_CLICK_BEHAVIOUR_TYPE_KEY = "on-click-behavior"; 102 private static final String ON_CLICK_OPEN_CHROME = "on-click-open-chrome"; 103 private static final String ON_CLICK_OPEN_PACKAGE = "on-click-open-package"; 104 private static final String PACKAGE_TO_OPEN_KEY = "package-to-open"; 105 106 private static final Handler sHandler = new Handler(Looper.getMainLooper()); 107 private static final String EXTRA_SDK_SDK_ENABLED_KEY = "sdkSdkCommEnabled"; 108 private static final String DROPDOWN_KEY_SDK_SANDBOX = "SDK_IN_SANDBOX"; 109 private static final String DROPDOWN_KEY_SDK_APP = "SDK_IN_APP"; 110 private static final String APP_OWNED_SDK_NAME = "app-sdk-1"; 111 112 // Saved instance state keys 113 private static final String SDKS_LOADED_KEY = "sdks_loaded"; 114 private static final String APP_OWNED_INTERFACE_REGISTERED = "app-owned-interface_registered"; 115 private static final String CUSTOMIZED_SDK_CONTEXT_ENABLED = "customized_sdk_context_enabled"; 116 private static final String SANDBOXED_SDK_BINDER = "com.android.sdksandboxclient.SANDBOXED_SDK"; 117 private static final String SANDBOXED_SDK_KEY = 118 "com.android.sdksandboxclient.SANDBOXED_SDK_KEY"; 119 private static final String DEATH_CALLBACKS_COUNT_KEY = 120 "com.android.sdksandboxclient.DEATH_CALLBACKS_COUNT_KEY"; 121 public static final int SNACKBAR_MAX_LINES = 4; 122 123 private Bundle mSavedInstanceState = new Bundle(); 124 private boolean mSdksLoaded = false; 125 private boolean mSdkToSdkCommEnabled = false; 126 private SdkSandboxManager mSdkSandboxManager; 127 private final Executor mExecutor = Executors.newSingleThreadExecutor(); 128 129 private View mRootLayout; 130 131 private Button mResetPreferencesButton; 132 private Button mLoadSdksButton; 133 private Button mDeathCallbackAddButton; 134 private Button mDeathCallbackRemoveButton; 135 private Button mNewBannerAdButton; 136 private ImageButton mBannerAdOptionsButton; 137 private Button mCreateFileButton; 138 private Button mSyncKeysButton; 139 private Button mSdkToSdkCommButton; 140 private Button mDumpSandboxButton; 141 private Button mNewFullScreenAd; 142 private Button mNewAppWebviewButton; 143 private Button mNewAppVideoButton; 144 private Button mReleaseAllSurfaceControlViewHostButton; 145 146 private SurfaceView mInScrollBannerView; 147 private SurfaceView mBottomBannerView; 148 149 private SandboxedSdk mSandboxedSdk; 150 private SharedPreferences mSharedPreferences; 151 private final Stack<SdkSandboxProcessDeathCallback> mDeathCallbacks = new Stack<>(); 152 private final Queue<SurfacePackage> mSurfacePackages = new LinkedList<>(); 153 154 @Override onCreate(Bundle savedInstanceState)155 public void onCreate(Bundle savedInstanceState) { 156 // TODO(b/294188354): This is temporarily disabled to unblock testing. Re-enable later. 157 // enableStrictMode(); 158 super.onCreate(savedInstanceState); 159 160 setAppTitle(); 161 162 mSdkSandboxManager = getApplicationContext().getSystemService(SdkSandboxManager.class); 163 if (savedInstanceState != null) { 164 mSavedInstanceState.putAll(savedInstanceState); 165 mSdksLoaded = savedInstanceState.getBoolean(SDKS_LOADED_KEY); 166 mSandboxedSdk = savedInstanceState.getParcelable(SANDBOXED_SDK_KEY); 167 int numDeathCallbacks = savedInstanceState.getInt(DEATH_CALLBACKS_COUNT_KEY); 168 for (int i = 0; i < numDeathCallbacks; i++) { 169 addDeathCallback(false); 170 } 171 } 172 173 mExecutor.execute( 174 () -> { 175 Looper.prepare(); 176 mSharedPreferences = 177 PreferenceManager.getDefaultSharedPreferences(MainActivity.this); 178 179 handleExtras(); 180 PreferenceManager.setDefaultValues(this, R.xml.banner_preferences, false); 181 }); 182 183 setContentView(R.layout.activity_main); 184 185 mRootLayout = findViewById(R.id.root_layout); 186 187 mBottomBannerView = findViewById(R.id.bottom_banner_view); 188 mBottomBannerView.setZOrderOnTop(true); 189 mBottomBannerView.setVisibility(View.INVISIBLE); 190 191 mInScrollBannerView = findViewById(R.id.in_scroll_banner_view); 192 mInScrollBannerView.setZOrderOnTop(true); 193 mInScrollBannerView.setVisibility(View.INVISIBLE); 194 195 mResetPreferencesButton = findViewById(R.id.reset_preferences_button); 196 mLoadSdksButton = findViewById(R.id.load_sdks_button); 197 mReleaseAllSurfaceControlViewHostButton = findViewById(R.id.release_all_scvh_button); 198 199 mDeathCallbackAddButton = findViewById(R.id.add_death_callback_button); 200 mDeathCallbackRemoveButton = findViewById(R.id.remove_death_callback_button); 201 202 mNewBannerAdButton = findViewById(R.id.new_banner_ad_button); 203 mBannerAdOptionsButton = findViewById(R.id.banner_ad_options_button); 204 mNewFullScreenAd = findViewById(R.id.new_fullscreen_ad_button); 205 206 mCreateFileButton = findViewById(R.id.create_file_button); 207 mSyncKeysButton = findViewById(R.id.sync_keys_button); 208 mSdkToSdkCommButton = findViewById(R.id.enable_sdk_sdk_button); 209 mDumpSandboxButton = findViewById(R.id.dump_sandbox_button); 210 mNewAppWebviewButton = findViewById(R.id.new_app_webview_button); 211 mNewAppVideoButton = findViewById(R.id.new_app_video_button); 212 213 configureFeatureFlagSection(); 214 215 registerResetPreferencesButton(); 216 registerLoadSdksButton(); 217 registerReleaseAllSurfaceControlViewHost(); 218 registerAddDeathCallbackButton(); 219 registerRemoveDeathCallbackButton(); 220 221 registerNewBannerAdButton(); 222 registerBannerAdOptionsButton(); 223 registerNewFullscreenAdButton(); 224 225 registerGetOrSendFileDescriptorButton(); 226 registerCreateFileButton(); 227 registerSyncKeysButton(); 228 registerSdkToSdkButton(); 229 registerDumpSandboxButton(); 230 registerNewAppWebviewButton(); 231 registerNewAppVideoButton(); 232 233 refreshLoadSdksButtonText(); 234 } 235 236 @Override onStart()237 public void onStart() { 238 super.onStart(); 239 /* 240 Resume video when app is active. 241 TODO (b/314953975) Should be handled on SDK side: 242 1) (after adding Client App state API): Resume when app is foreground 243 2) (after migration to ui-lib Visibility): Resume when PlayerView is visible 244 */ 245 withSdkApiIfLoaded(ISdkApi::notifyMainActivityStarted); 246 } 247 248 @Override onStop()249 public void onStop() { 250 super.onStop(); 251 /* 252 Pause video when app is not active. 253 TODO (b/314953975) Should be handled on SDK side: 254 1) (after adding Client App state API): Pause when app is background 255 2) (after migration to ui-lib Visibility): Pause when PlayerView is not visible 256 */ 257 withSdkApiIfLoaded(ISdkApi::notifyMainActivityStopped); 258 } 259 registerResetPreferencesButton()260 private void registerResetPreferencesButton() { 261 mResetPreferencesButton.setOnClickListener( 262 v -> 263 mExecutor.execute( 264 () -> { 265 mSharedPreferences.edit().clear().commit(); 266 PreferenceManager.setDefaultValues( 267 this, R.xml.banner_preferences, true); 268 })); 269 } 270 setAppTitle()271 private void setAppTitle() { 272 try { 273 final PackageInfo packageInfo = 274 getPackageManager().getPackageInfo(getPackageName(), /*flags=*/ 0); 275 final String versionName = packageInfo.versionName; 276 setTitle( 277 String.format( 278 "%s (%s)", 279 getResources().getString(R.string.title_activity_main), versionName)); 280 } catch (PackageManager.NameNotFoundException e) { 281 Log.e(TAG, "Could not find package " + getPackageName()); 282 } 283 } 284 handleExtras()285 private void handleExtras() { 286 Bundle extras = getIntent().getExtras(); 287 if (extras != null) { 288 final String videoUrl = extras.getString(VIDEO_URL_KEY); 289 mSharedPreferences.edit().putString("banner_video_url", videoUrl).apply(); 290 final String packageToOpen = extras.getString(PACKAGE_TO_OPEN_KEY); 291 mSharedPreferences.edit().putString("package_to_open", packageToOpen).apply(); 292 } 293 } 294 configureFeatureFlagSection()295 private void configureFeatureFlagSection() { 296 TextView featureFlagStatus = findViewById(R.id.feature_flags_status); 297 if(!mSdksLoaded) { 298 featureFlagStatus.setText("Load SDK to fetch status of feature flags"); 299 return; 300 } 301 302 if (!mSavedInstanceState.containsKey(CUSTOMIZED_SDK_CONTEXT_ENABLED)) { 303 try { 304 IBinder binder = mSandboxedSdk.getInterface(); 305 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 306 boolean result = sdkApi.isCustomizedSdkContextEnabled(); 307 mSavedInstanceState.putBoolean(CUSTOMIZED_SDK_CONTEXT_ENABLED, result); 308 } catch (RemoteException e) { 309 logAndDisplayMessage(e, "Failed to fetch feature flag status: %s", e); 310 } 311 } 312 313 boolean result = mSavedInstanceState.getBoolean(CUSTOMIZED_SDK_CONTEXT_ENABLED); 314 featureFlagStatus.post( 315 () -> { 316 featureFlagStatus.setText("CustomizedSdkContext Enabled: " + result); 317 }); 318 } 319 refreshLoadSdksButtonText()320 private void refreshLoadSdksButtonText() { 321 if (mSdksLoaded) { 322 mLoadSdksButton.post(() -> mLoadSdksButton.setText("Unload SDKs")); 323 } else { 324 mLoadSdksButton.post(() -> mLoadSdksButton.setText("Load SDKs")); 325 } 326 } 327 328 @Override onSaveInstanceState(@onNull Bundle outState)329 protected void onSaveInstanceState(@NonNull Bundle outState) { 330 super.onSaveInstanceState(outState); 331 outState.putAll(mSavedInstanceState); 332 outState.putBoolean(SDKS_LOADED_KEY, mSdksLoaded); 333 outState.putParcelable(SANDBOXED_SDK_KEY, mSandboxedSdk); 334 outState.putInt(DEATH_CALLBACKS_COUNT_KEY, mDeathCallbacks.size()); 335 } 336 registerReleaseAllSurfaceControlViewHost()337 private void registerReleaseAllSurfaceControlViewHost() { 338 mReleaseAllSurfaceControlViewHostButton.setOnClickListener( 339 v -> { 340 synchronized (mSurfacePackages) { 341 if (mSurfacePackages.isEmpty()) { 342 logAndDisplayMessage(INFO, "No SCVH to release."); 343 return; 344 } 345 while (!mSurfacePackages.isEmpty()) { 346 mSurfacePackages.poll().notifyDetachedFromWindow(); 347 } 348 mInScrollBannerView.setVisibility(View.INVISIBLE); 349 mBottomBannerView.setVisibility(View.INVISIBLE); 350 // TODO (b/314953975) Should be handled in Session.close() 351 withSdkApiIfLoaded(ISdkApi::notifyMainActivityStopped); 352 logAndDisplayMessage(INFO, "All SurfaceControlViewHost Released."); 353 } 354 }); 355 } 356 registerAddDeathCallbackButton()357 private void registerAddDeathCallbackButton() { 358 mDeathCallbackAddButton.setOnClickListener( 359 v -> { 360 synchronized (mDeathCallbacks) { 361 addDeathCallback(true); 362 } 363 }); 364 } 365 registerRemoveDeathCallbackButton()366 private void registerRemoveDeathCallbackButton() { 367 mDeathCallbackRemoveButton.setOnClickListener( 368 v -> { 369 synchronized (mDeathCallbacks) { 370 if (mDeathCallbacks.isEmpty()) { 371 logAndDisplayMessage(INFO, "No death callbacks to remove."); 372 return; 373 } 374 final int queueSize = mDeathCallbacks.size(); 375 SdkSandboxProcessDeathCallback deathCallback = mDeathCallbacks.pop(); 376 mSdkSandboxManager.removeSdkSandboxProcessDeathCallback(deathCallback); 377 logAndDisplayMessage(INFO, "Death callback #" + (queueSize) + " removed."); 378 } 379 }); 380 } 381 registerLoadSdksButton()382 private void registerLoadSdksButton() { 383 mLoadSdksButton.setOnClickListener( 384 v -> { 385 if (mSdksLoaded) { 386 resetStateForLoadSdkButton(); 387 return; 388 } 389 390 Bundle params = new Bundle(); 391 OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver = 392 new OutcomeReceiver<>() { 393 @Override 394 public void onResult(SandboxedSdk sandboxedSdk) { 395 mSandboxedSdk = sandboxedSdk; 396 IBinder binder = mSandboxedSdk.getInterface(); 397 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 398 try { 399 sdkApi.loadSdkBySdk(MEDIATEE_SDK_NAME); 400 } catch (Exception error) { 401 logAndDisplayMessage( 402 ERROR, "Failed to load all SDKs: %s", error); 403 return; 404 } 405 logAndDisplayMessage(INFO, "All SDKs Loaded successfully!"); 406 mSdksLoaded = true; 407 refreshLoadSdksButtonText(); 408 configureFeatureFlagSection(); 409 } 410 411 @Override 412 public void onError(LoadSdkException error) { 413 logAndDisplayMessage( 414 ERROR, "Failed to load first SDK: %s", error); 415 } 416 }; 417 Log.i(TAG, "Loading SDKs " + SDK_NAME + " and " + MEDIATEE_SDK_NAME); 418 mSdkSandboxManager.loadSdk(SDK_NAME, params, Runnable::run, receiver); 419 }); 420 } 421 resetStateForLoadSdkButton()422 private void resetStateForLoadSdkButton() { 423 Log.i(TAG, "Unloading SDKs " + SDK_NAME + " and " + MEDIATEE_SDK_NAME); 424 mSdkSandboxManager.unloadSdk(SDK_NAME); 425 mSdkSandboxManager.unloadSdk(MEDIATEE_SDK_NAME); 426 mSdksLoaded = false; 427 refreshLoadSdksButtonText(); 428 } 429 registerNewBannerAdButton()430 private void registerNewBannerAdButton() { 431 mNewBannerAdButton.setOnClickListener( 432 v -> { 433 if (mSdksLoaded) { 434 final BannerOptions options = 435 BannerOptions.fromSharedPreferences(mSharedPreferences); 436 Log.i(TAG, options.toString()); 437 438 final SurfaceView surfaceView = 439 (options.getPlacement() == BannerOptions.Placement.BOTTOM) 440 ? mBottomBannerView 441 : mInScrollBannerView; 442 443 int adSize = 0; 444 switch (options.getAdSize()) { 445 case SMALL: 446 { 447 adSize = 80; 448 break; 449 } 450 case MEDIUM: 451 { 452 adSize = 150; 453 break; 454 } 455 case LARGE: 456 { 457 adSize = 250; 458 break; 459 } 460 } 461 if (options.getViewType().equals(BannerOptions.ViewType.WEBVIEW)) { 462 adSize = 400; 463 } 464 ViewGroup.LayoutParams svParams = surfaceView.getLayoutParams(); 465 float factor = 466 getApplicationContext().getResources().getDisplayMetrics().density; 467 svParams.height = (int) (adSize * factor); 468 surfaceView.setLayoutParams(svParams); 469 470 final OutcomeReceiver<Bundle, RequestSurfacePackageException> receiver = 471 new RequestSurfacePackageReceiver(surfaceView); 472 473 final Bundle params = getRequestSurfacePackageParams(null, surfaceView); 474 475 switch (options.getViewType()) { 476 case INFLATED: 477 { 478 params.putString(VIEW_TYPE_KEY, VIEW_TYPE_INFLATED_VIEW); 479 break; 480 } 481 case VIDEO: 482 { 483 params.putString(VIEW_TYPE_KEY, VIDEO_VIEW_VALUE); 484 params.putString(VIDEO_URL_KEY, options.getVideoUrl()); 485 break; 486 } 487 case WEBVIEW: 488 { 489 params.putString(VIEW_TYPE_KEY, VIEW_TYPE_WEBVIEW); 490 break; 491 } 492 case AD_REFRESH: 493 { 494 params.putString(VIEW_TYPE_KEY, VIEW_TYPE_AD_REFRESH); 495 break; 496 } 497 case EDITTEXT: 498 { 499 params.putString(VIEW_TYPE_KEY, VIEW_TYPE_EDITTEXT); 500 break; 501 } 502 } 503 504 switch (options.getOnClick()) { 505 case OPEN_CHROME: 506 { 507 params.putString( 508 ON_CLICK_BEHAVIOUR_TYPE_KEY, ON_CLICK_OPEN_CHROME); 509 break; 510 } 511 case OPEN_PACKAGE: 512 { 513 params.putString( 514 ON_CLICK_BEHAVIOUR_TYPE_KEY, ON_CLICK_OPEN_PACKAGE); 515 params.putString( 516 PACKAGE_TO_OPEN_KEY, options.getmPackageToOpen()); 517 break; 518 } 519 } 520 sHandler.post( 521 () -> { 522 mSdkSandboxManager.requestSurfacePackage( 523 SDK_NAME, params, Runnable::run, receiver); 524 }); 525 } else { 526 logAndDisplayMessage(WARN, "Sdk is not loaded"); 527 } 528 }); 529 } 530 registerGetOrSendFileDescriptorButton()531 private void registerGetOrSendFileDescriptorButton() { 532 final Button mGetFileDescriptorButton = findViewById(R.id.get_filedescriptor_button); 533 final Button mSendFileDescriptorButton = findViewById(R.id.send_filedescriptor_button); 534 mGetFileDescriptorButton.setOnClickListener( 535 v -> { 536 Log.i(TAG, "isGetFileDescriptorCalled = " + String.valueOf(true)); 537 onGetOrSendFileDescriptorPressed(/*isGetFileDescriptorCalled=*/ true); 538 }); 539 mSendFileDescriptorButton.setOnClickListener( 540 v -> { 541 Log.i(TAG, "isGetFileDescriptorCalled = " + String.valueOf(false)); 542 onGetOrSendFileDescriptorPressed(/*isGetFileDescriptorCalled=*/ false); 543 }); 544 } 545 onGetOrSendFileDescriptorPressed(boolean isGetFileDescriptorCalled)546 private void onGetOrSendFileDescriptorPressed(boolean isGetFileDescriptorCalled) { 547 if (!mSdksLoaded) { 548 logAndDisplayMessage(WARN, "Sdk is not loaded"); 549 return; 550 } 551 Log.i(TAG, "Ready to transfer File Descriptor between APP and SDK"); 552 553 final AlertDialog.Builder builder = new AlertDialog.Builder(this); 554 builder.setTitle("Set the value for FileDescriptor"); 555 final EditText inputValue = new EditText(this); 556 inputValue.setHint("default"); 557 builder.setView(inputValue); 558 559 builder.setPositiveButton( 560 "Transfer", 561 (dialog, which) -> { 562 BackgroundThread.getExecutor() 563 .execute( 564 () -> { 565 final String inputValueString = 566 inputValue.getText().toString(); 567 if (inputValueString.isEmpty() 568 || inputValueString.length() > 1000) { 569 logAndDisplayMessage( 570 WARN, 571 "Input string cannot be empty or" 572 + " have more than 1000" 573 + " characters. Try again."); 574 return; 575 } 576 577 String value; 578 if (isGetFileDescriptorCalled) { 579 value = onGetFileDescriptorPressed(inputValueString); 580 } else { 581 value = onSendFileDescriptorPressed(inputValueString); 582 } 583 584 String methodName = 585 isGetFileDescriptorCalled 586 ? "getFileDescriptor" 587 : "sendFileDescriptor"; 588 589 if (inputValueString.equals(value)) { 590 logAndDisplayMessage( 591 INFO, 592 methodName 593 + " transfer successful, value sent" 594 + " = " 595 + inputValueString 596 + " , value received = " 597 + value); 598 } else { 599 logAndDisplayMessage( 600 WARN, 601 methodName 602 + " transfer unsuccessful, Value" 603 + " sent = " 604 + inputValueString 605 + " , Value received = " 606 + value); 607 } 608 }); 609 }); 610 builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 611 builder.show(); 612 } 613 614 /** 615 * This method receives a fileDescriptor from the SDK and opens it using a file inputstream and 616 * then reads the characters in the file and stores it in a String to return it. 617 */ onGetFileDescriptorPressed(String inputValueString)618 private String onGetFileDescriptorPressed(String inputValueString) { 619 String value; 620 try { 621 IBinder binder = mSandboxedSdk.getInterface(); 622 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 623 ParcelFileDescriptor pFd = sdkApi.getFileDescriptor(inputValueString); 624 FileInputStream fis = new FileInputStream(pFd.getFileDescriptor()); 625 // Reading fileInputStream and adding its 626 // value to a string 627 value = new String(fis.readAllBytes(), StandardCharsets.UTF_16); 628 fis.close(); 629 pFd.close(); 630 return value; 631 } catch (Exception e) { 632 logAndDisplayMessage(ERROR, "Failed to get FileDescriptor: %s", e); 633 } 634 return ""; 635 } 636 637 /** 638 * This method generates a file outputstream in the App and sends the generated FileDescriptor 639 * to SDK to parse it and then receives the parsed value from the SDK and returns it. 640 */ onSendFileDescriptorPressed(String inputValueString)641 private String onSendFileDescriptorPressed(String inputValueString) { 642 try { 643 final String fileName = "testParcelFileDescriptor"; 644 FileOutputStream fout = 645 getApplicationContext().openFileOutput(fileName, Context.MODE_PRIVATE); 646 // Writing inputValue String to a file 647 fout.write(inputValueString.getBytes(StandardCharsets.UTF_16)); 648 fout.close(); 649 File file = new File(getApplicationContext().getFilesDir(), fileName); 650 ParcelFileDescriptor pFd = 651 ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 652 IBinder binder = mSandboxedSdk.getInterface(); 653 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 654 String parsedValue = sdkApi.parseFileDescriptor(pFd); 655 pFd.close(); 656 return parsedValue; 657 } catch (Exception e) { 658 logAndDisplayMessage(ERROR, "Failed to send FileDescriptor: %s", e); 659 } 660 return ""; 661 } 662 registerBannerAdOptionsButton()663 private void registerBannerAdOptionsButton() { 664 mBannerAdOptionsButton.setOnClickListener( 665 v -> startActivity(new Intent(MainActivity.this, BannerOptionsActivity.class))); 666 } 667 registerCreateFileButton()668 private void registerCreateFileButton() { 669 mCreateFileButton.setOnClickListener( 670 v -> { 671 if (!mSdksLoaded) { 672 logAndDisplayMessage(WARN, "Sdk is not loaded"); 673 return; 674 } 675 AlertDialog.Builder builder = new AlertDialog.Builder(this); 676 builder.setTitle("Set size in MB (1-100)"); 677 final EditText input = new EditText(this); 678 input.setInputType(InputType.TYPE_CLASS_NUMBER); 679 builder.setView(input); 680 builder.setPositiveButton( 681 "Create", 682 (dialog, which) -> { 683 final String inputString = input.getText().toString(); 684 if (inputString.isEmpty() 685 || inputString.length() > 3 686 || Integer.parseInt(inputString) <= 0 687 || Integer.parseInt(inputString) > 100) { 688 logAndDisplayMessage( 689 WARN, "Please provide a value between 1 and 100"); 690 return; 691 } 692 final Integer sizeInMb = Integer.parseInt(inputString); 693 IBinder binder = mSandboxedSdk.getInterface(); 694 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 695 696 BackgroundThread.getExecutor() 697 .execute( 698 () -> { 699 try { 700 String response = 701 sdkApi.createFile(sizeInMb); 702 logAndDisplayMessage(INFO, response); 703 } catch (Exception e) { 704 logAndDisplayMessage( 705 e, 706 "Failed to create file with %d Mb", 707 sizeInMb); 708 } 709 }); 710 }); 711 builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 712 builder.show(); 713 }); 714 } 715 registerSdkToSdkButton()716 private void registerSdkToSdkButton() { 717 mSdkToSdkCommButton.setOnClickListener( 718 v -> { 719 mSdkToSdkCommEnabled = !mSdkToSdkCommEnabled; 720 if (mSdkToSdkCommEnabled) { 721 mSdkToSdkCommButton.setText("Disable SDK to SDK comm"); 722 logAndDisplayMessage(INFO, "Sdk Sdk Comm Enabled"); 723 AlertDialog.Builder builder = new AlertDialog.Builder(this); 724 builder.setTitle("Choose winning SDK"); 725 726 String[] items = 727 new String[] {DROPDOWN_KEY_SDK_SANDBOX, DROPDOWN_KEY_SDK_APP}; 728 ArrayAdapter<String> adapter = 729 new ArrayAdapter<>( 730 this, android.R.layout.simple_spinner_dropdown_item, items); 731 final Spinner dropdown = new Spinner(this); 732 dropdown.setAdapter(adapter); 733 734 LinearLayout linearLayout = new LinearLayout(this); 735 linearLayout.setOrientation(1); // 1 is for vertical orientation 736 linearLayout.addView(dropdown); 737 builder.setView(linearLayout); 738 739 builder.setPositiveButton( 740 "Request SP", 741 (dialog, which) -> { 742 final SurfaceView view = mBottomBannerView; 743 OutcomeReceiver<Bundle, RequestSurfacePackageException> 744 receiver = new RequestSurfacePackageReceiver(view); 745 final String dropDownKey = 746 dropdown.getSelectedItem().toString(); 747 if (dropDownKey.equals(DROPDOWN_KEY_SDK_APP)) { 748 if (!mSavedInstanceState.containsKey( 749 APP_OWNED_INTERFACE_REGISTERED)) { 750 // Register AppOwnedSdkInterface when activity first 751 // created 752 // TODO(b/284281064) : We should be checking sdk 753 // extension here 754 mSdkSandboxManager.registerAppOwnedSdkSandboxInterface( 755 new AppOwnedSdkSandboxInterface( 756 APP_OWNED_SDK_NAME, 757 (long) 1.01, 758 new AppOwnedSdkApi())); 759 mSavedInstanceState.putBoolean( 760 APP_OWNED_INTERFACE_REGISTERED, true); 761 } 762 } 763 mSdkSandboxManager.requestSurfacePackage( 764 SDK_NAME, 765 getRequestSurfacePackageParams(dropDownKey, view), 766 Runnable::run, 767 receiver); 768 }); 769 builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 770 builder.show(); 771 } else { 772 mSdkToSdkCommButton.setText("Enable SDK to SDK comm"); 773 logAndDisplayMessage(INFO, "Sdk Sdk Comm Disabled"); 774 } 775 }); 776 } 777 registerDumpSandboxButton()778 private void registerDumpSandboxButton() { 779 mDumpSandboxButton.setOnClickListener( 780 v -> { 781 if (!mSdksLoaded) { 782 logAndDisplayMessage(WARN, "Sdk is not loaded"); 783 return; 784 } 785 786 IBinder binder = mSandboxedSdk.getInterface(); 787 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 788 String sandboxDump = ""; 789 try { 790 sandboxDump = sdkApi.getSandboxDump(); 791 } catch (RemoteException e) { 792 // Do nothing, the correct text won't be displayed. 793 } 794 new AlertDialog.Builder(this) 795 .setTitle("Information provided by the sandbox") 796 .setMessage(sandboxDump) 797 .setNegativeButton("Cancel", null) 798 .show(); 799 }); 800 } 801 registerSyncKeysButton()802 private void registerSyncKeysButton() { 803 mSyncKeysButton.setOnClickListener( 804 v -> { 805 if (!mSdksLoaded) { 806 logAndDisplayMessage(WARN, "Sdk is not loaded"); 807 return; 808 } 809 810 final AlertDialog.Builder alert = new AlertDialog.Builder(this); 811 812 alert.setTitle("Set the key and value to sync"); 813 LinearLayout linearLayout = new LinearLayout(this); 814 linearLayout.setOrientation(1); // 1 is for vertical orientation 815 final EditText inputKey = new EditText(this); 816 inputKey.setHint("key"); 817 final EditText inputValue = new EditText(this); 818 inputValue.setHint("value"); 819 linearLayout.addView(inputKey); 820 linearLayout.addView(inputValue); 821 alert.setView(linearLayout); 822 823 alert.setPositiveButton( 824 "Sync", 825 (dialog, which) -> { 826 onSyncKeyPressed(inputKey, inputValue); 827 }); 828 alert.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); 829 alert.show(); 830 }); 831 } 832 onSyncKeyPressed(EditText inputKey, EditText inputValue)833 private void onSyncKeyPressed(EditText inputKey, EditText inputValue) { 834 BackgroundThread.getHandler() 835 .post( 836 () -> { 837 final SharedPreferences pref = 838 PreferenceManager.getDefaultSharedPreferences( 839 getApplicationContext()); 840 String keyToSync = inputKey.getText().toString(); 841 String valueToSync = inputValue.getText().toString(); 842 pref.edit().putString(keyToSync, valueToSync).commit(); 843 mSdkSandboxManager.addSyncedSharedPreferencesKeys(Set.of(keyToSync)); 844 IBinder binder = mSandboxedSdk.getInterface(); 845 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 846 try { 847 // Allow some time for data to sync 848 Thread.sleep(1000); 849 String syncedKeysValue = 850 sdkApi.getSyncedSharedPreferencesString(keyToSync); 851 if (syncedKeysValue.equals(valueToSync)) { 852 logAndDisplayMessage( 853 INFO, 854 "Key was synced successfully\n" 855 + "Key is : %s Value is : %s", 856 keyToSync, 857 syncedKeysValue); 858 } else { 859 logAndDisplayMessage(WARN, "Key was not synced"); 860 } 861 } catch (Exception e) { 862 logAndDisplayMessage(e, "Failed to sync keys (%s)", keyToSync); 863 } 864 }); 865 } 866 registerNewFullscreenAdButton()867 private void registerNewFullscreenAdButton() { 868 mNewFullScreenAd.setOnClickListener( 869 v -> { 870 if (!mSdksLoaded) { 871 logAndDisplayMessage(WARN, "Sdk is not loaded"); 872 return; 873 } 874 if (!SdkLevel.isAtLeastU()) { 875 logAndDisplayMessage(WARN, "Device should have Android U or above!"); 876 return; 877 } 878 IBinder binder = mSandboxedSdk.getInterface(); 879 ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 880 ActivityStarter starter = new ActivityStarter(this, mSdkSandboxManager); 881 882 final BannerOptions options = 883 BannerOptions.fromSharedPreferences(mSharedPreferences); 884 Bundle params = new Bundle(); 885 if (options.getViewType() == BannerOptions.ViewType.VIDEO) { 886 params.putString(VIDEO_URL_KEY, options.getVideoUrl()); 887 } 888 try { 889 sdkApi.startActivity(starter, params); 890 logAndDisplayMessage(INFO, "Started activity %s", starter); 891 } catch (RemoteException e) { 892 logAndDisplayMessage(e, "Failed to startActivity (%s)", starter); 893 } 894 }); 895 } 896 registerNewAppWebviewButton()897 private void registerNewAppWebviewButton() { 898 mNewAppWebviewButton.setOnClickListener( 899 v -> { 900 if (!mSdksLoaded) { 901 logAndDisplayMessage(WARN, "Sdk is not loaded"); 902 return; 903 } 904 IBinder binder = mSandboxedSdk.getInterface(); 905 Intent intent = new Intent(this, AppWebViewActivity.class); 906 intent.putExtra(SANDBOXED_SDK_BINDER, binder); 907 startActivity(intent); 908 }); 909 } 910 registerNewAppVideoButton()911 private void registerNewAppVideoButton() { 912 mNewAppVideoButton.setOnClickListener( 913 v -> { 914 final BannerOptions options = 915 BannerOptions.fromSharedPreferences(mSharedPreferences); 916 917 Intent intent = new Intent(this, AppVideoView.class); 918 intent.putExtra(AppVideoView.VIDEO_URL_KEY, options.getVideoUrl()); 919 920 startActivity(intent); 921 }); 922 } 923 getRequestSurfacePackageParams(String commType, SurfaceView surfaceView)924 private Bundle getRequestSurfacePackageParams(String commType, SurfaceView surfaceView) { 925 Bundle params = new Bundle(); 926 params.putInt(EXTRA_WIDTH_IN_PIXELS, surfaceView.getWidth()); 927 params.putInt(EXTRA_HEIGHT_IN_PIXELS, surfaceView.getLayoutParams().height); 928 params.putInt(EXTRA_DISPLAY_ID, getDisplay().getDisplayId()); 929 params.putBinder(EXTRA_HOST_TOKEN, surfaceView.getHostToken()); 930 params.putString(EXTRA_SDK_SDK_ENABLED_KEY, commType); 931 return params; 932 } 933 logAndDisplayMessage(int logLevel, String fmt, Object... args)934 private void logAndDisplayMessage(int logLevel, String fmt, Object... args) { 935 String message = String.format(fmt, args); 936 switch (logLevel) { 937 case DEBUG: 938 Log.d(TAG, message); 939 break; 940 case ERROR: 941 Log.e(TAG, message); 942 break; 943 case INFO: 944 Log.i(TAG, message); 945 break; 946 case VERBOSE: 947 Log.v(TAG, message); 948 break; 949 case WARN: 950 Log.w(TAG, message); 951 break; 952 default: 953 Log.w(TAG, "Invalid log level " + logLevel + " for message: " + message); 954 } 955 displayMessage(message); 956 } 957 logAndDisplayMessage(Exception e, String fmt, Object... args)958 private void logAndDisplayMessage(Exception e, String fmt, Object... args) { 959 String message = String.format(fmt, args); 960 Log.e(TAG, message, e); 961 displayMessage(message); 962 } 963 displayMessage(CharSequence message)964 private void displayMessage(CharSequence message) { 965 runOnUiThread( 966 () -> { 967 final Snackbar snackbar = 968 Snackbar.make(mRootLayout, message, Snackbar.LENGTH_LONG); 969 snackbar.setAction(R.string.snackbar_dismiss, v -> snackbar.dismiss()); 970 snackbar.setTextMaxLines(SNACKBAR_MAX_LINES); 971 snackbar.addCallback( 972 new BaseTransientBottomBar.BaseCallback<>() { 973 @Override 974 public void onDismissed(Snackbar transientBottomBar, int event) { 975 mBottomBannerView.setZOrderOnTop(true); 976 } 977 }); 978 979 mBottomBannerView.setZOrderOnTop(false); 980 snackbar.show(); 981 }); 982 } 983 addDeathCallback(boolean notifyAdded)984 private void addDeathCallback(boolean notifyAdded) { 985 final int queueSize = mDeathCallbacks.size(); 986 SdkSandboxProcessDeathCallback deathCallback = 987 () -> 988 logAndDisplayMessage( 989 INFO, "Death callback #" + (queueSize + 1) + " notified."); 990 mSdkSandboxManager.addSdkSandboxProcessDeathCallback(Runnable::run, deathCallback); 991 mDeathCallbacks.add(deathCallback); 992 if (notifyAdded) { 993 logAndDisplayMessage(INFO, "Death callback # " + (queueSize + 1) + " added."); 994 } 995 } 996 997 private class RequestSurfacePackageReceiver 998 implements OutcomeReceiver<Bundle, RequestSurfacePackageException> { 999 1000 private final SurfaceView mSurfaceView; 1001 RequestSurfacePackageReceiver(SurfaceView surfaceView)1002 private RequestSurfacePackageReceiver(SurfaceView surfaceView) { 1003 mSurfaceView = surfaceView; 1004 } 1005 1006 @Override onResult(Bundle result)1007 public void onResult(Bundle result) { 1008 sHandler.post( 1009 () -> { 1010 SurfacePackage surfacePackage = 1011 result.getParcelable(EXTRA_SURFACE_PACKAGE, SurfacePackage.class); 1012 mSurfaceView.setChildSurfacePackage(surfacePackage); 1013 mSurfacePackages.add(surfacePackage); 1014 mSurfaceView.setVisibility(View.VISIBLE); 1015 }); 1016 logAndDisplayMessage(INFO, "Rendered surface view"); 1017 } 1018 1019 @Override onError(@onNull RequestSurfacePackageException error)1020 public void onError(@NonNull RequestSurfacePackageException error) { 1021 logAndDisplayMessage(ERROR, "Failed: %s", error.getMessage()); 1022 } 1023 } 1024 1025 private static final class ActivityStarter extends IActivityStarter.Stub { 1026 private final Activity mActivity; 1027 private final SdkSandboxManager mSdkSandboxManager; 1028 ActivityStarter(Activity activity, SdkSandboxManager manager)1029 ActivityStarter(Activity activity, SdkSandboxManager manager) { 1030 this.mActivity = activity; 1031 this.mSdkSandboxManager = manager; 1032 } 1033 1034 @Override startActivity(IBinder token)1035 public void startActivity(IBinder token) throws RemoteException { 1036 mSdkSandboxManager.startSdkSandboxActivity(mActivity, token); 1037 } 1038 1039 @Override toString()1040 public String toString() { 1041 return mActivity.getComponentName().flattenToShortString(); 1042 } 1043 } 1044 enableStrictMode()1045 private void enableStrictMode() { 1046 StrictMode.setThreadPolicy( 1047 new StrictMode.ThreadPolicy.Builder() 1048 .detectAll() 1049 .penaltyLog() 1050 .penaltyDeath() 1051 .build()); 1052 StrictMode.setVmPolicy( 1053 new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().penaltyDeath().build()); 1054 } 1055 1056 private interface ConsumerWithException<T> { accept(T t)1057 void accept(T t) throws Exception; 1058 } 1059 withSdkApiIfLoaded(ConsumerWithException<ISdkApi> sdkApiConsumer)1060 private void withSdkApiIfLoaded(ConsumerWithException<ISdkApi> sdkApiConsumer) { 1061 if (!mSdksLoaded) { 1062 return; 1063 } 1064 final IBinder binder = mSandboxedSdk.getInterface(); 1065 final ISdkApi sdkApi = ISdkApi.Stub.asInterface(binder); 1066 if (sdkApi == null) { 1067 logAndDisplayMessage(ERROR, "Failed to get SdkApi: Invalid SDK object"); 1068 return; 1069 } 1070 try { 1071 sdkApiConsumer.accept(sdkApi); 1072 } catch (Exception error) { 1073 logAndDisplayMessage(ERROR, "Exception while calling SdkApi: %s", error); 1074 } 1075 } 1076 } 1077