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