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-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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,8 +160,84 @@ public final class AndroidClientInstrumentedTest {
|
||||||
By.text(context.getString(R.string.location_button_title))
|
By.text(context.getString(R.string.location_button_title))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
locationButton.click();
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForLocationPermission();
|
||||||
|
FixtureHttpServer.RecordedRequest callback =
|
||||||
|
server.awaitRequestContaining(
|
||||||
|
"/geolocation-",
|
||||||
|
35,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
);
|
||||||
|
assertNotNull(
|
||||||
|
"WebView geolocation did not return a callback",
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
assertFalse(
|
||||||
|
"WebView treated the granted Android permission as denied",
|
||||||
|
callback.path.endsWith("/geolocation-denied-1")
|
||||||
|
);
|
||||||
|
assertTrue("Android location permission was not retained", hasLocationPermission());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
UiObject2 allow = null;
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
|
@ -173,24 +285,6 @@ public final class AndroidClientInstrumentedTest {
|
||||||
|
|
||||||
assertNotNull("Android location permission prompt was not shown", allow);
|
assertNotNull("Android location permission prompt was not shown", allow);
|
||||||
allow.click();
|
allow.click();
|
||||||
|
|
||||||
waitForLocationPermission();
|
|
||||||
FixtureHttpServer.RecordedRequest callback =
|
|
||||||
server.awaitRequestContaining(
|
|
||||||
"/geolocation-",
|
|
||||||
35,
|
|
||||||
TimeUnit.SECONDS
|
|
||||||
);
|
|
||||||
assertNotNull(
|
|
||||||
"WebView geolocation did not return a callback",
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
assertFalse(
|
|
||||||
"WebView treated the granted Android permission as denied",
|
|
||||||
callback.path.endsWith("/geolocation-denied-1")
|
|
||||||
);
|
|
||||||
assertTrue("Android location permission was not retained", hasLocationPermission());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
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="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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user