Page MenuHomePhabricator

D9069.id38938.diff
No OneTemporary

D9069.id38938.diff

diff --git a/keyserver/src/push/crypto.js b/keyserver/src/push/crypto.js
--- a/keyserver/src/push/crypto.js
+++ b/keyserver/src/push/crypto.js
@@ -201,18 +201,23 @@
cookieID: string,
notification: AndroidNotification,
notificationSizeValidator?: AndroidNotification => boolean,
+ blobHolder?: ?string,
): Promise<{
+notification: AndroidNotification,
+payloadSizeExceeded: boolean,
+encryptionOrder?: number,
}> {
- const { id, keyserverID, badgeOnly, ...unencryptedPayload } =
- notification.data;
+ const { id, keyserverID, badgeOnly, ...rest } = notification.data;
let unencryptedData = { badgeOnly, keyserverID };
if (id) {
unencryptedData = { ...unencryptedData, id };
}
+ let unencryptedPayload = rest;
+ if (blobHolder) {
+ unencryptedPayload = { ...unencryptedPayload, blobHolder };
+ }
+
let payloadSizeValidator;
if (notificationSizeValidator) {
payloadSizeValidator = (
@@ -392,11 +397,12 @@
}>,
> {
const notificationPromises = devices.map(
- async ({ deviceToken, cookieID }) => {
+ async ({ deviceToken, cookieID, blobHolder }) => {
const notif = await encryptAndroidNotification(
cookieID,
notification,
notificationSizeValidator,
+ blobHolder,
);
return { deviceToken, cookieID, ...notif };
},
diff --git a/keyserver/src/push/send.js b/keyserver/src/push/send.js
--- a/keyserver/src/push/send.js
+++ b/keyserver/src/push/send.js
@@ -1267,11 +1267,11 @@
notificationsSizeValidator,
);
- const devicesWithExcessiveSize = notifsWithMessageInfos
+ const devicesWithExcessiveSizeNoHolders = notifsWithMessageInfos
.filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
.map(({ cookieID, deviceToken }) => ({ cookieID, deviceToken }));
- if (devicesWithExcessiveSize.length === 0) {
+ if (devicesWithExcessiveSizeNoHolders.length === 0) {
return notifsWithMessageInfos.map(
({ notification: notif, deviceToken, encryptionOrder }) => ({
notification: notif,
@@ -1285,12 +1285,13 @@
native: NEXT_CODE_VERSION,
});
- let blobHash, encryptionKey, blobUploadError;
+ let blobHash, blobHolders, encryptionKey, blobUploadError;
if (canQueryBlobService) {
- ({ blobHash, encryptionKey, blobUploadError } = await blobServiceUpload(
- JSON.stringify(copyWithMessageInfos.data),
- 1,
- ));
+ ({ blobHash, blobHolders, encryptionKey, blobUploadError } =
+ await blobServiceUpload(
+ JSON.stringify(copyWithMessageInfos.data),
+ devicesWithExcessiveSizeNoHolders.length,
+ ));
}
if (blobUploadError) {
@@ -1300,12 +1301,23 @@
);
}
- if (blobHash && encryptionKey) {
+ let devicesWithExcessiveSize = devicesWithExcessiveSizeNoHolders;
+ if (
+ blobHash &&
+ encryptionKey &&
+ blobHolders &&
+ blobHolders.length === devicesWithExcessiveSizeNoHolders.length
+ ) {
notification.data = {
...notification.data,
blobHash,
encryptionKey,
};
+
+ devicesWithExcessiveSize = blobHolders.map((holder, idx) => ({
+ ...devicesWithExcessiveSize[idx],
+ blobHolder: holder,
+ }));
}
const notifsWithoutMessageInfos = await prepareEncryptedAndroidNotifications(
diff --git a/native/android/app/build.gradle b/native/android/app/build.gradle
--- a/native/android/app/build.gradle
+++ b/native/android/app/build.gradle
@@ -709,6 +709,14 @@
} else {
implementation jscFlavor
}
+
+ def work_version = "2.8.1"
+ // (Java only)
+ implementation "androidx.work:work-runtime:$work_version"
+ // Guava for listenable future to solve the bug:
+ // https://stackoverflow.com/questions/64290141/android-studio-class-file-for-com-google-common-util-concurrent-listenablefuture
+ // https://github.com/google/ExoPlayer/issues/7993
+ implementation "com.google.guava:guava:31.0.1-android"
}
if (isNewArchitectureEnabled()) {
diff --git a/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidBlobClient.java b/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidBlobClient.java
--- a/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidBlobClient.java
+++ b/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidBlobClient.java
@@ -1,5 +1,12 @@
package app.comm.android.commservices;
+import android.content.Context;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.WorkRequest;
import app.comm.android.BuildConfig;
import app.comm.android.fbjni.CommSecureStore;
import java.io.IOException;
@@ -15,9 +22,6 @@
import org.json.JSONObject;
public class CommAndroidBlobClient {
- private static final String BLOB_SERVICE_URL = BuildConfig.DEBUG
- ? "https://blob.staging.commtechnologies.org"
- : "https://blob.commtechnologies.org";
// The FirebaseMessagingService docs state that message
// processing should complete within at most 20 seconds
// window. Therefore we limit http time call to 15 seconds
@@ -25,11 +29,17 @@
private static final int NOTIF_PROCESSING_TIME_LIMIT_SECONDS = 15;
// OkHttp docs advise to share OkHttpClient instances
// https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/#okhttpclients-should-be-shared
- private static final OkHttpClient httpClient =
+ public static final OkHttpClient httpClient =
new OkHttpClient.Builder()
.callTimeout(NOTIF_PROCESSING_TIME_LIMIT_SECONDS, TimeUnit.SECONDS)
.build();
+ public static final String BLOB_SERVICE_URL = BuildConfig.DEBUG
+ ? "https://blob.staging.commtechnologies.org"
+ : "https://blob.commtechnologies.org";
+ public static final String BLOB_HASH_KEY = "blob_hash";
+ public static final String BLOB_HOLDER_KEY = "holder";
+
public byte[] getBlobSync(String blobHash) throws IOException, JSONException {
String authToken = getAuthToken();
Request request = new Request.Builder()
@@ -47,7 +57,28 @@
return response.body().bytes();
}
- private String getAuthToken() throws JSONException {
+ public void scheduleDeferredBlobDeletion(
+ String blobHash,
+ String blobHolder,
+ Context context) {
+ Constraints constraints = new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build();
+ WorkRequest deleteBlobWorkRequest =
+ new OneTimeWorkRequest.Builder(CommAndroidDeleteBlobWork.class)
+ .setConstraints(constraints)
+ .setInitialDelay(
+ NOTIF_PROCESSING_TIME_LIMIT_SECONDS, TimeUnit.SECONDS)
+ .setInputData(new Data.Builder()
+ .putString(BLOB_HASH_KEY, blobHash)
+ .putString(BLOB_HOLDER_KEY, blobHolder)
+ .build())
+ .build();
+
+ WorkManager.getInstance(context).enqueue(deleteBlobWorkRequest);
+ }
+
+ public static String getAuthToken() throws JSONException {
// Authentication data are retrieved on every request
// since they might change while CommNotificationsHandler
// thread is running so we should not rely on caching
diff --git a/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidDeleteBlobWork.java b/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidDeleteBlobWork.java
new file mode 100644
--- /dev/null
+++ b/native/android/app/src/main/java/app/comm/android/commservices/CommAndroidDeleteBlobWork.java
@@ -0,0 +1,96 @@
+package app.comm.android.commservices;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.work.Data;
+import androidx.work.ListenableWorker.Result;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import java.io.IOException;
+import java.util.function.Consumer;
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class CommAndroidDeleteBlobWork extends Worker {
+
+ private static final int MAX_RETRY_ATTEMPTS = 10;
+
+ public CommAndroidDeleteBlobWork(Context context, WorkerParameters params) {
+ super(context, params);
+ }
+
+ @Override
+ public Result doWork() {
+ String blobHash =
+ getInputData().getString(CommAndroidBlobClient.BLOB_HASH_KEY);
+ String blobHolder =
+ getInputData().getString(CommAndroidBlobClient.BLOB_HOLDER_KEY);
+
+ String jsonBody;
+ try {
+ jsonBody = new JSONObject()
+ .put(CommAndroidBlobClient.BLOB_HASH_KEY, blobHash)
+ .put(CommAndroidBlobClient.BLOB_HOLDER_KEY, blobHolder)
+ .toString();
+ } catch (JSONException e) {
+ // This should never happen since the code
+ // throwing is just simple JSON creation.
+ // If it happens there is no way to retry
+ // so we fail immediately.
+ Log.w(
+ "COMM",
+ "Failed to create JSON from blob hash and holder provided.",
+ e);
+ return Result.failure();
+ }
+
+ String authToken;
+ try {
+ authToken = CommAndroidBlobClient.getAuthToken();
+ } catch (JSONException e) {
+ // In this case however it may happen that
+ // auth metadata got corrupted but will be
+ // fixed soon by event emitter. Therefore
+ // we should retry in this case.
+ return Result.retry();
+ }
+
+ RequestBody requestBody =
+ RequestBody.create(MediaType.parse("application/json"), jsonBody);
+ Request request = new Request.Builder()
+ .delete(requestBody)
+ .url(CommAndroidBlobClient.BLOB_SERVICE_URL + "/blob")
+ .header("Authorization", authToken)
+ .build();
+
+ try {
+ Response response =
+ CommAndroidBlobClient.httpClient.newCall(request).execute();
+ if (response.isSuccessful()) {
+ return Result.success();
+ }
+ Log.w(
+ "COMM",
+ "Failed to execute blob deletion request. HTTP code:" +
+ response.code() + " status: " + response.message());
+ return retryOrFail();
+ } catch (IOException e) {
+ Log.w(
+ "COMM",
+ "IO exception occurred while issuing blob deletion request.",
+ e);
+ return retryOrFail();
+ }
+ }
+
+ private Result retryOrFail() {
+ if (getRunAttemptCount() > MAX_RETRY_ATTEMPTS) {
+ return Result.failure();
+ }
+ return Result.retry();
+ }
+}
diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
--- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
+++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java
@@ -50,6 +50,7 @@
private static final String ENCRYPTED_PAYLOAD_KEY = "encryptedPayload";
private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed";
private static final String BLOB_HASH_KEY = "blobHash";
+ private static final String BLOB_HOLDER_KEY = "blobHolder";
private static final String AES_ENCRYPTION_KEY_LABEL = "encryptionKey";
private static final String GROUP_NOTIF_IDS_KEY = "groupNotifIDs";
private static final String COLLAPSE_ID_KEY = "collapseKey";
@@ -153,7 +154,8 @@
}
if (message.getData().get(BLOB_HASH_KEY) != null &&
- message.getData().get(AES_ENCRYPTION_KEY_LABEL) != null) {
+ message.getData().get(AES_ENCRYPTION_KEY_LABEL) != null &&
+ message.getData().get(BLOB_HOLDER_KEY) != null) {
handleLargeNotification(message);
}
@@ -294,6 +296,7 @@
private void handleLargeNotification(RemoteMessage message) {
String blobHash = message.getData().get(BLOB_HASH_KEY);
+ String blobHolder = message.getData().get(BLOB_HOLDER_KEY);
try {
byte[] largePayload = blobClient.getBlobSync(blobHash);
message = aesDecryptRemoteMessage(message, largePayload);
@@ -301,6 +304,8 @@
} catch (Exception e) {
Log.w("COMM", "Failure when handling large notification.", e);
}
+ blobClient.scheduleDeferredBlobDeletion(
+ blobHash, blobHolder, this.getApplicationContext());
}
private void addToThreadGroupAndDisplay(

File Metadata

Mime Type
text/plain
Expires
Sun, Nov 24, 12:29 AM (20 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2572490
Default Alt Text
D9069.id38938.diff (12 KB)

Event Timeline