Polish Android launch experience
This commit is contained in:
parent
578c77d7c5
commit
c4a9dbe2c2
|
|
@ -347,4 +347,7 @@ dependencies {
|
|||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-web:3.7.0")
|
||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.4.0")
|
||||
androidTestImplementation(
|
||||
"androidx.core.locationbutton:locationbutton-testing:1.0.0-alpha01"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,19 @@ import android.Manifest;
|
|||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.permissionui.LocationButtonRequest;
|
||||
import android.app.permissionui.LocationButtonSession;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
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.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
|
@ -40,6 +46,7 @@ import org.junit.runners.MethodSorters;
|
|||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@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
|
||||
public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
|
||||
assertFalse(hasLocationPermission());
|
||||
|
|
@ -124,56 +160,46 @@ public final class AndroidClientInstrumentedTest {
|
|||
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) {
|
||||
allow = device.wait(
|
||||
Until.findObject(
|
||||
By.res(
|
||||
"com.android.packageinstaller",
|
||||
"permission_allow_button"
|
||||
)
|
||||
),
|
||||
UI_TIMEOUT_MS
|
||||
);
|
||||
TestLocationButtonProvider finalProvider = provider;
|
||||
scenario.onActivity(activity -> {
|
||||
LocationButton button = activity.locationButtonForTesting();
|
||||
|
||||
if (button == null) {
|
||||
throw new AssertionError(
|
||||
"The native location button was not attached to its dialog"
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
FixtureHttpServer.RecordedRequest callback =
|
||||
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
|
||||
public void test02bTrackingRequiresExplicitNativeDisclosure() throws Exception {
|
||||
try (ActivityScenario<MainActivity> scenario = launch("/tracking-disclosure")) {
|
||||
|
|
@ -558,20 +652,29 @@ public final class AndroidClientInstrumentedTest {
|
|||
}
|
||||
|
||||
private void grantTrackingPermissions() throws Exception {
|
||||
String packageName = context.getPackageName();
|
||||
device.executeShellCommand(
|
||||
"pm grant " + packageName + " " + Manifest.permission.ACCESS_FINE_LOCATION
|
||||
);
|
||||
grantLocationPermission();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
device.executeShellCommand(
|
||||
"pm grant " + packageName + " " + Manifest.permission.POST_NOTIFICATIONS
|
||||
"pm grant "
|
||||
+ context.getPackageName()
|
||||
+ " "
|
||||
+ Manifest.permission.POST_NOTIFICATIONS
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ final class FixtureHttpServer implements Closeable {
|
|||
private volatile boolean running = true;
|
||||
private volatile boolean disconnectStopRequest;
|
||||
private volatile String disconnectNextGetPath;
|
||||
private volatile String delayedNextGetPath;
|
||||
private volatile long delayedNextGetMillis;
|
||||
|
||||
FixtureHttpServer() throws IOException {
|
||||
URI base = URI.create(BuildConfig.BASE_URL);
|
||||
|
|
@ -69,6 +71,11 @@ final class FixtureHttpServer implements Closeable {
|
|||
disconnectNextGetPath = path;
|
||||
}
|
||||
|
||||
void delayNextGet(String path, long delayMillis) {
|
||||
delayedNextGetPath = path;
|
||||
delayedNextGetMillis = delayMillis;
|
||||
}
|
||||
|
||||
RecordedRequest awaitRequestEndingWith(String suffix, long timeout, TimeUnit unit)
|
||||
throws InterruptedException {
|
||||
return awaitRequestAfterCount(suffix, 0, timeout, unit);
|
||||
|
|
@ -208,6 +215,19 @@ final class FixtureHttpServer implements Closeable {
|
|||
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)) {
|
||||
writeResponse(
|
||||
connection.getOutputStream(),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import android.os.Bundle;
|
|||
import android.os.CancellationSignal;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.GeolocationPermissions;
|
||||
import android.webkit.SslErrorHandler;
|
||||
|
|
@ -25,7 +24,6 @@ import android.webkit.WebResourceResponse;
|
|||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.ComponentActivity;
|
||||
|
|
@ -70,6 +68,7 @@ public final class MainActivity extends ComponentActivity {
|
|||
private static final boolean PAGE_LOAD_DIAGNOSTICS =
|
||||
BuildConfig.DEBUG || BuildConfig.SAFE_PAGE_LOAD_DIAGNOSTICS;
|
||||
private WebView webView;
|
||||
private View pageLoadingView;
|
||||
private TrustedOrigin trustedOrigin;
|
||||
private GeolocationPermissions.Callback pendingLocationCallback;
|
||||
private String pendingLocationOrigin;
|
||||
|
|
@ -82,6 +81,8 @@ public final class MainActivity extends ComponentActivity {
|
|||
private AlertDialog trackingDisclosureDialog;
|
||||
private boolean webLocationPermissionRequestInFlight;
|
||||
private boolean mainFrameLoadFailed;
|
||||
private long pageLoadGeneration;
|
||||
private long scheduledRevealGeneration = -1;
|
||||
private CredentialManager credentialManager;
|
||||
private ExecutorService nativeGoogleNetworkExecutor;
|
||||
private CancellationSignal nativeGoogleCancellationSignal;
|
||||
|
|
@ -143,23 +144,10 @@ public final class MainActivity extends ComponentActivity {
|
|||
}
|
||||
}
|
||||
);
|
||||
webView = new WebView(this);
|
||||
webView.setLayoutParams(
|
||||
new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
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);
|
||||
setContentView(R.layout.activity_main);
|
||||
View contentView = requireView(R.id.main_content);
|
||||
webView = requireView(R.id.web_view);
|
||||
pageLoadingView = requireView(R.id.page_loading);
|
||||
applySystemBarInsets(contentView);
|
||||
|
||||
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
|
||||
|
|
@ -201,9 +189,39 @@ public final class MainActivity extends ComponentActivity {
|
|||
|
||||
if (savedInstanceState == null || webView.restoreState(savedInstanceState) == null) {
|
||||
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) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
return;
|
||||
|
|
@ -420,6 +438,16 @@ public final class MainActivity extends ComponentActivity {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
LocationButton locationButtonForTesting() {
|
||||
AlertDialog dialog = locationPermissionDialog;
|
||||
|
||||
if (dialog == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dialog.findViewById(R.id.location_button);
|
||||
}
|
||||
|
||||
private boolean hasLocationPermission() {
|
||||
return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
|
|
@ -1089,6 +1117,7 @@ public final class MainActivity extends ComponentActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
hidePageLoading();
|
||||
dismissPageLoadError();
|
||||
pageLoadErrorDialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.page_load_failed_title)
|
||||
|
|
@ -1174,7 +1203,10 @@ public final class MainActivity extends ComponentActivity {
|
|||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
mainFrameLoadFailed = false;
|
||||
pageLoadGeneration++;
|
||||
scheduledRevealGeneration = -1;
|
||||
dismissPageLoadError();
|
||||
showPageLoading();
|
||||
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
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)) {
|
||||
view.stopLoading();
|
||||
hidePageLoading();
|
||||
openExternal(Uri.parse(url));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageCommitVisible(WebView view, String url) {
|
||||
revealPageWhenDrawn(view, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
if (!mainFrameLoadFailed) {
|
||||
dismissPageLoadError();
|
||||
revealPageWhenDrawn(view, url);
|
||||
}
|
||||
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
|
|
@ -1200,6 +1239,33 @@ public final class MainActivity extends ComponentActivity {
|
|||
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) {
|
||||
String path = uri.getPath();
|
||||
|
||||
|
|
|
|||
59
android/app/src/main/res/layout/activity_main.xml
Normal file
59
android/app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
<string name="no_link_handler">Нет приложения, которое может открыть эту ссылку.</string>
|
||||
<string name="page_load_failed_title">Страница недоступна</string>
|
||||
<string name="page_load_failed">Не удалось загрузить Who Need Help. Проверьте подключение и повторите попытку.</string>
|
||||
<string name="loading_page">Безопасная загрузка…</string>
|
||||
<string name="retry">Повторить</string>
|
||||
<string name="close">Закрыть</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<string name="no_link_handler">Немає застосунку, який може відкрити це посилання.</string>
|
||||
<string name="page_load_failed_title">Сторінка недоступна</string>
|
||||
<string name="page_load_failed">Не вдалося завантажити Who Need Help. Перевірте з’єднання та повторіть спробу.</string>
|
||||
<string name="loading_page">Безпечне завантаження…</string>
|
||||
<string name="retry">Повторити</string>
|
||||
<string name="close">Закрити</string>
|
||||
<string name="cancel">Скасувати</string>
|
||||
|
|
|
|||
|
|
@ -2,4 +2,7 @@
|
|||
<resources>
|
||||
<color name="brand_green">#14532D</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>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<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">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="close">Close</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
|
|
|
|||
|
|
@ -214,8 +214,28 @@ if [ "$instrumentation_status" -ne 0 ] \
|
|||
fi
|
||||
|
||||
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
|
||||
|
||||
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 \
|
||||
-e class org.whoneedhelp.mobile.TrackingProcessDeathProbeTest \
|
||||
org.whoneedhelp.mobile.debug.test/androidx.test.runner.AndroidJUnitRunner \
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user