Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3374651
D9069.id38923.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D9069.id38923.diff
View Options
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
@@ -1266,7 +1266,7 @@
notificationsSizeValidator,
);
- const devicesWithExcessiveSize = notifsWithMessageInfos
+ let devicesWithExcessiveSize = notifsWithMessageInfos
.filter(({ payloadSizeExceeded }) => payloadSizeExceeded)
.map(({ cookieID, deviceToken }) => ({ cookieID, deviceToken }));
@@ -1284,12 +1284,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),
+ devicesWithExcessiveSize.length,
+ ));
}
if (blobUploadError) {
@@ -1299,12 +1300,22 @@
);
}
- if (blobHash && encryptionKey) {
+ if (
+ blobHash &&
+ encryptionKey &&
+ blobHolders &&
+ blobHolders.length === devicesWithExcessiveSize.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 Exception {
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(
+ NOTIFICATION_PROCESSING_TIME_LIMIT, 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
Details
Attached
Mime Type
text/plain
Expires
Wed, Nov 27, 4:34 PM (20 h, 56 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2591000
Default Alt Text
D9069.id38923.diff (12 KB)
Attached To
Mode
D9069: Schedule blob deletion on Android when large notifications arrives
Attached
Detach File
Event Timeline
Log In to Comment