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