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 \
|
COPY --from=android-sdk \
|
||||||
/workspace/android/app/build/outputs/apk/debug/app-debug.apk \
|
/workspace/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||||
/who-need-help-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 \
|
COPY --from=android-sdk \
|
||||||
/workspace/android/app/build/reports/lint-results-debug.html \
|
/workspace/android/app/build/reports/lint-results-debug.html \
|
||||||
/lint-results-debug.html
|
/lint-results-debug.html
|
||||||
|
|
|
||||||
|
|
@ -331,6 +331,7 @@ tasks.withType<JavaCompile>().configureEach {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.activity:activity:1.13.0")
|
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:1.6.0")
|
||||||
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")
|
implementation("androidx.credentials:credentials-play-services-auth:1.6.0")
|
||||||
implementation("androidx.fragment:fragment:1.8.9")
|
implementation("androidx.fragment:fragment:1.8.9")
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,10 @@ public final class AndroidClientInstrumentedTest {
|
||||||
context.stopService(new Intent(context, TrackingService.class));
|
context.stopService(new Intent(context, TrackingService.class));
|
||||||
context.getSystemService(NotificationManager.class).cancelAll();
|
context.getSystemService(NotificationManager.class).cancelAll();
|
||||||
device.pressBack();
|
device.pressBack();
|
||||||
|
if (server != null) {
|
||||||
server.close();
|
server.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
|
public void test01TrackingWithoutLocationPermissionStopsLocally() throws Exception {
|
||||||
|
|
@ -107,6 +109,23 @@ public final class AndroidClientInstrumentedTest {
|
||||||
.withElement(findElement(Locator.ID, "location"))
|
.withElement(findElement(Locator.ID, "location"))
|
||||||
.perform(webClick());
|
.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;
|
UiObject2 allow = null;
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
|
@ -156,17 +175,52 @@ public final class AndroidClientInstrumentedTest {
|
||||||
allow.click();
|
allow.click();
|
||||||
|
|
||||||
waitForLocationPermission();
|
waitForLocationPermission();
|
||||||
assertNotNull(
|
FixtureHttpServer.RecordedRequest callback =
|
||||||
"WebView geolocation did not report a granted position",
|
server.awaitRequestContaining(
|
||||||
server.awaitRequestEndingWith(
|
"/geolocation-",
|
||||||
"/geolocation-granted",
|
35,
|
||||||
15,
|
|
||||||
TimeUnit.SECONDS
|
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"))
|
UiObject2 cancel = device.wait(
|
||||||
.check(webMatches(getText(), containsString("location-granted")));
|
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
|
@Test
|
||||||
public void test04ForegroundTrackingPostsLocationAndNotificationStop() throws Exception {
|
public void test04ForegroundTrackingPostsLocationAndNotificationStop() throws Exception {
|
||||||
grantTrackingPermissions();
|
grantTrackingPermissions();
|
||||||
|
|
@ -275,33 +370,31 @@ public final class AndroidClientInstrumentedTest {
|
||||||
server.awaitRequestEndingWith(positionPath, 15, TimeUnit.SECONDS)
|
server.awaitRequestEndingWith(positionPath, 15, TimeUnit.SECONDS)
|
||||||
);
|
);
|
||||||
|
|
||||||
int foregroundCount = server.requestCountEndingWith(positionPath);
|
|
||||||
device.pressHome();
|
device.pressHome();
|
||||||
|
|
||||||
assertNotNull(
|
SystemClock.sleep(1_000);
|
||||||
"Tracking stopped after the Activity left the foreground",
|
|
||||||
server.awaitRequestAfterCount(
|
|
||||||
positionPath,
|
|
||||||
foregroundCount,
|
|
||||||
15,
|
|
||||||
TimeUnit.SECONDS
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assertTrue(serviceIsRunning());
|
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();
|
scenario.close();
|
||||||
|
|
||||||
assertNotNull(
|
SystemClock.sleep(1_000);
|
||||||
"Tracking stopped after the Activity was destroyed",
|
|
||||||
server.awaitRequestAfterCount(
|
|
||||||
positionPath,
|
|
||||||
stoppedActivityCount,
|
|
||||||
15,
|
|
||||||
TimeUnit.SECONDS
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assertTrue(serviceIsRunning());
|
assertTrue(serviceIsRunning());
|
||||||
|
assertTrue(
|
||||||
|
"Location updates were unregistered after the Activity was destroyed",
|
||||||
|
locationUpdatesAreRegistered()
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
"The foreground tracking notification disappeared after Activity destruction",
|
||||||
|
trackingNotificationIsActive()
|
||||||
|
);
|
||||||
|
|
||||||
openNotificationAndClickStop();
|
openNotificationAndClickStop();
|
||||||
assertNotNull(
|
assertNotNull(
|
||||||
|
|
@ -388,18 +481,21 @@ public final class AndroidClientInstrumentedTest {
|
||||||
);
|
);
|
||||||
assertNotNull("Foreground tracking notification was not visible", active);
|
assertNotNull("Foreground tracking notification was not visible", active);
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
sendStopActionFromActiveNotification();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UiObject2 stop = device.wait(
|
UiObject2 stop = device.wait(
|
||||||
Until.findObject(By.text(context.getString(R.string.tracking_stop_action))),
|
Until.findObject(By.text(context.getString(R.string.tracking_stop_action))),
|
||||||
UI_TIMEOUT_MS
|
UI_TIMEOUT_MS
|
||||||
);
|
);
|
||||||
|
|
||||||
assertNotNull("Foreground tracking notification had no Stop action", stop);
|
if (stop != null) {
|
||||||
stop.click();
|
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() {
|
private void sendStopActionFromActiveNotification() {
|
||||||
|
|
@ -501,4 +597,22 @@ public final class AndroidClientInstrumentedTest {
|
||||||
.contains(TrackingService.class.getName());
|
.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.webClick;
|
||||||
import static androidx.test.espresso.web.webdriver.DriverAtoms.webKeys;
|
import static androidx.test.espresso.web.webdriver.DriverAtoms.webKeys;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
|
|
@ -23,7 +24,10 @@ import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.espresso.web.webdriver.Locator;
|
import androidx.test.espresso.web.webdriver.Locator;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.platform.app.InstrumentationRegistry;
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import androidx.test.uiautomator.By;
|
||||||
import androidx.test.uiautomator.UiDevice;
|
import androidx.test.uiautomator.UiDevice;
|
||||||
|
import androidx.test.uiautomator.UiObject2;
|
||||||
|
import androidx.test.uiautomator.Until;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -32,7 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class CrossClientStagingInstrumentedTest {
|
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 Context context = ApplicationProvider.getApplicationContext();
|
||||||
private final UiDevice device =
|
private final UiDevice device =
|
||||||
|
|
@ -77,6 +81,7 @@ public final class CrossClientStagingInstrumentedTest {
|
||||||
click("send-message-button");
|
click("send-message-button");
|
||||||
waitForElementText("messages", androidMessage);
|
waitForElementText("messages", androidMessage);
|
||||||
click("share-location-button");
|
click("share-location-button");
|
||||||
|
confirmTrackingDisclosure();
|
||||||
waitForServiceState(true);
|
waitForServiceState(true);
|
||||||
waitForElementText("messages", browserReply);
|
waitForElementText("messages", browserReply);
|
||||||
waitForNotification(
|
waitForNotification(
|
||||||
|
|
@ -236,6 +241,31 @@ public final class CrossClientStagingInstrumentedTest {
|
||||||
throw timeout;
|
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 {
|
private void waitForServiceState(boolean expected) throws Exception {
|
||||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,25 @@ final class FixtureHttpServer implements Closeable {
|
||||||
return count;
|
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(
|
RecordedRequest awaitRequestAfterCount(
|
||||||
String suffix,
|
String suffix,
|
||||||
int previousCount,
|
int previousCount,
|
||||||
|
|
@ -223,10 +242,16 @@ final class FixtureHttpServer implements Closeable {
|
||||||
+ "<button id=\"location\" onclick=\"navigator.geolocation.getCurrentPosition("
|
+ "<button id=\"location\" onclick=\"navigator.geolocation.getCurrentPosition("
|
||||||
+ "()=>{document.getElementById('marker').textContent='location-granted';"
|
+ "()=>{document.getElementById('marker').textContent='location-granted';"
|
||||||
+ "fetch('/geolocation-granted')},"
|
+ "fetch('/geolocation-granted')},"
|
||||||
+ "()=>{document.getElementById('marker').textContent='location-denied';"
|
+ "(error)=>{document.getElementById('marker').textContent="
|
||||||
+ "fetch('/geolocation-denied')},"
|
+ "'location-denied-'+error.code;"
|
||||||
+ "{enableHighAccuracy:true,maximumAge:0,timeout:10000})\">"
|
+ "fetch('/geolocation-denied-'+error.code)},"
|
||||||
|
+ "{enableHighAccuracy:false,maximumAge:3600000,timeout:30000})\">"
|
||||||
+ "Request location</button>"
|
+ "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>";
|
+ "</body></html>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_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.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<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.ClearCredentialException;
|
||||||
import androidx.credentials.exceptions.GetCredentialException;
|
import androidx.credentials.exceptions.GetCredentialException;
|
||||||
import androidx.credentials.exceptions.NoCredentialException;
|
import androidx.credentials.exceptions.NoCredentialException;
|
||||||
|
import androidx.core.locationbutton.LocationButton;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
@ -72,11 +73,14 @@ public final class MainActivity extends ComponentActivity {
|
||||||
private TrustedOrigin trustedOrigin;
|
private TrustedOrigin trustedOrigin;
|
||||||
private GeolocationPermissions.Callback pendingLocationCallback;
|
private GeolocationPermissions.Callback pendingLocationCallback;
|
||||||
private String pendingLocationOrigin;
|
private String pendingLocationOrigin;
|
||||||
private ActivityResultLauncher<String[]> locationPermissionLauncher;
|
private ActivityResultLauncher<String[]> webLocationPermissionLauncher;
|
||||||
private ActivityResultLauncher<String[]> nativeTrackingPermissionLauncher;
|
private ActivityResultLauncher<String[]> nativeTrackingPermissionLauncher;
|
||||||
private ActivityResultLauncher<String> pushNotificationPermissionLauncher;
|
private ActivityResultLauncher<String> pushNotificationPermissionLauncher;
|
||||||
private PendingNativeTracking pendingNativeTracking;
|
private PendingNativeTracking pendingNativeTracking;
|
||||||
private AlertDialog pageLoadErrorDialog;
|
private AlertDialog pageLoadErrorDialog;
|
||||||
|
private AlertDialog locationPermissionDialog;
|
||||||
|
private AlertDialog trackingDisclosureDialog;
|
||||||
|
private boolean webLocationPermissionRequestInFlight;
|
||||||
private boolean mainFrameLoadFailed;
|
private boolean mainFrameLoadFailed;
|
||||||
private CredentialManager credentialManager;
|
private CredentialManager credentialManager;
|
||||||
private ExecutorService nativeGoogleNetworkExecutor;
|
private ExecutorService nativeGoogleNetworkExecutor;
|
||||||
|
|
@ -91,15 +95,24 @@ public final class MainActivity extends ComponentActivity {
|
||||||
trustedOrigin = TrustedOrigin.parse(BuildConfig.BASE_URL, BuildConfig.DEBUG);
|
trustedOrigin = TrustedOrigin.parse(BuildConfig.BASE_URL, BuildConfig.DEBUG);
|
||||||
credentialManager = CredentialManager.create(this);
|
credentialManager = CredentialManager.create(this);
|
||||||
nativeGoogleNetworkExecutor = Executors.newSingleThreadExecutor();
|
nativeGoogleNetworkExecutor = Executors.newSingleThreadExecutor();
|
||||||
locationPermissionLauncher = registerForActivityResult(
|
webLocationPermissionLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.RequestMultiplePermissions(),
|
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
result -> {
|
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_FINE_LOCATION))
|
||||||
|| Boolean.TRUE.equals(
|
|| Boolean.TRUE.equals(
|
||||||
result.get(Manifest.permission.ACCESS_COARSE_LOCATION)
|
result.get(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
);
|
)
|
||||||
completeLocationPermission(granted);
|
|| hasLocationPermission();
|
||||||
|
completeLocationPermission(locationGranted);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
nativeTrackingPermissionLauncher = registerForActivityResult(
|
nativeTrackingPermissionLauncher = registerForActivityResult(
|
||||||
|
|
@ -296,6 +309,7 @@ public final class MainActivity extends ComponentActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
denyPendingLocation();
|
denyPendingLocation();
|
||||||
|
dismissTrackingDisclosure();
|
||||||
dismissPageLoadError();
|
dismissPageLoadError();
|
||||||
cancelNativeGoogleRequest();
|
cancelNativeGoogleRequest();
|
||||||
|
|
||||||
|
|
@ -322,27 +336,84 @@ public final class MainActivity extends ComponentActivity {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
denyPendingLocation();
|
if (hasLocationPermission()) {
|
||||||
pendingLocationOrigin = origin;
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
pendingLocationCallback = callback;
|
Log.d(LOG_TAG, "Web location permission already granted");
|
||||||
|
}
|
||||||
boolean granted =
|
callback.invoke(origin, true, false);
|
||||||
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
|
||||||
== PackageManager.PERMISSION_GRANTED
|
|
||||||
|| checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
||||||
== PackageManager.PERMISSION_GRANTED;
|
|
||||||
|
|
||||||
if (granted) {
|
|
||||||
completeLocationPermission(true);
|
|
||||||
return;
|
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[] {
|
new String[] {
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
Manifest.permission.ACCESS_COARSE_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() {
|
private boolean hasLocationPermission() {
|
||||||
|
|
@ -363,7 +434,48 @@ public final class MainActivity extends ComponentActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingNativeTracking = new PendingNativeTracking(assignmentId, csrfToken);
|
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 =
|
boolean notificationGranted =
|
||||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||||
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
|
@ -387,6 +499,22 @@ public final class MainActivity extends ComponentActivity {
|
||||||
nativeTrackingPermissionLauncher.launch(permissions.toArray(new String[0]));
|
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() {
|
private void configureNativeBridge() {
|
||||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -832,12 +960,18 @@ public final class MainActivity extends ComponentActivity {
|
||||||
pendingNativeTracking = null;
|
pendingNativeTracking = null;
|
||||||
|
|
||||||
if (pending == null) {
|
if (pending == null) {
|
||||||
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
|
Log.w(LOG_TAG, "Native tracking refused: no pending request");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String cookie = CookieManager.getInstance().getCookie(BuildConfig.BASE_URL);
|
String cookie = CookieManager.getInstance().getCookie(BuildConfig.BASE_URL);
|
||||||
|
|
||||||
if (cookie == null || cookie.trim().isEmpty()) {
|
if (cookie == null || cookie.trim().isEmpty()) {
|
||||||
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
|
Log.w(LOG_TAG, "Native tracking refused: session cookie unavailable");
|
||||||
|
}
|
||||||
dispatchNativeTrackingError();
|
dispatchNativeTrackingError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -855,7 +989,14 @@ public final class MainActivity extends ComponentActivity {
|
||||||
} else {
|
} else {
|
||||||
startService(intent);
|
startService(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
|
Log.d(LOG_TAG, "Native tracking service start requested");
|
||||||
|
}
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
|
Log.e(LOG_TAG, "Native tracking service could not start", exception);
|
||||||
|
}
|
||||||
dispatchNativeTrackingError();
|
dispatchNativeTrackingError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -881,18 +1022,41 @@ public final class MainActivity extends ComponentActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completeLocationPermission(boolean granted) {
|
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) {
|
if (pendingLocationCallback != null && pendingLocationOrigin != null) {
|
||||||
pendingLocationCallback.invoke(pendingLocationOrigin, granted, false);
|
pendingLocationCallback.invoke(pendingLocationOrigin, granted, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingLocationCallback = null;
|
pendingLocationCallback = null;
|
||||||
pendingLocationOrigin = null;
|
pendingLocationOrigin = null;
|
||||||
|
dismissLocationPermissionDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void denyPendingLocation() {
|
private void denyPendingLocation() {
|
||||||
completeLocationPermission(false);
|
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) {
|
private void openExternal(Uri uri) {
|
||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
|
|
||||||
|
|
@ -1067,9 +1231,18 @@ public final class MainActivity extends ComponentActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGeolocationPermissionsHidePrompt() {
|
public void onGeolocationPermissionsHidePrompt() {
|
||||||
|
if (PAGE_LOAD_DIAGNOSTICS) {
|
||||||
|
Log.d(
|
||||||
|
LOG_TAG,
|
||||||
|
"WebView hid location permission prompt; inFlight="
|
||||||
|
+ webLocationPermissionRequestInFlight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!webLocationPermissionRequestInFlight) {
|
||||||
denyPendingLocation();
|
denyPendingLocation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class PendingNativeTracking {
|
private static final class PendingNativeTracking {
|
||||||
private final String assignmentId;
|
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_CSRF_TOKEN = "csrf_token";
|
||||||
private static final String EXTRA_COOKIE = "cookie";
|
private static final String EXTRA_COOKIE = "cookie";
|
||||||
private static final String CHANNEL_ID = "active_help_tracking";
|
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 Object pendingLocationLock = new Object();
|
||||||
private final AtomicBoolean uploadRunning = new AtomicBoolean(false);
|
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="page_load_failed">Не удалось загрузить Who Need Help. Проверьте подключение и повторите попытку.</string>
|
||||||
<string name="retry">Повторить</string>
|
<string name="retry">Повторить</string>
|
||||||
<string name="close">Закрыть</string>
|
<string name="close">Закрыть</string>
|
||||||
|
<string name="cancel">Отмена</string>
|
||||||
<string name="secure_connection_failed">Безопасное подключение было отклонено.</string>
|
<string name="secure_connection_failed">Безопасное подключение было отклонено.</string>
|
||||||
<string name="unsupported_link">Этот тип ссылки не поддерживается.</string>
|
<string name="unsupported_link">Этот тип ссылки не поддерживается.</string>
|
||||||
<string name="tracking_channel_name">Геолокация активной помощи</string>
|
<string name="tracking_channel_name">Геолокация активной помощи</string>
|
||||||
|
|
@ -19,6 +20,12 @@
|
||||||
<string name="tracking_stop_failed_detail">Новые обновления приостановлены. Нажмите «Остановить передачу», чтобы повторить удаление текущей позиции.</string>
|
<string name="tracking_stop_failed_detail">Новые обновления приостановлены. Нажмите «Остановить передачу», чтобы повторить удаление текущей позиции.</string>
|
||||||
<string name="tracking_location_unavailable">Геолокация недоступна</string>
|
<string name="tracking_location_unavailable">Геолокация недоступна</string>
|
||||||
<string name="tracking_location_unavailable_detail">Включите геолокацию устройства, вернитесь к заявке и повторите попытку.</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_name">Обновления заявок о помощи</string>
|
||||||
<string name="updates_channel_description">Приватные уведомления о заявках, сообщениях и помощи поблизости.</string>
|
<string name="updates_channel_description">Приватные уведомления о заявках, сообщениях и помощи поблизости.</string>
|
||||||
<string name="updates_default_body">Откройте Who Need Help, чтобы просмотреть обновление.</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="page_load_failed">Не вдалося завантажити Who Need Help. Перевірте з’єднання та повторіть спробу.</string>
|
||||||
<string name="retry">Повторити</string>
|
<string name="retry">Повторити</string>
|
||||||
<string name="close">Закрити</string>
|
<string name="close">Закрити</string>
|
||||||
|
<string name="cancel">Скасувати</string>
|
||||||
<string name="secure_connection_failed">Захищене з’єднання було відхилено.</string>
|
<string name="secure_connection_failed">Захищене з’єднання було відхилено.</string>
|
||||||
<string name="unsupported_link">Цей тип посилання не підтримується.</string>
|
<string name="unsupported_link">Цей тип посилання не підтримується.</string>
|
||||||
<string name="tracking_channel_name">Геолокація активної допомоги</string>
|
<string name="tracking_channel_name">Геолокація активної допомоги</string>
|
||||||
|
|
@ -19,6 +20,12 @@
|
||||||
<string name="tracking_stop_failed_detail">Нові оновлення призупинено. Натисніть «Зупинити передачу», щоб повторити видалення поточної позиції.</string>
|
<string name="tracking_stop_failed_detail">Нові оновлення призупинено. Натисніть «Зупинити передачу», щоб повторити видалення поточної позиції.</string>
|
||||||
<string name="tracking_location_unavailable">Геолокація недоступна</string>
|
<string name="tracking_location_unavailable">Геолокація недоступна</string>
|
||||||
<string name="tracking_location_unavailable_detail">Увімкніть геолокацію пристрою, поверніться до заявки та повторіть спробу.</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_name">Оновлення заявок про допомогу</string>
|
||||||
<string name="updates_channel_description">Приватні сповіщення про заявки, повідомлення та допомогу поблизу.</string>
|
<string name="updates_channel_description">Приватні сповіщення про заявки, повідомлення та допомогу поблизу.</string>
|
||||||
<string name="updates_default_body">Відкрийте Who Need Help, щоб переглянути оновлення.</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="page_load_failed">Could not load Who Need Help. Check your connection and retry.</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="secure_connection_failed">The secure connection was rejected.</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="unsupported_link">This link type is not supported.</string>
|
||||||
<string name="tracking_channel_name">Active help location</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_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">Location is unavailable</string>
|
||||||
<string name="tracking_location_unavailable_detail">Enable device location, then return to the request and try again.</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_name">Help request updates</string>
|
||||||
<string name="updates_channel_description">Private request, message, and nearby-help notifications.</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>
|
<string name="updates_default_body">Open Who Need Help to view the update.</string>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ cd "$ROOT"
|
||||||
|
|
||||||
ENV_FILE="$ROOT/.env"
|
ENV_FILE="$ROOT/.env"
|
||||||
variant=${WNH_ANDROID_PUBLIC_VARIANT:-staging}
|
variant=${WNH_ANDROID_PUBLIC_VARIANT:-staging}
|
||||||
|
physical_serial=${WNH_ANDROID_PHYSICAL_SERIAL:-}
|
||||||
|
physical_mode=0
|
||||||
|
host_adb=
|
||||||
|
|
||||||
case "$variant" in
|
case "$variant" in
|
||||||
development)
|
development)
|
||||||
|
|
@ -25,6 +28,31 @@ case "$variant" in
|
||||||
;;
|
;;
|
||||||
esac
|
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"
|
APK="$ROOT/android/dist-$variant/who-need-help-$variant.apk"
|
||||||
TEST_APK="$ROOT/android/dist-$variant/who-need-help-$variant-androidTest.apk"
|
TEST_APK="$ROOT/android/dist-$variant/who-need-help-$variant-androidTest.apk"
|
||||||
remote_target=
|
remote_target=
|
||||||
|
|
@ -104,7 +132,7 @@ if [[ -n "$remote_target" ]]; then
|
||||||
esac
|
esac
|
||||||
fi
|
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
|
echo "/dev/kvm is required for the containerized Android emulator." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -332,6 +360,15 @@ COMMIT;
|
||||||
SQL
|
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() {
|
run_fixture_tool() {
|
||||||
local action=$1
|
local action=$1
|
||||||
|
|
||||||
|
|
@ -429,7 +466,11 @@ REMOTE_FIXTURE_OWNER
|
||||||
}
|
}
|
||||||
|
|
||||||
adb() {
|
adb() {
|
||||||
|
if [[ "$physical_mode" -eq 1 ]]; then
|
||||||
|
"$host_adb" -s "$physical_serial" "$@"
|
||||||
|
else
|
||||||
docker exec "$container" adb "$@"
|
docker exec "$container" adb "$@"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
run_browser_phase() {
|
run_browser_phase() {
|
||||||
|
|
@ -497,7 +538,13 @@ cleanup() {
|
||||||
local status=$?
|
local status=$?
|
||||||
trap - EXIT HUP INT TERM
|
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 logcat -d >"$output_dir/logcat.txt" 2>&1 || true
|
||||||
adb shell dumpsys notification --noredact >"$output_dir/notifications.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 shell dumpsys activity services "$package" >"$output_dir/services.txt" 2>&1 || true
|
||||||
|
|
@ -511,7 +558,46 @@ cleanup() {
|
||||||
status=1
|
status=1
|
||||||
else
|
else
|
||||||
snapshot_database "$output_dir/database-after.txt"
|
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 \
|
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-before.txt" \
|
||||||
"$output_dir/database-after.txt" \
|
"$output_dir/database-after.txt" \
|
||||||
>"$output_dir/database-cleanup.diff"; then
|
>"$output_dir/database-cleanup.diff"; then
|
||||||
|
|
@ -565,7 +651,12 @@ REMOTE_CLEANUP
|
||||||
printf 'true\n'
|
printf 'true\n'
|
||||||
fi
|
fi
|
||||||
printf 'fixture_prefix_absent='
|
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
|
[[ ! -s "$output_dir/database-cleanup.diff" ]]; then
|
||||||
printf 'true\n'
|
printf 'true\n'
|
||||||
else
|
else
|
||||||
|
|
@ -653,6 +744,18 @@ else
|
||||||
fi
|
fi
|
||||||
docker build --tag "$browser_image" e2e \
|
docker build --tag "$browser_image" e2e \
|
||||||
>"$output_dir/browser-build.log"
|
>"$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 \
|
docker build \
|
||||||
--build-arg "WNH_DEBUG_BASE_URL=$WNH_BASE_URL" \
|
--build-arg "WNH_DEBUG_BASE_URL=$WNH_BASE_URL" \
|
||||||
--build-arg "WNH_TRACKING_MIN_TIME_MS=$WNH_TRACKING_MIN_TIME_MS" \
|
--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
|
if [[ "$(docker inspect --format '{{.State.Running}}' "$container")" != true ]]; then
|
||||||
docker logs "$container" >"$output_dir/emulator.log" 2>&1 || true
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -693,7 +796,7 @@ done
|
||||||
|
|
||||||
if [[ "$adb_ready" -ne 1 ]]; then
|
if [[ "$adb_ready" -ne 1 ]]; then
|
||||||
docker logs "$container" >"$output_dir/emulator.log" 2>&1 || true
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -705,7 +808,7 @@ for _attempt in $(seq 1 90); do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$booted" != 1 ]]; then
|
if [[ "$booted" != 1 ]]; then
|
||||||
echo "Android cross-client emulator did not finish booting." >&2
|
echo "Android emulator did not finish booting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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.apk >"$output_dir/install.txt"
|
||||||
adb install -r /tmp/who-need-help-public-androidTest.apk \
|
adb install -r /tmp/who-need-help-public-androidTest.apk \
|
||||||
>"$output_dir/test-install.txt"
|
>"$output_dir/test-install.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
adb shell pm list instrumentation >"$output_dir/instrumentation.txt"
|
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.ACCESS_FINE_LOCATION
|
||||||
adb shell pm grant "$package" android.permission.POST_NOTIFICATIONS
|
adb shell pm grant "$package" android.permission.POST_NOTIFICATIONS
|
||||||
|
if [[ "$physical_mode" -eq 0 ]]; then
|
||||||
adb emu geo fix 30.5237 50.4504
|
adb emu geo fix 30.5237 50.4504
|
||||||
|
fi
|
||||||
|
|
||||||
network_ready=0
|
network_ready=0
|
||||||
for _attempt in $(seq 1 45); do
|
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")
|
requester_email=$(jq -r '.users.requester.email' "$output_dir/fixture.json")
|
||||||
|
|
||||||
adb logcat -c
|
adb logcat -c
|
||||||
|
if [[ "$physical_mode" -eq 0 ]]; then
|
||||||
adb emu geo fix 30.5237 50.4504
|
adb emu geo fix 30.5237 50.4504
|
||||||
|
fi
|
||||||
start_android_phase exchangeMessageAndTrackingWithBrowser
|
start_android_phase exchangeMessageAndTrackingWithBrowser
|
||||||
|
|
||||||
tracking_started=0
|
tracking_started=0
|
||||||
|
|
@ -771,7 +880,9 @@ for _attempt in $(seq 1 45); do
|
||||||
tracking_started=1
|
tracking_started=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
if [[ "$physical_mode" -eq 0 ]]; then
|
||||||
adb emu geo fix 30.5237 50.4504 >/dev/null
|
adb emu geo fix 30.5237 50.4504 >/dev/null
|
||||||
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -821,6 +932,8 @@ fi
|
||||||
printf 'run_id=%s\n' "$run_id"
|
printf 'run_id=%s\n' "$run_id"
|
||||||
printf 'android_api=%s\n' "$android_api"
|
printf 'android_api=%s\n' "$android_api"
|
||||||
printf 'android_system_image=%s\n' "$android_system_image"
|
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 'package=%s\n' "$package"
|
||||||
printf 'public_origin=%s\n' "$WNH_BASE_URL"
|
printf 'public_origin=%s\n' "$WNH_BASE_URL"
|
||||||
printf 'magic_link_login=true\n'
|
printf 'magic_link_login=true\n'
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ set -eu
|
||||||
|
|
||||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"}
|
ENV_FILE=${WNH_ENV_FILE:-"$ROOT/.env"}
|
||||||
|
DEBUG_BASE_URL_OVERRIDE=${WNH_DEBUG_BASE_URL_OVERRIDE:-}
|
||||||
|
|
||||||
case "$ENV_FILE" in
|
case "$ENV_FILE" in
|
||||||
/*) ;;
|
/*) ;;
|
||||||
|
|
@ -19,6 +20,10 @@ set -a
|
||||||
. "$ENV_FILE"
|
. "$ENV_FILE"
|
||||||
set +a
|
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_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_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_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ set -eu
|
||||||
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
ENV_FILE="$ROOT/.env"
|
ENV_FILE="$ROOT/.env"
|
||||||
config_home=${XDG_CONFIG_HOME:-"$HOME/.config"}
|
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/ensure-local-public-origin.sh"
|
||||||
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" development
|
"$ROOT/scripts/validate-android-environment.sh" "$ENV_FILE" development
|
||||||
|
|
@ -16,6 +13,10 @@ set -a
|
||||||
. "$ENV_FILE"
|
. "$ENV_FILE"
|
||||||
set +a
|
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_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_TRACKING_HTTP_TIMEOUT_MS:?Set WNH_TRACKING_HTTP_TIMEOUT_MS in .env}"
|
||||||
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in .env}"
|
: "${WNH_ANDROID_VERSION_CODE:?Set WNH_ANDROID_VERSION_CODE in .env}"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user