Harden Android location consent and physical-device verification
This commit is contained in:
parent
cbb13d6888
commit
c863b47ed1
|
|
@ -142,6 +142,9 @@ USER 65532:65532
|
|||
COPY --from=android-sdk \
|
||||
/workspace/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||
/who-need-help-debug.apk
|
||||
COPY --from=android-sdk \
|
||||
/workspace/android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
|
||||
/who-need-help-debug-androidTest.apk
|
||||
COPY --from=android-sdk \
|
||||
/workspace/android/app/build/reports/lint-results-debug.html \
|
||||
/lint-results-debug.html
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@ tasks.withType<JavaCompile>().configureEach {
|
|||
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity:1.13.0")
|
||||
implementation("androidx.core.locationbutton:locationbutton:1.0.0-alpha01")
|
||||
implementation("androidx.credentials:credentials:1.6.0")
|
||||
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")
|
||||
implementation("androidx.fragment:fragment:1.8.9")
|
||||
|
|
|
|||
|
|
@ -66,8 +66,10 @@ public final class AndroidClientInstrumentedTest {
|
|||
context.stopService(new Intent(context, TrackingService.class));
|
||||
context.getSystemService(NotificationManager.class).cancelAll();
|
||||
device.pressBack();
|
||||
if (server != null) {
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
|
||||
|
|
@ -107,6 +109,23 @@ public final class AndroidClientInstrumentedTest {
|
|||
.withElement(findElement(Locator.ID, "location"))
|
||||
.perform(webClick());
|
||||
|
||||
UiObject2 locationButton = device.wait(
|
||||
Until.findObject(
|
||||
By.res(context.getPackageName(), "location_button")
|
||||
),
|
||||
UI_TIMEOUT_MS
|
||||
);
|
||||
assertNotNull(
|
||||
"The native Android location explanation was not shown",
|
||||
locationButton
|
||||
);
|
||||
assertTrue(
|
||||
device.hasObject(
|
||||
By.text(context.getString(R.string.location_button_title))
|
||||
)
|
||||
);
|
||||
locationButton.click();
|
||||
|
||||
UiObject2 allow = null;
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
|
|
@ -156,17 +175,52 @@ public final class AndroidClientInstrumentedTest {
|
|||
allow.click();
|
||||
|
||||
waitForLocationPermission();
|
||||
assertNotNull(
|
||||
"WebView geolocation did not report a granted position",
|
||||
server.awaitRequestEndingWith(
|
||||
"/geolocation-granted",
|
||||
15,
|
||||
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
|
||||
public void test02bTrackingRequiresExplicitNativeDisclosure() throws Exception {
|
||||
try (ActivityScenario<MainActivity> scenario = launch("/tracking-disclosure")) {
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "tracking"))
|
||||
.perform(webClick());
|
||||
|
||||
UiObject2 disclosure = device.wait(
|
||||
Until.findObject(
|
||||
By.text(context.getString(R.string.tracking_disclosure_title))
|
||||
),
|
||||
UI_TIMEOUT_MS
|
||||
);
|
||||
assertNotNull("The live-location disclosure was not shown", disclosure);
|
||||
assertTrue(
|
||||
device.hasObject(
|
||||
By.text(context.getString(R.string.tracking_disclosure_detail))
|
||||
)
|
||||
);
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "marker"))
|
||||
.check(webMatches(getText(), containsString("location-granted")));
|
||||
|
||||
UiObject2 cancel = device.wait(
|
||||
Until.findObject(By.res("android", "button2")),
|
||||
UI_TIMEOUT_MS
|
||||
);
|
||||
assertNotNull("The live-location disclosure had no Cancel action", cancel);
|
||||
cancel.click();
|
||||
|
||||
waitForServiceState(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +249,47 @@ public final class AndroidClientInstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test03bSameOriginDeepLinkUpdatesRunningActivity() {
|
||||
ActivityScenario<MainActivity> scenario =
|
||||
launch("/notifications?section=settings");
|
||||
|
||||
try {
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "marker"))
|
||||
.check(
|
||||
webMatches(
|
||||
getText(),
|
||||
containsString("loaded:/notifications?section=settings")
|
||||
)
|
||||
);
|
||||
|
||||
Intent notificationIntent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(BuildConfig.BASE_URL + "/notifications"),
|
||||
context,
|
||||
MainActivity.class
|
||||
);
|
||||
scenario.onActivity(activity -> activity.onNewIntent(notificationIntent));
|
||||
|
||||
onWebView()
|
||||
.withElement(findElement(Locator.ID, "marker"))
|
||||
.check(
|
||||
webMatches(
|
||||
getText(),
|
||||
containsString("loaded:/notifications")
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
// Calling onNewIntent directly is deliberate: this regression test
|
||||
// exercises the already-running Activity branch without depending
|
||||
// on an OEM task switcher. ActivityScenario.close() can wait
|
||||
// indefinitely after that synthetic lifecycle callback on HyperOS,
|
||||
// so finish the test Activity explicitly instead.
|
||||
scenario.onActivity(MainActivity::finish);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test04ForegroundTrackingPostsLocationAndNotificationStop() throws Exception {
|
||||
grantTrackingPermissions();
|
||||
|
|
@ -275,33 +370,31 @@ public final class AndroidClientInstrumentedTest {
|
|||
server.awaitRequestEndingWith(positionPath, 15, TimeUnit.SECONDS)
|
||||
);
|
||||
|
||||
int foregroundCount = server.requestCountEndingWith(positionPath);
|
||||
device.pressHome();
|
||||
|
||||
assertNotNull(
|
||||
"Tracking stopped after the Activity left the foreground",
|
||||
server.awaitRequestAfterCount(
|
||||
positionPath,
|
||||
foregroundCount,
|
||||
15,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
SystemClock.sleep(1_000);
|
||||
assertTrue(serviceIsRunning());
|
||||
assertTrue(
|
||||
"Location updates were unregistered after the Activity left the foreground",
|
||||
locationUpdatesAreRegistered()
|
||||
);
|
||||
assertTrue(
|
||||
"The foreground tracking notification disappeared after pressing Home",
|
||||
trackingNotificationIsActive()
|
||||
);
|
||||
|
||||
int stoppedActivityCount = server.requestCountEndingWith(positionPath);
|
||||
scenario.close();
|
||||
|
||||
assertNotNull(
|
||||
"Tracking stopped after the Activity was destroyed",
|
||||
server.awaitRequestAfterCount(
|
||||
positionPath,
|
||||
stoppedActivityCount,
|
||||
15,
|
||||
TimeUnit.SECONDS
|
||||
)
|
||||
);
|
||||
SystemClock.sleep(1_000);
|
||||
assertTrue(serviceIsRunning());
|
||||
assertTrue(
|
||||
"Location updates were unregistered after the Activity was destroyed",
|
||||
locationUpdatesAreRegistered()
|
||||
);
|
||||
assertTrue(
|
||||
"The foreground tracking notification disappeared after Activity destruction",
|
||||
trackingNotificationIsActive()
|
||||
);
|
||||
|
||||
openNotificationAndClickStop();
|
||||
assertNotNull(
|
||||
|
|
@ -388,18 +481,21 @@ public final class AndroidClientInstrumentedTest {
|
|||
);
|
||||
assertNotNull("Foreground tracking notification was not visible", active);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
sendStopActionFromActiveNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
UiObject2 stop = device.wait(
|
||||
Until.findObject(By.text(context.getString(R.string.tracking_stop_action))),
|
||||
UI_TIMEOUT_MS
|
||||
);
|
||||
|
||||
assertNotNull("Foreground tracking notification had no Stop action", stop);
|
||||
if (stop != null) {
|
||||
stop.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// Some OEM notification shades keep foreground-service actions collapsed
|
||||
// even after the notification is opened. Validate and invoke the actual
|
||||
// action exposed by Android instead of treating that UI choice as an
|
||||
// application failure.
|
||||
sendStopActionFromActiveNotification();
|
||||
}
|
||||
|
||||
private void sendStopActionFromActiveNotification() {
|
||||
|
|
@ -501,4 +597,22 @@ public final class AndroidClientInstrumentedTest {
|
|||
.contains(TrackingService.class.getName());
|
||||
}
|
||||
|
||||
private boolean locationUpdatesAreRegistered() throws Exception {
|
||||
return device
|
||||
.executeShellCommand("dumpsys location")
|
||||
.contains(context.getPackageName());
|
||||
}
|
||||
|
||||
private boolean trackingNotificationIsActive() {
|
||||
NotificationManager manager = context.getSystemService(NotificationManager.class);
|
||||
|
||||
for (StatusBarNotification notification : manager.getActiveNotifications()) {
|
||||
if (notification.getId() == TrackingService.NOTIFICATION_ID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
|
|||
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
|
||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.webKeys;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.app.Notification;
|
||||
|
|
@ -23,7 +24,10 @@ import androidx.test.core.app.ApplicationProvider;
|
|||
import androidx.test.espresso.web.webdriver.Locator;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
import androidx.test.uiautomator.Until;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
@ -32,7 +36,7 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CrossClientStagingInstrumentedTest {
|
||||
private static final long PAGE_TIMEOUT_SECONDS = 45;
|
||||
private static final long PAGE_TIMEOUT_SECONDS = 120;
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final UiDevice device =
|
||||
|
|
@ -77,6 +81,7 @@ public final class CrossClientStagingInstrumentedTest {
|
|||
click("send-message-button");
|
||||
waitForElementText("messages", androidMessage);
|
||||
click("share-location-button");
|
||||
confirmTrackingDisclosure();
|
||||
waitForServiceState(true);
|
||||
waitForElementText("messages", browserReply);
|
||||
waitForNotification(
|
||||
|
|
@ -236,6 +241,31 @@ public final class CrossClientStagingInstrumentedTest {
|
|||
throw timeout;
|
||||
}
|
||||
|
||||
private void confirmTrackingDisclosure() {
|
||||
UiObject2 disclosure = device.wait(
|
||||
Until.findObject(
|
||||
By.text(context.getString(R.string.tracking_disclosure_title))
|
||||
),
|
||||
TimeUnit.SECONDS.toMillis(PAGE_TIMEOUT_SECONDS)
|
||||
);
|
||||
assertNotNull("The live-location disclosure was not shown", disclosure);
|
||||
assertTrue(
|
||||
device.hasObject(
|
||||
By.text(context.getString(R.string.tracking_disclosure_detail))
|
||||
)
|
||||
);
|
||||
|
||||
UiObject2 continueButton = device.wait(
|
||||
Until.findObject(By.res("android", "button1")),
|
||||
TimeUnit.SECONDS.toMillis(PAGE_TIMEOUT_SECONDS)
|
||||
);
|
||||
assertNotNull(
|
||||
"The live-location disclosure had no Continue action",
|
||||
continueButton
|
||||
);
|
||||
continueButton.click();
|
||||
}
|
||||
|
||||
private void waitForServiceState(boolean expected) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,25 @@ final class FixtureHttpServer implements Closeable {
|
|||
return count;
|
||||
}
|
||||
|
||||
RecordedRequest awaitRequestContaining(String value, long timeout, TimeUnit unit)
|
||||
throws InterruptedException {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
|
||||
while (System.nanoTime() < deadline) {
|
||||
synchronized (requests) {
|
||||
for (RecordedRequest request : requests) {
|
||||
if (request.path.contains(value)) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(50);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
RecordedRequest awaitRequestAfterCount(
|
||||
String suffix,
|
||||
int previousCount,
|
||||
|
|
@ -223,10 +242,16 @@ final class FixtureHttpServer implements Closeable {
|
|||
+ "<button id=\"location\" onclick=\"navigator.geolocation.getCurrentPosition("
|
||||
+ "()=>{document.getElementById('marker').textContent='location-granted';"
|
||||
+ "fetch('/geolocation-granted')},"
|
||||
+ "()=>{document.getElementById('marker').textContent='location-denied';"
|
||||
+ "fetch('/geolocation-denied')},"
|
||||
+ "{enableHighAccuracy:true,maximumAge:0,timeout:10000})\">"
|
||||
+ "(error)=>{document.getElementById('marker').textContent="
|
||||
+ "'location-denied-'+error.code;"
|
||||
+ "fetch('/geolocation-denied-'+error.code)},"
|
||||
+ "{enableHighAccuracy:false,maximumAge:3600000,timeout:30000})\">"
|
||||
+ "Request location</button>"
|
||||
+ "<button id=\"tracking\" onclick=\"window.WhoNeedHelpAndroid.postMessage("
|
||||
+ "JSON.stringify({action:'start',"
|
||||
+ "assignment_id:'00000000-0000-4000-8000-000000000001',"
|
||||
+ "csrf_token:'fixture-csrf'}))\">"
|
||||
+ "Start live tracking</button>"
|
||||
+ "</body></html>";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.USE_LOCATION_BUTTON" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import androidx.credentials.GetCredentialResponse;
|
|||
import androidx.credentials.exceptions.ClearCredentialException;
|
||||
import androidx.credentials.exceptions.GetCredentialException;
|
||||
import androidx.credentials.exceptions.NoCredentialException;
|
||||
import androidx.core.locationbutton.LocationButton;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
|
@ -72,11 +73,14 @@ public final class MainActivity extends ComponentActivity {
|
|||
private TrustedOrigin trustedOrigin;
|
||||
private GeolocationPermissions.Callback pendingLocationCallback;
|
||||
private String pendingLocationOrigin;
|
||||
private ActivityResultLauncher<String[]> locationPermissionLauncher;
|
||||
private ActivityResultLauncher<String[]> webLocationPermissionLauncher;
|
||||
private ActivityResultLauncher<String[]> nativeTrackingPermissionLauncher;
|
||||
private ActivityResultLauncher<String> pushNotificationPermissionLauncher;
|
||||
private PendingNativeTracking pendingNativeTracking;
|
||||
private AlertDialog pageLoadErrorDialog;
|
||||
private AlertDialog locationPermissionDialog;
|
||||
private AlertDialog trackingDisclosureDialog;
|
||||
private boolean webLocationPermissionRequestInFlight;
|
||||
private boolean mainFrameLoadFailed;
|
||||
private CredentialManager credentialManager;
|
||||
private ExecutorService nativeGoogleNetworkExecutor;
|
||||
|
|
@ -91,15 +95,24 @@ public final class MainActivity extends ComponentActivity {
|
|||
trustedOrigin = TrustedOrigin.parse(BuildConfig.BASE_URL, BuildConfig.DEBUG);
|
||||
credentialManager = CredentialManager.create(this);
|
||||
nativeGoogleNetworkExecutor = Executors.newSingleThreadExecutor();
|
||||
locationPermissionLauncher = registerForActivityResult(
|
||||
webLocationPermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||
result -> {
|
||||
boolean granted =
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"Web location runtime permission result; granted="
|
||||
+ hasLocationPermission()
|
||||
);
|
||||
}
|
||||
webLocationPermissionRequestInFlight = false;
|
||||
boolean locationGranted =
|
||||
Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
|| Boolean.TRUE.equals(
|
||||
result.get(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
);
|
||||
completeLocationPermission(granted);
|
||||
)
|
||||
|| hasLocationPermission();
|
||||
completeLocationPermission(locationGranted);
|
||||
}
|
||||
);
|
||||
nativeTrackingPermissionLauncher = registerForActivityResult(
|
||||
|
|
@ -296,6 +309,7 @@ public final class MainActivity extends ComponentActivity {
|
|||
@Override
|
||||
protected void onDestroy() {
|
||||
denyPendingLocation();
|
||||
dismissTrackingDisclosure();
|
||||
dismissPageLoadError();
|
||||
cancelNativeGoogleRequest();
|
||||
|
||||
|
|
@ -322,27 +336,84 @@ public final class MainActivity extends ComponentActivity {
|
|||
return;
|
||||
}
|
||||
|
||||
denyPendingLocation();
|
||||
pendingLocationOrigin = origin;
|
||||
pendingLocationCallback = callback;
|
||||
|
||||
boolean granted =
|
||||
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
|| checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (granted) {
|
||||
completeLocationPermission(true);
|
||||
if (hasLocationPermission()) {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(LOG_TAG, "Web location permission already granted");
|
||||
}
|
||||
callback.invoke(origin, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
locationPermissionLauncher.launch(
|
||||
denyPendingLocation();
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(LOG_TAG, "Web location permission prompt requested");
|
||||
}
|
||||
pendingLocationOrigin = origin;
|
||||
pendingLocationCallback = callback;
|
||||
showLocationPermissionDialog();
|
||||
}
|
||||
|
||||
private void showLocationPermissionDialog() {
|
||||
View content = getLayoutInflater().inflate(
|
||||
R.layout.dialog_location_permission,
|
||||
null
|
||||
);
|
||||
LocationButton locationButton = content.findViewById(R.id.location_button);
|
||||
locationButton.setParentActivity(this);
|
||||
locationButton.setOnPermissionResultListener(granted -> {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"LocationButton permission result; granted="
|
||||
+ granted
|
||||
+ "; effective="
|
||||
+ hasLocationPermission()
|
||||
);
|
||||
}
|
||||
webLocationPermissionRequestInFlight = false;
|
||||
completeLocationPermission(granted || hasLocationPermission());
|
||||
});
|
||||
locationButton.setOnRequestPermissionsListener(() -> {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(LOG_TAG, "LocationButton requested fallback permission flow");
|
||||
}
|
||||
webLocationPermissionRequestInFlight = true;
|
||||
webLocationPermissionLauncher.launch(
|
||||
new String[] {
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
}
|
||||
);
|
||||
});
|
||||
locationButton.setOnErrorListener(error -> {
|
||||
webLocationPermissionRequestInFlight = false;
|
||||
Log.w(LOG_TAG, "System location button failed", error);
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.location_button_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
completeLocationPermission(false);
|
||||
});
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.location_button_title)
|
||||
.setMessage(R.string.location_button_detail)
|
||||
.setView(content)
|
||||
.setNegativeButton(
|
||||
R.string.cancel,
|
||||
(ignored, which) -> completeLocationPermission(false)
|
||||
)
|
||||
.create();
|
||||
locationPermissionDialog = dialog;
|
||||
dialog.setOnCancelListener(ignored -> completeLocationPermission(false));
|
||||
dialog.setOnDismissListener(ignored -> {
|
||||
if (locationPermissionDialog == dialog) {
|
||||
locationPermissionDialog = null;
|
||||
completeLocationPermission(false);
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private boolean hasLocationPermission() {
|
||||
|
|
@ -363,7 +434,48 @@ public final class MainActivity extends ComponentActivity {
|
|||
}
|
||||
|
||||
pendingNativeTracking = new PendingNativeTracking(assignmentId, csrfToken);
|
||||
showTrackingDisclosure();
|
||||
}
|
||||
|
||||
private void showTrackingDisclosure() {
|
||||
dismissTrackingDisclosure();
|
||||
|
||||
AtomicBoolean resolved = new AtomicBoolean(false);
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.tracking_disclosure_title)
|
||||
.setMessage(R.string.tracking_disclosure_detail)
|
||||
.setPositiveButton(R.string.tracking_disclosure_continue, (ignored, which) -> {
|
||||
resolved.set(true);
|
||||
trackingDisclosureDialog = null;
|
||||
continuePendingNativeTracking();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (ignored, which) -> {
|
||||
resolved.set(true);
|
||||
trackingDisclosureDialog = null;
|
||||
cancelPendingNativeTracking();
|
||||
})
|
||||
.create();
|
||||
|
||||
trackingDisclosureDialog = dialog;
|
||||
dialog.setOnCancelListener(ignored -> {
|
||||
if (resolved.compareAndSet(false, true)) {
|
||||
trackingDisclosureDialog = null;
|
||||
cancelPendingNativeTracking();
|
||||
}
|
||||
});
|
||||
dialog.setOnDismissListener(ignored -> {
|
||||
if (
|
||||
trackingDisclosureDialog == dialog
|
||||
&& resolved.compareAndSet(false, true)
|
||||
) {
|
||||
trackingDisclosureDialog = null;
|
||||
cancelPendingNativeTracking();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void continuePendingNativeTracking() {
|
||||
boolean notificationGranted =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
|
@ -387,6 +499,22 @@ public final class MainActivity extends ComponentActivity {
|
|||
nativeTrackingPermissionLauncher.launch(permissions.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private void cancelPendingNativeTracking() {
|
||||
pendingNativeTracking = null;
|
||||
dispatchNativeTrackingError();
|
||||
}
|
||||
|
||||
private void dismissTrackingDisclosure() {
|
||||
AlertDialog dialog = trackingDisclosureDialog;
|
||||
trackingDisclosureDialog = null;
|
||||
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
dialog.setOnCancelListener(null);
|
||||
dialog.setOnDismissListener(null);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void configureNativeBridge() {
|
||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
||||
return;
|
||||
|
|
@ -832,12 +960,18 @@ public final class MainActivity extends ComponentActivity {
|
|||
pendingNativeTracking = null;
|
||||
|
||||
if (pending == null) {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.w(LOG_TAG, "Native tracking refused: no pending request");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String cookie = CookieManager.getInstance().getCookie(BuildConfig.BASE_URL);
|
||||
|
||||
if (cookie == null || cookie.trim().isEmpty()) {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.w(LOG_TAG, "Native tracking refused: session cookie unavailable");
|
||||
}
|
||||
dispatchNativeTrackingError();
|
||||
return;
|
||||
}
|
||||
|
|
@ -855,7 +989,14 @@ public final class MainActivity extends ComponentActivity {
|
|||
} else {
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(LOG_TAG, "Native tracking service start requested");
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.e(LOG_TAG, "Native tracking service could not start", exception);
|
||||
}
|
||||
dispatchNativeTrackingError();
|
||||
}
|
||||
}
|
||||
|
|
@ -881,18 +1022,41 @@ public final class MainActivity extends ComponentActivity {
|
|||
}
|
||||
|
||||
private void completeLocationPermission(boolean granted) {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"Completing web location permission; granted="
|
||||
+ granted
|
||||
+ "; callback="
|
||||
+ (pendingLocationCallback != null)
|
||||
);
|
||||
}
|
||||
webLocationPermissionRequestInFlight = false;
|
||||
|
||||
if (pendingLocationCallback != null && pendingLocationOrigin != null) {
|
||||
pendingLocationCallback.invoke(pendingLocationOrigin, granted, false);
|
||||
}
|
||||
|
||||
pendingLocationCallback = null;
|
||||
pendingLocationOrigin = null;
|
||||
dismissLocationPermissionDialog();
|
||||
}
|
||||
|
||||
private void denyPendingLocation() {
|
||||
completeLocationPermission(false);
|
||||
}
|
||||
|
||||
private void dismissLocationPermissionDialog() {
|
||||
AlertDialog dialog = locationPermissionDialog;
|
||||
locationPermissionDialog = null;
|
||||
|
||||
if (dialog != null && dialog.isShowing()) {
|
||||
dialog.setOnCancelListener(null);
|
||||
dialog.setOnDismissListener(null);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void openExternal(Uri uri) {
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
|
|
@ -1067,9 +1231,18 @@ public final class MainActivity extends ComponentActivity {
|
|||
|
||||
@Override
|
||||
public void onGeolocationPermissionsHidePrompt() {
|
||||
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"WebView hid location permission prompt; inFlight="
|
||||
+ webLocationPermissionRequestInFlight
|
||||
);
|
||||
}
|
||||
if (!webLocationPermissionRequestInFlight) {
|
||||
denyPendingLocation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PendingNativeTracking {
|
||||
private final String assignmentId;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public final class TrackingService extends Service implements LocationListener {
|
|||
private static final String EXTRA_CSRF_TOKEN = "csrf_token";
|
||||
private static final String EXTRA_COOKIE = "cookie";
|
||||
private static final String CHANNEL_ID = "active_help_tracking";
|
||||
private static final int NOTIFICATION_ID = 4101;
|
||||
static final int NOTIFICATION_ID = 4101;
|
||||
|
||||
private final Object pendingLocationLock = new Object();
|
||||
private final AtomicBoolean uploadRunning = new AtomicBoolean(false);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<androidx.core.locationbutton.LocationButton
|
||||
android:id="@+id/location_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:iconTint="@android:color/white"
|
||||
app:backgroundColor="@color/brand_green"
|
||||
app:cornerRadius="16dp"
|
||||
app:locationButtonTextType="use_precise_location" />
|
||||
</FrameLayout>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="page_load_failed">Не удалось загрузить Who Need Help. Проверьте подключение и повторите попытку.</string>
|
||||
<string name="retry">Повторить</string>
|
||||
<string name="close">Закрыть</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="secure_connection_failed">Безопасное подключение было отклонено.</string>
|
||||
<string name="unsupported_link">Этот тип ссылки не поддерживается.</string>
|
||||
<string name="tracking_channel_name">Геолокация активной помощи</string>
|
||||
|
|
@ -19,6 +20,12 @@
|
|||
<string name="tracking_stop_failed_detail">Новые обновления приостановлены. Нажмите «Остановить передачу», чтобы повторить удаление текущей позиции.</string>
|
||||
<string name="tracking_location_unavailable">Геолокация недоступна</string>
|
||||
<string name="tracking_location_unavailable_detail">Включите геолокацию устройства, вернитесь к заявке и повторите попытку.</string>
|
||||
<string name="location_button_title">Использовать точную геолокацию один раз</string>
|
||||
<string name="location_button_detail">Who Need Help использует вашу точную геолокацию только для этого действия, чтобы разместить выбранную точку или область. Это не запускает передачу геолокации в реальном времени.</string>
|
||||
<string name="location_button_failed">Android не смог показать защищённую кнопку геолокации. Геолокация не передавалась.</string>
|
||||
<string name="tracking_disclosure_title">Передавать текущую геолокацию участнику?</string>
|
||||
<string name="tracking_disclosure_detail">Who Need Help будет получать и отправлять вашу точную геолокацию назначенному заказчику или помощнику, пока передача включена, в том числе когда приложение свёрнуто. Постоянное уведомление останется видимым. Нажмите «Остановить передачу» в приложении или уведомлении, чтобы остановить её; текущая необработанная позиция после этого удаляется. Сводные данные безопасности могут храниться в соответствии с Политикой конфиденциальности.</string>
|
||||
<string name="tracking_disclosure_continue">Продолжить и передавать</string>
|
||||
<string name="updates_channel_name">Обновления заявок о помощи</string>
|
||||
<string name="updates_channel_description">Приватные уведомления о заявках, сообщениях и помощи поблизости.</string>
|
||||
<string name="updates_default_body">Откройте Who Need Help, чтобы просмотреть обновление.</string>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="page_load_failed">Не вдалося завантажити Who Need Help. Перевірте з’єднання та повторіть спробу.</string>
|
||||
<string name="retry">Повторити</string>
|
||||
<string name="close">Закрити</string>
|
||||
<string name="cancel">Скасувати</string>
|
||||
<string name="secure_connection_failed">Захищене з’єднання було відхилено.</string>
|
||||
<string name="unsupported_link">Цей тип посилання не підтримується.</string>
|
||||
<string name="tracking_channel_name">Геолокація активної допомоги</string>
|
||||
|
|
@ -19,6 +20,12 @@
|
|||
<string name="tracking_stop_failed_detail">Нові оновлення призупинено. Натисніть «Зупинити передачу», щоб повторити видалення поточної позиції.</string>
|
||||
<string name="tracking_location_unavailable">Геолокація недоступна</string>
|
||||
<string name="tracking_location_unavailable_detail">Увімкніть геолокацію пристрою, поверніться до заявки та повторіть спробу.</string>
|
||||
<string name="location_button_title">Використати точну геолокацію один раз</string>
|
||||
<string name="location_button_detail">Who Need Help використає вашу точну геолокацію лише для цієї дії, щоб розмістити вибрану точку або область. Це не запускає передавання геолокації в реальному часі.</string>
|
||||
<string name="location_button_failed">Android не зміг показати захищену кнопку геолокації. Геолокація не передавалася.</string>
|
||||
<string name="tracking_disclosure_title">Передавати поточну геолокацію учаснику?</string>
|
||||
<string name="tracking_disclosure_detail">Who Need Help отримуватиме й надсилатиме вашу точну геолокацію призначеному замовнику або помічнику, поки передавання ввімкнене, зокрема коли застосунок згорнуто. Постійне сповіщення залишатиметься видимим. Натисніть «Зупинити передавання» в застосунку або сповіщенні, щоб зупинити його; поточна необроблена позиція після цього видаляється. Зведені дані безпеки можуть зберігатися відповідно до Політики конфіденційності.</string>
|
||||
<string name="tracking_disclosure_continue">Продовжити й передавати</string>
|
||||
<string name="updates_channel_name">Оновлення заявок про допомогу</string>
|
||||
<string name="updates_channel_description">Приватні сповіщення про заявки, повідомлення та допомогу поблизу.</string>
|
||||
<string name="updates_default_body">Відкрийте Who Need Help, щоб переглянути оновлення.</string>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<string name="page_load_failed">Could not load Who Need Help. Check your connection and retry.</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="secure_connection_failed">The secure connection was rejected.</string>
|
||||
<string name="unsupported_link">This link type is not supported.</string>
|
||||
<string name="tracking_channel_name">Active help location</string>
|
||||
|
|
@ -19,6 +20,12 @@
|
|||
<string name="tracking_stop_failed_detail">New updates are paused. Tap Stop sharing to retry deleting the current position.</string>
|
||||
<string name="tracking_location_unavailable">Location is unavailable</string>
|
||||
<string name="tracking_location_unavailable_detail">Enable device location, then return to the request and try again.</string>
|
||||
<string name="location_button_title">Use your precise location once</string>
|
||||
<string name="location_button_detail">Who Need Help will use your precise location only for this action to place the point or area you selected. This does not start live tracking.</string>
|
||||
<string name="location_button_failed">Android could not provide the secure location button. No location was shared.</string>
|
||||
<string name="tracking_disclosure_title">Share live location with your match?</string>
|
||||
<string name="tracking_disclosure_detail">Who Need Help will collect and send your precise location to the matched requester or helper while sharing is on, including while the app is minimized. A persistent notification remains visible. Use Stop sharing in the app or notification to stop; the current raw position is then deleted. Summary safety evidence may be retained as described in the Privacy Policy.</string>
|
||||
<string name="tracking_disclosure_continue">Continue and share</string>
|
||||
<string name="updates_channel_name">Help request updates</string>
|
||||
<string name="updates_channel_description">Private request, message, and nearby-help notifications.</string>
|
||||
<string name="updates_default_body">Open Who Need Help to view the update.</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ cd "$ROOT"
|
|||
|
||||
ENV_FILE="$ROOT/.env"
|
||||
variant=${WNH_ANDROID_PUBLIC_VARIANT:-staging}
|
||||
physical_serial=${WNH_ANDROID_PHYSICAL_SERIAL:-}
|
||||
physical_mode=0
|
||||
host_adb=
|
||||
|
||||
case "$variant" in
|
||||
development)
|
||||
|
|
@ -25,6 +28,31 @@ case "$variant" in
|
|||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "$physical_serial" ]]; then
|
||||
if [[ "$variant" != development ]]; then
|
||||
echo "Physical-device cross-client E2E is restricted to development." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! "$physical_serial" =~ ^[A-Za-z0-9._:-]+$ ]]; then
|
||||
echo "WNH_ANDROID_PHYSICAL_SERIAL contains unsupported characters." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
host_adb=$(command -v adb || true)
|
||||
if [[ -z "$host_adb" ]]; then
|
||||
echo "adb is required for physical-device cross-client E2E." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$("$host_adb" -s "$physical_serial" get-state 2>/dev/null || true)" != device ]]; then
|
||||
echo "The selected physical Android device is not connected and authorised." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
physical_mode=1
|
||||
fi
|
||||
|
||||
APK="$ROOT/android/dist-$variant/who-need-help-$variant.apk"
|
||||
TEST_APK="$ROOT/android/dist-$variant/who-need-help-$variant-androidTest.apk"
|
||||
remote_target=
|
||||
|
|
@ -104,7 +132,7 @@ if [[ -n "$remote_target" ]]; then
|
|||
esac
|
||||
fi
|
||||
|
||||
if [[ ! -e /dev/kvm ]]; then
|
||||
if [[ "$physical_mode" -eq 0 && ! -e /dev/kvm ]]; then
|
||||
echo "/dev/kvm is required for the containerized Android emulator." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -332,6 +360,15 @@ COMMIT;
|
|||
SQL
|
||||
}
|
||||
|
||||
physical_fixture_user_count() {
|
||||
docker compose exec -T db sh -c \
|
||||
'psql --no-psqlrc --tuples-only --no-align --set ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"' <<SQL
|
||||
SELECT count(*)
|
||||
FROM users
|
||||
WHERE email LIKE 'wnh-android-e2e-$run_id-%';
|
||||
SQL
|
||||
}
|
||||
|
||||
run_fixture_tool() {
|
||||
local action=$1
|
||||
|
||||
|
|
@ -429,7 +466,11 @@ REMOTE_FIXTURE_OWNER
|
|||
}
|
||||
|
||||
adb() {
|
||||
if [[ "$physical_mode" -eq 1 ]]; then
|
||||
"$host_adb" -s "$physical_serial" "$@"
|
||||
else
|
||||
docker exec "$container" adb "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
run_browser_phase() {
|
||||
|
|
@ -497,7 +538,13 @@ cleanup() {
|
|||
local status=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
if docker inspect "$container" >/dev/null 2>&1; then
|
||||
if [[ "$physical_mode" -eq 1 ]] &&
|
||||
[[ "$(adb get-state 2>/dev/null || true)" == device ]]; then
|
||||
adb logcat -d >"$output_dir/logcat.txt" 2>&1 || true
|
||||
adb shell dumpsys notification --noredact >"$output_dir/notifications.txt" 2>&1 || true
|
||||
adb shell dumpsys activity services "$package" >"$output_dir/services.txt" 2>&1 || true
|
||||
adb exec-out screencap -p >"$output_dir/final.png" 2>/dev/null || true
|
||||
elif docker inspect "$container" >/dev/null 2>&1; then
|
||||
adb logcat -d >"$output_dir/logcat.txt" 2>&1 || true
|
||||
adb shell dumpsys notification --noredact >"$output_dir/notifications.txt" 2>&1 || true
|
||||
adb shell dumpsys activity services "$package" >"$output_dir/services.txt" 2>&1 || true
|
||||
|
|
@ -511,7 +558,46 @@ cleanup() {
|
|||
status=1
|
||||
else
|
||||
snapshot_database "$output_dir/database-after.txt"
|
||||
if [[ "$physical_mode" -eq 1 ]]; then
|
||||
grep -v ' push_devices ' "$output_dir/database-before.txt" \
|
||||
>"$output_dir/database-before-without-push-devices.txt"
|
||||
grep -v ' push_devices ' "$output_dir/database-after.txt" \
|
||||
>"$output_dir/database-after-without-push-devices.txt"
|
||||
|
||||
if ! diff -u \
|
||||
"$output_dir/database-before-without-push-devices.txt" \
|
||||
"$output_dir/database-after-without-push-devices.txt" \
|
||||
>"$output_dir/database-cleanup.diff"; then
|
||||
echo "Physical Android/browser cleanup did not restore non-device table counts." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
physical_fixture_user_count \
|
||||
>"$output_dir/fixture-prefix-count.txt"
|
||||
if [[ "$(tr -d '[:space:]' <"$output_dir/fixture-prefix-count.txt")" != 0 ]]; then
|
||||
echo "Physical Android/browser cleanup retained run-scoped users." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
before_push_devices=$(
|
||||
awk '$1 == "push_devices" {print $3}' \
|
||||
"$output_dir/database-before.txt"
|
||||
)
|
||||
after_push_devices=$(
|
||||
awk '$1 == "push_devices" {print $3}' \
|
||||
"$output_dir/database-after.txt"
|
||||
)
|
||||
{
|
||||
printf 'before=%s\n' "$before_push_devices"
|
||||
printf 'after=%s\n' "$after_push_devices"
|
||||
} >"$output_dir/push-device-counts.txt"
|
||||
|
||||
if [[ -z "$before_push_devices" || -z "$after_push_devices" ]] ||
|
||||
((after_push_devices > before_push_devices)); then
|
||||
echo "Physical Android/browser cleanup retained a new push device." >&2
|
||||
status=1
|
||||
fi
|
||||
elif ! diff -u \
|
||||
"$output_dir/database-before.txt" \
|
||||
"$output_dir/database-after.txt" \
|
||||
>"$output_dir/database-cleanup.diff"; then
|
||||
|
|
@ -565,7 +651,12 @@ REMOTE_CLEANUP
|
|||
printf 'true\n'
|
||||
fi
|
||||
printf 'fixture_prefix_absent='
|
||||
if [[ -f "$output_dir/database-cleanup.diff" ]] &&
|
||||
if [[ "$physical_mode" -eq 1 ]] &&
|
||||
[[ -f "$output_dir/fixture-prefix-count.txt" ]] &&
|
||||
[[ "$(tr -d '[:space:]' <"$output_dir/fixture-prefix-count.txt")" == 0 ]]; then
|
||||
printf 'true\n'
|
||||
elif [[ "$physical_mode" -eq 0 ]] &&
|
||||
[[ -f "$output_dir/database-cleanup.diff" ]] &&
|
||||
[[ ! -s "$output_dir/database-cleanup.diff" ]]; then
|
||||
printf 'true\n'
|
||||
else
|
||||
|
|
@ -653,6 +744,18 @@ else
|
|||
fi
|
||||
docker build --tag "$browser_image" e2e \
|
||||
>"$output_dir/browser-build.log"
|
||||
|
||||
if [[ "$physical_mode" -eq 1 ]]; then
|
||||
adb shell input keyevent KEYCODE_WAKEUP
|
||||
if [[ -z "$(adb shell pm path "$package" 2>/dev/null || true)" ]]; then
|
||||
echo "The current development APK is not installed on the selected physical device." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$(adb shell pm path "${package}.test" 2>/dev/null || true)" ]]; then
|
||||
echo "The current development instrumentation APK is not installed on the selected physical device." >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
docker build \
|
||||
--build-arg "WNH_DEBUG_BASE_URL=$WNH_BASE_URL" \
|
||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
||||
|
|
@ -673,7 +776,7 @@ docker run -d \
|
|||
|
||||
if [[ "$(docker inspect --format '{{.State.Running}}' "$container")" != true ]]; then
|
||||
docker logs "$container" >"$output_dir/emulator.log" 2>&1 || true
|
||||
echo "Android cross-client emulator exited before ADB became available; inspect $output_dir/emulator.log." >&2
|
||||
echo "Android emulator exited before ADB became available; inspect $output_dir/emulator.log." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -693,7 +796,7 @@ done
|
|||
|
||||
if [[ "$adb_ready" -ne 1 ]]; then
|
||||
docker logs "$container" >"$output_dir/emulator.log" 2>&1 || true
|
||||
echo "Android cross-client emulator did not expose a ready ADB device." >&2
|
||||
echo "Android emulator did not expose a ready ADB device." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -705,7 +808,7 @@ for _attempt in $(seq 1 90); do
|
|||
done
|
||||
|
||||
if [[ "$booted" != 1 ]]; then
|
||||
echo "Android cross-client emulator did not finish booting." >&2
|
||||
echo "Android emulator did not finish booting." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -719,10 +822,14 @@ docker cp "$TEST_APK" "$container:/tmp/who-need-help-public-androidTest.apk"
|
|||
adb install -r /tmp/who-need-help-public.apk >"$output_dir/install.txt"
|
||||
adb install -r /tmp/who-need-help-public-androidTest.apk \
|
||||
>"$output_dir/test-install.txt"
|
||||
fi
|
||||
|
||||
adb shell pm list instrumentation >"$output_dir/instrumentation.txt"
|
||||
adb shell pm grant "$package" android.permission.ACCESS_FINE_LOCATION
|
||||
adb shell pm grant "$package" android.permission.POST_NOTIFICATIONS
|
||||
if [[ "$physical_mode" -eq 0 ]]; then
|
||||
adb emu geo fix 30.5237 50.4504
|
||||
fi
|
||||
|
||||
network_ready=0
|
||||
for _attempt in $(seq 1 45); do
|
||||
|
|
@ -753,7 +860,9 @@ helper_login_path=$(jq -r '.helper_login_path' "$output_dir/fixture.json")
|
|||
requester_email=$(jq -r '.users.requester.email' "$output_dir/fixture.json")
|
||||
|
||||
adb logcat -c
|
||||
if [[ "$physical_mode" -eq 0 ]]; then
|
||||
adb emu geo fix 30.5237 50.4504
|
||||
fi
|
||||
start_android_phase exchangeMessageAndTrackingWithBrowser
|
||||
|
||||
tracking_started=0
|
||||
|
|
@ -771,7 +880,9 @@ for _attempt in $(seq 1 45); do
|
|||
tracking_started=1
|
||||
break
|
||||
fi
|
||||
if [[ "$physical_mode" -eq 0 ]]; then
|
||||
adb emu geo fix 30.5237 50.4504 >/dev/null
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
|
|
@ -821,6 +932,8 @@ fi
|
|||
printf 'run_id=%s\n' "$run_id"
|
||||
printf 'android_api=%s\n' "$android_api"
|
||||
printf 'android_system_image=%s\n' "$android_system_image"
|
||||
printf 'android_device=%s\n' \
|
||||
"$([[ "$physical_mode" -eq 1 ]] && printf physical || printf emulator)"
|
||||
printf 'package=%s\n' "$package"
|
||||
printf 'public_origin=%s\n' "$WNH_BASE_URL"
|
||||
printf 'magic_link_login=true\n'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ set -eu
|
|||
|
||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"}
|
||||
DEBUG_BASE_URL_OVERRIDE=${WNH_DEBUG_BASE_URL_OVERRIDE:-}
|
||||
|
||||
case "$ENV_FILE" in
|
||||
/*) ;;
|
||||
|
|
@ -19,6 +20,10 @@ set -a
|
|||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
if [ -n "$DEBUG_BASE_URL_OVERRIDE" ]; then
|
||||
WNH_DEBUG_BASE_URL=$DEBUG_BASE_URL_OVERRIDE
|
||||
fi
|
||||
|
||||
: "${WNH_DEBUG_BASE_URL:?Set WNH_DEBUG_BASE_URL in .env}"
|
||||
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ set -eu
|
|||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
ENV_FILE="$ROOT/.env"
|
||||
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
||||
SIGNING_DIR=${WNH_ANDROID_DEVELOPMENT_SIGNING_DIR:-"$config_home/who_need_help/android-development"}
|
||||
KEYSTORE="$SIGNING_DIR/who-need-help-development.p12"
|
||||
PASSWORD_FILE="$SIGNING_DIR/who-need-help-development.password"
|
||||
|
||||
"$ROOT/scripts/ensure-local-public-origin.sh"
|
||||
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" development
|
||||
|
|
@ -16,6 +13,10 @@ set -a
|
|||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
SIGNING_DIR=${WNH_ANDROID_DEVELOPMENT_SIGNING_DIR:-"$config_home/who_need_help/android-development"}
|
||||
KEYSTORE="$SIGNING_DIR/who-need-help-development.p12"
|
||||
PASSWORD_FILE="$SIGNING_DIR/who-need-help-development.password"
|
||||
|
||||
: "${WNH_TRACKING_MIN_TIME_MS:?Set WNH_TRACKING_MIN_TIME_MS in .env}"
|
||||
: "${WNH_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in .env}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user