Improve network reliability during resumable uploads.

This commit is contained in:
Greyson Parrelli 2020-10-13 17:37:14 -04:00
parent 322c139c26
commit 84ec6dd458
10 changed files with 52 additions and 167 deletions

View file

@ -216,16 +216,6 @@ public class ApplicationDependencies {
return frameRateTracker; return frameRateTracker;
} }
public static synchronized @NonNull KeyValueStore getKeyValueStore() {
assertInitialization();
if (keyValueStore == null) {
keyValueStore = provider.provideKeyValueStore();
}
return keyValueStore;
}
public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() { public static synchronized @NonNull MegaphoneRepository getMegaphoneRepository() {
assertInitialization(); assertInitialization();
@ -283,7 +273,6 @@ public class ApplicationDependencies {
@NonNull LiveRecipientCache provideRecipientCache(); @NonNull LiveRecipientCache provideRecipientCache();
@NonNull JobManager provideJobManager(); @NonNull JobManager provideJobManager();
@NonNull FrameRateTracker provideFrameRateTracker(); @NonNull FrameRateTracker provideFrameRateTracker();
@NonNull KeyValueStore provideKeyValueStore();
@NonNull MegaphoneRepository provideMegaphoneRepository(); @NonNull MegaphoneRepository provideMegaphoneRepository();
@NonNull EarlyMessageCache provideEarlyMessageCache(); @NonNull EarlyMessageCache provideEarlyMessageCache();
@NonNull MessageNotifier provideMessageNotifier(); @NonNull MessageNotifier provideMessageNotifier();

View file

@ -153,12 +153,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
return new FrameRateTracker(context); return new FrameRateTracker(context);
} }
@Override
public @NonNull KeyValueStore provideKeyValueStore() {
return new KeyValueStore(context);
}
@Override
public @NonNull MegaphoneRepository provideMegaphoneRepository() { public @NonNull MegaphoneRepository provideMegaphoneRepository() {
return new MegaphoneRepository(context); return new MegaphoneRepository(context);
} }

View file

@ -9,6 +9,7 @@ public final class InternalValues extends SignalStoreValues {
public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes"; public static final String GV2_IGNORE_SERVER_CHANGES = "internal.gv2.ignore_server_changes";
public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes"; public static final String GV2_IGNORE_P2P_CHANGES = "internal.gv2.ignore_p2p_changes";
public static final String RECIPIENT_DETAILS = "internal.recipient_details"; public static final String RECIPIENT_DETAILS = "internal.recipient_details";
public static final String FORCE_CENSORSHIP = "internal.force_censorship";
InternalValues(KeyValueStore store) { InternalValues(KeyValueStore store) {
super(store); super(store);
@ -59,4 +60,11 @@ public final class InternalValues extends SignalStoreValues {
public synchronized boolean recipientDetails() { public synchronized boolean recipientDetails() {
return FeatureFlags.internalUser() && getBoolean(RECIPIENT_DETAILS, false); return FeatureFlags.internalUser() && getBoolean(RECIPIENT_DETAILS, false);
} }
/**
* Force the app to behave as if it is in a country where Signal is censored.
*/
public synchronized boolean forcedCensorship() {
return FeatureFlags.internalUser() && getBoolean(FORCE_CENSORSHIP, false);
}
} }

View file

@ -29,7 +29,7 @@ public final class SignalStore {
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues; private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
private SignalStore() { private SignalStore() {
this.store = ApplicationDependencies.getKeyValueStore(); this.store = new KeyValueStore(ApplicationDependencies.getApplication());
this.kbsValues = new KbsValues(store); this.kbsValues = new KbsValues(store);
this.registrationValues = new RegistrationValues(store); this.registrationValues = new RegistrationValues(store);
this.pinValues = new PinValues(store); this.pinValues = new PinValues(store);

View file

@ -39,6 +39,7 @@ public class InternalOptionsPreferenceFragment extends CorrectedPreferenceFragme
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().gv2ForceInvites()); initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_FORCE_INVITES, SignalStore.internalValues().gv2ForceInvites());
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges()); initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_SERVER_CHANGES, SignalStore.internalValues().gv2IgnoreServerChanges());
initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges()); initializeSwitchPreference(preferenceDataStore, InternalValues.GV2_IGNORE_P2P_CHANGES, SignalStore.internalValues().gv2IgnoreP2PChanges());
initializeSwitchPreference(preferenceDataStore, InternalValues.FORCE_CENSORSHIP, SignalStore.internalValues().forcedCensorship());
findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> { findPreference("pref_refresh_attributes").setOnPreferenceClickListener(preference -> {
ApplicationDependencies.getJobManager() ApplicationDependencies.getJobManager()

View file

@ -6,6 +6,7 @@ import android.content.Context;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.CustomDns; import org.thoughtcrime.securesms.net.CustomDns;
import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor; import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor;
import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor; import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor;
@ -239,6 +240,10 @@ public class SignalServiceNetworkAccess {
public SignalServiceConfiguration getConfiguration(@Nullable String localNumber) { public SignalServiceConfiguration getConfiguration(@Nullable String localNumber) {
if (localNumber == null) return this.uncensoredConfiguration; if (localNumber == null) return this.uncensoredConfiguration;
if (SignalStore.internalValues().forcedCensorship()) {
return this.censorshipConfiguration.get(COUNTRY_CODE_QATAR);
}
for (String censoredRegion : this.censoredCountries) { for (String censoredRegion : this.censoredCountries) {
if (localNumber.startsWith(censoredRegion)) { if (localNumber.startsWith(censoredRegion)) {
return this.censorshipConfiguration.get(censoredRegion); return this.censorshipConfiguration.get(censoredRegion);

View file

@ -2231,6 +2231,9 @@
<string name="preferences__internal_storage_service" translatable="false">Storage service</string> <string name="preferences__internal_storage_service" translatable="false">Storage service</string>
<string name="preferences__internal_force_storage_service_sync" translatable="false">Overwrite remote data</string> <string name="preferences__internal_force_storage_service_sync" translatable="false">Overwrite remote data</string>
<string name="preferences__internal_force_storage_service_sync_description" translatable="false">Forces remote storage to match the local device state.</string> <string name="preferences__internal_force_storage_service_sync_description" translatable="false">Forces remote storage to match the local device state.</string>
<string name="preferences__internal_network" translatable="false">Network</string>
<string name="preferences__internal_force_censorship" translatable="false">Force censorship</string>
<string name="preferences__internal_force_censorship_description" translatable="false">Force the app to behave as if it is in a country where Signal is censored.</string>
<!-- **************************************** --> <!-- **************************************** -->
<!-- menus --> <!-- menus -->

View file

@ -75,4 +75,15 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:title="@string/preferences__internal_network">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false"
android:key="internal.force_censorship"
android:summary="@string/preferences__internal_force_censorship_description"
android:title="@string/preferences__internal_force_censorship" />
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -1,130 +0,0 @@
package org.thoughtcrime.securesms.megaphone;
import android.app.Application;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KbsValues;
import org.thoughtcrime.securesms.keyvalue.RegistrationValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ApplicationDependencies.class, SignalStore.class, FeatureFlags.class, RegistrationValues.class, KbsValues.class, TextSecurePreferences.class })
public class PinsForAllScheduleTest extends BaseUnitTest {
private final PinsForAllSchedule testSubject = new PinsForAllSchedule();
private final RegistrationValues registrationValues = mock(RegistrationValues.class);
private final KbsValues kbsValues = mock(KbsValues.class);
@Before
public void setUp() throws Exception {
super.setUp();
mockStatic(ApplicationDependencies.class);
mockStatic(SignalStore.class);
mockStatic(FeatureFlags.class);
mockStatic(TextSecurePreferences.class);
mockStatic(Log.class);
when(ApplicationDependencies.getApplication()).thenReturn(mock(Application.class));
when(SignalStore.registrationValues()).thenReturn(registrationValues);
when(SignalStore.kbsValues()).thenReturn(kbsValues);
when(TextSecurePreferences.isV1RegistrationLockEnabled(any())).thenReturn(false);
}
@Test
public void givenFirstVisibleIsZero_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long firstVisible = 0;
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(firstVisible, 0);
// THEN
assertFalse(result);
}
@Test
public void givenFirstVisibleIsNow_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long now = System.currentTimeMillis();
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(now, now);
// THEN
assertFalse(result);
}
@Test
public void givenFirstVisibleIsHalfFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long now = System.currentTimeMillis();
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN / 2);
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
// THEN
assertFalse(result);
}
// TODO [greyson]
@Ignore
@Test
public void givenFirstVisibleIsFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectTrue() {
// GIVEN
long now = System.currentTimeMillis();
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN);
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
// THEN
assertTrue(result);
}
@Test
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
when(kbsValues.hasPin()).thenReturn(true);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertFalse(result);
}
@Test
public void whenUserIsNotANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectTrue() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertTrue(result);
}
}

View file

@ -1108,25 +1108,10 @@ public class PushServiceSocket {
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build(); .build();
final HttpUrl endpointUrl = HttpUrl.get(connectionHolder.url);
final HttpUrl signedHttpUrl;
try {
signedHttpUrl = HttpUrl.get(signedUrl);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Server returned a malformed signed url: " + signedUrl);
throw new IOException("Server returned a malformed signed url", e);
}
final HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme(endpointUrl.scheme()) Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, signedUrl))
.host(endpointUrl.host())
.port(endpointUrl.port())
.encodedPath(endpointUrl.encodedPath())
.addEncodedPathSegments(signedHttpUrl.encodedPath().substring(1))
.encodedQuery(signedHttpUrl.encodedQuery())
.encodedFragment(signedHttpUrl.encodedFragment());
Request.Builder request = new Request.Builder().url(urlBuilder.build())
.post(RequestBody.create(null, "")); .post(RequestBody.create(null, ""));
for (Map.Entry<String, String> header : headers.entrySet()) { for (Map.Entry<String, String> header : headers.entrySet()) {
if (!header.getKey().equalsIgnoreCase("host")) { if (!header.getKey().equalsIgnoreCase("host")) {
request.header(header.getKey(), header.getValue()); request.header(header.getKey(), header.getValue());
@ -1186,7 +1171,7 @@ public class PushServiceSocket {
return file.getTransmittedDigest(); return file.getTransmittedDigest();
} }
Request.Builder request = new Request.Builder().url(resumableUrl) Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, resumableUrl))
.put(file) .put(file)
.addHeader("Content-Range", resumeInfo.contentRange); .addHeader("Content-Range", resumeInfo.contentRange);
@ -1229,7 +1214,7 @@ public class PushServiceSocket {
final long offset; final long offset;
final String contentRange; final String contentRange;
Request.Builder request = new Request.Builder().url(resumableUrl) Request.Builder request = new Request.Builder().url(buildConfiguredUrl(connectionHolder, resumableUrl))
.put(RequestBody.create(null, "")) .put(RequestBody.create(null, ""))
.addHeader("Content-Range", String.format(Locale.US, "bytes */%d", contentLength)); .addHeader("Content-Range", String.format(Locale.US, "bytes */%d", contentLength));
@ -1279,6 +1264,25 @@ public class PushServiceSocket {
return new ResumeInfo(contentRange, offset); return new ResumeInfo(contentRange, offset);
} }
private static HttpUrl buildConfiguredUrl(ConnectionHolder connectionHolder, String url) throws IOException {
final HttpUrl endpointUrl = HttpUrl.get(connectionHolder.url);
final HttpUrl resumableHttpUrl;
try {
resumableHttpUrl = HttpUrl.get(url);
} catch (IllegalArgumentException e) {
throw new IOException("Malformed URL!", e);
}
return new HttpUrl.Builder().scheme(endpointUrl.scheme())
.host(endpointUrl.host())
.port(endpointUrl.port())
.encodedPath(endpointUrl.encodedPath())
.addEncodedPathSegments(resumableHttpUrl.encodedPath().substring(1))
.encodedQuery(resumableHttpUrl.encodedQuery())
.encodedFragment(resumableHttpUrl.encodedFragment())
.build();
}
private String makeServiceRequest(String urlFragment, String method, String jsonBody) private String makeServiceRequest(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException throws NonSuccessfulResponseCodeException, PushNetworkException
{ {