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