1 /* 2 * Copyright (C) 2024 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 android.healthconnect.cts.utils; 18 19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 20 21 import static com.android.compatibility.common.util.UiAutomatorUtils.getUiDevice; 22 23 import android.app.Activity; 24 import android.app.Instrumentation; 25 import android.content.Intent; 26 import android.os.Bundle; 27 28 import androidx.annotation.Nullable; 29 import androidx.test.core.app.ActivityScenario; 30 31 /** 32 * Activity which starts another activity and forwards the result back to the caller. 33 * 34 * <p>The intent to start a new activity with is encoded as a parcelable extra with key 35 * Intent.EXTRA_INTENT. 36 * 37 * <p>This is useful for two reasons: 1. ActivityScenario often have problems with launching an 38 * activity which belongs to another process. ProxyActivity can be used as a workaround. 2. It can 39 * be used by a test app so that a CTS test can start an activity on behalf of the test app. 40 */ 41 public class ProxyActivity extends Activity { 42 public static final String PROXY_ACTIVITY_ACTION = 43 "android.healthconnect.cts.ACTION_START_ACTIVITY_FOR_RESULT"; 44 public static final String PROXY_ACTIVITY_ERROR = 45 "android.healthconnect.cts.PROXY_ACTIVITY_ERROR"; 46 private static final int REQUEST_CODE = 1; 47 48 @Override onCreate(@ullable Bundle savedInstanceState)49 protected void onCreate(@Nullable Bundle savedInstanceState) { 50 super.onCreate(savedInstanceState); 51 52 var requestIntent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT, Intent.class); 53 54 if (requestIntent == null) { 55 finishWithException(new IllegalArgumentException("Missing EXTRA_INTENT extra")); 56 return; 57 } 58 59 try { 60 startActivityForResult(requestIntent, REQUEST_CODE); 61 } catch (Exception e) { 62 finishWithException(e); 63 } 64 } 65 66 @Override onActivityResult(int requestCode, int resultCode, Intent data)67 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 68 if (requestCode != REQUEST_CODE) { 69 finishWithException( 70 new IllegalArgumentException("Unexpected request code " + requestCode)); 71 return; 72 } 73 74 setResult(resultCode, data); 75 finish(); 76 } 77 78 /** 79 * Launch an activity for result with the given intent and executes the runnable once the 80 * activity gets created. 81 * 82 * <p>ActivityScenario.launchActivityForResult doesn't work with activities running in another 83 * process as it times out on waiting for the remote activity to become CREATED. Apparently this 84 * happens because ActivityScenario uses ActivityLifecycleMonitor to track the activity state, 85 * however state changes of the activities which belong to other processes don't trigger the 86 * lifecycle callback and therefore ActivityScenario thinks that the target activity is always 87 * in the PRE_ON_CREATE state even when it's not true. As a workaround this method launches the 88 * proxy activity in the same process which then forwards the intent to the target remote 89 * activity. 90 * 91 * <p>The app calling this method must have {@link ProxyActivity} declared in the manifest. 92 * 93 * <p>Use {@link #launchActivityForResult(Intent)} if no interaction with the activity is 94 * required instead of passing a no-op runnable to this method as the latter is flaky. 95 */ launchActivityForResult( Intent intent, Runnable runnable)96 public static Instrumentation.ActivityResult launchActivityForResult( 97 Intent intent, Runnable runnable) throws Exception { 98 99 Intent containerIntent = new Intent(getInstrumentation().getContext(), ProxyActivity.class); 100 containerIntent.putExtra(Intent.EXTRA_INTENT, intent); 101 102 var scenario = ActivityScenario.launchActivityForResult(containerIntent); 103 if (runnable != null) { 104 scenario.onActivity( 105 activity -> { 106 getUiDevice().waitForIdle(); 107 runnable.run(); 108 }); 109 } 110 111 Instrumentation.ActivityResult result = scenario.getResult(); 112 113 Intent resultData = result.getResultData(); 114 if (resultData != null) { 115 Exception exception = 116 result.getResultData() 117 .getParcelableExtra(PROXY_ACTIVITY_ERROR, Exception.class); 118 119 if (exception != null) { 120 throw exception; 121 } 122 } 123 124 return result; 125 } 126 127 /** 128 * Same as {@link #launchActivityForResult(Intent, Runnable)} for cases when an interaction with 129 * the activity is not required. 130 */ launchActivityForResult(Intent intent)131 public static Instrumentation.ActivityResult launchActivityForResult(Intent intent) 132 throws Exception { 133 return launchActivityForResult(intent, null); 134 } 135 finishWithException(Exception e)136 private void finishWithException(Exception e) { 137 Intent errorIntent = new Intent(); 138 errorIntent.putExtra(PROXY_ACTIVITY_ERROR, e); 139 setResult(RESULT_CANCELED, errorIntent); 140 finish(); 141 } 142 } 143