Polish Android launch experience

This commit is contained in:
SimpleTest 2026-08-03 18:46:51 +03:00
parent 578c77d7c5
commit c4a9dbe2c2
10 changed files with 348 additions and 71 deletions

View File

@ -347,4 +347,7 @@ dependencies {
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-web:3.7.0") androidTestImplementation("androidx.test.espresso:espresso-web:3.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0")
androidTestImplementation(
"androidx.core.locationbutton:locationbutton-testing:1.0.0-alpha01"
)
} }

View File

@ -14,13 +14,19 @@ import android.Manifest;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.permissionui.LocationButtonRequest;
import android.app.permissionui.LocationButtonSession;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.view.ViewGroup;
import androidx.core.locationbutton.LocationButton;
import androidx.core.locationbutton.LocationButtonCompat;
import androidx.core.locationbutton.testing.TestLocationButtonProvider;
import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -40,6 +46,7 @@ import org.junit.runners.MethodSorters;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -71,6 +78,35 @@ public final class AndroidClientInstrumentedTest {
} }
} }
@Test
public void test00ColdStartShowsLoadingStateUntilFirstPageIsVisible() throws Exception {
server.delayNextGet("/delayed-start", 1_500);
try (ActivityScenario<MainActivity> scenario = launch("/delayed-start")) {
assertTrue(
"Cold start did not expose a native loading state",
device.wait(
Until.hasObject(
By.res(context.getPackageName(), "page_loading")
),
1_000
)
);
assertTrue(
"Native loading state remained after the first page became visible",
device.wait(
Until.gone(
By.res(context.getPackageName(), "page_loading")
),
UI_TIMEOUT_MS
)
);
onWebView()
.withElement(findElement(Locator.ID, "marker"))
.check(webMatches(getText(), containsString("loaded:/delayed-start")));
}
}
@Test @Test
public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception { public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
assertFalse(hasLocationPermission()); assertFalse(hasLocationPermission());
@ -124,56 +160,46 @@ public final class AndroidClientInstrumentedTest {
By.text(context.getString(R.string.location_button_title)) By.text(context.getString(R.string.location_button_title))
) )
); );
locationButton.click();
UiObject2 allow = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN) {
AtomicReference<LocationButtonSession> session = new AtomicReference<>();
TestLocationButtonProvider provider = new TestLocationButtonProvider(context) {
@Override
protected void onSessionRequestReceived(
LocationButtonRequest request,
LocationButtonSession openedSession
) {
session.set(openedSession);
}
};
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { TestLocationButtonProvider finalProvider = provider;
allow = device.wait( scenario.onActivity(activity -> {
Until.findObject( LocationButton button = activity.locationButtonForTesting();
By.res(
"com.android.packageinstaller", if (button == null) {
"permission_allow_button" throw new AssertionError(
) "The native location button was not attached to its dialog"
), );
UI_TIMEOUT_MS }
);
ViewGroup parent = (ViewGroup) button.getParent();
int index = parent.indexOfChild(button);
ViewGroup.LayoutParams layoutParams = button.getLayoutParams();
parent.removeViewAt(index);
LocationButtonCompat.setLocationButtonProvider(button, finalProvider);
parent.addView(button, index, layoutParams);
});
LocationButtonSession openedSession = waitForLocationButtonSession(session);
grantLocationPermission();
provider.notifyPermissionResult(openedSession, true);
} else {
locationButton.click();
clickLegacyLocationPermissionAllow();
} }
if (allow == null && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(
By.res(
"com.google.android.packageinstaller",
"permission_allow_button"
)
),
UI_TIMEOUT_MS
);
}
if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(
By.res(
LOCATION_PERMISSION_CONTROLLER,
"permission_allow_foreground_only_button"
)
),
UI_TIMEOUT_MS
);
}
if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(By.textContains("While using the app")),
UI_TIMEOUT_MS
);
}
assertNotNull("Android location permission prompt was not shown", allow);
allow.click();
waitForLocationPermission(); waitForLocationPermission();
FixtureHttpServer.RecordedRequest callback = FixtureHttpServer.RecordedRequest callback =
server.awaitRequestContaining( server.awaitRequestContaining(
@ -193,6 +219,74 @@ public final class AndroidClientInstrumentedTest {
} }
} }
private LocationButtonSession waitForLocationButtonSession(
AtomicReference<LocationButtonSession> session
) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(UI_TIMEOUT_MS);
while (System.nanoTime() < deadline) {
LocationButtonSession openedSession = session.get();
if (openedSession != null) {
return openedSession;
}
Thread.sleep(50);
}
throw new AssertionError("The Android 17 test location session did not open");
}
private void clickLegacyLocationPermissionAllow() {
UiObject2 allow = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(
By.res(
"com.android.packageinstaller",
"permission_allow_button"
)
),
UI_TIMEOUT_MS
);
}
if (allow == null && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(
By.res(
"com.google.android.packageinstaller",
"permission_allow_button"
)
),
UI_TIMEOUT_MS
);
}
if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(
By.res(
LOCATION_PERMISSION_CONTROLLER,
"permission_allow_foreground_only_button"
)
),
UI_TIMEOUT_MS
);
}
if (allow == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
allow = device.wait(
Until.findObject(By.textContains("While using the app")),
UI_TIMEOUT_MS
);
}
assertNotNull("Android location permission prompt was not shown", allow);
allow.click();
}
@Test @Test
public void test02bTrackingRequiresExplicitNativeDisclosure() throws Exception { public void test02bTrackingRequiresExplicitNativeDisclosure() throws Exception {
try (ActivityScenario<MainActivity> scenario = launch("/tracking-disclosure")) { try (ActivityScenario<MainActivity> scenario = launch("/tracking-disclosure")) {
@ -558,20 +652,29 @@ public final class AndroidClientInstrumentedTest {
} }
private void grantTrackingPermissions() throws Exception { private void grantTrackingPermissions() throws Exception {
String packageName = context.getPackageName(); grantLocationPermission();
device.executeShellCommand(
"pm grant " + packageName + " " + Manifest.permission.ACCESS_FINE_LOCATION
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
device.executeShellCommand( device.executeShellCommand(
"pm grant " + packageName + " " + Manifest.permission.POST_NOTIFICATIONS "pm grant "
+ context.getPackageName()
+ " "
+ Manifest.permission.POST_NOTIFICATIONS
); );
} }
assertTrue(hasLocationPermission()); assertTrue(hasLocationPermission());
} }
private void grantLocationPermission() throws Exception {
device.executeShellCommand(
"pm grant "
+ context.getPackageName()
+ " "
+ Manifest.permission.ACCESS_FINE_LOCATION
);
}
private void waitForServiceState(boolean expected) throws Exception { private void waitForServiceState(boolean expected) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);

View File

@ -39,6 +39,8 @@ final class FixtureHttpServer implements Closeable {
private volatile boolean running = true; private volatile boolean running = true;
private volatile boolean disconnectStopRequest; private volatile boolean disconnectStopRequest;
private volatile String disconnectNextGetPath; private volatile String disconnectNextGetPath;
private volatile String delayedNextGetPath;
private volatile long delayedNextGetMillis;
FixtureHttpServer() throws IOException { FixtureHttpServer() throws IOException {
URI base = URI.create(BuildConfig.BASE_URL); URI base = URI.create(BuildConfig.BASE_URL);
@ -69,6 +71,11 @@ final class FixtureHttpServer implements Closeable {
disconnectNextGetPath = path; disconnectNextGetPath = path;
} }
void delayNextGet(String path, long delayMillis) {
delayedNextGetPath = path;
delayedNextGetMillis = delayMillis;
}
RecordedRequest awaitRequestEndingWith(String suffix, long timeout, TimeUnit unit) RecordedRequest awaitRequestEndingWith(String suffix, long timeout, TimeUnit unit)
throws InterruptedException { throws InterruptedException {
return awaitRequestAfterCount(suffix, 0, timeout, unit); return awaitRequestAfterCount(suffix, 0, timeout, unit);
@ -208,6 +215,19 @@ final class FixtureHttpServer implements Closeable {
return; return;
} }
if ("GET".equals(request.method) && request.path.equals(delayedNextGetPath)) {
long delayMillis = delayedNextGetMillis;
delayedNextGetPath = null;
delayedNextGetMillis = 0;
try {
Thread.sleep(delayMillis);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
return;
}
}
if ("GET".equals(request.method)) { if ("GET".equals(request.method)) {
writeResponse( writeResponse(
connection.getOutputStream(), connection.getOutputStream(),

View File

@ -14,7 +14,6 @@ import android.os.Bundle;
import android.os.CancellationSignal; import android.os.CancellationSignal;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions; import android.webkit.GeolocationPermissions;
import android.webkit.SslErrorHandler; import android.webkit.SslErrorHandler;
@ -25,7 +24,6 @@ import android.webkit.WebResourceResponse;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.ComponentActivity; import androidx.activity.ComponentActivity;
@ -70,6 +68,7 @@ public final class MainActivity extends ComponentActivity {
private static final boolean PAGE_LOAD_DIAGNOSTICS = private static final boolean PAGE_LOAD_DIAGNOSTICS =
BuildConfig.DEBUG || BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS; BuildConfig.DEBUG || BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS;
private WebView webView; private WebView webView;
private View pageLoadingView;
private TrustedOrigin trustedOrigin; private TrustedOrigin trustedOrigin;
private GeolocationPermissions.Callback pendingLocationCallback; private GeolocationPermissions.Callback pendingLocationCallback;
private String pendingLocationOrigin; private String pendingLocationOrigin;
@ -82,6 +81,8 @@ public final class MainActivity extends ComponentActivity {
private AlertDialog trackingDisclosureDialog; private AlertDialog trackingDisclosureDialog;
private boolean webLocationPermissionRequestInFlight; private boolean webLocationPermissionRequestInFlight;
private boolean mainFrameLoadFailed; private boolean mainFrameLoadFailed;
private long pageLoadGeneration;
private long scheduledRevealGeneration = -1;
private CredentialManager credentialManager; private CredentialManager credentialManager;
private ExecutorService nativeGoogleNetworkExecutor; private ExecutorService nativeGoogleNetworkExecutor;
private CancellationSignal nativeGoogleCancellationSignal; private CancellationSignal nativeGoogleCancellationSignal;
@ -143,23 +144,10 @@ public final class MainActivity extends ComponentActivity {
} }
} }
); );
webView = new WebView(this); setContentView(R.layout.activity_main);
webView.setLayoutParams( View contentView = requireView(R.id.main_content);
new ViewGroup.LayoutParams( webView = requireView(R.id.web_view);
ViewGroup.LayoutParams.MATCH_PARENT, pageLoadingView = requireView(R.id.page_loading);
ViewGroup.LayoutParams.MATCH_PARENT
)
);
FrameLayout contentView = new FrameLayout(this);
contentView.setId(R.id.main_content);
contentView.setLayoutParams(
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
);
contentView.addView(webView);
setContentView(contentView);
applySystemBarInsets(contentView); applySystemBarInsets(contentView);
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG); WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
@ -201,9 +189,39 @@ public final class MainActivity extends ComponentActivity {
if (savedInstanceState == null || webView.restoreState(savedInstanceState) == null) { if (savedInstanceState == null || webView.restoreState(savedInstanceState) == null) {
webView.loadUrl(initialUrl()); webView.loadUrl(initialUrl());
} else {
hidePageLoading();
} }
} }
private <T extends View> T requireView(int id) {
T view = findViewById(id);
if (view == null) {
throw new IllegalStateException("Required activity view is missing: " + id);
}
return view;
}
private void showPageLoading() {
if (pageLoadingView == null || webView == null) {
return;
}
webView.setVisibility(View.INVISIBLE);
pageLoadingView.setVisibility(View.VISIBLE);
}
private void hidePageLoading() {
if (pageLoadingView == null || webView == null) {
return;
}
webView.setVisibility(View.VISIBLE);
pageLoadingView.setVisibility(View.GONE);
}
private void applySystemBarInsets(View contentView) { private void applySystemBarInsets(View contentView) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
return; return;
@ -420,6 +438,16 @@ public final class MainActivity extends ComponentActivity {
dialog.show(); dialog.show();
} }
LocationButton locationButtonForTesting() {
AlertDialog dialog = locationPermissionDialog;
if (dialog == null) {
return null;
}
return dialog.findViewById(R.id.location_button);
}
private boolean hasLocationPermission() { private boolean hasLocationPermission() {
return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED == PackageManager.PERMISSION_GRANTED
@ -1089,6 +1117,7 @@ public final class MainActivity extends ComponentActivity {
return; return;
} }
hidePageLoading();
dismissPageLoadError(); dismissPageLoadError();
pageLoadErrorDialog = new AlertDialog.Builder(this) pageLoadErrorDialog = new AlertDialog.Builder(this)
.setTitle(R.string.page_load_failed_title) .setTitle(R.string.page_load_failed_title)
@ -1174,7 +1203,10 @@ public final class MainActivity extends ComponentActivity {
@Override @Override
public void onPageStarted(WebView view, String url, Bitmap favicon) { public void onPageStarted(WebView view, String url, Bitmap favicon) {
mainFrameLoadFailed = false; mainFrameLoadFailed = false;
pageLoadGeneration++;
scheduledRevealGeneration = -1;
dismissPageLoadError(); dismissPageLoadError();
showPageLoading();
if (PAGE_LOAD_DIAGNOSTICS) { if (PAGE_LOAD_DIAGNOSTICS) {
Log.d(LOG_TAG, "Main-frame load started: path=" + safeLogPath(Uri.parse(url))); Log.d(LOG_TAG, "Main-frame load started: path=" + safeLogPath(Uri.parse(url)));
@ -1182,14 +1214,21 @@ public final class MainActivity extends ComponentActivity {
if (!trustedOrigin.matches(url)) { if (!trustedOrigin.matches(url)) {
view.stopLoading(); view.stopLoading();
hidePageLoading();
openExternal(Uri.parse(url)); openExternal(Uri.parse(url));
} }
} }
@Override
public void onPageCommitVisible(WebView view, String url) {
revealPageWhenDrawn(view, url);
}
@Override @Override
public void onPageFinished(WebView view, String url) { public void onPageFinished(WebView view, String url) {
if (!mainFrameLoadFailed) { if (!mainFrameLoadFailed) {
dismissPageLoadError(); dismissPageLoadError();
revealPageWhenDrawn(view, url);
} }
if (PAGE_LOAD_DIAGNOSTICS) { if (PAGE_LOAD_DIAGNOSTICS) {
@ -1200,6 +1239,33 @@ public final class MainActivity extends ComponentActivity {
dispatchPendingPushToken(); dispatchPendingPushToken();
} }
private void revealPageWhenDrawn(WebView view, String url) {
if (
mainFrameLoadFailed
|| !trustedOrigin.matches(url)
|| scheduledRevealGeneration == pageLoadGeneration
) {
return;
}
long generation = pageLoadGeneration;
scheduledRevealGeneration = generation;
view.postVisualStateCallback(
generation,
new WebView.VisualStateCallback() {
@Override
public void onComplete(long requestId) {
if (
requestId == pageLoadGeneration
&& !mainFrameLoadFailed
) {
hidePageLoading();
}
}
}
);
}
private void scheduleMapDiagnostics(WebView view, Uri uri) { private void scheduleMapDiagnostics(WebView view, Uri uri) {
String path = uri.getPath(); String path = uri.getPath();

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame">
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/page_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/app_surface"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
android:text="@string/app_name"
android:textColor="@color/app_text"
android:textSize="24sp" />
<ProgressBar
android:id="@+id/page_loading_progress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="28dp"
android:contentDescription="@string/loading_page"
android:indeterminate="true"
android:indeterminateTint="@color/brand_green" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:gravity="center"
android:text="@string/loading_page"
android:textColor="@color/app_text_secondary"
android:textSize="15sp" />
</LinearLayout>
</FrameLayout>

View File

@ -4,6 +4,7 @@
<string name="no_link_handler">Нет приложения, которое может открыть эту ссылку.</string> <string name="no_link_handler">Нет приложения, которое может открыть эту ссылку.</string>
<string name="page_load_failed_title">Страница недоступна</string> <string name="page_load_failed_title">Страница недоступна</string>
<string name="page_load_failed">Не удалось загрузить Who Need Help. Проверьте подключение и повторите попытку.</string> <string name="page_load_failed">Не удалось загрузить Who Need Help. Проверьте подключение и повторите попытку.</string>
<string name="loading_page">Безопасная загрузка…</string>
<string name="retry">Повторить</string> <string name="retry">Повторить</string>
<string name="close">Закрыть</string> <string name="close">Закрыть</string>
<string name="cancel">Отмена</string> <string name="cancel">Отмена</string>

View File

@ -4,6 +4,7 @@
<string name="no_link_handler">Немає застосунку, який може відкрити це посилання.</string> <string name="no_link_handler">Немає застосунку, який може відкрити це посилання.</string>
<string name="page_load_failed_title">Сторінка недоступна</string> <string name="page_load_failed_title">Сторінка недоступна</string>
<string name="page_load_failed">Не вдалося завантажити Who Need Help. Перевірте з’єднання та повторіть спробу.</string> <string name="page_load_failed">Не вдалося завантажити Who Need Help. Перевірте з’єднання та повторіть спробу.</string>
<string name="loading_page">Безпечне завантаження…</string>
<string name="retry">Повторити</string> <string name="retry">Повторити</string>
<string name="close">Закрити</string> <string name="close">Закрити</string>
<string name="cancel">Скасувати</string> <string name="cancel">Скасувати</string>

View File

@ -2,4 +2,7 @@
<resources> <resources>
<color name="brand_green">#14532D</color> <color name="brand_green">#14532D</color>
<color name="brand_green_dark">#0A2F1A</color> <color name="brand_green_dark">#0A2F1A</color>
<color name="app_surface">#FAFAFA</color>
<color name="app_text">#202124</color>
<color name="app_text_secondary">#5F6368</color>
</resources> </resources>

View File

@ -4,6 +4,7 @@
<string name="no_link_handler">No app can open this link.</string> <string name="no_link_handler">No app can open this link.</string>
<string name="page_load_failed_title">Page unavailable</string> <string name="page_load_failed_title">Page unavailable</string>
<string name="page_load_failed">Could not load Who Need Help. Check your connection and retry.</string> <string name="page_load_failed">Could not load Who Need Help. Check your connection and retry.</string>
<string name="loading_page">Loading securely…</string>
<string name="retry">Retry</string> <string name="retry">Retry</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>

View File

@ -214,8 +214,28 @@ if [ "$instrumentation_status" -ne 0 ] \
fi fi
docker exec "$container" adb shell am force-stop org.whoneedhelp.mobile.debug docker exec "$container" adb shell am force-stop org.whoneedhelp.mobile.debug
docker exec "$container" adb root >"$output/adb-root.txt" set +e
docker exec "$container" adb root >"$output/adb-root.txt" 2>&1
adb_root_status=$?
set -e
docker exec "$container" adb wait-for-device docker exec "$container" adb wait-for-device
if [ "$adb_root_status" -ne 0 ]; then
adb_uid=$(docker exec "$container" adb shell id -u | tr -d '\r')
if [ "$adb_uid" != "0" ]; then
docker exec "$container" adb root >>"$output/adb-root.txt" 2>&1
docker exec "$container" adb wait-for-device
fi
fi
adb_uid=$(docker exec "$container" adb shell id -u | tr -d '\r')
if [ "$adb_uid" != "0" ]; then
echo "The process-death probe requires a root-capable emulator; observed uid=$adb_uid." >&2
exit 1
fi
docker exec "$container" adb shell am instrument -w -r \ docker exec "$container" adb shell am instrument -w -r \
-e class org.whoneedhelp.mobile.TrackingProcessDeathProbeTest \ -e class org.whoneedhelp.mobile.TrackingProcessDeathProbeTest \
org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \ org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \