diff --git a/CHANGELOG.md b/CHANGELOG.md index a18be9e6..713869a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# Build 230 (1.5) +2024-03-04 + +- Backward compatibility with ongoin location sharing +- Fix a small markdown bug + +# ~~Build 229 (1.5)~~ +2024-02-26 + +- Location sharing comes out of beta! +- Fix a crash with large 16-bit color depth PNG images +- Restart button in troubleshooting activity +- Update WebRTC and Sqlite-JDBC +- Emoji 15.1 support + # Build 227 (1.4) 2024-01-30 diff --git a/obv_engine/engine/build.gradle b/obv_engine/engine/build.gradle index 2737990c..b268b453 100644 --- a/obv_engine/engine/build.gradle +++ b/obv_engine/engine/build.gradle @@ -7,16 +7,16 @@ repositories { dependencies { // do not update further: jackson >2.13 does not work on older Android APIs implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4' - implementation files('libs/sqlite-jdbc-3.42.0.1.jar') + implementation files('libs/sqlite-jdbc-3.45.1.0.jar') - implementation 'org.slf4j:slf4j-api:2.0.11' - implementation 'org.slf4j:slf4j-simple:2.0.11' + implementation 'org.slf4j:slf4j-api:2.0.12' + implementation 'org.slf4j:slf4j-simple:2.0.12' - implementation 'org.bitbucket.b_c:jose4j:0.9.4' + implementation 'org.bitbucket.b_c:jose4j:0.9.5' implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'net.iharder:base64:2.3.9' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.xerial:sqlite-jdbc:3.42.0.1' // only here to check if a new version is available + testImplementation 'org.xerial:sqlite-jdbc:3.45.1.0' // only here to check if a new version is available } diff --git a/obv_engine/engine/libs/sqlite-jdbc-3.42.0.1.jar b/obv_engine/engine/libs/sqlite-jdbc-3.42.0.1.jar deleted file mode 100644 index 4d0384d7..00000000 Binary files a/obv_engine/engine/libs/sqlite-jdbc-3.42.0.1.jar and /dev/null differ diff --git a/obv_engine/engine/libs/sqlite-jdbc-3.45.1.0.jar b/obv_engine/engine/libs/sqlite-jdbc-3.45.1.0.jar new file mode 100644 index 00000000..e18b9cf2 Binary files /dev/null and b/obv_engine/engine/libs/sqlite-jdbc-3.45.1.0.jar differ diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java index 80ee55ac..0270b46e 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java @@ -28,6 +28,7 @@ public class PushNotificationTypeAndParameters { public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX = 0x12; public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON = 0x13; + // public static final byte PUSH_NOTIFICATION_TYPE_ANDROID_EXPERIMENT = (byte) (0x80); // public static final byte PUSH_NOTIFICATION_TYPE_NONE = (byte) 0xff; public final byte pushNotificationType; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java index c3cfab6d..fc6a4732 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java @@ -85,6 +85,7 @@ import io.olvid.engine.engine.types.JsonGroupDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.JsonIdentityDetails; import io.olvid.engine.engine.types.JsonIdentityDetailsWithVersionAndPhoto; +import io.olvid.engine.engine.types.JsonOsmStyle; import io.olvid.engine.engine.types.ObvAttachment; import io.olvid.engine.engine.types.ObvBackupKeyInformation; import io.olvid.engine.engine.types.ObvBackupKeyVerificationOutput; @@ -2425,10 +2426,10 @@ public void queryServerWellKnown(String server) { } @Override - public String getOsmServerUrl(byte[] bytesOwnedIdentity) { + public List getOsmStyles(byte[] bytesOwnedIdentity) { try { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - return fetchManager.getOsmServerUrl(ownedIdentity.getServer()); + return fetchManager.getOsmStyles(ownedIdentity.getServer()); } catch (Exception ignored) { return null; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java index 8c8c99b9..7a8df23b 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java @@ -236,7 +236,7 @@ enum ApiKeyStatus { void startFreeTrial(byte[] bytesOwnedIdentity); void verifyReceipt(byte[] bytesOwnedIdentity, String storeToken); void queryServerWellKnown(String server); - String getOsmServerUrl(byte[] bytesOwnedIdentity); + List getOsmStyles(byte[] bytesOwnedIdentity); String getAddressServerUrl(byte[] bytesOwnedIdentity); void propagateAppSyncAtomToAllOwnedIdentitiesOtherDevicesIfNeeded(ObvSyncAtom obvSyncAtom) throws Exception; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonOsmStyle.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonOsmStyle.java new file mode 100644 index 00000000..0cbd4708 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonOsmStyle.java @@ -0,0 +1,41 @@ +/* + * Olvid for Android + * Copyright ยฉ 2019-2024 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Collections; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class JsonOsmStyle { + public String id; // should never be null + public Map name; // should never be null + public String url; // should never be null + + public JsonOsmStyle() {} + + public JsonOsmStyle(String id, String url) { + this.id = id; + this.name = Collections.emptyMap(); + this.url = url; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java index 49e5f88c..91c670ac 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java @@ -21,6 +21,7 @@ import java.sql.SQLException; +import java.util.List; import java.util.UUID; import io.olvid.engine.datatypes.Identity; @@ -31,6 +32,7 @@ import io.olvid.engine.datatypes.containers.ReceivedAttachment; import io.olvid.engine.datatypes.containers.ServerQuery; import io.olvid.engine.datatypes.key.symmetric.AuthEncKey; +import io.olvid.engine.engine.types.JsonOsmStyle; public interface NetworkFetchDelegate { void downloadMessages(Identity ownedIdentity, UID deviceUid); @@ -65,6 +67,6 @@ public interface NetworkFetchDelegate { void startFreeTrial(Identity ownedIdentity); void verifyReceipt(Identity ownedIdentity, String storeToken); void queryServerWellKnown(String server); - String getOsmServerUrl(String server); + List getOsmStyles(String server); String getAddressServerUrl(String server); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java index 2db09dc3..c88e46d6 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.sql.SQLException; +import java.util.List; import java.util.Objects; import java.util.UUID; @@ -41,6 +42,7 @@ import io.olvid.engine.datatypes.containers.ServerQuery; import io.olvid.engine.datatypes.key.symmetric.AuthEncKey; import io.olvid.engine.encoder.DecodingException; +import io.olvid.engine.engine.types.JsonOsmStyle; import io.olvid.engine.metamanager.ChannelDelegate; import io.olvid.engine.metamanager.CreateSessionDelegate; import io.olvid.engine.metamanager.IdentityDelegate; @@ -585,9 +587,9 @@ public void queryServerWellKnown(String server) { } @Override - public String getOsmServerUrl(String server) { + public List getOsmStyles(String server) { try { - return wellKnownCoordinator.getOsmUrl(server); + return wellKnownCoordinator.getOsmStyles(server); } catch (Exception ignored) { return null; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/DownloadAttachmentCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/DownloadAttachmentCoordinator.java index 13fafa15..a6c2c70e 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/DownloadAttachmentCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/DownloadAttachmentCoordinator.java @@ -120,7 +120,7 @@ private void queueNewDownloadAttachmentOperation(Identity ownedIdentity, UID mes Logger.d("Download attachment coordinator queueing new DownloadAttachmentOperation."); DownloadAttachmentOperation op = new DownloadAttachmentOperation(fetchManagerSessionFactory, sslSocketFactory, ownedIdentity, messageUid, attachmentNumber, priorityCategory, initialPriority, this,null, this); switch (priorityCategory) { - case DownloadAttachmentPriorityCategory.WEIGHT: + case DownloadAttachmentPriorityCategory.WEIGHT: { downloadAttachmentOperationWeightQueue.queue(op); PriorityOperation lowestPriorityExecutingOperation = downloadAttachmentOperationWeightQueue.getExecutingOperationThatShouldBeCancelledWhenQueueingWithHigherPriority(); if (lowestPriorityExecutingOperation != null && lowestPriorityExecutingOperation.getPriority() > initialPriority) { @@ -128,14 +128,16 @@ private void queueNewDownloadAttachmentOperation(Identity ownedIdentity, UID mes lowestPriorityExecutingOperation.cancel(DownloadAttachmentOperation.RFC_DOES_NOT_HAVE_THE_HIGHEST_PRIORITY); } break; - case DownloadAttachmentPriorityCategory.TIMESTAMP: + } + case DownloadAttachmentPriorityCategory.TIMESTAMP: { downloadAttachmentOperationTimestampQueue.queue(op); - lowestPriorityExecutingOperation = downloadAttachmentOperationTimestampQueue.getExecutingOperationThatShouldBeCancelledWhenQueueingWithHigherPriority(); + PriorityOperation lowestPriorityExecutingOperation = downloadAttachmentOperationTimestampQueue.getExecutingOperationThatShouldBeCancelledWhenQueueingWithHigherPriority(); if (lowestPriorityExecutingOperation != null && lowestPriorityExecutingOperation.getPriority() > initialPriority) { Logger.d("Canceling a DownloadAttachmentOperation with lower priority " + lowestPriorityExecutingOperation.getPriority()); lowestPriorityExecutingOperation.cancel(DownloadAttachmentOperation.RFC_DOES_NOT_HAVE_THE_HIGHEST_PRIORITY); } break; + } default: Logger.w("Trying to queue a DownloadAttachmentOperation with unknown priorityCategory " + priorityCategory); } @@ -187,7 +189,7 @@ public void onCancelCallback(Operation operation) { case DownloadAttachmentOperation.RFC_INVALID_CHUNK: case DownloadAttachmentOperation.RFC_ATTACHMENT_CANNOT_BE_FETCHED: case DownloadAttachmentOperation.RFC_UNABLE_TO_WRITE_CHUNK_TO_FILE: - case DownloadAttachmentOperation.RFC_UPLOAD_CANCELLED_BY_SENDER: + case DownloadAttachmentOperation.RFC_UPLOAD_CANCELLED_BY_SENDER: { // We do not try to download the attachment again and mark it for deletion. We notify that the downloadAttachment failed. try (FetchManagerSession fetchManagerSession = fetchManagerSessionFactory.getSession()) { InboxAttachment attachment = InboxAttachment.get(fetchManagerSession, ownedIdentity, messageUid, attachmentNumber); @@ -208,6 +210,7 @@ public void onCancelCallback(Operation operation) { userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_FAILED_ATTACHMENT_NUMBER_KEY, attachmentNumber); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_FAILED, userInfo); break; + } case DownloadAttachmentOperation.RFC_ATTACHMENT_CANNOT_BE_FOUND: case DownloadAttachmentOperation.RFC_FETCH_NOT_REQUESTED: case DownloadAttachmentOperation.RFC_MARKED_FOR_DELETION: @@ -217,27 +220,29 @@ public void onCancelCallback(Operation operation) { // wait for identity to become active again waitForIdentityReactivation(ownedIdentity, messageUid, attachmentNumber, priorityCategory, initialPriority); break; - case DownloadAttachmentOperation.RFC_DOES_NOT_HAVE_THE_HIGHEST_PRIORITY: + case DownloadAttachmentOperation.RFC_DOES_NOT_HAVE_THE_HIGHEST_PRIORITY: { queueNewDownloadAttachmentOperation(ownedIdentity, messageUid, attachmentNumber, priorityCategory, initialPriority); - userInfo = new HashMap<>(); + HashMap userInfo = new HashMap<>(); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_OWNED_IDENTITY_KEY, ownedIdentity); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_MESSAGE_UID_KEY, messageUid); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_ATTACHMENT_NUMBER, attachmentNumber); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED, userInfo); break; + } case DownloadAttachmentOperation.RFC_INVALID_SIGNED_URL: { waitForRefreshedUrls(ownedIdentity, messageUid, attachmentNumber, priorityCategory, initialPriority); refreshInboxAttachmentSignedUrlDelegate.refreshInboxAttachmentSignedUrl(ownedIdentity, messageUid, attachmentNumber); break; } - case DownloadAttachmentOperation.RFC_DOWNLOAD_PAUSED: - userInfo = new HashMap<>(); + case DownloadAttachmentOperation.RFC_DOWNLOAD_PAUSED: { + HashMap userInfo = new HashMap<>(); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_OWNED_IDENTITY_KEY, ownedIdentity); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_MESSAGE_UID_KEY, messageUid); userInfo.put(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED_ATTACHMENT_NUMBER, attachmentNumber); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_ATTACHMENT_DOWNLOAD_WAS_PAUSED, userInfo); break; + } case DownloadAttachmentOperation.RFC_NOT_YET_AVAILABLE_ON_SERVER: case DownloadAttachmentOperation.RFC_NETWORK_ERROR: default: diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WellKnownCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WellKnownCoordinator.java index 2a3ed232..94b3edbd 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WellKnownCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WellKnownCoordinator.java @@ -40,6 +40,7 @@ import io.olvid.engine.datatypes.NoDuplicateOperationQueue; import io.olvid.engine.datatypes.Operation; import io.olvid.engine.datatypes.notifications.DownloadNotifications; +import io.olvid.engine.engine.types.JsonOsmStyle; import io.olvid.engine.metamanager.NotificationPostingDelegate; import io.olvid.engine.networkfetch.databases.CachedWellKnown; import io.olvid.engine.networkfetch.datatypes.FetchManagerSession; @@ -153,13 +154,13 @@ public void onFinishCallback(Operation operation) { if (updated) { HashMap userInfo = new HashMap<>(); userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED_SERVER_KEY, server); - userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED_SERVER_CONFIG_KEY, jsonWellKnown.getServerConfig()); - userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED_APP_INFO_KEY, jsonWellKnown.getAppInfo()); + userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED_SERVER_CONFIG_KEY, jsonWellKnown.serverConfig); + userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED_APP_INFO_KEY, jsonWellKnown.appInfo); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_WELL_KNOWN_UPDATED, userInfo); } else { HashMap userInfo = new HashMap<>(); userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_DOWNLOAD_SUCCESS_SERVER_KEY, wellKnownDownloadOperation.getServer()); - userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_DOWNLOAD_SUCCESS_APP_INFO_KEY, jsonWellKnown.getAppInfo()); + userInfo.put(DownloadNotifications.NOTIFICATION_WELL_KNOWN_DOWNLOAD_SUCCESS_APP_INFO_KEY, jsonWellKnown.appInfo); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_WELL_KNOWN_DOWNLOAD_SUCCESS, userInfo); } } @@ -191,7 +192,7 @@ public String getWsUrl(String server) throws NotCachedException { if (jsonWellKnown.serverConfig == null) { return null; } - return jsonWellKnown.serverConfig.getWebSocketUrl(); + return jsonWellKnown.serverConfig.webSocketUrl; } @Override @@ -211,7 +212,7 @@ public List getTurnUrls(String server) throws NotCachedException { } @Override - public String getOsmUrl(String server) throws NotCachedException { + public List getOsmStyles(String server) throws NotCachedException { if (!cacheInitialized) { throw new NotCachedException(); } @@ -223,7 +224,7 @@ public String getOsmUrl(String server) throws NotCachedException { if (jsonWellKnown.serverConfig == null) { return null; } - return jsonWellKnown.serverConfig.osmServerUrl; + return jsonWellKnown.serverConfig.osmStyles; } @Override @@ -244,75 +245,24 @@ public String getAddressUrl(String server) throws NotCachedException { @JsonIgnoreProperties(ignoreUnknown = true) public static class JsonWellKnown { - JsonWellKnownServerConfig serverConfig; - Map appInfo; - - @JsonProperty("server") - public JsonWellKnownServerConfig getServerConfig() { - return serverConfig; - } - @JsonProperty("server") - public void setServerConfig(JsonWellKnownServerConfig serverConfig) { - this.serverConfig = serverConfig; - } - + public JsonWellKnownServerConfig serverConfig; @JsonProperty("app") - public Map getAppInfo() { - return appInfo; - } - - @JsonProperty("app") - public void setAppInfo(Map appInfo) { - this.appInfo = appInfo; - } + public Map appInfo; } @JsonIgnoreProperties(ignoreUnknown = true) public static class JsonWellKnownServerConfig { - String webSocketUrl; - List turnServerUrls; - String osmServerUrl; - String addressServerUrl; - @JsonProperty("ws_server") - public String getWebSocketUrl() { - return webSocketUrl; - } - - @JsonProperty("ws_server") - public void setWebSocketUrl(String webSocketUrl) { - this.webSocketUrl = webSocketUrl; - } - - @JsonProperty("turn_servers") - public List getTurnServerUrls() { - return turnServerUrls; - } - + public String webSocketUrl; @JsonProperty("turn_servers") - public void setTurnServerUrls(List turnServerUrls) { - this.turnServerUrls = turnServerUrls; - } - - @JsonProperty("osm_server") - public String getOsmServerUrl() { - return osmServerUrl; - } - - @JsonProperty("osm_server") - public void setOsmServerUrl(String osmServerUrl) { - this.osmServerUrl = osmServerUrl; - } - + public List turnServerUrls; + // no longer used since we have osmStyles + // @JsonProperty("osm_server") + // public String osmServerUrl; @JsonProperty("address_server") - public String getAddressServerUrl() { - return addressServerUrl; - } - - @JsonProperty("address_server") - public void setAddressServerUrl(String addressServerUrl) { - this.addressServerUrl = addressServerUrl; - } + public String addressServerUrl; + @JsonProperty("osm_styles") + public List osmStyles; } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/datatypes/WellKnownCacheDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/datatypes/WellKnownCacheDelegate.java index 77a45b47..40020763 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/datatypes/WellKnownCacheDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/datatypes/WellKnownCacheDelegate.java @@ -22,11 +22,12 @@ import java.util.List; +import io.olvid.engine.engine.types.JsonOsmStyle; import io.olvid.engine.networkfetch.coordinators.WellKnownCoordinator; public interface WellKnownCacheDelegate { String getWsUrl(String server) throws WellKnownCoordinator.NotCachedException; List getTurnUrls(String server) throws WellKnownCoordinator.NotCachedException; - String getOsmUrl(String server) throws WellKnownCoordinator.NotCachedException; + List getOsmStyles(String server) throws WellKnownCoordinator.NotCachedException; String getAddressUrl(String server) throws WellKnownCoordinator.NotCachedException; } diff --git a/obv_messenger/app/build.gradle b/obv_messenger/app/build.gradle index 4da6a851..2d4bc74d 100644 --- a/obv_messenger/app/build.gradle +++ b/obv_messenger/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "io.olvid.messenger" minSdkVersion 21 targetSdk 34 - versionCode 227 - versionName "1.4" + versionCode 230 + versionName "1.5" vectorDrawables.useSupportLibrary true multiDexEnabled true resourceConfigurations += ['en', 'fr'] @@ -162,7 +162,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.emoji2:emoji2:1.4.0' - implementation 'androidx.emoji2:emoji2-bundled:1.4.0' + implementation 'androidx.emoji2:emoji2-bundled:1.5.0-alpha01' implementation 'androidx.activity:activity:1.8.2' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.camera:camera-camera2:1.3.1' @@ -175,9 +175,9 @@ dependencies { implementation 'androidx.media3:media3-exoplayer:1.2.1' implementation 'androidx.media3:media3-ui:1.2.1' implementation 'androidx.media:media:1.7.0' - implementation 'androidx.navigation:navigation-fragment:2.7.6' - implementation 'androidx.navigation:navigation-ui:2.7.6' - implementation 'androidx.navigation:navigation-compose:2.7.6' + implementation 'androidx.navigation:navigation-fragment:2.7.7' + implementation 'androidx.navigation:navigation-ui:2.7.7' + implementation 'androidx.navigation:navigation-compose:2.7.7' implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.room:room-runtime:2.6.1' @@ -186,7 +186,7 @@ dependencies { implementation 'androidx.work:work-runtime:2.9.0' implementation 'androidx.datastore:datastore-preferences:1.0.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0' implementation 'org.jsoup:jsoup:1.17.2' // webclient @@ -194,7 +194,7 @@ dependencies { // openID implementation 'net.openid:appauth:0.11.1' - implementation 'org.bitbucket.b_c:jose4j:0.9.4' + implementation 'org.bitbucket.b_c:jose4j:0.9.5' // map libre integration implementation 'org.maplibre.gl:android-sdk:10.2.0' @@ -212,7 +212,7 @@ dependencies { fullImplementation 'com.android.billingclient:billing:6.1.0' // Firebase - fullImplementation('com.google.firebase:firebase-messaging:23.4.0') { + fullImplementation('com.google.firebase:firebase-messaging:23.4.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -223,11 +223,11 @@ dependencies { fullImplementation 'com.google.android.gms:play-services-location:21.1.0' // Google Drive - fullImplementation 'com.google.android.gms:play-services-auth:20.7.0' + fullImplementation 'com.google.android.gms:play-services-auth:21.0.0' fullImplementation('com.google.http-client:google-http-client-gson:1.44.1') { exclude group: 'org.apache.httpcomponents' } - fullImplementation('com.google.api-client:google-api-client-android:2.2.0') { + fullImplementation('com.google.api-client:google-api-client-android:2.3.0') { exclude group: 'org.apache.httpcomponents' } fullImplementation('com.google.apis:google-api-services-drive:v3-rev20221219-2.0.0') { diff --git a/obv_messenger/app/src/full/java/io/olvid/messenger/discussion/location/MapViewGoogleMapsFragment.java b/obv_messenger/app/src/full/java/io/olvid/messenger/discussion/location/MapViewGoogleMapsFragment.java index 652d1e2e..ba33ed59 100644 --- a/obv_messenger/app/src/full/java/io/olvid/messenger/discussion/location/MapViewGoogleMapsFragment.java +++ b/obv_messenger/app/src/full/java/io/olvid/messenger/discussion/location/MapViewGoogleMapsFragment.java @@ -26,9 +26,13 @@ import android.os.Handler; import android.os.Looper; import android.util.TypedValue; +import android.view.Gravity; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.PopupMenu; import android.widget.RelativeLayout; import androidx.annotation.NonNull; @@ -131,9 +135,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onMapReady(@NonNull GoogleMap googleMap) { this.googleMap = googleMap; - googleMap.getUiSettings().setCompassEnabled(true); - repositionCompass(); - // setup listeners for map gestures googleMap.setOnCameraMoveStartedListener((reason) -> { currentCameraCenterLiveData.postValue(null); @@ -149,9 +150,16 @@ public void onMapReady(@NonNull GoogleMap googleMap) { // customize map googleMap.getUiSettings().setMyLocationButtonEnabled(false); + googleMap.getUiSettings().setCompassEnabled(true); + googleMap.getUiSettings().setMapToolbarEnabled(true); + repositionCompass(); + googleMap.setBuildingsEnabled(false); googleMap.setIndoorEnabled(false); googleMap.setTrafficEnabled(false); + // reuse the map type that was previously used + googleMap.setMapType(SettingsActivity.getLocationLastGoogleMapType()); + // setup markers listeners googleMap.setOnMarkerClickListener(marker -> { @@ -171,22 +179,22 @@ public void onMapReady(@NonNull GoogleMap googleMap) { private void repositionCompass() { try { - int twelveDp = (int) (12 * activity.getResources().getDisplayMetrics().density); View mapView = mapFragment.getView(); if (mapView != null) { View compass = mapView.findViewWithTag("GoogleMapCompass"); if (compass != null) { + int sixteenDp = (int) (16 * activity.getResources().getDisplayMetrics().density); compass.post(() -> { try { // create layoutParams, giving it our wanted width and height(important, by default the width is "match parent") - RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(compass.getHeight(), compass.getHeight()); + RelativeLayout.LayoutParams rlp = new RelativeLayout.LayoutParams(sixteenDp * 2, sixteenDp * 2); // position on top right rlp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0); rlp.addRule(RelativeLayout.ALIGN_PARENT_TOP); rlp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); rlp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0); //give compass margin - rlp.setMargins(0, twelveDp * 2, twelveDp, 0); + rlp.setMargins(0, sixteenDp * 4, sixteenDp, 0); compass.setLayoutParams(rlp); } catch (Exception ex) { ex.printStackTrace(); @@ -289,31 +297,72 @@ public void launchMapSnapshot(@NonNull Consumer onSnapshotReadyCallback) } @Override - public void setGestureEnabled(boolean enabled) { - if (googleMap == null) { - Logger.i("GoogleMapMapView: setGestureEnabled: googleMap is not ready to use"); - return; - } - googleMap.getUiSettings().setAllGesturesEnabled(enabled); + void setLayersButtonVisibilitySetter(Consumer layersButtonVisibilitySetter) { + layersButtonVisibilitySetter.accept(true); } @Override - public void setOnMapClickListener(Runnable clickListener) { - if (googleMap == null) { - Logger.i("GoogleMapMapView: setOnMapClickListener: googleMap is not ready to use"); - return; + void onLayersButtonClicked(View view) { + if (googleMap != null) { + PopupMenu popup = new PopupMenu(activity, view, Gravity.TOP | Gravity.END); + Menu menu = popup.getMenu(); + MenuItem normal = menu.add(0, GoogleMap.MAP_TYPE_NORMAL, 0, R.string.menu_action_google_maps_normal); + MenuItem satellite = menu.add(0, GoogleMap.MAP_TYPE_SATELLITE, 1, R.string.menu_action_google_maps_satellite); + MenuItem hybrid = menu.add(0, GoogleMap.MAP_TYPE_HYBRID, 2, R.string.menu_action_google_maps_hybrid); + MenuItem terrain = menu.add(0, GoogleMap.MAP_TYPE_TERRAIN, 3, R.string.menu_action_google_maps_terrain); + menu.setGroupCheckable(0, true, true); + switch (googleMap.getMapType()) { + case GoogleMap.MAP_TYPE_NORMAL: + normal.setChecked(true); + break; + case GoogleMap.MAP_TYPE_SATELLITE: + satellite.setChecked(true); + break; + case GoogleMap.MAP_TYPE_HYBRID: + hybrid.setChecked(true); + break; + case GoogleMap.MAP_TYPE_TERRAIN: + terrain.setChecked(true); + break; + } + popup.setOnMenuItemClickListener(item -> { + if (item.getItemId() != googleMap.getMapType()) { + googleMap.setMapType(item.getItemId()); + SettingsActivity.setLocationLastGoogleMapType(item.getItemId()); + } + return true; + }); + popup.show(); } - googleMap.setOnMapClickListener((latLng) -> clickListener.run()); } - @Override - public void setOnMapLongClickListener(Runnable clickListener) { - if (googleMap == null) { - Logger.i("GoogleMapMapView: setOnMapLongClickListener: googleMap is not ready to use"); - return; - } - googleMap.setOnMapLongClickListener((latLng) -> clickListener.run()); - } + + // @Override +// public void setGestureEnabled(boolean enabled) { +// if (googleMap == null) { +// Logger.i("GoogleMapMapView: setGestureEnabled: googleMap is not ready to use"); +// return; +// } +// googleMap.getUiSettings().setAllGesturesEnabled(enabled); +// } +// +// @Override +// public void setOnMapClickListener(Runnable clickListener) { +// if (googleMap == null) { +// Logger.i("GoogleMapMapView: setOnMapClickListener: googleMap is not ready to use"); +// return; +// } +// googleMap.setOnMapClickListener((latLng) -> clickListener.run()); +// } +// +// @Override +// public void setOnMapLongClickListener(Runnable clickListener) { +// if (googleMap == null) { +// Logger.i("GoogleMapMapView: setOnMapLongClickListener: googleMap is not ready to use"); +// return; +// } +// googleMap.setOnMapLongClickListener((latLng) -> clickListener.run()); +// } @Override public void setOnMapReadyCallback(@Nullable Runnable callback) { @@ -427,7 +476,7 @@ public void centerOnMarkers(boolean animate, boolean includeMyLocation) { googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(0, 0), 0)); } else if (bounds.second == null) { // count == 1 || ((latNorth-latSouth < 0.005) && (lonEast-lonWest < 0.005)) // else center on single symbol - float zoom = Float.max(DEFAULT_ZOOM, googleMap.getCameraPosition().zoom); + float zoom = markersPositions.size() == 1 ? Float.max(DEFAULT_ZOOM, googleMap.getCameraPosition().zoom) : DEFAULT_ZOOM; if (animate) { googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(bounds.first.toGoogleMaps(), zoom), TRANSITION_DURATION_MS, null); } else { diff --git a/obv_messenger/app/src/full/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java b/obv_messenger/app/src/full/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java index 1e03b409..c5d6b99a 100644 --- a/obv_messenger/app/src/full/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java +++ b/obv_messenger/app/src/full/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java @@ -36,17 +36,34 @@ public class ObvFirebaseMessagingService extends FirebaseMessagingService { private static Long lastPushNotificationTimestamp = null; + private static int deprioritizedMessageCount = 0; + private static int highPriorityMessageCount = 0; public static Long getLastPushNotificationTimestamp() { return lastPushNotificationTimestamp; } + public static int getDeprioritizedMessageCount() { + return deprioritizedMessageCount; + } + + public static int getHighPriorityMessageCount() { + return highPriorityMessageCount; + } + @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { super.onMessageReceived(remoteMessage); lastPushNotificationTimestamp = System.currentTimeMillis(); - Logger.d("FIREBASE Message received"); + Logger.d("FIREBASE Message received. Priority: " + ((remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH) ? "HIGH" : "NORMAL")); + if (remoteMessage.getOriginalPriority() == RemoteMessage.PRIORITY_HIGH) { + highPriorityMessageCount++; + if (remoteMessage.getPriority() != remoteMessage.getOriginalPriority()) { + deprioritizedMessageCount++; + Logger.e("message was deprioritized!"); + } + } Map data = remoteMessage.getData(); String identityString = data.get("identity"); String topic = data.get("topic"); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/App.java b/obv_messenger/app/src/main/java/io/olvid/messenger/App.java index 9cfeb074..3adcaf1f 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/App.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/App.java @@ -22,6 +22,8 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -634,6 +636,12 @@ public static void openLink(Context context, Uri uri) { final AlertDialog.Builder builder = new SecureAlertDialogBuilder(context, R.style.CustomAlertDialog) .setTitle(R.string.dialog_title_confirm_open_link) .setMessage(uri.toString()) + .setNeutralButton(R.string.button_label_copy, (dialog, which) -> { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(uri.toString(), uri.toString()); + clipboard.setPrimaryClip(clip); + toast(R.string.toast_message_link_copied, Toast.LENGTH_SHORT); + }) .setPositiveButton(R.string.button_label_ok, (dialog, which) -> { try { context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/AppBackupAndSyncDelegate.java b/obv_messenger/app/src/main/java/io/olvid/messenger/AppBackupAndSyncDelegate.java index b493118d..c5303737 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/AppBackupAndSyncDelegate.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/AppBackupAndSyncDelegate.java @@ -38,6 +38,7 @@ import io.olvid.messenger.databases.entity.sync.AppSyncSnapshot; import io.olvid.messenger.databases.tasks.OwnedDevicesSynchronisationWithEngineTask; import io.olvid.messenger.openid.KeycloakManager; +import io.olvid.messenger.settings.SettingsActivity; public class AppBackupAndSyncDelegate implements ObvBackupAndSyncDelegate { EngineNotificationListener engineNotificationListener; diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java b/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java index 45bb2104..0affefed 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/AppSingleton.java @@ -177,15 +177,14 @@ private AppSingleton() { App.runThread(new SetContactsAndPendingMembersFirstNamesTask()); } - // TODO: enable this once location is no longer in beta -// if (lastBuildExecuted != 0 && lastBuildExecuted < 171) { -// // if the user has customized attach icon order, add the send location icon so they see it -// List icons = SettingsActivity.getComposeMessageIconPreferredOrder(); -// if (icons != null && !icons.contains(ComposeMessageFragment.ICON_SEND_LOCATION)) { -// icons.add(0, ComposeMessageFragment.ICON_SEND_LOCATION); -// SettingsActivity.setComposeMessageIconPreferredOrder(icons); -// } -// } + if (lastBuildExecuted != 0 && lastBuildExecuted < 229) { + // if the user has customized attach icon order, add the send location icon so they see it + List icons = SettingsActivity.getComposeMessageIconPreferredOrder(); + if (icons != null && !icons.contains(ComposeMessageFragment.ICON_SEND_LOCATION)) { + icons.add(0, ComposeMessageFragment.ICON_SEND_LOCATION); + SettingsActivity.setComposeMessageIconPreferredOrder(icons); + } + } { // generate App directories diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/activities/ContactDetailsActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/activities/ContactDetailsActivity.java index edb77481..22683253 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/activities/ContactDetailsActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/activities/ContactDetailsActivity.java @@ -219,7 +219,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { View groupEmptyView = findViewById(R.id.contact_group_list_empty_view); contactGroupDiscussionsFragment = new FilteredDiscussionListFragment(); - contactGroupDiscussionsFragment.removeBottomPadding(); + contactGroupDiscussionsFragment.setBottomPadding(0); contactGroupDiscussionsFragment.setEmptyView(groupEmptyView); contactGroupDiscussionsFragment.setOnClickDelegate((view, searchableDiscussion) -> App.openDiscussionActivity(view.getContext(), searchableDiscussion.discussionId)); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/EmojiList.java b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/EmojiList.java index 6c33fc91..2c12ff79 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/EmojiList.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/EmojiList.java @@ -71,6 +71,8 @@ public class EmojiList { {"๐Ÿ˜ฎโ€๐Ÿ’จ"}, {"๐Ÿคฅ"}, {"๐Ÿซจ"}, + {"๐Ÿ™‚โ€โ†”๏ธ"}, + {"๐Ÿ™‚โ€โ†•๏ธ"}, {"๐Ÿ˜Œ"}, {"๐Ÿ˜”"}, {"๐Ÿ˜ช"}, @@ -429,24 +431,42 @@ public class EmojiList { {"๐Ÿšถ", "๐Ÿšถ๐Ÿป", "๐Ÿšถ๐Ÿผ", "๐Ÿšถ๐Ÿฝ", "๐Ÿšถ๐Ÿพ", "๐Ÿšถ๐Ÿฟ"}, {"๐Ÿšถโ€โ™‚๏ธ", "๐Ÿšถ๐Ÿปโ€โ™‚๏ธ", "๐Ÿšถ๐Ÿผโ€โ™‚๏ธ", "๐Ÿšถ๐Ÿฝโ€โ™‚๏ธ", "๐Ÿšถ๐Ÿพโ€โ™‚๏ธ", "๐Ÿšถ๐Ÿฟโ€โ™‚๏ธ"}, {"๐Ÿšถโ€โ™€๏ธ", "๐Ÿšถ๐Ÿปโ€โ™€๏ธ", "๐Ÿšถ๐Ÿผโ€โ™€๏ธ", "๐Ÿšถ๐Ÿฝโ€โ™€๏ธ", "๐Ÿšถ๐Ÿพโ€โ™€๏ธ", "๐Ÿšถ๐Ÿฟโ€โ™€๏ธ"}, + {"๐Ÿšถโ€โžก๏ธ", "๐Ÿšถ๐Ÿปโ€โžก๏ธ", "๐Ÿšถ๐Ÿผโ€โžก๏ธ", "๐Ÿšถ๐Ÿฝโ€โžก๏ธ", "๐Ÿšถ๐Ÿพโ€โžก๏ธ", "๐Ÿšถ๐Ÿฟโ€โžก๏ธ"}, + {"๐Ÿšถโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ"}, + {"๐Ÿšถโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿšถ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ"}, {"๐Ÿง", "๐Ÿง๐Ÿป", "๐Ÿง๐Ÿผ", "๐Ÿง๐Ÿฝ", "๐Ÿง๐Ÿพ", "๐Ÿง๐Ÿฟ"}, {"๐Ÿงโ€โ™‚๏ธ", "๐Ÿง๐Ÿปโ€โ™‚๏ธ", "๐Ÿง๐Ÿผโ€โ™‚๏ธ", "๐Ÿง๐Ÿฝโ€โ™‚๏ธ", "๐Ÿง๐Ÿพโ€โ™‚๏ธ", "๐Ÿง๐Ÿฟโ€โ™‚๏ธ"}, {"๐Ÿงโ€โ™€๏ธ", "๐Ÿง๐Ÿปโ€โ™€๏ธ", "๐Ÿง๐Ÿผโ€โ™€๏ธ", "๐Ÿง๐Ÿฝโ€โ™€๏ธ", "๐Ÿง๐Ÿพโ€โ™€๏ธ", "๐Ÿง๐Ÿฟโ€โ™€๏ธ"}, {"๐ŸงŽ", "๐ŸงŽ๐Ÿป", "๐ŸงŽ๐Ÿผ", "๐ŸงŽ๐Ÿฝ", "๐ŸงŽ๐Ÿพ", "๐ŸงŽ๐Ÿฟ"}, {"๐ŸงŽโ€โ™‚๏ธ", "๐ŸงŽ๐Ÿปโ€โ™‚๏ธ", "๐ŸงŽ๐Ÿผโ€โ™‚๏ธ", "๐ŸงŽ๐Ÿฝโ€โ™‚๏ธ", "๐ŸงŽ๐Ÿพโ€โ™‚๏ธ", "๐ŸงŽ๐Ÿฟโ€โ™‚๏ธ"}, {"๐ŸงŽโ€โ™€๏ธ", "๐ŸงŽ๐Ÿปโ€โ™€๏ธ", "๐ŸงŽ๐Ÿผโ€โ™€๏ธ", "๐ŸงŽ๐Ÿฝโ€โ™€๏ธ", "๐ŸงŽ๐Ÿพโ€โ™€๏ธ", "๐ŸงŽ๐Ÿฟโ€โ™€๏ธ"}, + {"๐ŸงŽโ€โžก๏ธ", "๐ŸงŽ๐Ÿปโ€โžก๏ธ", "๐ŸงŽ๐Ÿผโ€โžก๏ธ", "๐ŸงŽ๐Ÿฝโ€โžก๏ธ", "๐ŸงŽ๐Ÿพโ€โžก๏ธ", "๐ŸงŽ๐Ÿฟโ€โžก๏ธ"}, + {"๐ŸงŽโ€โ™€๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ"}, + {"๐ŸงŽโ€โ™‚๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", "๐ŸงŽ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ"}, {"๐Ÿง‘โ€๐Ÿฆฏ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆฏ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆฏ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฏ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆฏ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฏ"}, + {"๐Ÿง‘โ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ"}, {"๐Ÿ‘จโ€๐Ÿฆฏ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฏ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฏ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฏ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฏ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฏ"}, + {"๐Ÿ‘จโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ"}, {"๐Ÿ‘ฉโ€๐Ÿฆฏ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฏ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฏ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฏ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฏ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฏ"}, + {"๐Ÿ‘ฉโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฏโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฏโ€โžก๏ธ"}, {"๐Ÿง‘โ€๐Ÿฆผ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆผ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆผ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆผ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆผ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆผ"}, + {"๐Ÿง‘โ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ"}, {"๐Ÿ‘จโ€๐Ÿฆผ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆผ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆผ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆผ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆผ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆผ"}, + {"๐Ÿ‘จโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ"}, {"๐Ÿ‘ฉโ€๐Ÿฆผ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆผ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆผ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆผ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆผ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆผ"}, + {"๐Ÿ‘ฉโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆผโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆผโ€โžก๏ธ"}, {"๐Ÿง‘โ€๐Ÿฆฝ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆฝ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆฝ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฝ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆฝ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฝ"}, + {"๐Ÿง‘โ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿง‘๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿง‘๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿง‘๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿง‘๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿง‘๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ"}, {"๐Ÿ‘จโ€๐Ÿฆฝ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฝ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฝ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฝ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฝ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฝ"}, + {"๐Ÿ‘จโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ"}, {"๐Ÿ‘ฉโ€๐Ÿฆฝ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฝ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฝ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฝ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฝ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฝ"}, + {"๐Ÿ‘ฉโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿปโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿผโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿพโ€๐Ÿฆฝโ€โžก๏ธ", "๐Ÿ‘ฉ๐Ÿฟโ€๐Ÿฆฝโ€โžก๏ธ"}, {"๐Ÿƒ", "๐Ÿƒ๐Ÿป", "๐Ÿƒ๐Ÿผ", "๐Ÿƒ๐Ÿฝ", "๐Ÿƒ๐Ÿพ", "๐Ÿƒ๐Ÿฟ"}, {"๐Ÿƒโ€โ™‚๏ธ", "๐Ÿƒ๐Ÿปโ€โ™‚๏ธ", "๐Ÿƒ๐Ÿผโ€โ™‚๏ธ", "๐Ÿƒ๐Ÿฝโ€โ™‚๏ธ", "๐Ÿƒ๐Ÿพโ€โ™‚๏ธ", "๐Ÿƒ๐Ÿฟโ€โ™‚๏ธ"}, {"๐Ÿƒโ€โ™€๏ธ", "๐Ÿƒ๐Ÿปโ€โ™€๏ธ", "๐Ÿƒ๐Ÿผโ€โ™€๏ธ", "๐Ÿƒ๐Ÿฝโ€โ™€๏ธ", "๐Ÿƒ๐Ÿพโ€โ™€๏ธ", "๐Ÿƒ๐Ÿฟโ€โ™€๏ธ"}, + {"๐Ÿƒโ€โžก๏ธ", "๐Ÿƒ๐Ÿปโ€โžก๏ธ", "๐Ÿƒ๐Ÿผโ€โžก๏ธ", "๐Ÿƒ๐Ÿฝโ€โžก๏ธ", "๐Ÿƒ๐Ÿพโ€โžก๏ธ", "๐Ÿƒ๐Ÿฟโ€โžก๏ธ"}, + {"๐Ÿƒโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿปโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿผโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿฝโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿพโ€โ™€๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿฟโ€โ™€๏ธโ€โžก๏ธ"}, + {"๐Ÿƒโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿปโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿผโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿฝโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿพโ€โ™‚๏ธโ€โžก๏ธ", "๐Ÿƒ๐Ÿฟโ€โ™‚๏ธโ€โžก๏ธ"}, {"๐Ÿ’ƒ", "๐Ÿ’ƒ๐Ÿป", "๐Ÿ’ƒ๐Ÿผ", "๐Ÿ’ƒ๐Ÿฝ", "๐Ÿ’ƒ๐Ÿพ", "๐Ÿ’ƒ๐Ÿฟ"}, {"๐Ÿ•บ", "๐Ÿ•บ๐Ÿป", "๐Ÿ•บ๐Ÿผ", "๐Ÿ•บ๐Ÿฝ", "๐Ÿ•บ๐Ÿพ", "๐Ÿ•บ๐Ÿฟ"}, {"๐Ÿ•ด๏ธ", "๐Ÿ•ด๐Ÿป", "๐Ÿ•ด๐Ÿผ", "๐Ÿ•ด๐Ÿฝ", "๐Ÿ•ด๐Ÿพ", "๐Ÿ•ด๐Ÿฟ"}, @@ -519,7 +539,6 @@ public class EmojiList { {"๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘จ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ"}, {"๐Ÿ‘จโ€โค๏ธโ€๐Ÿ‘จ", "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘จ๐Ÿปโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘จ๐Ÿผโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘จ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘จ๐Ÿพโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ", "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿป", "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿผ", "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฝ", "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿพ", "๐Ÿ‘จ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘จ๐Ÿฟ"}, {"๐Ÿ‘ฉโ€โค๏ธโ€๐Ÿ‘ฉ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿปโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿผโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿฝโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿพโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿป", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿผ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฝ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿพ", "๐Ÿ‘ฉ๐Ÿฟโ€โค๏ธโ€๐Ÿ‘ฉ๐Ÿฟ"}, - {"๐Ÿ‘ช"}, {"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆ"}, {"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง"}, {"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"}, @@ -549,6 +568,11 @@ public class EmojiList { {"๐Ÿ‘ค"}, {"๐Ÿ‘ฅ"}, {"๐Ÿซ‚"}, + {"๐Ÿ‘ช"}, + {"๐Ÿง‘โ€๐Ÿง‘โ€๐Ÿง’"}, + {"๐Ÿง‘โ€๐Ÿง‘โ€๐Ÿง’โ€๐Ÿง’"}, + {"๐Ÿง‘โ€๐Ÿง’"}, + {"๐Ÿง‘โ€๐Ÿง’โ€๐Ÿง’"}, {"๐Ÿ‘ฃ"}, {"๐Ÿต"}, {"๐Ÿ’"}, @@ -637,6 +661,7 @@ public class EmojiList { {"๐Ÿชฝ"}, {"๐Ÿฆโ€โฌ›"}, {"๐Ÿชฟ"}, + {"๐Ÿฆโ€๐Ÿ”ฅ"}, {"๐Ÿธ"}, {"๐ŸŠ"}, {"๐Ÿข"}, @@ -707,6 +732,7 @@ public class EmojiList { {"๐Ÿ‰"}, {"๐ŸŠ"}, {"๐Ÿ‹"}, + {"๐Ÿ‹โ€๐ŸŸฉ"}, {"๐ŸŒ"}, {"๐Ÿ"}, {"๐Ÿฅญ"}, @@ -738,6 +764,7 @@ public class EmojiList { {"๐ŸŒฐ"}, {"๐Ÿซš"}, {"๐Ÿซ›"}, + {"๐Ÿ„โ€๐ŸŸซ"}, {"๐Ÿž"}, {"๐Ÿฅ"}, {"๐Ÿฅ–"}, @@ -1346,6 +1373,7 @@ public class EmojiList { {"โš–๏ธ"}, {"๐Ÿฆฏ"}, {"๐Ÿ”—"}, + {"โ›“๏ธโ€๐Ÿ’ฅ"}, {"โ›“๏ธ"}, {"๐Ÿช"}, {"๐Ÿงฐ"}, @@ -1910,21 +1938,21 @@ public static int offsetForEmojiGroup(EmojiGroup group) { case SMILEYS_EMOTION: return 0; case PEOPLE_BODY: - return 166; + return 168; case ANIMALS_NATURE: - return 529; + return 553; case FOOD_DRINK: - return 681; + return 706; case TRAVEL_PLACES: - return 814; + return 841; case ACTIVITIES: - return 1032; + return 1059; case OBJECTS: - return 1117; + return 1144; case SYMBOLS: - return 1378; + return 1406; case FLAGS: - return 1601; + return 1629; } return -1; } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/LocationShareQuality.java b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/LocationShareQuality.java new file mode 100644 index 00000000..abc1245f --- /dev/null +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/LocationShareQuality.java @@ -0,0 +1,97 @@ +/* + * Olvid for Android + * Copyright ยฉ 2019-2024 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.messenger.customClasses; + + +import android.content.Context; + +import io.olvid.messenger.R; + +public enum LocationShareQuality { + QUALITY_PRECISE(1), + QUALITY_BALANCED(2), + QUALITY_POWER_SAVE(3); + + public final int value; + + LocationShareQuality(int value) { + this.value = value; + } + + public static LocationShareQuality fromValue(int value) { + switch (value) { + case 3: + return QUALITY_POWER_SAVE; + case 2: + return QUALITY_BALANCED; + case 1: + default: + return QUALITY_PRECISE; + } + } + + public CharSequence getFullString(Context context) { + switch (this) { + case QUALITY_BALANCED: + return context.getText(R.string.location_sharing_quality_balanced_full_string); + case QUALITY_POWER_SAVE: + return context.getText(R.string.location_sharing_quality_power_save_full_string); + case QUALITY_PRECISE: + default: + return context.getText(R.string.location_sharing_quality_precise_full_string); + } + } + + public long getMinUpdateFrequencyMs() { + switch (this) { + case QUALITY_BALANCED: + return 12_000; + case QUALITY_POWER_SAVE: + return 60_000; + case QUALITY_PRECISE: + default: + return 3_000; + } + } + + // for this, we pick slightly smaller durations to have some leeway when checking if this was elapsed + public long getDefaultUpdateFrequencyMs() { + switch (this) { + case QUALITY_BALANCED: + return 110_000; // 10 min - 10s + case QUALITY_POWER_SAVE: + return 1_750_000; // 30 min - 50s + case QUALITY_PRECISE: + default: + return 28_000; // 30s - 2s + } + } + public int getMinUpdateDistanceMeters() { + switch (this) { + case QUALITY_BALANCED: + return 20; + case QUALITY_POWER_SAVE: + return 100; + case QUALITY_PRECISE: + default: + return 5; + } + } +} diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/Markdown.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/Markdown.kt index fab1f7df..d87675e1 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/Markdown.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/Markdown.kt @@ -439,21 +439,20 @@ private fun Editable.setMarkdownSpanFromNode( is Heading -> { node.sourceSpans.first().let { sourceSpan -> - val delimiterStart = indexOf(MarkdownTag.HEADING.delimiter.repeat(node.level)) val start = sourceSpan.columnIndex + lineOffsets[sourceSpan.lineIndex] - if (get(delimiterStart).toString() == MarkdownTag.HEADING.delimiter) { // ensure heading is triggered by # and not underlying - or = + if (get(start).toString() == MarkdownTag.HEADING.delimiter) { // ensure heading is triggered by # and not underlying - or = setSpan( markdownSpan, - delimiterStart + node.level + 1, + start + node.level + 1, (sourceSpan.columnIndex + lineOffsets[sourceSpan.lineIndex] + sourceSpan.length).coerceAtLeast( - delimiterStart + node.level + 1 + start + node.level + 1 ), SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE ) setSpan( MarkdownDelimiter(color = highlightColor), start, - delimiterStart + node.level + 1, + start + node.level + 1, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE ) } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtils.java b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtils.java index 31fe022a..efd04581 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtils.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtils.java @@ -56,6 +56,7 @@ public class PreviewUtils { public static final int MAX_SIZE = 100_000; // constant to indicate that previews should be as large as possible + public static final int MAX_PREVIEW_PIXEL_SIZE = 4_000; // previews larger than this could be larger than MAX_BITMAP_SIZE public static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; // region Implement a local in-memory cache of miniature bitmaps @@ -162,6 +163,11 @@ public static Bitmap getBitmapPreview(Fyle fyle, FyleMessageJoinWithStatus fyleM if (fyle.sha256 == null) { return null; } + + if (previewPixelSize > MAX_PREVIEW_PIXEL_SIZE) { + previewPixelSize = MAX_PREVIEW_PIXEL_SIZE; + } + String cacheKey = Logger.toHexString(fyle.sha256) + "_" + fyleMessageJoinWithStatus.getNonNullMimeType(); if (fyle.isComplete()) { SizeAndBitmap sizeAndBitmap = thumbnailCache.get(cacheKey); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtilsWithDrawables.java b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtilsWithDrawables.java index 62604478..0a03529a 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtilsWithDrawables.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/customClasses/PreviewUtilsWithDrawables.java @@ -156,13 +156,21 @@ static class PreviewUtilsScaleDownListener implements ImageDecoder.OnHeaderDecod private final int previewPixelSize; public PreviewUtilsScaleDownListener(int previewPixelSize) { - this.previewPixelSize = previewPixelSize; + if (previewPixelSize != PreviewUtils.MAX_SIZE && previewPixelSize > PreviewUtils.MAX_PREVIEW_PIXEL_SIZE) { + this.previewPixelSize = PreviewUtils.MAX_PREVIEW_PIXEL_SIZE; + } else { + this.previewPixelSize = previewPixelSize; + } } @Override public void onHeaderDecoded(@NonNull ImageDecoder decoder, @NonNull ImageDecoder.ImageInfo info, @NonNull ImageDecoder.Source source) { width = info.getSize().getWidth(); height = info.getSize().getHeight(); + int maxPixelBytes = 4; + if (info.getColorSpace() != null && !info.getColorSpace().isSrgb()) { + maxPixelBytes = 8; + } partial = false; if (previewPixelSize != PreviewUtils.MAX_SIZE) { int size = Math.max(info.getSize().getWidth(), info.getSize().getHeight()); @@ -170,8 +178,8 @@ public void onHeaderDecoded(@NonNull ImageDecoder decoder, @NonNull ImageDecoder int subSampling = size / previewPixelSize; decoder.setTargetSampleSize(subSampling); } - } else if (4 * width * height > PreviewUtils.MAX_BITMAP_SIZE) { - int scaled = (int) Math.sqrt((double) (4 * width * height) / PreviewUtils.MAX_BITMAP_SIZE) + 1; + } else if (maxPixelBytes * width * height > PreviewUtils.MAX_BITMAP_SIZE) { + int scaled = (int) Math.sqrt((double) (maxPixelBytes * width * height) / PreviewUtils.MAX_BITMAP_SIZE) + 1; decoder.setTargetSize(width / scaled, height / scaled); } decoder.setOnPartialImageListener(this); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/AppDatabaseOpenCallback.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/AppDatabaseOpenCallback.java index 01b8aafa..e55199c1 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/AppDatabaseOpenCallback.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/AppDatabaseOpenCallback.java @@ -161,12 +161,12 @@ public void run() { Logger.w("Error cleaning up pre-discussions without invitation."); } - // Update all Messages/attachments + // Update all attachments try { engine.resendAllAttachmentNotifications(); } catch (Exception e) { e.printStackTrace(); - Logger.w("Error syncing Room messages with Engine attachments."); + Logger.w("Error syncing Room attachments with Engine attachments."); } // Check status of all uploading/downloading Fyle @@ -202,6 +202,9 @@ public void run() { downloadingFyle.sendReturnReceipt(FyleMessageJoinWithStatus.RECEPTION_STATUS_DELIVERED, null); } } else if (engine.isInboxAttachmentReceived(downloadingFyle.bytesOwnedIdentity, downloadingFyle.engineMessageIdentifier, downloadingFyle.engineNumber)) { + // If a message still exists in the engine, a download completed notification will be resent by the call to resendAllAttachmentNotifications() above. + // However, if the message no longer exists, all we can do is mark the app attachment as downloaded, + // but we do not have anything in the engine to fetch to actually complete the download downloadingFyle.status = FyleMessageJoinWithStatus.STATUS_COMPLETE; db.fyleMessageJoinWithStatusDao().updateStatus(downloadingFyle.messageId, downloadingFyle.fyleId, downloadingFyle.status); downloadingFyle.sendReturnReceipt(FyleMessageJoinWithStatus.RECEPTION_STATUS_DELIVERED, null); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/DiscussionDao.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/DiscussionDao.java index 50701cc6..fda4bbfd 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/DiscussionDao.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/DiscussionDao.java @@ -158,7 +158,7 @@ public abstract class DiscussionDao { @Transaction @Query("SELECT " + PREFIX_DISCUSSION_COLUMNS + ", " + - " message.*, unread.count AS unread_count, (unreadMention.count != 0) AS unread_mention, " + + " message.*, unread.count AS unread_count, (unreadMention.count != 0) AS unread_mention, (locations.count != 0) AS locations_shared, " + PREFIX_DISCUSSION_CUSTOMIZATION_COLUMNS + " FROM " + Discussion.TABLE_NAME + " AS disc " + " LEFT JOIN ( SELECT id, " + Message.SENDER_SEQUENCE_NUMBER + ", " + @@ -178,6 +178,9 @@ public abstract class DiscussionDao { " ON unread." + Message.DISCUSSION_ID + " = disc.id " + " LEFT JOIN ( SELECT COUNT(*) AS count, " + Message.DISCUSSION_ID + " FROM " + Message.TABLE_NAME + " WHERE " + Message.STATUS + " = " + Message.STATUS_UNREAD + " AND " + Message.MENTIONED + " = 1" + " GROUP BY " + Message.DISCUSSION_ID + " ) AS unreadMention " + " ON unreadMention." + Message.DISCUSSION_ID + " = disc.id " + + " LEFT JOIN ( SELECT COUNT(*) as count, " + Message.DISCUSSION_ID + " FROM " + Message.TABLE_NAME + " WHERE " + Message.JSON_LOCATION + " NOT NULL " + " AND " + + Message.LOCATION_TYPE + " = " + Message.LOCATION_TYPE_SHARE + ") AS locations" + + " ON locations." + Message.DISCUSSION_ID + " = disc.id " + " LEFT JOIN " + DiscussionCustomization.TABLE_NAME + " AS cust " + " ON cust." + DiscussionCustomization.DISCUSSION_ID + " = disc.id " + " WHERE disc." + Discussion.BYTES_OWNED_IDENTITY + " = :bytesOwnedIdentity " + @@ -202,7 +205,7 @@ public abstract class DiscussionDao { " disc." + Discussion.ACTIVE + " AS disc_" + Discussion.ACTIVE + ", " + " disc." + Discussion.TRUST_LEVEL + " AS disc_" + Discussion.TRUST_LEVEL + ", " + " disc." + Discussion.STATUS + " AS disc_" + Discussion.STATUS + ", " + - " message.*, unread.count AS unread_count, (unreadMention.count != 0) AS unread_mention, " + + " message.*, unread.count AS unread_count, (unreadMention.count != 0) AS unread_mention, 0 AS locations_shared, " + PREFIX_DISCUSSION_CUSTOMIZATION_COLUMNS + " FROM " + Discussion.TABLE_NAME + " AS disc " + " LEFT JOIN ( SELECT id, " + Message.SENDER_SEQUENCE_NUMBER + ", " + @@ -513,6 +516,9 @@ public static class DiscussionAndLastMessage { @ColumnInfo(name = "unread_mention") public boolean unreadMention; + + @ColumnInfo(name = "locations_shared") + public boolean locationsShared; } public static class DiscussionAndGroupMembersNames { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/MessageDao.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/MessageDao.java index 75f1ff8d..df5d73bc 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/MessageDao.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/MessageDao.java @@ -172,6 +172,14 @@ public interface MessageDao { @Query("SELECT * FROM " + Message.TABLE_NAME + " WHERE " + Message.DISCUSSION_ID + " = :discussionId AND " + Message.STATUS + " != " + Message.STATUS_DRAFT + " ORDER BY " + Message.SORT_INDEX + " ASC") LiveData> getDiscussionMessages(long discussionId); + @Query("SELECT * FROM " + Message.TABLE_NAME + + " WHERE " + Message.DISCUSSION_ID + " = :discussionId " + + " AND " + Message.STATUS + " != " + Message.STATUS_DRAFT + + " AND " + Message.MESSAGE_TYPE + " != " + Message.TYPE_GROUP_MEMBER_JOINED + + " AND " + Message.MESSAGE_TYPE + " != " + Message.TYPE_GROUP_MEMBER_LEFT + + " ORDER BY " + Message.SORT_INDEX + " ASC") + LiveData> getDiscussionMessagesWithoutGroupMemberChanges(long discussionId); + @Query("SELECT * FROM " + Message.TABLE_NAME + " WHERE " + Message.DISCUSSION_ID + " = :discussionId AND " + Message.STATUS + " != " + Message.STATUS_DRAFT + " ORDER BY " + Message.SORT_INDEX + " DESC LIMIT :count") LiveData> getLastDiscussionMessages(long discussionId, int count); @@ -501,8 +509,7 @@ public interface MessageDao { " AND " + Message.LOCATION_TYPE + " = " + Message.LOCATION_TYPE_SHARE + " AND " + Message.DISCUSSION_ID + " = :discussionId " + " AND " + Message.SENDER_IDENTIFIER + " = :senderIdentifier " + - " ORDER BY " + Message.SORT_INDEX + " ASC " - ) + " ORDER BY " + Message.SORT_INDEX + " ASC ") List getCurrentLocationSharingMessagesOfIdentityInDiscussion(byte[] senderIdentifier, long discussionId); @Query("SELECT id FROM " + Message.TABLE_NAME + diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLocation.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLocation.java index 38ea5a45..a00d33e5 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLocation.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLocation.java @@ -35,6 +35,7 @@ import java.util.Locale; import io.olvid.messenger.R; +import io.olvid.messenger.customClasses.LocationShareQuality; @JsonIgnoreProperties(ignoreUnknown = true) public class JsonLocation { @@ -50,7 +51,8 @@ public class JsonLocation { public long timestamp; // location timestamp // -- sharing message fields -- public Long count; // null if not sharing - public Long sharingInterval; // null if not sharing (else in ms) + public Integer quality; // one of QUALITY_PRECISE, QUALITY_BALANCED, or QUALITY_POWER_SAVE for sharing. Null for TYPE_SEND +// public Long sharingInterval; // null if not sharing (else in ms) public Long sharingExpiration; // can be null if endless sharing (else in ms) // -- location -- public double latitude; @@ -63,7 +65,8 @@ public class JsonLocation { public JsonLocation() { } - public JsonLocation(int type, @Nullable Long sharingExpiration, @Nullable Long sharingInterval, @Nullable Long count, double latitude, double longitude, Double altitude, Float precision, long timestamp) { + @JsonIgnore + public JsonLocation(int type, @Nullable Long sharingExpiration, @Nullable LocationShareQuality quality, @Nullable Long count, double latitude, double longitude, Double altitude, Float precision, long timestamp) { this.latitude = latitude; this.longitude = longitude; this.altitude = altitude; @@ -72,14 +75,14 @@ public JsonLocation(int type, @Nullable Long sharingExpiration, @Nullable Long s this.type = type; this.count = count; this.sharingExpiration = sharingExpiration; - this.sharingInterval = sharingInterval; + this.quality = (quality == null) ? null : quality.value; } - public static JsonLocation startSharingLocationMessage(@Nullable Long sharingExpiration, @NotNull Long interval, @NotNull Location location) { + public static JsonLocation startSharingLocationMessage(@Nullable Long sharingExpiration, LocationShareQuality quality, @NotNull Location location) { return new JsonLocation( TYPE_SHARING, sharingExpiration, - interval, + quality, 1L, location.getLatitude(), location.getLongitude(), @@ -93,7 +96,7 @@ public static JsonLocation updateSharingLocationMessage(@NotNull JsonLocation or return new JsonLocation( TYPE_SHARING, originalJsonLocation.getSharingExpiration(), - originalJsonLocation.getSharingInterval(), + originalJsonLocation.getShareQuality(), originalJsonLocation.getCount() + 1, location.getLatitude(), location.getLongitude(), @@ -145,16 +148,35 @@ public void setSharingExpiration(Long sharingExpiration) { this.sharingExpiration = sharingExpiration; } - @JsonProperty("i") - public Long getSharingInterval() { - return sharingInterval; + @JsonIgnore + @Nullable + public LocationShareQuality getShareQuality() { + if (quality == null) { + return null; + } + return LocationShareQuality.fromValue(quality); + } + + @JsonProperty("q") + public Integer getQuality() { + return quality; } - @JsonProperty("i") - public void setSharingInterval(Long sharingInterval) { - this.sharingInterval = sharingInterval; + @JsonProperty("q") + public void setQuality(Integer quality) { + this.quality = quality; } + // @JsonProperty("i") +// public Long getSharingInterval() { +// return sharingInterval; +// } +// +// @JsonProperty("i") +// public void setSharingInterval(Long sharingInterval) { +// this.sharingInterval = sharingInterval; +// } + // -- message metadata -- @JsonProperty("t") public int getType() { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostLocationMessageInDiscussionTask.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostLocationMessageInDiscussionTask.java index a442524b..acedde4d 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostLocationMessageInDiscussionTask.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostLocationMessageInDiscussionTask.java @@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull; import io.olvid.engine.Logger; +import io.olvid.messenger.customClasses.LocationShareQuality; import io.olvid.messenger.databases.AppDatabase; import io.olvid.messenger.databases.entity.Discussion; import io.olvid.messenger.databases.entity.DiscussionCustomization; @@ -41,7 +42,7 @@ public class PostLocationMessageInDiscussionTask implements Runnable { private final boolean showToast; private final Location location; private final Long shareExpirationInMs; - private final Long shareIntervalInMs; + private final LocationShareQuality quality; private final boolean isSharingLocationMessage; // post simple location message @@ -50,17 +51,17 @@ public static PostLocationMessageInDiscussionTask postSendLocationMessageInDiscu } // post sharing location message - public static PostLocationMessageInDiscussionTask startLocationSharingInDiscussionTask(Location location, long discussionId, boolean showToast, @Nullable Long shareExpiration, @NotNull Long interval) { - return new PostLocationMessageInDiscussionTask(location, discussionId, showToast, shareExpiration, interval, true); + public static PostLocationMessageInDiscussionTask startLocationSharingInDiscussionTask(Location location, long discussionId, boolean showToast, @Nullable Long shareExpiration, @NotNull LocationShareQuality quality) { + return new PostLocationMessageInDiscussionTask(location, discussionId, showToast, shareExpiration, quality, true); } - private PostLocationMessageInDiscussionTask(@NotNull Location location, long discussionId, boolean showToast, @Nullable Long shareExpirationInMs, @Nullable Long shareIntervalInMs, boolean isSharingLocationMessage) { + private PostLocationMessageInDiscussionTask(@NotNull Location location, long discussionId, boolean showToast, @Nullable Long shareExpirationInMs, @Nullable LocationShareQuality quality, boolean isSharingLocationMessage) { this.db = AppDatabase.getInstance(); this.discussionId = discussionId; this.showToast = showToast; this.location = location; this.shareExpirationInMs = shareExpirationInMs; - this.shareIntervalInMs = shareIntervalInMs; + this.quality = quality; this.isSharingLocationMessage = isSharingLocationMessage; } @@ -80,7 +81,7 @@ public void run() { final JsonMessage jsonMessage = new JsonMessage(); if (isSharingLocationMessage) { - jsonMessage.setJsonLocation(JsonLocation.startSharingLocationMessage(shareExpirationInMs, shareIntervalInMs, location)); + jsonMessage.setJsonLocation(JsonLocation.startSharingLocationMessage(shareExpirationInMs, quality, location)); } else { jsonMessage.setJsonLocation(JsonLocation.sendLocationMessage(location)); } @@ -115,7 +116,7 @@ public void run() { // start sharing location service for this discussion if (isSharingLocationMessage) { - UnifiedForegroundService.LocationSharingSubService.startSharingInDiscussion(discussionId, shareExpirationInMs, shareIntervalInMs, message.id); + UnifiedForegroundService.LocationSharingSubService.startSharingInDiscussion(discussionId, shareExpirationInMs, quality, message.id); } }); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostOsmLocationMessageInDiscussionTask.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostOsmLocationMessageInDiscussionTask.java index 3bf6ea3c..4b48b53a 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostOsmLocationMessageInDiscussionTask.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PostOsmLocationMessageInDiscussionTask.java @@ -71,7 +71,9 @@ public void run() { final JsonMessage jsonMessage = new JsonMessage(); JsonLocation jsonLocation = JsonLocation.sendLocationMessage(location); - jsonLocation.setAddress(address); + if (address != null) { + jsonLocation.setAddress(address.trim()); + } jsonMessage.setJsonLocation(jsonLocation); jsonMessage.setBody(jsonMessage.getJsonLocation().getLocationMessageBody()); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/UpdateLocationMessageTask.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/UpdateLocationMessageTask.java index bd48b79f..79540b03 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/UpdateLocationMessageTask.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/UpdateLocationMessageTask.java @@ -105,7 +105,9 @@ public void run() { try { obvPostMessageOutput = Message.postUpdateMessageMessage(originalMessage); if (!obvPostMessageOutput.isMessagePostedForAtLeastOneContact()) { - throw new Exception("Message not sent"); + Logger.w("Trying to post location update message in discussion without contacts. Stopping location share."); + UnifiedForegroundService.LocationSharingSubService.stopSharingInDiscussion(discussionId, true); + return; } } catch (Exception e) { Logger.e("UpdateLocationMessageTask: Unable to update location message", e); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/SettingsPojo_0.java b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/SettingsPojo_0.java index 2ab8d56c..b967b5b9 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/SettingsPojo_0.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/backup/SettingsPojo_0.java @@ -38,6 +38,7 @@ import io.olvid.messenger.AppSingleton; import io.olvid.messenger.BuildConfig; import io.olvid.messenger.activities.ShortcutActivity; +import io.olvid.messenger.customClasses.LocationShareQuality; import io.olvid.messenger.databases.AppDatabase; import io.olvid.messenger.databases.entity.Contact; import io.olvid.messenger.databases.entity.Invitation; @@ -96,9 +97,8 @@ public class SettingsPojo_0 { public String map_integration; public String custom_osm_server; - public String osm_language; public Long location_share_duration; - public Long location_share_interval; + public Integer location_share_quality; public Boolean disable_address_lookup; public String custom_address_server; @@ -191,9 +191,8 @@ static SettingsPojo_0 build() { settingsPojo.map_integration = SettingsActivity.getLocationIntegration().getString(); settingsPojo.custom_osm_server = (SettingsActivity.getLocationIntegration() == SettingsActivity.LocationIntegrationEnum.CUSTOM_OSM) ? SettingsActivity.getLocationCustomOsmServerUrl() : null; - settingsPojo.osm_language = SettingsActivity.getLocationOpenStreetMapRawLanguage(); settingsPojo.location_share_duration = SettingsActivity.getLocationDefaultSharingDurationValue(); - settingsPojo.location_share_interval = SettingsActivity.getLocationDefaultSharingIntervalValue(); + settingsPojo.location_share_quality = SettingsActivity.getLocationDefaultShareQuality().value; settingsPojo.disable_address_lookup = SettingsActivity.getLocationDisableAddressLookup(); settingsPojo.custom_address_server = SettingsActivity.getLocationCustomAddressServer(); @@ -325,9 +324,8 @@ public void restore() { if (default_existence_duration != null && default_existence_duration > 0) { SettingsActivity.setDefaultDiscussionExistenceDuration(default_existence_duration); } if (map_integration != null) { SettingsActivity.setLocationIntegration(map_integration, custom_osm_server); } - if (osm_language != null) { SettingsActivity.setLocationOpenStreetMapRawLanguage(osm_language); } if (location_share_duration != null) { SettingsActivity.setLocationDefaultSharingDurationValue(location_share_duration); } - if (location_share_interval != null) { SettingsActivity.setLocationDefaultSharingIntervalValue(location_share_interval); } + if (location_share_quality != null) { SettingsActivity.setLocationDefaultShareQuality(LocationShareQuality.fromValue(location_share_quality)); } if (disable_address_lookup != null) { SettingsActivity.setLocationDisableAddressLookup(disable_address_lookup); } if (custom_address_server != null) { SettingsActivity.setLocationCustomAddressServer(custom_address_server); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionActivity.java index 91897116..01f1157e 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionActivity.java @@ -68,9 +68,12 @@ import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.view.animation.AlphaAnimation; import android.view.animation.Animation; +import android.view.animation.AnimationSet; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; +import android.view.animation.TranslateAnimation; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.LinearLayout; @@ -251,7 +254,8 @@ private enum ViewType { private EmptyRecyclerView messageRecyclerView; private LinearLayoutManager messageListLinearLayoutManager; private FloatingActionButton scrollDownFab; - private FloatingActionButton locationSharingFab; + private ViewGroup locationSharingGroup; + private TextView locationSharingTextView; private ImageView rootBackgroundImageView; private DiscussionDelegate discussionDelegate; @@ -492,6 +496,11 @@ public void setAdditionalBottomPadding(int paddingPx) { scrolling = (state == RecyclerView.SCROLL_STATE_DRAGGING) || (state == RecyclerView.SCROLL_STATE_SETTLING); if (scrolling ^ wasScrolling) { messageRecyclerView.invalidate(); + if (scrolling) { + setLocationSharingGroupVisibility(false, false); + } else { + setLocationSharingGroupVisibility(true, false); + } } messageDateItemDecoration.setScrolling(scrolling); if (state == RecyclerView.SCROLL_STATE_DRAGGING) { @@ -509,20 +518,35 @@ public void setAdditionalBottomPadding(int paddingPx) { scrollDownFab = findViewById(R.id.discussion_scroll_down_fab); scrollDownFab.setOnClickListener(this); - locationSharingFab = findViewById(R.id.discussion_location_sharing_fab); - locationSharingFab.setOnClickListener(this); + locationSharingGroup = findViewById(R.id.discussion_location_sharing_group); + locationSharingTextView = locationSharingGroup.findViewById(R.id.discussion_location_sharing_text_view); + locationSharingGroup.setOnClickListener(this); + locationSharingGroup.findViewById(R.id.discussion_location_sharing_menu_dots).setOnClickListener(this); discussionViewModel.getCurrentlySharingLocationMessagesLiveData().observe(this, messages -> { if (messages == null || messages.size() == 0) { - locationSharingFab.hide(); + if (locationSharingGroup.getVisibility() == View.VISIBLE) { + setLocationSharingGroupVisibility(false, true); + } return; } + if (locationSharingGroup.getVisibility() == View.GONE) { + locationSharingGroup.setVisibility(View.VISIBLE); + setLocationSharingGroupVisibility(true, false); + } if (UnifiedForegroundService.LocationSharingSubService.isDiscussionSharingLocation(discussionViewModel.getDiscussionId())) { - locationSharingFab.setBackgroundTintList(ColorStateList.valueOf(ResourcesCompat.getColor(getResources(), R.color.red, null))); + locationSharingGroup.setBackgroundResource(R.drawable.background_rounded_dialog_red_outline); + if (messages.size() == 1) { + locationSharingTextView.setText(R.string.label_sharing_your_location); + } else { + locationSharingTextView.setText(getResources().getQuantityString(R.plurals.label_you_and_xxx_contacts_sharing_their_location, messages.size() - 1, messages.size() - 1)); + } + locationSharingTextView.requestLayout(); } else { - locationSharingFab.setBackgroundTintList(ColorStateList.valueOf(ResourcesCompat.getColor(getResources(), R.color.green, null))); + locationSharingGroup.setBackgroundResource(R.drawable.background_rounded_dialog); + locationSharingTextView.setText(getResources().getQuantityString(R.plurals.label_xxx_contacts_sharing_their_position, messages.size(), messages.size())); + locationSharingTextView.requestLayout(); } - locationSharingFab.show(); }); // when a cached name or photo changes --> reload the messages @@ -909,15 +933,16 @@ private void enterEditMode(@NonNull Message message) { // prevent editing location messages return; } - - // keep values and save draft after edit mode is on - Message previousDraft = composeMessageViewModel.getDraftMessage().getValue(); - CharSequence rawText = composeMessageViewModel.getRawNewMessageText(); - Pair> trimAndMentions = Utils.removeProtectionFEFFsAndTrim(rawText == null ? "" : rawText, mentionViewModel.getMentions()); - App.runThread(new SaveDraftTask(discussionViewModel.getDiscussionId(), trimAndMentions.first, previousDraft, trimAndMentions.second)); - composeMessageViewModel.setDraftMessageEdit(message); - if (composeMessageDelegate != null) { - composeMessageDelegate.showSoftInputKeyboard(); + if (discussionViewModel.getDiscussionId() != null) { + // keep values and save draft after edit mode is on + Message previousDraft = composeMessageViewModel.getDraftMessage().getValue(); + CharSequence rawText = composeMessageViewModel.getRawNewMessageText(); + Pair> trimAndMentions = Utils.removeProtectionFEFFsAndTrim(rawText == null ? "" : rawText, mentionViewModel.getMentions()); + App.runThread(new SaveDraftTask(discussionViewModel.getDiscussionId(), trimAndMentions.first, previousDraft, trimAndMentions.second)); + composeMessageViewModel.setDraftMessageEdit(message); + if (composeMessageDelegate != null) { + composeMessageDelegate.showSoftInputKeyboard(); + } } } @@ -1553,9 +1578,9 @@ public void onClick(View view) { } } } - } else if (id == R.id.discussion_location_sharing_fab) { - if (discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue() == null - || discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue().size() == 0) { + } else if (id == R.id.discussion_location_sharing_group) { + List sharingMessages = discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue(); + if (sharingMessages == null || sharingMessages.size() == 0) { return; } @@ -1564,7 +1589,7 @@ public void onClick(View view) { case OSM: case CUSTOM_OSM: case MAPS: { - new FullscreenMapDialogFragment(null, discussionViewModel.getDiscussionId(), SettingsActivity.getLocationIntegration()) + FullscreenMapDialogFragment.newInstance(null, discussionViewModel.getDiscussionId(), SettingsActivity.getLocationIntegration()) .show(getSupportFragmentManager(), FULL_SCREEN_MAP_FRAGMENT_TAG); break; } @@ -1572,48 +1597,88 @@ public void onClick(View view) { case BASIC: case NONE: default: { - // if only one sharing message go to it, do not show menu - if (discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue().size() == 1) { - messageListAdapter.requestScrollToMessageId(discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue().get(0).id, true, true); - return; - } - - PopupMenu locationMessagePopUp = new PopupMenu(DiscussionActivity.this, view); - List messagesShownInMenu = new ArrayList<>(discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue().size()); + locationGoToMessageOrShowPopup(sharingMessages, view); + } + } + } else if (id == R.id.discussion_location_sharing_menu_dots) { + List sharingMessages = discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue(); + if (sharingMessages == null || sharingMessages.size() == 0) { + return; + } - int index = 0; - // handle current identity first - for (Message message : discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue()) { - if (message.messageType == Message.TYPE_OUTBOUND_MESSAGE) { - locationMessagePopUp.getMenu().add(0, messagesShownInMenu.size(), index, "You"); - messagesShownInMenu.add(message); - break; - } + PopupMenu popup = new PopupMenu(this, view); + Menu menu = popup.getMenu(); + menu.add(0, 1, 1, R.string.menu_action_go_to_message); + if (UnifiedForegroundService.LocationSharingSubService.isDiscussionSharingLocation(discussionViewModel.getDiscussionId())) { + menu.add(0, 2, 2, R.string.menu_action_location_message_stop_sharing); + } + SettingsActivity.LocationIntegrationEnum locationIntegration = SettingsActivity.getLocationIntegration(); + if (locationIntegration == SettingsActivity.LocationIntegrationEnum.OSM || locationIntegration == SettingsActivity.LocationIntegrationEnum.CUSTOM_OSM || locationIntegration == SettingsActivity.LocationIntegrationEnum.MAPS) { + menu.add(0, 3, 3, R.string.menu_action_open_map); + } + popup.setOnMenuItemClickListener(item -> { + switch (item.getItemId()) { + case 1: { + locationGoToMessageOrShowPopup(sharingMessages, view); + break; } - - for (Message message : discussionViewModel.getCurrentlySharingLocationMessagesLiveData().getValue()) { - if (message.messageType != Message.TYPE_OUTBOUND_MESSAGE) { - String displayName = AppSingleton.getContactCustomDisplayName(message.senderIdentifier); - locationMessagePopUp.getMenu().add(0, messagesShownInMenu.size(), index, displayName); - messagesShownInMenu.add(message); - } + case 2: { + UnifiedForegroundService.LocationSharingSubService.stopSharingInDiscussion(discussionViewModel.getDiscussionId(), false); + break; } - - locationMessagePopUp.setOnMenuItemClickListener((item) -> { - if (item.getItemId() < 0 || item.getItemId() >= messagesShownInMenu.size()) { - return false; - } - Message message = messagesShownInMenu.get(item.getItemId()); - messageListAdapter.requestScrollToMessageId(message.id, true, true); - return true; - }); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - locationMessagePopUp.setGravity(Gravity.TOP | Gravity.END); + case 3: { + FullscreenMapDialogFragment.newInstance(null, discussionViewModel.getDiscussionId(), locationIntegration) + .show(getSupportFragmentManager(), FULL_SCREEN_MAP_FRAGMENT_TAG); + break; } - locationMessagePopUp.show(); } + return true; + }); + popup.show(); + } + } + + private void locationGoToMessageOrShowPopup(@NonNull List sharingMessages, View view) { + // if only one sharing message go to it, do not show menu + if (sharingMessages.size() == 1) { + messageListAdapter.requestScrollToMessageId(sharingMessages.get(0).id, true, true); + return; + } + + // otherwise, build a popup menu + PopupMenu locationMessagePopUp = new PopupMenu(this, view); + List messagesShownInMenu = new ArrayList<>(sharingMessages.size()); + + int index = 0; + // show owned identity first + for (Message message : sharingMessages) { + if (message.messageType == Message.TYPE_OUTBOUND_MESSAGE) { + locationMessagePopUp.getMenu().add(0, messagesShownInMenu.size(), index, "You"); + messagesShownInMenu.add(message); + break; + } + } + + for (Message message : sharingMessages) { + if (message.messageType != Message.TYPE_OUTBOUND_MESSAGE) { + String displayName = AppSingleton.getContactCustomDisplayName(message.senderIdentifier); + locationMessagePopUp.getMenu().add(0, messagesShownInMenu.size(), index, displayName); + messagesShownInMenu.add(message); + } + } + + locationMessagePopUp.setOnMenuItemClickListener((item) -> { + if (item.getItemId() < 0 || item.getItemId() >= messagesShownInMenu.size()) { + return false; } + Message message = messagesShownInMenu.get(item.getItemId()); + messageListAdapter.requestScrollToMessageId(message.id, true, true); + return true; + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + locationMessagePopUp.setGravity(Gravity.TOP | Gravity.END); } + locationMessagePopUp.show(); } @@ -3063,8 +3128,8 @@ public void onBindViewHolder(@NonNull final MessageViewHolder holder, int positi holder.adapter.setHidden(expiration.getVisibilityDuration(), readOnce, true); holder.attachmentFyles.observe(DiscussionActivity.this, holder.adapter); } - } else if (message.messageType == Message.TYPE_INBOUND_MESSAGE - || message.messageType == Message.TYPE_OUTBOUND_MESSAGE) { + } else if ((message.messageType == Message.TYPE_INBOUND_MESSAGE + || message.messageType == Message.TYPE_OUTBOUND_MESSAGE)) { if (holder.standardHeaderView != null && holder.ephemeralHeaderView != null) { // true for inbound messages holder.standardHeaderView.setVisibility(View.VISIBLE); holder.ephemeralHeaderView.setVisibility(View.GONE); @@ -3138,7 +3203,9 @@ public void onBindViewHolder(@NonNull final MessageViewHolder holder, int positi } } - Utils.applyBodyWithSpans(holder.messageContentTextView, discussionViewModel.getDiscussion().getValue() == null ? null : discussionViewModel.getDiscussion().getValue().bytesOwnedIdentity, message, getHighlightPatternsForMessage(message), true, true); + if (!message.isLocationMessage()) { + Utils.applyBodyWithSpans(holder.messageContentTextView, discussionViewModel.getDiscussion().getValue() == null ? null : discussionViewModel.getDiscussion().getValue().bytesOwnedIdentity, message, getHighlightPatternsForMessage(message), true, true); + } if (message.hasAttachments()) { if (holder.attachmentFyles != null) { @@ -3500,11 +3567,12 @@ private void initLinkPreviewViewHolder(MessageViewHolder holder, long targetMess constraintSet.setMargin(R.id.message_link_preview_image, ConstraintLayout.LayoutParams.END, DipKt.toPx(4, DiscussionActivity.this)); constraintSet.constrainWidth(R.id.message_link_preview_image, ConstraintLayout.LayoutParams.MATCH_CONSTRAINT); constraintSet.constrainHeight(R.id.message_link_preview_image, ConstraintLayout.LayoutParams.MATCH_CONSTRAINT); - float ratio = openGraph.getBitmap().getWidth() / (float) openGraph.getBitmap().getHeight(); + //noinspection DataFlowIssue + float ratio = openGraph.getBitmap().getWidth() / (float) openGraph.getBitmap().getHeight(); if (ratio < 0.7) { ratio = 0.7f; } - constraintSet.setDimensionRatio(R.id.message_link_preview_image, "" + ratio); + constraintSet.setDimensionRatio(R.id.message_link_preview_image, Float.toString(ratio)); constraintSet.clear(R.id.message_link_preview_description, ConstraintSet.BOTTOM); holder.messageLinkPreviewTitle.setPadding(DipKt.toPx(8, DiscussionActivity.this), 0, DipKt.toPx(4, DiscussionActivity.this), 0); holder.messageLinkPreviewDescription.setPadding(DipKt.toPx(8, DiscussionActivity.this), 0, DipKt.toPx(4, DiscussionActivity.this), 0); @@ -3733,7 +3801,7 @@ public void onClick(View v) { case CUSTOM_OSM: case MAPS: { // if a map integration is configured: open fullscreen map (behaviour will change depending on message.locationType) - new FullscreenMapDialogFragment(message, discussionViewModel.getDiscussionId(), SettingsActivity.getLocationIntegration()) + FullscreenMapDialogFragment.newInstance(message, discussionViewModel.getDiscussionId(), SettingsActivity.getLocationIntegration()) .show(getSupportFragmentManager(), FULL_SCREEN_MAP_FRAGMENT_TAG); break; } @@ -4722,6 +4790,48 @@ public void onClick(View view) { } } + private void setLocationSharingGroupVisibility(boolean visible, boolean setViewGoneAfterAnimation) { + locationSharingGroup.clearAnimation(); + int offset = (int) (32 * getResources().getDisplayMetrics().density); // 32 dp in pixels + if (locationSharingGroup.getVisibility() == View.VISIBLE) { + AnimationSet set = new AnimationSet(true); + + Animation translate = new TranslateAnimation(0, 0, + visible ? -offset : 0, + visible ? 0 : -offset); + translate.setDuration(250); + translate.setFillAfter(true); + + Animation fade = new AlphaAnimation( + visible ? 0f : 1f, + visible ? 1f : 0f); + fade.setDuration(250); + fade.setFillAfter(true); + + set.addAnimation(translate); + set.addAnimation(fade); + set.setFillAfter(true); + + if (!visible && setViewGoneAfterAnimation) { + set.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + locationSharingGroup.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + } + locationSharingGroup.startAnimation(set); + } + } + public interface DiscussionDelegate { void markMessagesRead(); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionViewModel.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionViewModel.java index 5ebe94cc..5df42b92 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionViewModel.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/DiscussionViewModel.java @@ -45,6 +45,7 @@ import io.olvid.messenger.databases.entity.Invitation; import io.olvid.messenger.databases.entity.Message; import io.olvid.messenger.databases.entity.OwnedIdentity; +import io.olvid.messenger.settings.SettingsActivity; public class DiscussionViewModel extends ViewModel { @@ -97,7 +98,11 @@ public DiscussionViewModel() { if (discussionId == null) { return null; } - return db.messageDao().getDiscussionMessages(discussionId); + if (SettingsActivity.getHideGroupMemberChanges()) { + return db.messageDao().getDiscussionMessagesWithoutGroupMemberChanges(discussionId); + } else { + return db.messageDao().getDiscussionMessages(discussionId); + } }); discussionLiveData = Transformations.switchMap(discussionIdLiveData, (Long discussionId) -> { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/ForwardMessagesDialogFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/ForwardMessagesDialogFragment.java index 96338204..fdd95f66 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/ForwardMessagesDialogFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/ForwardMessagesDialogFragment.java @@ -58,6 +58,7 @@ import io.olvid.messenger.customClasses.InitialView; import io.olvid.messenger.databases.AppDatabase; import io.olvid.messenger.databases.dao.DiscussionDao; +import io.olvid.messenger.databases.entity.Discussion; import io.olvid.messenger.databases.entity.OwnedIdentity; import io.olvid.messenger.databases.tasks.ForwardMessagesTask; import io.olvid.messenger.fragments.FilteredDiscussionListFragment; @@ -169,7 +170,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c FilteredDiscussionListFragment filteredDiscussionListFragment = new FilteredDiscussionListFragment(); - LiveData> unfilteredDiscussions = Transformations.switchMap(viewModel.getForwardMessageOwnedIdentityLiveData(), new Function1>>() { + LiveData> unfilteredDiscussions = Transformations.switchMap(viewModel.getForwardMessageOwnedIdentityLiveData(), new Function1<>() { byte[] bytesOwnedIdentity = null; @Override @@ -192,6 +193,7 @@ public LiveData> invoke(Owned }); filteredDiscussionListFragment.setUseDialogBackground(true); + filteredDiscussionListFragment.setBottomPadding(8); filteredDiscussionListFragment.setShowPinned(true); filteredDiscussionListFragment.setUnfilteredDiscussions(unfilteredDiscussions); filteredDiscussionListFragment.setDiscussionFilterEditText(contactNameFilter); @@ -283,6 +285,17 @@ public void onClick(View view) { } dismiss(); App.runThread(new ForwardMessagesTask(viewModel.getMessageIdsToForward(), selectedDiscussionIds)); + + OwnedIdentity forwardOwnedIdentity = viewModel.getForwardMessageOwnedIdentityLiveData().getValue(); + Discussion originalDiscussion = viewModel.getDiscussion().getValue(); + if (selectedDiscussionIds.size() == 1 + && forwardOwnedIdentity != null + && originalDiscussion != null + && Arrays.equals(forwardOwnedIdentity.bytesOwnedIdentity, originalDiscussion.bytesOwnedIdentity) + && selectedDiscussionIds.get(0) != originalDiscussion.id) { + // we have forwarded a message in a single discussion, using the current owned identity + App.openDiscussionActivity(activity, selectedDiscussionIds.get(0)); + } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/compose/ComposeMessageFragment.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/compose/ComposeMessageFragment.kt index 5569f5d6..fcede033 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/compose/ComposeMessageFragment.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/compose/ComposeMessageFragment.kt @@ -1208,9 +1208,7 @@ class ComposeMessageFragment : Fragment(R.layout.fragment_discussion_compose), O } BASIC -> { - val dialogFragment = SendLocationBasicDialogFragment( - discussionViewModel.discussionId - ) + val dialogFragment = SendLocationBasicDialogFragment.newInstance(discussionViewModel.discussionId) dialogFragment.show(childFragmentManager, "send-location-fragment-basic") } @@ -1587,9 +1585,6 @@ class ComposeMessageFragment : Fragment(R.layout.fragment_discussion_compose), O } else { adapterIcons!!.add(-1) } - if (!SettingsActivity.getBetaFeaturesEnabled()) { - adapterIcons!!.remove(ICON_SEND_LOCATION) - } adapter.submitList(adapterIcons) val builder = Builder( iconOrderRecyclerView.context, R.style.CustomAlertDialog @@ -1687,9 +1682,6 @@ class ComposeMessageFragment : Fragment(R.layout.fragment_discussion_compose), O icons.remove(ICON_TAKE_PICTURE) icons.remove(ICON_TAKE_VIDEO) } - if (!SettingsActivity.getBetaFeaturesEnabled()) { - icons.remove(ICON_SEND_LOCATION) - } // Compose area layout // 4 + 36 + (36 x icon_count) + 4 || 6 + 24 + 2 + [ text ] + 32 || 4 = 112 + [ text ] + (36 x icon_count) @@ -1713,9 +1705,6 @@ class ComposeMessageFragment : Fragment(R.layout.fragment_discussion_compose), O } } } - if (!SettingsActivity.getBetaFeaturesEnabled()) { - iconsOverflow.remove(ICON_SEND_LOCATION) - } attachIconsGroup.removeAllViews() for (icon in iconsShown) { val imageView = ImageView(ContextThemeWrapper(activity, R.style.SubtleBlueRipple)) diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/FullscreenMapDialogFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/FullscreenMapDialogFragment.java index 3a90148a..adb28e84 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/FullscreenMapDialogFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/FullscreenMapDialogFragment.java @@ -33,6 +33,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.Window; +import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -50,6 +51,7 @@ import java.util.Set; import io.olvid.messenger.App; +import io.olvid.messenger.AppSingleton; import io.olvid.messenger.R; import io.olvid.messenger.customClasses.InitialView; import io.olvid.messenger.databases.AppDatabase; @@ -58,33 +60,51 @@ import io.olvid.messenger.settings.SettingsActivity; public class FullscreenMapDialogFragment extends AbstractLocationDialogFragment { - - private final Message message; - private final long discussionId; - private final SettingsActivity.LocationIntegrationEnum integration; + public static final String DISCUSSION_ID_KEY = "discussion_id"; + public static final String INTEGRATION_KEY = "integration"; + public static final String MESSAGE_LOCATION_TYPE_KEY = "message_location_type"; + public static final String MESSAGE_JSON_LOCATION_KEY = "message_json_location"; + public static final String MESSAGE_ID_KEY = "message_id"; + public static final String MESSAGE_SENDER_IDENTIFIER_KEY = "message_sender_identifier"; + public static final String MESSAGE_CONTENT_BODY_KEY = "message_content_body"; + +// private final Message message; + private Long messageId; + private byte[] messageSenderIdentifier; + private int messageLocationType; + private JsonLocation messageJsonLocation; + private String messageContentBody; + private long discussionId; + private SettingsActivity.LocationIntegrationEnum integration; private FragmentActivity activity; - private View rootView; - MapViewAbstractFragment mapView; - private FloatingActionButton centerOnMarkersFab; private FloatingActionButton openInThirdPartyAppFab; - private FloatingActionButton backFab; private LiveData> sharingLocationMessageLiveData; private final List currentlyShownMessagesIdList = new ArrayList<>(); // contains message id of all messages with a symbol currently shown on map // need to center on marker on first call of sharingLocationMessageLiveData observer private boolean centerOnMarkersOnNextLocationMessagesUpdate; - // show a sharing location map for a discussion or message - public FullscreenMapDialogFragment(Message message, long discussionId, SettingsActivity.LocationIntegrationEnum integration) { - this.message = message; - this.discussionId = discussionId; - this.integration = integration; + public static FullscreenMapDialogFragment newInstance(@Nullable Message message, long discussionId, SettingsActivity.LocationIntegrationEnum integration) { + FullscreenMapDialogFragment fragment = new FullscreenMapDialogFragment(); + Bundle args = new Bundle(); + if (message != null) { + args.putLong(MESSAGE_ID_KEY, message.id); + args.putByteArray(MESSAGE_SENDER_IDENTIFIER_KEY, message.senderIdentifier); + args.putInt(MESSAGE_LOCATION_TYPE_KEY, message.locationType); + args.putString(MESSAGE_JSON_LOCATION_KEY, message.jsonLocation); + args.putString(MESSAGE_CONTENT_BODY_KEY, message.contentBody); + } + args.putLong(DISCUSSION_ID_KEY, discussionId); + args.putInt(INTEGRATION_KEY, integration.ordinal()); + fragment.setArguments(args); + return fragment; } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -94,6 +114,24 @@ public void onCreate(@Nullable Bundle savedInstanceState) { // make fragment transparent setStyle(DialogFragment.STYLE_NO_TITLE, R.style.AppTheme_NoActionBar_Transparent); + + Bundle arguments = getArguments(); + if (arguments != null) { + if (arguments.containsKey(MESSAGE_ID_KEY)) { + messageId = arguments.getLong(MESSAGE_ID_KEY); + messageSenderIdentifier = arguments.getByteArray(MESSAGE_SENDER_IDENTIFIER_KEY); + messageLocationType = arguments.getInt(MESSAGE_LOCATION_TYPE_KEY); + String serializedJsonLocation = arguments.getString(MESSAGE_JSON_LOCATION_KEY); + try { + messageJsonLocation = AppSingleton.getJsonObjectMapper().readValue(serializedJsonLocation, JsonLocation.class); + } catch (Exception ignored) { } + messageContentBody = arguments.getString(MESSAGE_CONTENT_BODY_KEY); + } + discussionId = arguments.getLong(DISCUSSION_ID_KEY); + integration = SettingsActivity.LocationIntegrationEnum.values()[arguments.getInt(INTEGRATION_KEY)]; + } else { + dismiss(); + } } @Override @@ -112,7 +150,7 @@ public void onStart() { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - rootView = inflater.inflate(R.layout.fragment_fullscreen_map, container, false); + View rootView = inflater.inflate(R.layout.fragment_fullscreen_map, container, false); mapView = MapFragmentProvider.getMapFragmentForProvider(integration); if (mapView == null) { @@ -123,19 +161,22 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mapView.setOnMapReadyCallback(this::onMapReadyCallback); - centerOnMarkersFab = rootView.findViewById(R.id.fullscreen_map_center_on_markers_fab); - openInThirdPartyAppFab = rootView.findViewById(R.id.fullscreen_map_open_in_third_party_app_fab); - backFab = rootView.findViewById(R.id.fullscreen_map_back_fab); + ImageView layersButton = rootView.findViewById(R.id.fullscreen_map_layers_button); + mapView.setLayersButtonVisibilitySetter((Boolean visible) -> layersButton.setVisibility((visible != null && visible) ? View.VISIBLE : View.GONE)); + layersButton.setOnClickListener(mapView::onLayersButtonClicked); // setup fabs + FloatingActionButton centerOnMarkersFab = rootView.findViewById(R.id.fullscreen_map_center_on_markers_fab); centerOnMarkersFab.setImageDrawable(AppCompatResources.getDrawable(activity, R.drawable.ic_location_center_on_markers)); centerOnMarkersFab.setOnClickListener(this::handleCenterOnMarkersFabClick); centerOnMarkersFab.setVisibility(View.VISIBLE); + openInThirdPartyAppFab = rootView.findViewById(R.id.fullscreen_map_open_in_third_party_app_fab); openInThirdPartyAppFab.setImageDrawable(AppCompatResources.getDrawable(activity, R.drawable.ic_open_location_in_third_party_app_48)); openInThirdPartyAppFab.setOnClickListener(this::handleOpenInThirdPartyAppFabClick); openInThirdPartyAppFab.setVisibility(View.VISIBLE); + FloatingActionButton backFab = rootView.findViewById(R.id.fullscreen_map_back_fab); backFab.setOnClickListener(this::handleBackFabClick); return rootView; @@ -147,12 +188,13 @@ public void onMapReadyCallback() { mapView.setEnableCurrentLocation(isLocationPermissionGranted(this.activity) && isLocationEnabled()); // if showing a location or a finished sharing: zoom on location, center camera and add a pointer on it - if (message != null && message.locationType != Message.LOCATION_TYPE_SHARE) { - JsonLocation location = message.getJsonLocation(); - if (message.locationType == Message.LOCATION_TYPE_SEND) { - mapView.addMarker(message.id, getPinMarkerIcon(), new LatLngWrapper(location), location.getPrecision()); - } else if (message.locationType == Message.LOCATION_TYPE_SHARE_FINISHED) { - mapView.addMarker(message.id, getInitialViewMarkerIcon(message.senderIdentifier), new LatLngWrapper(location), location.getPrecision()); + if (messageId != null && messageLocationType != Message.LOCATION_TYPE_SHARE) { + if (messageJsonLocation != null) { + if (messageLocationType == Message.LOCATION_TYPE_SEND) { + mapView.addMarker(messageId, getPinMarkerIcon(), new LatLngWrapper(messageJsonLocation), messageJsonLocation.getPrecision()); + } else if (messageLocationType == Message.LOCATION_TYPE_SHARE_FINISHED) { + mapView.addMarker(messageId, getInitialViewMarkerIcon(messageSenderIdentifier), new LatLngWrapper(messageJsonLocation), messageJsonLocation.getPrecision()); + } } mapView.centerOnMarkers(false, false); } else { @@ -213,9 +255,8 @@ private void handleOpenInThirdPartyAppFabClick(View view) { // if live sharing, open bottom sheet, otherwise open third party app on fab click if (sharingLocationMessageLiveData != null && sharingLocationMessageLiveData.getValue() != null) { FullscreenMapBottomSheetDialog.newInstance(discussionId, this).show(activity.getSupportFragmentManager(), "fullscreen-map-bottom-sheet"); - } else if (message != null) { - JsonLocation jsonLocation = message.getJsonLocation(); - App.openLocationInMapApplication(activity, jsonLocation.getTruncatedLatitudeString(), jsonLocation.getTruncatedLongitudeString(), message.contentBody, null); + } else if (messageId != null) { + App.openLocationInMapApplication(activity, messageJsonLocation.getTruncatedLatitudeString(), messageJsonLocation.getTruncatedLongitudeString(), messageContentBody, null); } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapLibreCustomAttributionDialogManager.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapLibreCustomAttributionDialogManager.java index 7d2ccdfd..17b98f82 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapLibreCustomAttributionDialogManager.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapLibreCustomAttributionDialogManager.java @@ -22,7 +22,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.net.Uri; import android.widget.ArrayAdapter; import android.widget.Toast; @@ -41,14 +40,16 @@ public class MapLibreCustomAttributionDialogManager extends AttributionDialogMan private final String[] attributionsNames = new String[]{ "ยฉ OpenMapTiles", "ยฉ OpenStreetMap contributors", + "ยฉ ProtoMaps", "ยฉ MapLibre", "ยฉ Olvid" }; private final String[] attributionsUrls = new String[]{ "https://www.openmaptiles.org/", - "https://www.openstreetmap.org/copyright ", + "https://www.openstreetmap.org/copyright", + "https://github.com/protomaps/basemaps", "https://maplibre.org/", - "https://olvid.io" + "https://olvid.io/" }; public MapLibreCustomAttributionDialogManager(@NonNull Context context, @NonNull MapboxMap mapboxMap) { @@ -67,8 +68,7 @@ protected void showAttributionDialog(@NonNull String[] attributionTitles) { @Override public void onClick(DialogInterface dialog, int which) { try { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(attributionsUrls[which])); - context.startActivity(intent); + App.openLink(context, Uri.parse(attributionsUrls[which])); } catch (Exception e) { e.printStackTrace(); App.toast(R.string.toast_message_unable_to_open_url, Toast.LENGTH_SHORT); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewAbstractFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewAbstractFragment.java index b637d73a..7de2491f 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewAbstractFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewAbstractFragment.java @@ -20,6 +20,7 @@ package io.olvid.messenger.discussion.location; import android.graphics.Bitmap; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -50,10 +51,13 @@ public abstract class MapViewAbstractFragment extends Fragment { abstract double getCameraZoom(); abstract void launchMapSnapshot(@NonNull Consumer onSnapshotReadyCallback); + abstract void setLayersButtonVisibilitySetter(Consumer layersButtonVisibilitySetter); + abstract void onLayersButtonClicked(View view); + // customize map - abstract void setGestureEnabled(boolean enabled); - abstract void setOnMapClickListener(Runnable clickListener); - abstract void setOnMapLongClickListener(Runnable clickListener); +// abstract void setGestureEnabled(boolean enabled); +// abstract void setOnMapClickListener(Runnable clickListener); +// abstract void setOnMapLongClickListener(Runnable clickListener); // markers api (use first call in onMapReadyCallback) abstract void addMarker(long id, Bitmap icon, @NonNull LatLngWrapper latLngWrapper, @Nullable Float precision); @@ -99,14 +103,14 @@ protected final Pair computeBounds(List( - new LatLngWrapper((latSouth + latNorth) / 2, - (west + east) / 2), + new LatLngWrapper((latSouth + latNorth) / 2, (west + east) / 2), null); } else { return new Pair<>(new LatLngWrapper(latSouth, west), new LatLngWrapper(latNorth, east)); } } } + } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewMapLibreFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewMapLibreFragment.java index ba7ad92e..3dbe5d29 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewMapLibreFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/MapViewMapLibreFragment.java @@ -27,26 +27,40 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.location.Criteria; import android.location.Location; import android.location.LocationManager; import android.os.Build; import android.os.Bundle; import android.os.Looper; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.StyleSpan; +import android.util.ArrayMap; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.PopupMenu; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.core.location.LocationManagerCompat; import androidx.core.util.Consumer; import androidx.core.util.Pair; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.MutableLiveData; +import com.mapbox.geojson.MultiPolygon; +import com.mapbox.geojson.Point; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.camera.CameraUpdate; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; @@ -62,13 +76,24 @@ import com.mapbox.mapboxsdk.plugins.annotation.Symbol; import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager; import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions; +import com.mapbox.mapboxsdk.style.layers.FillLayer; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.LineLayer; +import com.mapbox.mapboxsdk.style.layers.PropertyValue; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; +import com.mapbox.mapboxsdk.style.sources.Source; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executor; import io.olvid.engine.Logger; +import io.olvid.engine.engine.types.JsonOsmStyle; import io.olvid.messenger.App; import io.olvid.messenger.AppSingleton; import io.olvid.messenger.R; @@ -77,12 +102,13 @@ public class MapViewMapLibreFragment extends MapViewAbstractFragment implements OnMapReadyCallback { - private static final String DEFAULT_OSM_SERVER_URL = "https://map.olvid.io"; - private static final String OSM_STYLE_NAME = "planet"; + private static final String FALLBACK_OSM_STYLE_URL = "https://map.olvid.io/styles/osm.json"; + public static final String OSM_STYLE_LANGUAGE_PLACEHOLDER = "[LANG]"; private static final double DEFAULT_ZOOM = 15; private static final int TRANSITION_DURATION_MS = 500; @Nullable private Runnable onMapReadyCallback = null; + @Nullable private Consumer layersButtonVisibilitySetter = null; private FragmentActivity activity; private SupportMapFragment mapFragment; @@ -91,6 +117,7 @@ public class MapViewMapLibreFragment extends MapViewAbstractFragment implements // symbols and markers private @Nullable SymbolManager symbolManager; private final HashMap symbolsByIdHashMap = new HashMap<>(); + private final HashMap> precisionCirclesHashMap = new HashMap<>(); // current camera center live data (set to null if camera is moving) private final MutableLiveData currentCameraCenterLiveData = new MutableLiveData<>(); @@ -100,7 +127,9 @@ public class MapViewMapLibreFragment extends MapViewAbstractFragment implements // store previously centered marker to unset Zindex private Symbol currentlyCenteredSymbol = null; - private String osmServerUrl = DEFAULT_OSM_SERVER_URL; + @NonNull + private Map osmServerStyles = Collections.emptyMap(); + private String currentStyleId = null; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -109,6 +138,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { // get current activity this.activity = requireActivity(); + loadStyleUrls(); + // init MapView Mapbox.getInstance(this.activity); @@ -116,11 +147,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { MapboxMapOptions options = MapboxMapOptions.createFromAttributes(this.activity); mapFragment = SupportMapFragment.newInstance(options); mapFragment.getMapAsync(this); - - String osmServer = AppSingleton.getEngine().getOsmServerUrl(AppSingleton.getBytesCurrentIdentity()); - if (osmServer != null) { - this.osmServerUrl = osmServer; - } } @Nullable @@ -137,21 +163,56 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c } - private String getStyleUrl() { + private void loadStyleUrls() { if (SettingsActivity.getLocationIntegration() == SettingsActivity.LocationIntegrationEnum.OSM) { - String language = SettingsActivity.getLocationOpenStreetMapLanguage(); - if (language != null) { - return String.format("%s/styles/%s-%s/style.json", osmServerUrl, OSM_STYLE_NAME, language); + List osmStyles = AppSingleton.getEngine().getOsmStyles(AppSingleton.getBytesCurrentIdentity()); + if (osmStyles == null || osmStyles.size() == 0) { + loadFallbackStyleUrl(); + } else { + currentStyleId = SettingsActivity.getLocationLastOsmStyleId(); + osmServerStyles = new LinkedHashMap<>(); + for (JsonOsmStyle osmStyle : osmStyles) { + if (osmStyle.id != null && osmStyle.url != null && osmStyle.name != null) { + osmServerStyles.put(osmStyle.id, osmStyle); + } + } } } else if (SettingsActivity.getLocationIntegration() == SettingsActivity.LocationIntegrationEnum.CUSTOM_OSM) { - return SettingsActivity.getLocationCustomOsmServerUrl(); + currentStyleId = "custom"; + osmServerStyles = Collections.singletonMap("custom", new JsonOsmStyle("custom", SettingsActivity.getLocationCustomOsmServerUrl())); + } else { + loadFallbackStyleUrl(); } - return getFallbackStyleUrl(); } - private String getFallbackStyleUrl() { + private void loadFallbackStyleUrl() { triedStyleFallbackUrl = true; - return String.format("%s/styles/%s/style.json", osmServerUrl, OSM_STYLE_NAME); + currentStyleId = "fallback"; + osmServerStyles = Collections.singletonMap("fallback", new JsonOsmStyle("fallback", FALLBACK_OSM_STYLE_URL)); + } + + @NonNull + private String getStyleUrl() { + if (currentStyleId != null) { + JsonOsmStyle osmStyle = osmServerStyles.get(currentStyleId); + if (osmStyle != null) { + return replaceLanguageInStyleUrl(osmStyle); + } + } + for (Map.Entry osmStyleEntry : osmServerStyles.entrySet()) { + currentStyleId = osmStyleEntry.getKey(); + return replaceLanguageInStyleUrl(osmStyleEntry.getValue()); + } + return ""; + } + + @NonNull + private String replaceLanguageInStyleUrl(@NonNull JsonOsmStyle osmStyle) { + String language = activity.getString(R.string.language_short_string); + if (osmStyle.name.containsKey(language)) { + return osmStyle.url.replace(OSM_STYLE_LANGUAGE_PLACEHOLDER, "_" + language); + } + return osmStyle.url.replace(OSM_STYLE_LANGUAGE_PLACEHOLDER, "_en"); } @Override @@ -164,12 +225,38 @@ public void onMapReady(@NonNull MapboxMap mapboxMap) { ((MapView) mapView).addOnDidFailLoadingMapListener((errorMessage) -> { Logger.w("OSM style not found, trying fallback style"); if (!triedStyleFallbackUrl) { - mapboxMap.setStyle(new Style.Builder().fromUri(getFallbackStyleUrl()), this::onStyleLoaded); + loadFallbackStyleUrl(); + mapboxMap.setStyle(new Style.Builder().fromUri(getStyleUrl()), this::onStyleLoaded); } }); } - mapboxMap.setStyle(new Style.Builder().fromUri(getStyleUrl()), this::onStyleLoaded); + // set style, with a callback for the loading of the first style + mapboxMap.setStyle(new Style.Builder().fromUri(getStyleUrl()), this::onFirstStyleLoaded); + } + + public void onFirstStyleLoaded(Style style) { + if (style == null || mapboxMap == null) { + Logger.i("MapLibre.onStyleLoaded: map not initialized or style is null"); + return; + } + + // first run the normal callback + onStyleLoaded(style); + + // then run thing that need to be run only once + // setup listeners for map gestures + mapboxMap.addOnCameraMoveStartedListener((reason) -> { + currentCameraCenterLiveData.postValue(null); + + if (reason == MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE) { + setCurrentlyCenteredSymbol(null); + } + }); + //noinspection DataFlowIssue + mapboxMap.addOnCameraIdleListener(() -> currentCameraCenterLiveData.postValue(new LatLngWrapper(mapboxMap.getCameraPosition().target))); + //noinspection DataFlowIssue + mapboxMap.addOnCameraMoveCancelListener(() -> currentCameraCenterLiveData.postValue(new LatLngWrapper(mapboxMap.getCameraPosition().target))); } public void onStyleLoaded(Style style) { @@ -180,23 +267,31 @@ public void onStyleLoaded(Style style) { // setup ui mapboxMap.getUiSettings().setCompassEnabled(true); - int twelveDp = (int) (12 * activity.getResources().getDisplayMetrics().density); - mapboxMap.getUiSettings().setCompassMargins(0, twelveDp * 2, twelveDp, 0); + Drawable compass = ContextCompat.getDrawable(activity, R.drawable.map_compass); + if (compass != null) { + mapboxMap.getUiSettings().setCompassImage(compass); + } + int sixteenDp = (int) (16 * activity.getResources().getDisplayMetrics().density); + mapboxMap.getUiSettings().setCompassMargins(0, (osmServerStyles.size() > 1) ? sixteenDp * 4 : sixteenDp, sixteenDp, 0); mapboxMap.getUiSettings().setCompassGravity(Gravity.TOP | Gravity.END); - mapboxMap.getUiSettings().setLogoEnabled(false); + if (layersButtonVisibilitySetter != null) { + layersButtonVisibilitySetter.accept(osmServerStyles.size() > 1); + } + // show and change attributions mapboxMap.getUiSettings().setAttributionEnabled(true); mapboxMap.getUiSettings().setAttributionGravity(Gravity.BOTTOM | Gravity.START); - mapboxMap.getUiSettings().setAttributionMargins(twelveDp, twelveDp, twelveDp, twelveDp); + mapboxMap.getUiSettings().setAttributionMargins(sixteenDp, sixteenDp, sixteenDp, sixteenDp); mapboxMap.getUiSettings().setAttributionDialogManager(new MapLibreCustomAttributionDialogManager(this.activity, mapboxMap)); + // create symbol manager and set options if (mapFragment.getView() != null) { symbolManager = new SymbolManager((MapView) mapFragment.getView(), mapboxMap, style); symbolManager.setIconAllowOverlap(true); - symbolManager.setIconIgnorePlacement(false); + symbolManager.setIconIgnorePlacement(true); symbolManager.addClickListener((symbol) -> { // center on all markers if clicking on same marker when we have multiple markers (to continue following marker if there is only one) @@ -207,21 +302,13 @@ public void onStyleLoaded(Style style) { } return true; }); - } - else { + } else { Logger.w("Symbol manager cannot be created !"); } - // setup listeners for map gestures - mapboxMap.addOnCameraMoveStartedListener((reason) -> { - currentCameraCenterLiveData.postValue(null); + // in case a circle was already added, recompute the precision circles layer + recomputePrecisionCirclesLayer(); - if (reason == MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE) { - setCurrentlyCenteredSymbol(null); - } - }); - mapboxMap.addOnCameraIdleListener(() -> currentCameraCenterLiveData.postValue(new LatLngWrapper(mapboxMap.getCameraPosition().target))); - mapboxMap.addOnCameraMoveCancelListener(() -> currentCameraCenterLiveData.postValue(new LatLngWrapper(mapboxMap.getCameraPosition().target))); // call parent callback if set if (onMapReadyCallback != null) { @@ -407,39 +494,86 @@ public void launchMapSnapshot(@NonNull Consumer onSnapshotReadyCallback) onSnapshotReadyCallback.accept(result); }); } - @Override - public void setGestureEnabled(boolean enabled) { - if (mapboxMap == null || mapboxMap.getStyle() == null) { - Logger.i("MapLibreMapView: setGestureEnabled: mapboxMap is not ready to use"); - return; - } - mapboxMap.getUiSettings().setAllGesturesEnabled(enabled); - } @Override - public void setOnMapClickListener(Runnable clickListener) { - if (mapboxMap == null || mapboxMap.getStyle() == null) { - Logger.i("MapLibreMapView: setOnMapClickListener: mapboxMap is not ready to use"); - return; + void setLayersButtonVisibilitySetter(Consumer layersButtonVisibilitySetter) { + this.layersButtonVisibilitySetter = layersButtonVisibilitySetter; + if (currentStyleId != null) { + layersButtonVisibilitySetter.accept(osmServerStyles.size() > 1); } - mapboxMap.addOnMapClickListener((latLng) -> { - clickListener.run(); - return false; - }); } @Override - public void setOnMapLongClickListener(Runnable clickListener) { - if (mapboxMap == null || mapboxMap.getStyle() == null) { - Logger.i("MapLibreMapView: setOnMapLongClickListener: mapboxMap is not ready to use"); + void onLayersButtonClicked(View view) { + if (osmServerStyles.size() <= 1) { return; } - mapboxMap.addOnMapLongClickListener((latLng) -> { - clickListener.run(); - return false; + ArrayList menuStyleIds = new ArrayList<>(osmServerStyles.size()); + PopupMenu popup = new PopupMenu(activity, view, Gravity.TOP | Gravity.END); + Menu menu = popup.getMenu(); + int index = 0; + String lang = getString(R.string.language_short_string); + for (JsonOsmStyle osmStyle : osmServerStyles.values()) { + menuStyleIds.add(osmStyle.id); + CharSequence text; + if (osmStyle.name.containsKey(lang)) { + text = osmStyle.name.get(lang); + } else { + text = osmStyle.name.get("en"); + } + MenuItem menuItem = menu.add(0, index, index, text); + if (Objects.equals(currentStyleId, osmStyle.id)) { + menuItem.setChecked(true); + } + index++; + } + menu.setGroupCheckable(0, true, true); + popup.setOnMenuItemClickListener(item -> { + if (!Objects.equals(currentStyleId, menuStyleIds.get(item.getItemId()))) { + currentStyleId = menuStyleIds.get(item.getItemId()); + SettingsActivity.setLocationLastOsmStyleId(currentStyleId); + if (mapboxMap != null) { + mapboxMap.setStyle(new Style.Builder().fromUri(getStyleUrl()), this::onStyleLoaded); + } + } + return true; }); + popup.show(); } + // @Override +// public void setGestureEnabled(boolean enabled) { +// if (mapboxMap == null || mapboxMap.getStyle() == null) { +// Logger.i("MapLibreMapView: setGestureEnabled: mapboxMap is not ready to use"); +// return; +// } +// mapboxMap.getUiSettings().setAllGesturesEnabled(enabled); +// } +// +// @Override +// public void setOnMapClickListener(Runnable clickListener) { +// if (mapboxMap == null || mapboxMap.getStyle() == null) { +// Logger.i("MapLibreMapView: setOnMapClickListener: mapboxMap is not ready to use"); +// return; +// } +// mapboxMap.addOnMapClickListener((latLng) -> { +// clickListener.run(); +// return false; +// }); +// } +// +// @Override +// public void setOnMapLongClickListener(Runnable clickListener) { +// if (mapboxMap == null || mapboxMap.getStyle() == null) { +// Logger.i("MapLibreMapView: setOnMapLongClickListener: mapboxMap is not ready to use"); +// return; +// } +// mapboxMap.addOnMapLongClickListener((latLng) -> { +// clickListener.run(); +// return false; +// }); +// } + @Override public void setOnMapReadyCallback(@Nullable Runnable onMapReadyCallback) { this.onMapReadyCallback = onMapReadyCallback; @@ -464,6 +598,13 @@ public void addMarker(long id, Bitmap icon, @NonNull LatLngWrapper latLngWrapper .withLatLng(latLngWrapper.toMapLibre()) .withSymbolSortKey(0F)); symbolsByIdHashMap.put(id, symbol); + + if (precision != null) { + precisionCirclesHashMap.put(id, computePrecisionCirclePolyline(latLngWrapper.toMapLibre(), precision)); + recomputePrecisionCirclesLayer(); + } else if (precisionCirclesHashMap.remove(id) != null) { + recomputePrecisionCirclesLayer(); + } } @Override @@ -481,6 +622,13 @@ public void updateMarker(long id, @NonNull LatLngWrapper latLngWrapper, @Nullabl centerOnMarker(id, true); } } + + if (precision != null) { + precisionCirclesHashMap.put(id, computePrecisionCirclePolyline(latLngWrapper.toMapLibre(), precision)); + recomputePrecisionCirclesLayer(); + } else if (precisionCirclesHashMap.remove(id) != null) { + recomputePrecisionCirclesLayer(); + } } @Override @@ -500,6 +648,9 @@ public void removeMarker(long id) { mapboxMap.getStyle().removeImage("marker-icon" + id); } } + if (precisionCirclesHashMap.remove(id) != null) { + recomputePrecisionCirclesLayer(); + } } @Override @@ -532,7 +683,7 @@ public void centerOnMarkers(boolean animate, boolean includeMyLocation) { mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(LatLngBounds.world(), 0), TRANSITION_DURATION_MS, cancelableCallback); } else if (bounds.second == null) { // else center on single symbol - double zoom = Math.max(DEFAULT_ZOOM, mapboxMap.getCameraPosition().zoom); + double zoom = markersPositions.size() == 1 ? Math.max(DEFAULT_ZOOM, mapboxMap.getCameraPosition().zoom) : DEFAULT_ZOOM; if (animate) { mapboxMap.getUiSettings().setAllGesturesEnabled(false); mapboxMap.easeCamera(CameraUpdateFactory.newLatLngZoom(bounds.first.toMapLibre(), zoom), TRANSITION_DURATION_MS, cancelableCallback); @@ -545,8 +696,7 @@ public void centerOnMarkers(boolean animate, boolean includeMyLocation) { if (animate) { mapboxMap.getUiSettings().setAllGesturesEnabled(false); mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(LatLngBounds.from(bounds.second.getLatitude(), bounds.second.getLongitude(), bounds.first.getLatitude(), bounds.first.getLongitude()), padding), TRANSITION_DURATION_MS, cancelableCallback); - } - else { + } else { mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(LatLngBounds.from(bounds.second.getLatitude(), bounds.second.getLongitude(), bounds.first.getLatitude(), bounds.first.getLongitude()), padding)); } } @@ -599,4 +749,72 @@ private void setCurrentlyCenteredSymbol(@Nullable Symbol symbol) { currentlyCenteredSymbol = symbol; } + + + private final static int CIRCLE_POLYLINE_STEPS = 90; + @NonNull + private List computePrecisionCirclePolyline(@NonNull LatLng center, float radiusMeters) { + // convert radius meters to radians + double radiusRadians = (double) radiusMeters / 6373000d; + // convert center coordinates to radians too + double centerLat = (center.getLatitude() % 360) * Math.PI / 180; + double centerLong = (center.getLongitude() % 360) * Math.PI / 180; + + List points = new ArrayList<>(); + for (int i = 0; i < CIRCLE_POLYLINE_STEPS; i++) { + double angle = i * 2 * Math.PI / CIRCLE_POLYLINE_STEPS; + double pointLat = Math.asin(Math.sin(centerLat) * Math.cos(radiusRadians) + Math.cos(centerLat) * Math.sin(radiusRadians) * Math.cos(angle)); + double pointLong = centerLong + Math.atan2( + Math.sin(angle) * Math.sin(radiusRadians) * Math.cos(centerLat), + Math.cos(radiusRadians) - Math.sin(centerLat) * Math.sin(pointLat) + ); + points.add(Point.fromLngLat( + pointLong * 180 / Math.PI, + pointLat * 180 / Math.PI + )); + } + + // re-add the first point to close the curve + points.add(points.get(0)); + return points; + } + + + private void recomputePrecisionCirclesLayer() { + if (mapboxMap != null && symbolManager != null) { + Style style = mapboxMap.getStyle(); + if (style != null) { + // remove any outdated source/layer + Layer circlesLayer = style.getLayer("circles-layer"); + if (circlesLayer != null) { + style.removeLayer(circlesLayer); + } + Layer circleOutlinesLayer = style.getLayer("circle-outlines-layer"); + if (circleOutlinesLayer != null) { + style.removeLayer(circleOutlinesLayer); + } + Source circlesSource = style.getSource("precision-circles"); + if (circlesSource != null) { + style.removeSource(circlesSource); + } + + List>> circles = new ArrayList<>(); + for (List circle : precisionCirclesHashMap.values()) { + circles.add(Collections.singletonList(circle)); + } + circlesSource = new GeoJsonSource("precision-circles", MultiPolygon.fromLngLats(circles)); + circlesLayer = new FillLayer("circles-layer", "precision-circles").withProperties( + new PropertyValue("fill-color", "#0099ff"), + new PropertyValue("fill-opacity", 0.094f) + ); + circleOutlinesLayer = new LineLayer("circle-outlines-layer", "precision-circles").withProperties( + new PropertyValue("line-color", "#0099ff"), + new PropertyValue("line-opacity", 0.266f) + ); + style.addSource(circlesSource); + style.addLayerBelow(circlesLayer, symbolManager.getLayerId()); + style.addLayerAbove(circleOutlinesLayer, "circles-layer"); + } + } + } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/RequestAndUpdateAddressFieldTask.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/RequestAndUpdateAddressFieldTask.java index 88801567..84c90c11 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/RequestAndUpdateAddressFieldTask.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/RequestAndUpdateAddressFieldTask.java @@ -19,6 +19,8 @@ package io.olvid.messenger.discussion.location; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,18 +39,21 @@ import io.olvid.engine.Logger; import io.olvid.messenger.AppSingleton; +import io.olvid.messenger.R; import io.olvid.messenger.settings.SettingsActivity; public class RequestAndUpdateAddressFieldTask implements Runnable { public static final double MIN_ZOOM_FOR_REQUESTS = 15; + private final Context context; private final double latitude; private final double longitude; private final AddressCallback addressCallback; private final static List featuresLayersPriority = new ArrayList<>(Arrays.asList("address", "street", "venue", "country", "macroregion", "region")); // venue: points of interest, businesses, things with walls private final String peliasServer; - RequestAndUpdateAddressFieldTask(@NonNull String peliasServer, @NonNull LatLngWrapper latLngWrapper, AddressCallback addressCallback) { + RequestAndUpdateAddressFieldTask(@NonNull Context context, @NonNull String peliasServer, @NonNull LatLngWrapper latLngWrapper, AddressCallback addressCallback) { + this.context = context; this.peliasServer = peliasServer; this.latitude = latLngWrapper.getLatitude(); this.longitude = latLngWrapper.getLongitude(); @@ -58,11 +63,7 @@ public class RequestAndUpdateAddressFieldTask implements Runnable { @Override public void run() { try { - String lang = SettingsActivity.getLocationOpenStreetMapLanguage(); - URL requestUrl = (lang != null) ? - new URL(String.format(Locale.ENGLISH, "%s/v1/reverse?point.lat=%f&point.lon=%f&lang=%s", peliasServer, latitude, longitude, lang)) - : - new URL(String.format(Locale.ENGLISH, "%s/v1/reverse?point.lat=%f&point.lon=%f", peliasServer, latitude, longitude)); + URL requestUrl = new URL(String.format(Locale.ENGLISH, "%s/v1/reverse?point.lat=%f&point.lon=%f&lang=%s", peliasServer, latitude, longitude, context.getString(R.string.language_short_string))); HttpURLConnection connection = (HttpURLConnection) requestUrl.openConnection(); if (connection instanceof HttpsURLConnection && AppSingleton.getSslSocketFactory() != null) { ((HttpsURLConnection) connection).setSSLSocketFactory(AppSingleton.getSslSocketFactory()); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationBasicDialogFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationBasicDialogFragment.java index e09ccf04..b954d47d 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationBasicDialogFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationBasicDialogFragment.java @@ -59,24 +59,27 @@ import io.olvid.messenger.App; import io.olvid.messenger.R; import io.olvid.messenger.customClasses.HandlerExecutor; +import io.olvid.messenger.customClasses.LocationShareQuality; import io.olvid.messenger.databases.entity.jsons.JsonLocation; import io.olvid.messenger.databases.tasks.PostLocationMessageInDiscussionTask; import io.olvid.messenger.settings.SettingsActivity; public class SendLocationBasicDialogFragment extends AbstractLocationDialogFragment implements View.OnClickListener { + public static final String DISCUSSION_ID_KEY = "discussion_id"; + public static final String CONTINUOUS_SHARING_KEY = "continuous_sharing"; private static final double GREEN_PRECISION_LIMIT = 20.0; private static final double ORANGE_PRECISION_LIMIT = 50.0; // used to manually request location permission and activation on first launch private boolean firstCallToOnStart = true; // map integrations use this fragment for sharing location, behaviour is a little bit different - private final boolean continuousLocationSharing; + private long discussionId; + private boolean continuousLocationSharing; private LocationManager locationManager; private FragmentActivity activity; - private final long discussionId; private Location currentLocation = null; private final LocationListenerCompat locationListenerCompat = this::onLocationUpdate; @@ -91,39 +94,46 @@ public class SendLocationBasicDialogFragment extends AbstractLocationDialogFragm @SuppressLint("UseSwitchCompatOrMaterialCode") private Switch shareLocationSwitch; private TextView shareLocationDurationTextView; - private Long shareLocationCurrentDuration = null; - private long shareLocationCurrentInterval; + private Long shareLocationCurrentDuration; + private LocationShareQuality shareLocationCurrentQuality; private ConstraintLayout shareLocationIntervalLayout; private TextView shareLocationIntervalTextView; private Button validateButton; - // os need an empty public constructor - public SendLocationBasicDialogFragment() { - this.discussionId = 0; - this.continuousLocationSharing = false; + public static SendLocationBasicDialogFragment newInstance(long discussionId) { + return newInstance(discussionId, false); } - public SendLocationBasicDialogFragment(long discussionId) { - super(); - this.discussionId = discussionId; - this.continuousLocationSharing = false; + public static SendLocationBasicDialogFragment newInstance(long discussionId, boolean continuousLocationSharing) { + SendLocationBasicDialogFragment fragment = new SendLocationBasicDialogFragment(); + Bundle args = new Bundle(); + args.putLong(DISCUSSION_ID_KEY, discussionId); + args.putBoolean(CONTINUOUS_SHARING_KEY, continuousLocationSharing); + fragment.setArguments(args); + return fragment; } - // used when sharing location with some map integration - public SendLocationBasicDialogFragment(long discussionId, boolean continuousLocationSharing) { - super(); - this.discussionId = discussionId; - this.continuousLocationSharing = continuousLocationSharing; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // get activity + this.activity = requireActivity(); + + Bundle arguments = getArguments(); + if (arguments != null) { + this.discussionId = arguments.getLong(DISCUSSION_ID_KEY); + this.continuousLocationSharing = arguments.getBoolean(CONTINUOUS_SHARING_KEY); + } else { + dismiss(); + } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - // get activity - this.activity = requireActivity(); - // inflate view rootView = inflater.inflate(R.layout.fragment_send_location_basic, container, false); @@ -145,8 +155,9 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c cancelButton.setOnClickListener(this); // share duration default value - shareLocationDurationTextView.setText(SettingsActivity.getLocationDefaultSharingDurationLongString(activity)); shareLocationCurrentDuration = SettingsActivity.getLocationDefaultSharingDurationValue(); + shareLocationDurationTextView.setText(SettingsActivity.getLocationDefaultSharingDurationLongString(activity)); + // share duration dropdown menu setup shareLocationDurationTextView.setOnClickListener((view) -> { ShareLocationPopupMenu shareLocationPopupMenu = ShareLocationPopupMenu.getDurationPopUpMenu(this.activity, view); @@ -164,17 +175,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c shareLocationPopupMenu.show(); }); - // share interval default value - shareLocationIntervalTextView.setText(SettingsActivity.getLocationDefaultSharingIntervalLongString(activity)); - shareLocationCurrentInterval = SettingsActivity.getLocationDefaultSharingIntervalValue(); + // share quality default value + shareLocationCurrentQuality = SettingsActivity.getLocationDefaultShareQuality(); + shareLocationIntervalTextView.setText(shareLocationCurrentQuality.getFullString(activity)); + // share duration dropdown menu setup shareLocationIntervalTextView.setOnClickListener((view) -> { - ShareLocationPopupMenu shareLocationPopupMenu = ShareLocationPopupMenu.getIntervalPopUpMenu(this.activity, view); + ShareLocationPopupMenu shareLocationPopupMenu = ShareLocationPopupMenu.getQualityPopUpMenu(this.activity, view); shareLocationPopupMenu.setOnMenuItemClickListener(item -> { // set text shareLocationIntervalTextView.setText(shareLocationPopupMenu.getItemLongString(item)); - // keep duration in memory - shareLocationCurrentInterval = shareLocationPopupMenu.getItemDuration(item); + // keep quality in memory + shareLocationCurrentQuality = shareLocationPopupMenu.getItemQuality(item); // enable sharing if it was not if (!shareLocationSwitch.isChecked()) { shareLocationSwitch.setChecked(true); @@ -385,7 +397,7 @@ private void startSharingLocation() { } // post first location message (will start location sharing service) - App.runThread(PostLocationMessageInDiscussionTask.startLocationSharingInDiscussionTask(currentLocation, discussionId, true, shareExpirationInMs, shareLocationCurrentInterval)); + App.runThread(PostLocationMessageInDiscussionTask.startLocationSharingInDiscussionTask(currentLocation, discussionId, true, shareExpirationInMs, shareLocationCurrentQuality)); } // dismiss if request for permission or location activation was canceled diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationMapDialogFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationMapDialogFragment.java index 26a7fe29..56db061e 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationMapDialogFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/SendLocationMapDialogFragment.java @@ -75,9 +75,6 @@ public class SendLocationMapDialogFragment extends AbstractLocationDialogFragmen private MapViewAbstractFragment mapView; - private FloatingActionButton currentLocationButtonFab; - private FloatingActionButton backButtonFab; - private TextView addressTextView; private TextView fetchingAddressTextView; private boolean showAddress = false; @@ -188,8 +185,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c loadingSpinnerLayout = rootView.findViewById(R.id.send_location_map_loading_spinner_layout); - currentLocationButtonFab = rootView.findViewById(R.id.send_location_maps_current_location_fab); - backButtonFab = rootView.findViewById(R.id.send_location_maps_back_fab); + FloatingActionButton currentLocationButtonFab = rootView.findViewById(R.id.send_location_maps_current_location_fab); + FloatingActionButton backButtonFab = rootView.findViewById(R.id.send_location_maps_back_fab); + ImageView layersButton = rootView.findViewById(R.id.send_location_layers_button); + mapView.setLayersButtonVisibilitySetter((Boolean visible) -> layersButton.setVisibility((visible != null && visible) ? View.VISIBLE : View.GONE)); + layersButton.setOnClickListener(mapView::onLayersButtonClicked); // setup button rootView.findViewById(R.id.button_send_location).setOnClickListener(this::handleSendLocationButtonClick); @@ -220,7 +220,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c currentAddressLiveData.postValue(" "); } else { currentAddressLiveData.postValue(null); - this.addressRequestTask = new RequestAndUpdateAddressFieldTask(peliasServer, latLngWrapper, (RequestAndUpdateAddressFieldTask requestTask, @Nullable String address) -> { + this.addressRequestTask = new RequestAndUpdateAddressFieldTask(activity, peliasServer, latLngWrapper, (RequestAndUpdateAddressFieldTask requestTask, @Nullable String address) -> { if (addressRequestTask == requestTask) { if (address == null) { currentAddressLiveData.postValue(""); @@ -359,7 +359,7 @@ public void handleSendLocationButtonClick(View v) { } public void handleShareLocationButtonClick(View v) { - SendLocationBasicDialogFragment fragment = new SendLocationBasicDialogFragment(discussionId, true); + SendLocationBasicDialogFragment fragment = SendLocationBasicDialogFragment.newInstance(discussionId, true); fragment.show(getChildFragmentManager(), "send-location-fragment-share-dialog"); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/ShareLocationPopupMenu.java b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/ShareLocationPopupMenu.java index a7d06459..1d38250e 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/ShareLocationPopupMenu.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/discussion/location/ShareLocationPopupMenu.java @@ -25,11 +25,13 @@ import android.widget.PopupMenu; import io.olvid.messenger.R; +import io.olvid.messenger.customClasses.LocationShareQuality; public class ShareLocationPopupMenu extends PopupMenu { private long[] durationArray; - private String[] shortStringArray; - private String[] longStringArray; + private LocationShareQuality[] qualityArray; + private CharSequence[] shortStringArray; + private CharSequence[] longStringArray; public static ShareLocationPopupMenu getDurationPopUpMenu(Context context, View anchor) { ShareLocationPopupMenu popupMenu = new ShareLocationPopupMenu(context, anchor); @@ -40,8 +42,8 @@ public static ShareLocationPopupMenu getDurationPopUpMenu(Context context, View for (int i = 0; i < stringDurationArray.length; i++) { popupMenu.durationArray[i] = Long.parseLong(stringDurationArray[i]); } - popupMenu.shortStringArray = context.getResources().getStringArray(R.array.share_location_duration_short_strings); - popupMenu.longStringArray = context.getResources().getStringArray(R.array.share_location_duration_long_strings); + popupMenu.shortStringArray = context.getResources().getTextArray(R.array.share_location_duration_short_strings); + popupMenu.longStringArray = context.getResources().getTextArray(R.array.share_location_duration_long_strings); for (int i = 0 ; i < popupMenu.durationArray.length; i++) { popupMenu.getMenu().add(0, i, i, popupMenu.shortStringArray[i]); @@ -49,19 +51,19 @@ public static ShareLocationPopupMenu getDurationPopUpMenu(Context context, View return popupMenu; } - public static ShareLocationPopupMenu getIntervalPopUpMenu(Context context, View anchor) { + public static ShareLocationPopupMenu getQualityPopUpMenu(Context context, View anchor) { ShareLocationPopupMenu popupMenu = new ShareLocationPopupMenu(context, anchor); // convert duration string array to int array - String[] stringDurationArray = context.getResources().getStringArray(R.array.share_location_interval_values); - popupMenu.durationArray = new long[stringDurationArray.length]; - for (int i = 0; i < stringDurationArray.length; i++) { - popupMenu.durationArray[i] = Long.parseLong(stringDurationArray[i]); + String[] stringQualityArray = context.getResources().getStringArray(R.array.share_location_quality_values); + popupMenu.qualityArray = new LocationShareQuality[stringQualityArray.length]; + for (int i = 0; i < stringQualityArray.length; i++) { + popupMenu.qualityArray[i] = LocationShareQuality.fromValue(Integer.parseInt(stringQualityArray[i])); } - popupMenu.shortStringArray = context.getResources().getStringArray(R.array.share_location_interval_short_strings); - popupMenu.longStringArray = context.getResources().getStringArray(R.array.share_location_interval_long_strings); + popupMenu.shortStringArray = context.getResources().getTextArray(R.array.share_location_quality_short_strings); + popupMenu.longStringArray = context.getResources().getTextArray(R.array.share_location_quality_long_strings); - for (int i = 0 ; i < popupMenu.durationArray.length; i++) { + for (int i = 0 ; i < popupMenu.qualityArray.length; i++) { popupMenu.getMenu().add(0, i, i, popupMenu.shortStringArray[i]); } return popupMenu; @@ -81,7 +83,14 @@ public Long getItemDuration(MenuItem item) { return durationArray[item.getItemId()]; } - public String getItemLongString(MenuItem item) { + public LocationShareQuality getItemQuality(MenuItem item) { + if (item.getItemId() < 0 || item.getItemId() >= qualityArray.length) { + return null; + } + return qualityArray[item.getItemId()]; + } + + public CharSequence getItemLongString(MenuItem item) { if (item.getItemId() < 0 || item.getItemId() >= longStringArray.length) { return null; } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/FilteredDiscussionListFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/FilteredDiscussionListFragment.java index 5bf41837..73c50c37 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/FilteredDiscussionListFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/FilteredDiscussionListFragment.java @@ -73,7 +73,7 @@ public class FilteredDiscussionListFragment extends Fragment implements TextWatc private Observer> selectedDiscussionIdsObserver; - private boolean removeBottomPadding = false; + private Integer bottomPaddingDp = null; private boolean useDialogBackground = false; private boolean showPinned = false; private boolean selectable = false; @@ -107,8 +107,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c filteredDiscussionListViewModel.getFilteredDiscussions().observe(getViewLifecycleOwner(), filteredDiscussionListAdapter); recyclerView.setAdapter(filteredDiscussionListAdapter); - if (removeBottomPadding) { - recyclerView.setPadding(0,0,0,0); + if (bottomPaddingDp != null) { + try { + float density = inflater.getContext().getResources().getConfiguration().densityDpi / 160f; + recyclerView.setPadding(0, 0, 0, (int) (bottomPaddingDp * density)); + } catch (Exception ignored) { } } if (emptyView != null) { recyclerView.setEmptyView(emptyView); @@ -196,8 +199,8 @@ public void setOnClickDelegate(FilteredDiscussionListOnClickDelegate onClickDele this.onClickDelegate = onClickDelegate; } - public void removeBottomPadding() { - removeBottomPadding = true; + public void setBottomPadding(int bottomPaddingDp) { + this.bottomPaddingDp = bottomPaddingDp; } public void setUseDialogBackground(boolean useDialogBackground) { diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/dialog/DiscussionSearchDialogFragment.java b/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/dialog/DiscussionSearchDialogFragment.java index 477ec62d..47a41ed3 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/dialog/DiscussionSearchDialogFragment.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/fragments/dialog/DiscussionSearchDialogFragment.java @@ -92,7 +92,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c cancelButton.setOnClickListener(v -> dismiss()); FilteredDiscussionListFragment filteredDiscussionListFragment = new FilteredDiscussionListFragment(); - filteredDiscussionListFragment.removeBottomPadding(); + filteredDiscussionListFragment.setBottomPadding(0); if (AppSingleton.getBytesCurrentIdentity() != null) { filteredDiscussionListFragment.setUnfilteredDiscussions(AppDatabase.getInstance().discussionDao().getAllWithGroupMembersNames(AppSingleton.getBytesCurrentIdentity())); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsActivity.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsActivity.kt index f65e5e97..de502072 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsActivity.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsActivity.kt @@ -351,9 +351,11 @@ class GroupV2DetailsActivity : LockableActivity(), EngineNotificationListener, O val bytesOwnedIdentity = groupDetailsViewModel.bytesOwnedIdentity val bytesGroupIdentifier = groupDetailsViewModel.bytesGroupIdentifier App.runThread { - val detailsAndPhotos = AppSingleton.getEngine() + val detailsAndPhotos: ObvGroupV2DetailsAndPhotos? = AppSingleton.getEngine() .getGroupV2DetailsAndPhotos(bytesOwnedIdentity, bytesGroupIdentifier) - runOnUiThread { displayEngineGroupCards(detailsAndPhotos) } + if (detailsAndPhotos != null) { + runOnUiThread { displayEngineGroupCards(detailsAndPhotos) } + } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt index 06b989c5..65ea56bd 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListItem.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -85,6 +86,7 @@ fun DiscussionListItem( locked: Boolean, mentioned: Boolean, pinned: Boolean, + locationsShared: Boolean, attachmentCount: Int, onClick: () -> Unit, isPreDiscussion :Boolean, @@ -218,6 +220,14 @@ fun DiscussionListItem( ) } + AnimatedVisibility(visible = locationsShared) { + Image( + modifier = Modifier.size(22.dp), + painter = painterResource(id = R.drawable.ic_attach_location), + contentDescription = "location", + colorFilter = ColorFilter.tint(colorResource(id = R.color.olvid_gradient_contrasted)) + ) + } AnimatedVisibility(visible = mentioned) { Image( modifier = Modifier.size(20.dp), @@ -367,6 +377,7 @@ private fun DiscussionListItemPreview() { mentioned = true, locked = false, pinned = true, + locationsShared = true, isPreDiscussion = false, attachmentCount = 3, onClick = {}, diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt index 73ee96fb..8d0e1fab 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/main/discussions/DiscussionListScreen.kt @@ -128,8 +128,9 @@ fun DiscussionListScreen( unreadCount = unreadCount, muted = discussionCustomization?.shouldMuteNotifications() == true, locked = discussion.isLocked && invitation == null, - mentioned = discussionAndLastMessage.unreadMention, + mentioned = unreadMention, pinned = discussion.pinned, + locationsShared = locationsShared, attachmentCount = if (message?.isLocationMessage == true) 0 else message?.totalAttachmentCount ?: 0, onClick = { onClick(discussion) }, isPreDiscussion = discussion.isPreDiscussion, diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/AndroidNotificationManager.java b/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/AndroidNotificationManager.java index 85537467..18216241 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/AndroidNotificationManager.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/AndroidNotificationManager.java @@ -1644,12 +1644,6 @@ public static void displayLocationErrorNotification(@NonNull LocationErrorType e builder.setContentIntent(contentPendingIntent); break; } - case UPDATE_TIMEOUT: { - builder.setSmallIcon(R.drawable.ic_location_current_location_disabled) - .setContentTitle(App.getContext().getString(R.string.text_notification_location_updates_unavailable_title)) - .setContentText(App.getContext().getString(R.string.text_notification_location_updates_unavailable_message)); - break; - } } notificationManager.notify(getLocationErrorNotificationId(errorType), builder.build()); }); @@ -1661,7 +1655,6 @@ public static void clearLocationNotification(@Nullable LocationErrorType errorTy if (errorType == null) { clearLocationNotification(LocationErrorType.LOCATION_DISABLED); clearLocationNotification(LocationErrorType.LOCATION_PERMISSION_DENIED); - clearLocationNotification(LocationErrorType.UPDATE_TIMEOUT); } else { notificationManager.cancel(getLocationErrorNotificationId(errorType)); } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/LocationErrorType.java b/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/LocationErrorType.java index 0cc93ebf..3cb91de9 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/LocationErrorType.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/notifications/LocationErrorType.java @@ -22,5 +22,4 @@ public enum LocationErrorType { LOCATION_DISABLED, LOCATION_PERMISSION_DENIED, - UPDATE_TIMEOUT, } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/services/PeriodicTasksScheduler.java b/obv_messenger/app/src/main/java/io/olvid/messenger/services/PeriodicTasksScheduler.java index 5b10bbce..82546cf9 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/services/PeriodicTasksScheduler.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/services/PeriodicTasksScheduler.java @@ -63,7 +63,7 @@ public class PeriodicTasksScheduler { public static final long PERIODIC_TASKS_DEBUG_LOG_MAX_SIZE = 1_000_000L; public static final String DOWNLOAD_MESSAGES_WORK_NAME = "download_messages"; - public static final int DOWNLOAD_MESSAGES_INTERVAL_IN_SECONDS = 900; // this is the minimum allowed by Android + public static final int DOWNLOAD_MESSAGES_INTERVAL_IN_SECONDS = 1_800; // 900 is the minimum allowed by Android --> we increase it a bit to reduce useless message polling public static final String VACUUM_DB_WORK_NAME = "vacuum_db"; public static final int VACUUM_DB_INTERVAL_IN_HOURS = 11; diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/services/UnifiedForegroundService.java b/obv_messenger/app/src/main/java/io/olvid/messenger/services/UnifiedForegroundService.java index c1aa98a2..b83faf7c 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/services/UnifiedForegroundService.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/services/UnifiedForegroundService.java @@ -74,6 +74,7 @@ import io.olvid.messenger.R; import io.olvid.messenger.customClasses.BytesKey; import io.olvid.messenger.customClasses.HandlerExecutor; +import io.olvid.messenger.customClasses.LocationShareQuality; import io.olvid.messenger.customClasses.LockScreenOrNotActivity; import io.olvid.messenger.customClasses.LockableActivity; import io.olvid.messenger.customClasses.PreviewUtils; @@ -334,6 +335,7 @@ public static boolean willUnifiedForegroundServiceStartForeground() { return showLockScreenNotification || showWebClientNotification || showPermanentNotification || showMessageSendingNotification || showLocationSharingNotification; } + @SuppressLint("ForegroundServiceType") private void stopOrRestartForegroundService() { boolean showWebClientNotification = webClientSubService != null && WebClientSubService.isRunning; boolean showLockScreenNotification = SettingsActivity.useApplicationLockScreen() && (!LockSubService.locked || SettingsActivity.keepLockServiceOpen()); @@ -997,7 +999,7 @@ public static class LocationSharingSubService { private static final String SYNC_SHARING_MESSAGES_ACTION = "sync"; // check share location message in db and expires them or restart sharing private static final String DISCUSSION_ID_INTENT_EXTRA = "discussion_id"; // long private static final String SHARING_EXPIRATION_INTENT_EXTRA = "sharing_duration"; // long (optional) - private static final String SHARING_INTERVAL_INTENT_EXTRA = "sharing_interval"; // long + private static final String SHARING_QUALITY_INTENT_EXTRA = "sharing_quality"; // int private static final String MESSAGE_ID_INTENT_EXTRA = "message_id"; // id of message to update with new location private static UnifiedForegroundService unifiedForegroundService = null; @@ -1015,8 +1017,8 @@ synchronized void onStartCommand(@NonNull String action, @NonNull Intent intent) long discussionId = intent.getLongExtra(DISCUSSION_ID_INTENT_EXTRA, -1); long messageId = intent.getLongExtra(MESSAGE_ID_INTENT_EXTRA, -1); long shareExpiration = intent.getLongExtra(SHARING_EXPIRATION_INTENT_EXTRA, -1); // optional (in ms) - long shareInterval = intent.getLongExtra(SHARING_INTERVAL_INTENT_EXTRA, -1); // (in ms) - if (discussionId != -1 && shareInterval != -1 && messageId != -1) { + int shareQuality = intent.getIntExtra(SHARING_QUALITY_INTENT_EXTRA, LocationShareQuality.QUALITY_PRECISE.value); // value of io.olvid.messenger.customClasses.LocationShareQuality + if (discussionId != -1 && messageId != -1) { App.runThread(() -> { //noinspection SynchronizeOnNonFinalField synchronized (subscriber) { @@ -1027,9 +1029,8 @@ synchronized void onStartCommand(@NonNull String action, @NonNull Intent intent) Discussion discussion = AppDatabase.getInstance().discussionDao().getById(discussionId); if (discussion != null) { // if expiration is not set use null value (endless sharing) - subscriber.startSharingInDiscussion(discussion.bytesOwnedIdentity, discussionId, shareExpiration == -1 ? null : shareExpiration, shareInterval, messageId); - } - else { + subscriber.startSharingInDiscussion(discussion.bytesOwnedIdentity, discussionId, shareExpiration == -1 ? null : shareExpiration, LocationShareQuality.fromValue(shareQuality), messageId); + } else { Logger.e("LocationSharingSubService: discussion not found in database !!"); } if (!isSharingLocation && subscriber.isCurrentlySharingLocation()) { @@ -1075,7 +1076,7 @@ synchronized void onStartCommand(@NonNull String action, @NonNull Intent intent) restartSharingIntent.putExtra(LocationSharingSubService.DISCUSSION_ID_INTENT_EXTRA, message.discussionId); restartSharingIntent.putExtra(LocationSharingSubService.MESSAGE_ID_INTENT_EXTRA, message.id); restartSharingIntent.putExtra(LocationSharingSubService.SHARING_EXPIRATION_INTENT_EXTRA, jsonLocation.getSharingExpiration()); - restartSharingIntent.putExtra(LocationSharingSubService.SHARING_INTERVAL_INTENT_EXTRA, jsonLocation.getSharingInterval()); + restartSharingIntent.putExtra(LocationSharingSubService.SHARING_QUALITY_INTENT_EXTRA, jsonLocation.getQuality()); onStartCommand(START_SHARING_ACTION, restartSharingIntent); } } @@ -1123,13 +1124,13 @@ public static Pair getSingleSharingOwnedIdentityAndDiscussionId() return null; } - public static void startSharingInDiscussion(long discussionId, @Nullable Long shareExpirationInMs, long shareIntervalInMs, long messageId) { + public static void startSharingInDiscussion(long discussionId, @Nullable Long shareExpirationInMs, LocationShareQuality quality, long messageId) { Intent startSharingPositionIntent = new Intent(App.getContext(), UnifiedForegroundService.class); startSharingPositionIntent.putExtra(SUB_SERVICE_INTENT_EXTRA, SUB_SERVICE_LOCATION_SHARING); startSharingPositionIntent.setAction(LocationSharingSubService.START_SHARING_ACTION); startSharingPositionIntent.putExtra(LocationSharingSubService.DISCUSSION_ID_INTENT_EXTRA, discussionId); startSharingPositionIntent.putExtra(LocationSharingSubService.SHARING_EXPIRATION_INTENT_EXTRA, shareExpirationInMs); - startSharingPositionIntent.putExtra(LocationSharingSubService.SHARING_INTERVAL_INTENT_EXTRA, shareIntervalInMs); + startSharingPositionIntent.putExtra(LocationSharingSubService.SHARING_QUALITY_INTENT_EXTRA, quality.value); startSharingPositionIntent.putExtra(LocationSharingSubService.MESSAGE_ID_INTENT_EXTRA, messageId); App.getContext().startService(startSharingPositionIntent); } @@ -1191,34 +1192,30 @@ public static void stopSharing() { // ---------- Core sharing engine: subscribe to location updates and handle updates of location // ---------- for every registered discussion private static class LocationUpdatesSubscriber implements LocationListenerCompat { + public static final long RETRY_DELAY_MS = 60_000; // after an error, wait this long before retying + private final HashMap holdersByDiscussionId = new HashMap<>(); // we consider that if user started sharing location we obviously used location manager before private final LocationManager locationManager = (LocationManager) App.getContext().getSystemService(Context.LOCATION_SERVICE); private final Executor executor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? App.getContext().getMainExecutor() : new HandlerExecutor(Looper.getMainLooper()); private final Handler timerHandler = new Handler(Looper.getMainLooper()); - private final Runnable locationUpdateTimedOutTask = this::locationUpdateTimedOut; private final LocationListenerCompat fakeLocationListenerForGps = (Location location) -> {}; - private Long currentSharingInterval; // return null if holdersByDiscussionId is empty - private @Nullable Long getShortestShareInterval() { - Long shortest = null; + private @Nullable LocationShareQuality getMostPreciseRequestedQuality() { + LocationShareQuality mostPrecise = null; for (Map.Entry entry : holdersByDiscussionId.entrySet()) { - if (shortest == null) { - shortest = entry.getValue().shareInterval; - } else { - shortest = Math.min(shortest, entry.getValue().shareInterval); + if (mostPrecise == null || entry.getValue().quality.value < mostPrecise.value) { + mostPrecise = entry.getValue().quality; } } - return shortest; + return mostPrecise; } @SuppressLint("MissingPermission") private void refreshLocationUpdatesSubscription() { synchronized (holdersByDiscussionId) { try { - timerHandler.removeCallbacks(this.locationUpdateTimedOutTask); - // if no more sharing discussion: remove location updates if (holdersByDiscussionId.isEmpty()) { Logger.d("No more discussion sharing location, removing updates"); @@ -1233,57 +1230,73 @@ private void refreshLocationUpdatesSubscription() { return; } - currentSharingInterval = getShortestShareInterval(); - if (currentSharingInterval == null) { + LocationShareQuality mostPreciseShareQuality = getMostPreciseRequestedQuality(); + if (mostPreciseShareQuality == null) { // this never happens, currentSharingInterval is null only if holdersByDiscussionId is empty return; } if (!isLocationPermissionGranted()) { AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.LOCATION_PERMISSION_DENIED); - timerHandler.postDelayed(this::refreshLocationUpdatesSubscription, currentSharingInterval); + timerHandler.postDelayed(this::refreshLocationUpdatesSubscription, RETRY_DELAY_MS); return; } else { AndroidNotificationManager.clearLocationNotification(LocationErrorType.LOCATION_PERMISSION_DENIED); } - // if location disabled or permission is denied: show notification and setup a timer if (!isLocationEnabled()) { AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.LOCATION_DISABLED); - timerHandler.postDelayed(this::refreshLocationUpdatesSubscription, currentSharingInterval); + timerHandler.postDelayed(this::refreshLocationUpdatesSubscription, RETRY_DELAY_MS); return; } else { AndroidNotificationManager.clearLocationNotification(LocationErrorType.LOCATION_DISABLED); } - LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(currentSharingInterval) - .setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY) - .build(); - String provider = locationManager.getBestProvider(new Criteria(), true); - if (provider == null) { - Logger.e("LocationSharingSubService: could not find a BestProvider"); - AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.UPDATE_TIMEOUT); - timerHandler.postDelayed(this::refreshLocationUpdatesSubscription, currentSharingInterval); - return; - } else { - AndroidNotificationManager.clearLocationNotification(LocationErrorType.UPDATE_TIMEOUT); + int locationQuality; + switch (mostPreciseShareQuality) { + case QUALITY_BALANCED: + locationQuality = LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY; + break; + case QUALITY_POWER_SAVE: + locationQuality = LocationRequestCompat.QUALITY_LOW_POWER; + break; + case QUALITY_PRECISE: + default: + locationQuality = LocationRequestCompat.QUALITY_HIGH_ACCURACY; + break; } - // remove previous updates + LocationRequestCompat locationRequest = new LocationRequestCompat.Builder(mostPreciseShareQuality.getMinUpdateFrequencyMs()) + .setQuality(locationQuality) + .setMaxUpdateDelayMillis(mostPreciseShareQuality.getDefaultUpdateFrequencyMs()) + .build(); + + + // unregister any previously registered provider LocationManagerCompat.removeUpdates(locationManager, this); LocationManagerCompat.removeUpdates(locationManager, fakeLocationListenerForGps); - // re-subscribe with new parameters - LocationManagerCompat.requestLocationUpdates(locationManager, provider, locationRequest, executor, this); - if (!Objects.equals(provider, LocationManager.GPS_PROVIDER) && locationManager.getProviders(true).contains(LocationManager.GPS_PROVIDER)) { - // if the provider is not the GPS, but the GPS is available, still enable gps callback (on Android 12 emulator, fused does not always trigger updates on its own) - LocationManagerCompat.requestLocationUpdates(locationManager, LocationManager.GPS_PROVIDER, locationRequest, executor, fakeLocationListenerForGps); - } - // start time out - timerHandler.postDelayed(this.locationUpdateTimedOutTask, 2 * currentSharingInterval); + List providers = locationManager.getProviders(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && providers.contains(LocationManager.FUSED_PROVIDER)) { + LocationManagerCompat.requestLocationUpdates(locationManager, LocationManager.FUSED_PROVIDER, locationRequest, executor, this); + if (providers.contains(LocationManager.GPS_PROVIDER)) { + // also enable gps callback (on Android 12 emulator, fused does not always trigger updates on its own) + LocationManagerCompat.requestLocationUpdates(locationManager, LocationManager.GPS_PROVIDER, locationRequest, executor, fakeLocationListenerForGps); + } + } else if (providers.contains(LocationManager.GPS_PROVIDER) || providers.contains(LocationManager.NETWORK_PROVIDER)) { + if (providers.contains(LocationManager.GPS_PROVIDER)) { + LocationManagerCompat.requestLocationUpdates(locationManager, LocationManager.GPS_PROVIDER, locationRequest, executor, this); + } + if (providers.contains(LocationManager.NETWORK_PROVIDER)) { + LocationManagerCompat.requestLocationUpdates(locationManager, LocationManager.NETWORK_PROVIDER, locationRequest, executor, this); + } + } else { + // device does not have a GPS enabled? display a random error message. This should never happen on real devices. + AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.LOCATION_PERMISSION_DENIED); + } } catch (Exception e) { - Logger.e("SharingLocationService: registerToLocationUpdates: unexpected exception !", e); + Logger.e("SharingLocationService: requestLocationUpdates() unexpected exception!", e); } } } @@ -1298,8 +1311,45 @@ synchronized public void onLocationChanged(@NonNull Location location) { LocationSharingSubService.stopSharingInDiscussion(holder.discussionId, false); continue; } - // currentSharingInterval is the min between all holder sharingIntervals --> only send update if the next update message is expected to be sent before our next location update - if (holder.nextUpdateTimestamp <= System.currentTimeMillis() + currentSharingInterval) { + + // We filter here to avoid too frequent updates if the position did not change much + boolean sendUpdate; + String provider = location.getProvider(); + if (provider == null) { + provider = ""; + } + switch (provider) { + case LocationManager.GPS_PROVIDER: { + sendUpdate = (( + holder.lastSharedLocation == null + || LocationManager.NETWORK_PROVIDER.equals(holder.lastSharedLocation.getProvider()) // always prefer gps updates to previously received network updates + || location.distanceTo(holder.lastSharedLocation) > holder.quality.getMinUpdateDistanceMeters()) + && (System.currentTimeMillis() - holder.lastUpdateTimestamp > .9 * holder.quality.getMinUpdateFrequencyMs())) + || (System.currentTimeMillis() - holder.lastUpdateTimestamp > holder.quality.getDefaultUpdateFrequencyMs()); + break; + } + case LocationManager.NETWORK_PROVIDER: { + // when receiving network updates, if the previous update was GPS, only send if we are almost sure no gps update was available since then + if (holder.lastSharedLocation != null && LocationManager.GPS_PROVIDER.equals(holder.lastSharedLocation.getProvider())) { + sendUpdate = System.currentTimeMillis() - holder.lastUpdateTimestamp > holder.quality.getDefaultUpdateFrequencyMs() + holder.quality.getMinUpdateFrequencyMs(); + } else { + // don't update too frequently between network updates to give the GPS a chance to send a better update + sendUpdate = (holder.lastSharedLocation == null || location.distanceTo(holder.lastSharedLocation) > holder.quality.getMinUpdateDistanceMeters()) + && (System.currentTimeMillis() - holder.lastUpdateTimestamp > .9 * holder.quality.getMinUpdateFrequencyMs()) + || (System.currentTimeMillis() - holder.lastUpdateTimestamp > holder.quality.getDefaultUpdateFrequencyMs()); + } + break; + } + case LocationManager.FUSED_PROVIDER: + default: { + sendUpdate = (holder.lastSharedLocation == null || location.distanceTo(holder.lastSharedLocation) > holder.quality.getMinUpdateDistanceMeters()) + && (System.currentTimeMillis() - holder.lastUpdateTimestamp > .9 * holder.quality.getMinUpdateFrequencyMs()) + || (System.currentTimeMillis() - holder.lastUpdateTimestamp > holder.quality.getDefaultUpdateFrequencyMs()); + break; + } + } + + if (sendUpdate) { holder.updateLocation(location); } } @@ -1307,10 +1357,6 @@ synchronized public void onLocationChanged(@NonNull Location location) { // clear all notifications AndroidNotificationManager.clearLocationNotification(null); - - // reinitialize time out - timerHandler.removeCallbacks(this.locationUpdateTimedOutTask); - timerHandler.postDelayed(this.locationUpdateTimedOutTask, 2 * currentSharingInterval); } catch (Exception e) { Logger.e("SharingLocationService: onLocationChanged: unexpected exception !", e); } @@ -1326,23 +1372,6 @@ public void onProviderDisabled(@NonNull String provider) { AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.LOCATION_DISABLED); } - // Timer tasks - synchronized private void locationUpdateTimedOut() { - AndroidNotificationManager.displayLocationErrorNotification(LocationErrorType.UPDATE_TIMEOUT); - - // check sharing has expired - synchronized (holdersByDiscussionId) { - for (Map.Entry entry : holdersByDiscussionId.entrySet()) { - if (entry.getValue().shareExpiration != null && entry.getValue().shareExpiration < System.currentTimeMillis()) { - endSharingInDiscussion(entry.getKey(), false); - } - } - } - - // refresh timeout - timerHandler.removeCallbacks(this.locationUpdateTimedOutTask); - timerHandler.postDelayed(this.locationUpdateTimedOutTask, currentSharingInterval); - } // PUBLIC API public boolean isSharingLocationInDiscussion(long discussionId) { @@ -1352,7 +1381,7 @@ public boolean isCurrentlySharingLocation() { return holdersByDiscussionId.size() != 0; } - synchronized void startSharingInDiscussion(byte[] bytesOwnedIdentity, long discussionId, @Nullable Long shareExpiration, long shareInterval, long messageId) { + synchronized void startSharingInDiscussion(byte[] bytesOwnedIdentity, long discussionId, @Nullable Long shareExpiration, LocationShareQuality quality, long messageId) { synchronized (holdersByDiscussionId) { // check if not already sharing DiscussionSharingHolder holder = holdersByDiscussionId.get(discussionId); @@ -1360,7 +1389,7 @@ synchronized void startSharingInDiscussion(byte[] bytesOwnedIdentity, long discu holder.endSharing(false); } - holder = new DiscussionSharingHolder(bytesOwnedIdentity, discussionId, shareExpiration, shareInterval, messageId); + holder = new DiscussionSharingHolder(bytesOwnedIdentity, discussionId, shareExpiration, quality, messageId); holdersByDiscussionId.put(discussionId, holder); refreshLocationUpdatesSubscription(); } @@ -1416,22 +1445,25 @@ private static class DiscussionSharingHolder { private final byte[] bytesOwnedIdentity; private final long discussionId; private final @Nullable Long shareExpiration; - private final long shareInterval; + private final LocationShareQuality quality; private final long messageId; - private long nextUpdateTimestamp; + private Location lastSharedLocation; + private long lastUpdateTimestamp; - DiscussionSharingHolder(byte[] bytesOwnedIdentity, long discussionId, @Nullable Long shareExpiration, long shareInterval, long messageId) { + DiscussionSharingHolder(byte[] bytesOwnedIdentity, long discussionId, @Nullable Long shareExpiration, LocationShareQuality quality, long messageId) { this.bytesOwnedIdentity = bytesOwnedIdentity; this.discussionId = discussionId; this.shareExpiration = shareExpiration; - this.shareInterval = shareInterval; + this.quality = quality; this.messageId = messageId; - this.nextUpdateTimestamp = 0; + this.lastSharedLocation = null; + this.lastUpdateTimestamp = 0; } void updateLocation(Location location) { + this.lastSharedLocation = location; + this.lastUpdateTimestamp = System.currentTimeMillis(); App.runThread(UpdateLocationMessageTask.createPostSharingLocationUpdateMessage(this.discussionId, this.messageId, location)); - this.nextUpdateTimestamp = System.currentTimeMillis() + this.shareInterval; } void endSharing(boolean synchronously) { @@ -1451,8 +1483,7 @@ void endSharing(boolean synchronously) { // need run task synchronously in some databases tasks if (synchronously) { task.run(); - } - else { + } else { App.runThread(task); } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/LocationPreferenceFragment.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/LocationPreferenceFragment.kt index a52a539e..6323252d 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/LocationPreferenceFragment.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/LocationPreferenceFragment.kt @@ -43,20 +43,16 @@ class LocationPreferenceFragment : PreferenceFragmentCompat() { val screen = preferenceScreen ?: return val mapIntegrationPreference = screen.findPreference(SettingsActivity.PREF_KEY_LOCATION_INTEGRATION) - val osmLanguagePreference = screen.findPreference(SettingsActivity.PREF_KEY_LOCATION_OSM_LANGUAGE) - if (mapIntegrationPreference != null && osmLanguagePreference != null) { + if (mapIntegrationPreference != null) { mapIntegrationPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { preference: Preference? -> LocationIntegrationSelectorDialog(mapIntegrationPreference.context, true, object : OnIntegrationSelectedListener { override fun onIntegrationSelected(integration: LocationIntegrationEnum, customOsmServerUrl: String?) { SettingsActivity.setLocationIntegration(integration.string, customOsmServerUrl) mapIntegrationPreference.value = integration.string - osmLanguagePreference.isVisible = integration == LocationIntegrationEnum.OSM } }).show() true } - - osmLanguagePreference.isVisible = SettingsActivity.getLocationIntegration() == LocationIntegrationEnum.OSM } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java index 8ae1f5f0..f9978f76 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/settings/SettingsActivity.java @@ -72,6 +72,7 @@ import io.olvid.messenger.BuildConfig; import io.olvid.messenger.R; import io.olvid.messenger.billing.BillingUtils; +import io.olvid.messenger.customClasses.LocationShareQuality; import io.olvid.messenger.customClasses.LockableActivity; import io.olvid.messenger.customClasses.SecureAlertDialogBuilder; import io.olvid.messenger.customClasses.StringUtils; @@ -234,6 +235,9 @@ public String getStringValue() { } } + static final String PREF_KEY_HIDE_GROUP_MEMBER_CHANGES = "pref_key_hide_group_member_changes"; + static final boolean PREF_KEY_HIDE_GROUP_MEMBER_CHANGES_DEFAULT = false; + static final String PREF_KEY_SHOW_TRUST_LEVELS = "pref_key_show_trust_levels"; static final boolean PREF_KEY_SHOW_TRUST_LEVELS_DEFAULT = false; @@ -453,15 +457,17 @@ public String getString() { static final String PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION = "pref_key_location_share_duration"; static final long PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION_DEFAULT = 3_600_000L; - static final String PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL = "pref_key_location_share_interval"; - static final long PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL_DEFAULT = 60_000L; +// static final String PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL = "pref_key_location_share_interval"; +// static final long PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL_DEFAULT = 60_000L; + + static final String PREF_KEY_LOCATION_DEFAULT_SHARE_QUALITY = "pref_key_location_default_share_quality"; + static final LocationShareQuality PREF_KEY_LOCATION_DEFAULT_SHARE_QUALITY_DEFAULT = LocationShareQuality.QUALITY_PRECISE; static final String PREF_KEY_LOCATION_HIDE_ERROR_NOTIFICATIONS = "pref_key_location_hide_error_notifications"; static final boolean PREF_KEY_LOCATION_HIDE_ERROR_NOTIFICATIONS_DEFAULT = false; - static final String PREF_KEY_LOCATION_OSM_LANGUAGE = "pref_key_location_osm_language"; - static final String PREF_KEY_LOCATION_OSM_LANGUAGE_DEFAULT = "default"; - static final String PREF_KEY_LOCATION_OSM_LANGUAGE_INTERNATIONAL = "intl"; + static final String PREF_KEY_LOCATION_LAST_OSM_STYLE_ID = "pref_key_location_last_osm_style_id"; + static final String PREF_KEY_LOCATION_LAST_GOOGLE_MAP_TYPE = "pref_key_location_last_google_map_type"; static final String PREF_KEY_LOCATION_DISABLE_ADDRESS_LOOKUP = "pref_key_location_disable_address_lookup"; static final boolean PREF_KEY_LOCATION_DISABLE_ADDRESS_LOOKUP_DEFAULT = false; @@ -502,10 +508,6 @@ protected void onCreate(final Bundle savedInstanceState) { Preference preference = headersPreferenceFragment.findPreference(prefKey); if (preference != null) { int position = preference.getOrder(); - // TODO: remove this once location is no longer in beta - if (!getBetaFeaturesEnabled() && position > 6) { - position--; - } RecyclerView recyclerView = headersPreferenceFragment.getListView(); if (recyclerView != null) { RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(position); @@ -567,6 +569,9 @@ public boolean onOptionsItemSelected(MenuItem item) { date = StringUtils.getLongNiceDateString(this, ObvFirebaseMessagingService.getLastPushNotificationTimestamp()); } sb.append(getString(R.string.dialog_message_about_last_push_notification, date)); + + sb.append("\n"); + sb.append(getString(R.string.dialog_message_about_deprioritized_push_notification, ObvFirebaseMessagingService.getDeprioritizedMessageCount(), ObvFirebaseMessagingService.getHighPriorityMessageCount())); } if (BuildConfig.USE_GOOGLE_LIBS) { @@ -635,15 +640,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { BillingUtils.loadSubscriptionSettingsHeader(activity, getPreferenceScreen()); } - { - if (!getBetaFeaturesEnabled()) { - Preference locationHeaderPreference = findPreference(PREF_HEADER_KEY_LOCATION); - if (locationHeaderPreference != null) { - getPreferenceScreen().removePreference(locationHeaderPreference); - } - } - } - { App.runThread(() -> { if (AppDatabase.getInstance().actionShortcutConfigurationDao().countAll() > 0) { @@ -1163,6 +1159,16 @@ public static void setAutoJoinGroups(@NonNull AutoJoinGroupsCategory autoJoinGro editor.apply(); } + public static boolean getHideGroupMemberChanges() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getBoolean(PREF_KEY_HIDE_GROUP_MEMBER_CHANGES, PREF_KEY_HIDE_GROUP_MEMBER_CHANGES_DEFAULT); + } + + public static void setHideGroupMemberChanges(boolean hide) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); + editor.putBoolean(PREF_KEY_HIDE_GROUP_MEMBER_CHANGES, hide); + editor.apply(); + } + public static boolean showTrustLevels() { return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getBoolean(PREF_KEY_SHOW_TRUST_LEVELS, PREF_KEY_SHOW_TRUST_LEVELS_DEFAULT); } @@ -1381,6 +1387,7 @@ public static void setDisablePushNotifications(boolean disablePushNotifications) editor.apply(); } + public static final int PIN_SALT_LENGTH = 8; public static void savePIN(String PIN, boolean pinIsAPassword) { @@ -1987,26 +1994,6 @@ public static LocationIntegrationEnum getLocationIntegration() { } } - // return language set by user ot system default language - @Nullable - public static String getLocationOpenStreetMapLanguage() { - if (getLocationIntegration() != LocationIntegrationEnum.OSM) { - // do not use the custom language for non-osm integrations - return Locale.getDefault().getLanguage(); - } - String language = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_OSM_LANGUAGE, PREF_KEY_LOCATION_OSM_LANGUAGE_DEFAULT); - switch (language) { - case PREF_KEY_LOCATION_OSM_LANGUAGE_DEFAULT: { - return Locale.getDefault().getLanguage(); - } - case PREF_KEY_LOCATION_OSM_LANGUAGE_INTERNATIONAL: { - return null; - } - default: { - return language; - } - } - } public static void setLocationIntegration(@Nullable String integrationString, @Nullable String customOsmServerUrl) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); @@ -2037,41 +2024,42 @@ public static void setLocationIntegration(@Nullable String integrationString, @N } @Nullable - public static String getLocationCustomOsmServerUrl() { - return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_CUSTOM_OSM_SERVER, null); + public static String getLocationLastOsmStyleId() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_LAST_OSM_STYLE_ID, null); } - public static String getLocationOpenStreetMapRawLanguage() { - return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_OSM_LANGUAGE, PREF_KEY_LOCATION_OSM_LANGUAGE_DEFAULT); + public static void setLocationLastOsmStyleId(@NonNull String id) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); + editor.putString(PREF_KEY_LOCATION_LAST_OSM_STYLE_ID, id); + editor.apply(); + } + + public static int getLocationLastGoogleMapType() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getInt(PREF_KEY_LOCATION_LAST_GOOGLE_MAP_TYPE, 1); // 1 is GoogleMap.TYPE_NORMAL } - public static void setLocationOpenStreetMapRawLanguage(String language) { + public static void setLocationLastGoogleMapType(int mapType) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); - editor.putString(PREF_KEY_LOCATION_OSM_LANGUAGE, language); + editor.putInt(PREF_KEY_LOCATION_LAST_GOOGLE_MAP_TYPE, mapType); editor.apply(); } + @Nullable + public static String getLocationCustomOsmServerUrl() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_CUSTOM_OSM_SERVER, null); + } public static Long getLocationDefaultSharingDurationValue() { String durationString = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION, null); - - if (durationString == null) { - return PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION_DEFAULT; - } - try { - return Long.parseLong(durationString); - } catch (NumberFormatException e) { - return PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION_DEFAULT; + if (durationString != null) { + try { + return Long.parseLong(durationString); + } catch (NumberFormatException ignored) { } } + return PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION_DEFAULT; } - public static void setLocationDefaultSharingDurationValue(long duration) { - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); - editor.putString(PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION, Long.toString(duration)); - editor.apply(); - } - - public static String getLocationDefaultSharingDurationLongString(Context context) { + public static CharSequence getLocationDefaultSharingDurationLongString(Context context) { String duration = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION, null); // default value not set @@ -2080,7 +2068,7 @@ public static String getLocationDefaultSharingDurationLongString(Context context } String[] valuesArray = context.getResources().getStringArray(R.array.share_location_duration_values); - String[] longStringArray = context.getResources().getStringArray(R.array.share_location_duration_long_strings); + CharSequence[] longStringArray = context.getResources().getTextArray(R.array.share_location_duration_long_strings); int index = Arrays.asList(valuesArray).indexOf(duration); if (index >= 0 && index < longStringArray.length) { @@ -2091,50 +2079,31 @@ public static String getLocationDefaultSharingDurationLongString(Context context if (longStringArray.length == 0) { return ""; } - return longStringArray[0]; + return longStringArray[0]; } - public static long getLocationDefaultSharingIntervalValue() { - String interval = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL, null); + public static void setLocationDefaultSharingDurationValue(long duration) { + SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); + editor.putString(PREF_KEY_LOCATION_DEFAULT_SHARE_DURATION, Long.toString(duration)); + editor.apply(); + } - if (interval == null) { - return PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL_DEFAULT; - } - try { - return Long.parseLong(interval); - } catch (NumberFormatException e) { - return PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL_DEFAULT; + public static LocationShareQuality getLocationDefaultShareQuality() { + String qualityString = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_DEFAULT_SHARE_QUALITY, null); + if (qualityString != null) { + try { + return LocationShareQuality.fromValue(Integer.parseInt(qualityString)); + } catch (Exception ignored) { } } + return PREF_KEY_LOCATION_DEFAULT_SHARE_QUALITY_DEFAULT; } - public static void setLocationDefaultSharingIntervalValue(long interval) { + public static void setLocationDefaultShareQuality(LocationShareQuality quality) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit(); - editor.putString(PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL, Long.toString(interval)); + editor.putString(PREF_KEY_LOCATION_DEFAULT_SHARE_QUALITY, Integer.toString(quality.value)); editor.apply(); } - public static String getLocationDefaultSharingIntervalLongString(Context context) { - String duration = PreferenceManager.getDefaultSharedPreferences(App.getContext()).getString(PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL, null); - - // default value not set - if (duration == null) { - duration = String.valueOf(PREF_KEY_LOCATION_DEFAULT_SHARE_INTERVAL_DEFAULT); - } - - String[] valuesArray = context.getResources().getStringArray(R.array.share_location_interval_values); - String[] longStringArray = context.getResources().getStringArray(R.array.share_location_interval_long_strings); - - int index = Arrays.asList(valuesArray).indexOf(duration); - if (index >= 0 && index < longStringArray.length) { - return longStringArray[index]; - } - - // fallback mechanism - if (longStringArray.length == 0) { - return ""; - } - return longStringArray[0]; - } public static boolean getLocationDisableAddressLookup() { return PreferenceManager.getDefaultSharedPreferences(App.getContext()).getBoolean(PREF_KEY_LOCATION_DISABLE_ADDRESS_LOOKUP, PREF_KEY_LOCATION_DISABLE_ADDRESS_LOOKUP_DEFAULT); diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt index 90d9b1a4..defbd0a0 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingActivity.kt @@ -77,6 +77,7 @@ import io.olvid.messenger.settings.SettingsActivity import kotlinx.coroutines.delay import kotlinx.coroutines.launch + @OptIn(ExperimentalPermissionsApi::class) class TroubleshootingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -132,6 +133,8 @@ class TroubleshootingActivity : ComponentActivity() { val list : ArrayList> = ArrayList() // triple is (valid, critical, TroubleshootItemType) if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { list.add(Triple(postNotificationsState.status.isGranted, true, TroubleshootingItemType.NOTIFICATIONS)) + } else { + list.add(Triple(true, false, TroubleshootingItemType.NOTIFICATIONS)) } if (VERSION.SDK_INT >= VERSION_CODES.M) { @@ -239,16 +242,17 @@ class TroubleshootingActivity : ComponentActivity() { ObvFirebaseMessagingService.getLastPushNotificationTimestamp() ) } - ) + ) + "\n" + stringResource(id = string.dialog_message_about_deprioritized_push_notification, ObvFirebaseMessagingService.getDeprioritizedMessageCount(), ObvFirebaseMessagingService.getHighPriorityMessageCount()) } else "", titleInvalid = stringResource(id = string.troubleshooting_notifications_invalid_title), descriptionInvalid = stringResource(id = string.troubleshooting_notifications_invalid_description), valid = postNotificationsState.status.isGranted ) { TextButton( - onClick = - { - notificationsPermissionLauncher.launch(permission.POST_NOTIFICATIONS) + onClick = { + if (VERSION.SDK_INT >= VERSION_CODES.M) { + notificationsPermissionLauncher.launch(permission.POST_NOTIFICATIONS) + } } ) { Text(text = stringResource(id = string.troubleshooting_request_permission)) @@ -512,6 +516,8 @@ class TroubleshootingActivity : ComponentActivity() { } AppVersionHeader(betaEnabled = SettingsActivity.getBetaFeaturesEnabled()) + + RestartAppButton() } } } diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt index 6a41a6b5..f0c37766 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/troubleshooting/TroubleshootingComponents.kt @@ -19,6 +19,8 @@ package io.olvid.messenger.troubleshooting +import android.content.DialogInterface +import android.content.Intent import android.content.res.Configuration import android.os.Build import android.os.Build.VERSION @@ -46,8 +48,10 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.ripple.rememberRipple @@ -86,6 +90,7 @@ import io.olvid.messenger.R import io.olvid.messenger.R.color import io.olvid.messenger.R.drawable import io.olvid.messenger.R.string +import io.olvid.messenger.customClasses.SecureAlertDialogBuilder import io.olvid.messenger.customClasses.formatMarkdown import io.olvid.messenger.main.Utils import kotlinx.coroutines.delay @@ -109,7 +114,7 @@ fun AppVersionHeader(betaEnabled: Boolean) { modifier = Modifier .widthIn(max = 400.dp) .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp), + .padding(start = 16.dp, end = 16.dp), text = AnnotatedString(stringResource( string.troubleshooting_header, BuildConfig.VERSION_NAME + if (betaEnabled) " beta" else "", @@ -124,6 +129,41 @@ fun AppVersionHeader(betaEnabled: Boolean) { } } +@Composable +fun RestartAppButton() { + val context = LocalContext.current + Row ( + modifier = Modifier.widthIn(max = 400.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + OutlinedButton( + colors = ButtonDefaults.outlinedButtonColors(contentColor = colorResource(id = R.color.red)), + border = BorderStroke(1.dp, colorResource(id = R.color.red)), + onClick = { + SecureAlertDialogBuilder(context, R.style.CustomAlertDialog) + .setTitle(R.string.dialog_title_restart_app) + .setMessage(R.string.dialog_message_restart_app) + .setNegativeButton(R.string.button_label_cancel, null) + .setPositiveButton(R.string.button_label_force_restart, object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface?, which: Int) { + try { + val packageManager = context.packageManager + val intent = packageManager.getLaunchIntentForPackage(context.packageName) + val mainIntent = Intent.makeRestartActivityTask(intent!!.component) + mainIntent.setPackage(context.packageName) + context.startActivity(mainIntent) + Runtime.getRuntime().exit(0) + } catch (_: Exception) {} + } + }) + .create().show() + }) { + Text(text = stringResource(id = R.string.button_label_force_restart)) + } + } +} + + @Composable fun FaqLinkHeader(openFaq: () -> Unit, onBack: () -> Unit) { Column( diff --git a/obv_messenger/app/src/main/java/io/olvid/messenger/widget/ActionShortcutConfigurationActivity.java b/obv_messenger/app/src/main/java/io/olvid/messenger/widget/ActionShortcutConfigurationActivity.java index 99611bcd..53b5fca6 100644 --- a/obv_messenger/app/src/main/java/io/olvid/messenger/widget/ActionShortcutConfigurationActivity.java +++ b/obv_messenger/app/src/main/java/io/olvid/messenger/widget/ActionShortcutConfigurationActivity.java @@ -782,7 +782,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c cancelButton.setOnClickListener(v -> dismiss()); FilteredDiscussionListFragment filteredDiscussionListFragment = new FilteredDiscussionListFragment(); - filteredDiscussionListFragment.removeBottomPadding(); + filteredDiscussionListFragment.setBottomPadding(0); filteredDiscussionListFragment.setShowPinned(true); filteredDiscussionListFragment.setUnfilteredDiscussions(viewModel.getDiscussionListLiveData()); filteredDiscussionListFragment.setDiscussionFilterEditText(dialogContactNameFilter); diff --git a/obv_messenger/app/src/main/jniLibs/arm64-v8a/libsqlitejdbc.so b/obv_messenger/app/src/main/jniLibs/arm64-v8a/libsqlitejdbc.so index a0357420..672be1b6 100644 Binary files a/obv_messenger/app/src/main/jniLibs/arm64-v8a/libsqlitejdbc.so and b/obv_messenger/app/src/main/jniLibs/arm64-v8a/libsqlitejdbc.so differ diff --git a/obv_messenger/app/src/main/jniLibs/armeabi-v7a/libsqlitejdbc.so b/obv_messenger/app/src/main/jniLibs/armeabi-v7a/libsqlitejdbc.so index 488b178e..c5637b0b 100644 Binary files a/obv_messenger/app/src/main/jniLibs/armeabi-v7a/libsqlitejdbc.so and b/obv_messenger/app/src/main/jniLibs/armeabi-v7a/libsqlitejdbc.so differ diff --git a/obv_messenger/app/src/main/jniLibs/x86/libsqlitejdbc.so b/obv_messenger/app/src/main/jniLibs/x86/libsqlitejdbc.so index d25b344e..98c5180d 100644 Binary files a/obv_messenger/app/src/main/jniLibs/x86/libsqlitejdbc.so and b/obv_messenger/app/src/main/jniLibs/x86/libsqlitejdbc.so differ diff --git a/obv_messenger/app/src/main/jniLibs/x86_64/libsqlitejdbc.so b/obv_messenger/app/src/main/jniLibs/x86_64/libsqlitejdbc.so index b12ca7b3..9a47adb4 100644 Binary files a/obv_messenger/app/src/main/jniLibs/x86_64/libsqlitejdbc.so and b/obv_messenger/app/src/main/jniLibs/x86_64/libsqlitejdbc.so differ diff --git a/obv_messenger/app/src/main/res/drawable/background_gradient_dialog_background_bottom.xml b/obv_messenger/app/src/main/res/drawable/background_gradient_dialog_background_bottom.xml new file mode 100644 index 00000000..f8cf7886 --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/background_gradient_dialog_background_bottom.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/drawable/background_rounded_dialog_red_outline.xml b/obv_messenger/app/src/main/res/drawable/background_rounded_dialog_red_outline.xml new file mode 100644 index 00000000..e676ac5a --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/background_rounded_dialog_red_outline.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/drawable/ic_location_sharing_grey.xml b/obv_messenger/app/src/main/res/drawable/ic_location_sharing_grey.xml new file mode 100644 index 00000000..c50af346 --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/ic_location_sharing_grey.xml @@ -0,0 +1,8 @@ + + + diff --git a/obv_messenger/app/src/main/res/drawable/ic_pref_hide_group_member_changes.xml b/obv_messenger/app/src/main/res/drawable/ic_pref_hide_group_member_changes.xml new file mode 100644 index 00000000..8c72159c --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/ic_pref_hide_group_member_changes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/obv_messenger/app/src/main/res/drawable/ic_three_dots_grey.xml b/obv_messenger/app/src/main/res/drawable/ic_three_dots_grey.xml new file mode 100644 index 00000000..890ab26c --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/ic_three_dots_grey.xml @@ -0,0 +1,5 @@ + + + diff --git a/obv_messenger/app/src/main/res/drawable/map_compass.xml b/obv_messenger/app/src/main/res/drawable/map_compass.xml new file mode 100644 index 00000000..79ad062e --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/map_compass.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/obv_messenger/app/src/main/res/drawable/map_layers.xml b/obv_messenger/app/src/main/res/drawable/map_layers.xml new file mode 100644 index 00000000..2af72a5a --- /dev/null +++ b/obv_messenger/app/src/main/res/drawable/map_layers.xml @@ -0,0 +1,6 @@ + + + + diff --git a/obv_messenger/app/src/main/res/layout/activity_discussion.xml b/obv_messenger/app/src/main/res/layout/activity_discussion.xml index 6ff65937..c2d295bb 100644 --- a/obv_messenger/app/src/main/res/layout/activity_discussion.xml +++ b/obv_messenger/app/src/main/res/layout/activity_discussion.xml @@ -29,7 +29,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/discussion_toolbar" - app:layout_constraintBottom_toBottomOf="@id/fab_layout" + app:layout_constraintBottom_toTopOf="@id/discussion_invitation" app:layout_constraintVertical_bias=".2" android:gravity="center_horizontal"> - - + + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + - + - diff --git a/obv_messenger/app/src/main/res/layout/dialog_fragment_forward_messages.xml b/obv_messenger/app/src/main/res/layout/dialog_fragment_forward_messages.xml index 46465b4d..91daf1c2 100644 --- a/obv_messenger/app/src/main/res/layout/dialog_fragment_forward_messages.xml +++ b/obv_messenger/app/src/main/res/layout/dialog_fragment_forward_messages.xml @@ -171,11 +171,20 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/button_forward" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/discussion_filter"/> + + + + + - + \ No newline at end of file diff --git a/obv_messenger/app/src/main/res/layout/fragment_send_location_map.xml b/obv_messenger/app/src/main/res/layout/fragment_send_location_map.xml index f1df3c44..7f052e2a 100644 --- a/obv_messenger/app/src/main/res/layout/fragment_send_location_map.xml +++ b/obv_messenger/app/src/main/res/layout/fragment_send_location_map.xml @@ -116,6 +116,20 @@ android:drawablePadding="8dp" android:drawableStart="@drawable/ic_location_sharing" /> + + + Envoyer Partager 1\u00a0heure - Partager la position pendant 1\u00a0heure + Partager la position pendant 1ย heure 3\u00a0heures - Partager la position pendant 3\u00a0heures + Partager la position pendant 3ย heures Pas de limite - Partager la position indรฉfiniement - 1\u00a0minute - Envoyer la position toutes les minutes - 5\u00a0minutes - Envoyer la position toutes les 5\u00a0minutes - 1\u00a0heure - Envoyer la position toutes les heures + Partager la position indรฉfiniement + Prรฉcis + ร‰quilibrรฉ + ร‰conomie d\'รฉnergie + Mode de partage prรฉcis + Mode de partage รฉquilibrรฉ + Mode de partage รฉconomie d\'รฉnergie Partage de la position jusqu\'ร  %1$s Arrรชter le partage Partage terminรฉ @@ -1488,11 +1488,6 @@ Durรฉe par dรฉfaut du partage Intรฉgration avec un service de cartographie Masquer toutes les notifications d\'erreurs relatives au partage de position - Langue des cartes OpenStreetMap - Langue du systรจme - Anglais - Franรงais - International Attributions Trop d\'essais, veuillez attendreโ€ฆ %1$02ds @@ -1603,6 +1598,7 @@ Dรฉcouvrez les nouveaux groupes\u00a0! Olvid permet maintenant d\'avoir plusieurs administrateurs par groupe\u00a0๐Ÿ‘‘\u00a0! Cela ne change rien ร  vos ยซ\u00a0anciens groupes\u00a0ยป, mais fonctionnera pour tous les nouveaux groupes que vous crรฉerez ร  partir de maintenant. Pour en bรฉnรฉficier, vos contacts doivent avoir la derniรจre version d\'Olvid.\n\nVous voulez essayer\u00a0? Rendez vous sur l\'onglet des groupes et crรฉez un nouveau groupe. Une fois crรฉรฉ, vous pourrez le modifier pour accorder les droits d\'administration ร  autant de participants que vous le voulez\u00a0! Derniรจre notification push\u00a0: %1$s + Notifications push dรฉpriorisรฉsย : %1$d/%2$d [Message trop long pour รชtre affichรฉ dans le client web] Raccourci invalide ร‰mettre des appels sรฉcurisรฉs\n(disponible grรขce ร  un autre profil) @@ -1804,7 +1800,7 @@ Ne rien faire Aller au message Personnalisรฉe - Visible une seul fois + Visible une seule fois Durรฉe de visibilitรฉ Durรฉe d\'existence Les messages et piรจces jointes visibles une seule fois sont effacรฉs quand l\'on sort de la discussion. @@ -1829,6 +1825,25 @@ ร‰couter ร  vitesse normale (1\u00d7) ร‰couter plus rapidement (1,5\u00d7) ร‰couter le plus vite possible (2\u00d7) + Mode de partage par dรฉfaut + Use experimental push notification priority + Vous partagez votre position + Ouvrir la carte + fr + Normale + Satellite + Relief + Hybride + Copier + Lien copiรฉ dans le presse-papier + Redรฉmarrer l\'application + Redรฉmarrer l\'applicationย ? + Souhaitez-vous forcer le redรฉmarrage d\'Olvidย ? + Appel chiffrรฉ de bout en bout + Masquer les messages de modification de groupe + Une fois cette option activรฉe, les messages indiquant qu\'un membre ร  rejoint ou quittรฉ un groupe ne seront plus affichรฉs dans vos discussions. + + @@ -1963,4 +1978,12 @@ Vous avez actuellement un message supprimรฉ ร  distance dans vos discussions. Souhaitez-vous le supprimerย ? Vous avez actuellement %1$d messages supprimรฉs ร  distance dans vos discussions. Souhaitez-vous tous les supprimerย ? + + %1$d contact partage sa position + %1$d contacts partagent leur position + + + Vous and %1$d autre partagez votre position + Vous and %1$d autres partagez votre position + diff --git a/obv_messenger/app/src/main/res/values/arrays.xml b/obv_messenger/app/src/main/res/values/arrays.xml index f1035f73..19cda49f 100644 --- a/obv_messenger/app/src/main/res/values/arrays.xml +++ b/obv_messenger/app/src/main/res/values/arrays.xml @@ -364,34 +364,22 @@ @string/location_sharing_duration_endless_full_string - - 60000 - 300000 - 3600000 + + 1 + 2 + 3 - - @string/location_sharing_interval_one_minute_short_string - @string/location_sharing_interval_five_minutes_short_string - @string/location_sharing_interval_one_hour_short_string + + @string/location_sharing_quality_precise_short_string + @string/location_sharing_quality_balanced_short_string + @string/location_sharing_quality_power_save_short_string - - @string/location_sharing_interval_one_minute_full_string - @string/location_sharing_interval_five_minutes_full_string - @string/location_sharing_interval_one_hour_full_string + + @string/location_sharing_quality_precise_full_string + @string/location_sharing_quality_balanced_full_string + @string/location_sharing_quality_power_save_full_string - - @string/location_map_language_default - @string/location_map_language_english - @string/location_map_language_french - @string/location_map_language_international - - - default - en - fr - intl - @string/pref_text_default_device_language @string/pref_text_english diff --git a/obv_messenger/app/src/main/res/values/strings.xml b/obv_messenger/app/src/main/res/values/strings.xml index e0f58489..67b83d0f 100644 --- a/obv_messenger/app/src/main/res/values/strings.xml +++ b/obv_messenger/app/src/main/res/values/strings.xml @@ -22,7 +22,7 @@ Smaller than 100MB Never download automatically Always download automatically - Abort + Abort Accept App settings Cancel @@ -183,7 +183,7 @@ Edit group details Edit my Details Group Details - Import invitation from Clipboard + Import invitation from clipboard Leave group Open Recreate secure channels @@ -1472,17 +1472,17 @@ Send Start sharing 1\u00a0hour - Share location for 1\u00a0hour + Share location for 1ย hour 3\u00a0hours - Share location for 3\u00a0hours + Share location for 3ย hours Endless - Share location until stopped - 1\u00a0minute - Send location every minute - 5\u00a0minutes - Send location every 5\u00a0minutes - 1\u00a0hour - Send location every hour + Share location until stopped + Precise + Balanced + Power save + Precise location sharing mode + Balanced location sharing mode + Power save location sharing mode Sharing location until: %1$s Stop sharing Location sharing ended @@ -1511,11 +1511,6 @@ Default share duration Integration with a map provider Hide all error notifications related to location sharing - OpenStreetMap language - Use system language - English - French - International Attributions Too many attempts, please waitโ€ฆ %1$02ds @@ -1633,6 +1628,7 @@ Introducing new groups! Olvid now supports new groups with multiple administrators\u00a0๐Ÿ‘‘! This does not change anything to your existing \"old groups\" but applies to all groups you create from now on. To benefit from this, your contacts need to use the latest version of Olvid.\n\nWant to try this out? Tap on the group tab and create a new group. Once created, you may edit it to grant administrator privilege to as many members as you wish! Latest push notification: %1$s + De-prioritized push notifications: %1$d/%2$d [Message too large to display in web client] Invalid shortcut Initiate secure calls\n(available through another profile) @@ -1768,7 +1764,7 @@ Key escrow successful Key escrow failed - Notifactions + Notifications To be notified when a contact sends you a message, you need to allow Olvid to display notifications. Notifications disabled Notifications are currently disabled for Olvid. Unless you enable them, you will not be notified when receiving a message from your contacts. @@ -1859,6 +1855,26 @@ Play at normal speed (1\u00d7) Play at a faster speed (1.5\u00d7) Play at the fastest speed (2\u00d7) + Default share quality + Use experimental push notification priority + You are sharing your location + Open map + en + Normal + Satellite + Terrain + Hybrid + Copy + Link copied to clipboard + Restart application + Restart application? + Do you want to force a restart of Olvid? + End-to-end encrypted call + Hide group member change messages + When activated, messages indicating that a group member joined or left a group will not be displayed in your discussions. + + + @@ -1991,4 +2007,12 @@ You currently have 1 remote deleted message in your discussions. Do you want to delete it? You currently have %1$d remote deleted messages in your discussions. Do you want to delete all such markers? + + 1 contact is sharing their location + %1$d contacts are sharing their location + + + You and 1 other are sharing their location + You and %1$d others are sharing their location + diff --git a/obv_messenger/app/src/main/res/xml/fragment_preferences_contacts_and_groups.xml b/obv_messenger/app/src/main/res/xml/fragment_preferences_contacts_and_groups.xml index 632134e8..ecadc65c 100644 --- a/obv_messenger/app/src/main/res/xml/fragment_preferences_contacts_and_groups.xml +++ b/obv_messenger/app/src/main/res/xml/fragment_preferences_contacts_and_groups.xml @@ -11,6 +11,16 @@ app:useSimpleSummaryProvider="true" android:icon="@drawable/ic_pref_group" android:order="10" /> + + + - - diff --git a/obv_messenger/app/src/main/res/xml/fragment_preferences_privacy.xml b/obv_messenger/app/src/main/res/xml/fragment_preferences_privacy.xml index 04801b10..b6a3118f 100644 --- a/obv_messenger/app/src/main/res/xml/fragment_preferences_privacy.xml +++ b/obv_messenger/app/src/main/res/xml/fragment_preferences_privacy.xml @@ -60,5 +60,5 @@ android:summary="@string/pref_permanent_websocket_summary" android:title="@string/pref_permanent_websocket_title" android:icon="@drawable/ic_pref_websocket" - android:order="51" /> + android:order="52" /> \ No newline at end of file diff --git a/obv_messenger/app/src/nogoogle/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java b/obv_messenger/app/src/nogoogle/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java index e6af807a..3dec67a6 100644 --- a/obv_messenger/app/src/nogoogle/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java +++ b/obv_messenger/app/src/nogoogle/java/io/olvid/messenger/firebase/ObvFirebaseMessagingService.java @@ -23,4 +23,11 @@ public class ObvFirebaseMessagingService { public static Long getLastPushNotificationTimestamp() { return null; } + public static int getDeprioritizedMessageCount() { + return 0; + } + + public static int getHighPriorityMessageCount() { + return 0; + } } \ No newline at end of file diff --git a/obv_messenger/libwebrtc/libwebrtc.aar b/obv_messenger/libwebrtc/libwebrtc.aar index e224d990..951d97af 100644 Binary files a/obv_messenger/libwebrtc/libwebrtc.aar and b/obv_messenger/libwebrtc/libwebrtc.aar differ diff --git a/obv_messenger/libwebrtc/olvid_6261.patch b/obv_messenger/libwebrtc/olvid_6261.patch new file mode 100644 index 00000000..09f66f06 --- /dev/null +++ b/obv_messenger/libwebrtc/olvid_6261.patch @@ -0,0 +1,219 @@ +diff --git a/p2p/base/basic_packet_socket_factory.cc b/p2p/base/basic_packet_socket_factory.cc +index 6a811af71a..c832c92238 100644 +--- a/p2p/base/basic_packet_socket_factory.cc ++++ b/p2p/base/basic_packet_socket_factory.cc +@@ -202,4 +202,54 @@ int BasicPacketSocketFactory::BindSocket(Socket* socket, + return ret; + } + ++ ++ ++ ++// Proxy support ++ProxyPacketSocketFactory::ProxyPacketSocketFactory(SocketFactory* socket_factory, SocketAddress proxy_socket_address, std::string& user_agent) : ++ basic_packet_socket_factory_(new BasicPacketSocketFactory(socket_factory)) ++{ ++ if (proxy_socket_address.IsComplete()) { ++ proxy_info_.type = PROXY_HTTPS; ++ proxy_info_.address = proxy_socket_address; ++ } else { ++ proxy_info_.type = PROXY_NONE; ++ } ++ user_agent_ = user_agent; ++} ++ ++ProxyPacketSocketFactory::~ProxyPacketSocketFactory() {} ++ ++AsyncPacketSocket* ProxyPacketSocketFactory::CreateUdpSocket( ++ const SocketAddress& address, ++ uint16_t min_port, ++ uint16_t max_port) { ++ return basic_packet_socket_factory_->CreateUdpSocket(address, min_port, max_port); ++} ++ ++AsyncListenSocket* ProxyPacketSocketFactory::CreateServerTcpSocket(const SocketAddress& local_address, ++ uint16_t min_port, ++ uint16_t max_port, ++ int opts) { ++ return basic_packet_socket_factory_->CreateServerTcpSocket(local_address, min_port, max_port, opts); ++} ++ ++AsyncPacketSocket* ProxyPacketSocketFactory::CreateClientTcpSocket( ++ const SocketAddress& local_address, ++ const SocketAddress& remote_address, ++ const ProxyInfo& proxy_info, ++ const std::string& user_agent, ++ const PacketSocketTcpOptions& tcp_options) { ++ if (user_agent_.empty()) { ++ return basic_packet_socket_factory_->CreateClientTcpSocket(local_address, remote_address, proxy_info_, user_agent, tcp_options); ++ } else { ++ return basic_packet_socket_factory_->CreateClientTcpSocket(local_address, remote_address, proxy_info_, user_agent_, tcp_options); ++ } ++} ++ ++std::unique_ptr ProxyPacketSocketFactory::CreateAsyncDnsResolver() { ++ return basic_packet_socket_factory_->CreateAsyncDnsResolver(); ++} ++ ++ + } // namespace rtc +diff --git a/p2p/base/basic_packet_socket_factory.h b/p2p/base/basic_packet_socket_factory.h +index f9bdf7b2c7..bdeaffff59 100644 +--- a/p2p/base/basic_packet_socket_factory.h ++++ b/p2p/base/basic_packet_socket_factory.h +@@ -60,6 +60,36 @@ class RTC_EXPORT BasicPacketSocketFactory : public PacketSocketFactory { + SocketFactory* socket_factory_; + }; + ++ ++// Proxy support ++class RTC_EXPORT ProxyPacketSocketFactory : public PacketSocketFactory { ++ public: ++ explicit ProxyPacketSocketFactory(SocketFactory* socket_factory, SocketAddress proxy_socket_address, std::string& user_agent); ++ ~ProxyPacketSocketFactory() override; ++ ++ AsyncPacketSocket* CreateUdpSocket(const SocketAddress& local_address, ++ uint16_t min_port, ++ uint16_t max_port) override; ++ AsyncListenSocket* CreateServerTcpSocket(const SocketAddress& local_address, ++ uint16_t min_port, ++ uint16_t max_port, ++ int opts) override; ++ AsyncPacketSocket* CreateClientTcpSocket( ++ const SocketAddress& local_address, ++ const SocketAddress& remote_address, ++ const ProxyInfo& proxy_info, ++ const std::string& user_agent, ++ const PacketSocketTcpOptions& tcp_options) override; ++ ++ std::unique_ptr CreateAsyncDnsResolver() ++ override; ++ ++ private: ++ BasicPacketSocketFactory* basic_packet_socket_factory_; ++ ProxyInfo proxy_info_; ++ std::string user_agent_; ++}; ++ + } // namespace rtc + + #endif // P2P_BASE_BASIC_PACKET_SOCKET_FACTORY_H_ +diff --git a/sdk/android/api/org/webrtc/PeerConnectionFactory.java b/sdk/android/api/org/webrtc/PeerConnectionFactory.java +index c46718fdd6..905b30521e 100644 +--- a/sdk/android/api/org/webrtc/PeerConnectionFactory.java ++++ b/sdk/android/api/org/webrtc/PeerConnectionFactory.java +@@ -178,6 +178,10 @@ public class PeerConnectionFactory { + @Nullable private NetworkControllerFactoryFactory networkControllerFactoryFactory; + @Nullable private NetworkStatePredictorFactoryFactory networkStatePredictorFactoryFactory; + @Nullable private NetEqFactoryFactory neteqFactoryFactory; ++ // Proxy support ++ @Nullable private String proxyAddress; ++ private int proxyPort; ++ @Nullable private String userAgent; + + private Builder() {} + +@@ -259,6 +263,18 @@ public class PeerConnectionFactory { + return this; + } + ++ // Proxy support ++ public Builder setHttpsProxy(String proxyAddress, int proxyPort) { ++ this.proxyAddress = proxyAddress; ++ this.proxyPort = proxyPort; ++ return this; ++ } ++ ++ public Builder setUserAgent(String userAgent) { ++ this.userAgent = userAgent; ++ return this; ++ } ++ + public PeerConnectionFactory createPeerConnectionFactory() { + checkInitializeHasBeenCalled(); + if (audioDeviceModule == null) { +@@ -278,7 +294,9 @@ public class PeerConnectionFactory { + networkStatePredictorFactoryFactory == null + ? 0 + : networkStatePredictorFactoryFactory.createNativeNetworkStatePredictorFactory(), +- neteqFactoryFactory == null ? 0 : neteqFactoryFactory.createNativeNetEqFactory()); ++ neteqFactoryFactory == null ? 0 : neteqFactoryFactory.createNativeNetEqFactory(), ++ // Proxy support ++ proxyAddress, proxyPort, userAgent); + } + } + +@@ -607,7 +625,8 @@ public class PeerConnectionFactory { + long audioDecoderFactory, VideoEncoderFactory encoderFactory, + VideoDecoderFactory decoderFactory, long nativeAudioProcessor, + long nativeFecControllerFactory, long nativeNetworkControllerFactory, +- long nativeNetworkStatePredictorFactory, long neteqFactory); ++ long nativeNetworkStatePredictorFactory, long neteqFactory, ++ String proxyAddress, int proxyPort, String userAgent); // Proxy support + + private static native long nativeCreatePeerConnection(long factory, + PeerConnection.RTCConfiguration rtcConfig, MediaConstraints constraints, long nativeObserver, +diff --git a/sdk/android/src/jni/pc/peer_connection_factory.cc b/sdk/android/src/jni/pc/peer_connection_factory.cc +index 9a21e10ede..b318c856aa 100644 +--- a/sdk/android/src/jni/pc/peer_connection_factory.cc ++++ b/sdk/android/src/jni/pc/peer_connection_factory.cc +@@ -25,6 +25,10 @@ + #include "rtc_base/event_tracer.h" + #include "rtc_base/physical_socket_server.h" + #include "rtc_base/thread.h" ++// Proxy support ++#include "rtc_base/socket_address.h" ++#include "p2p/base/basic_packet_socket_factory.h" ++ + #include "sdk/android/generated_peerconnection_jni/PeerConnectionFactory_jni.h" + #include "sdk/android/native_api/jni/java_types.h" + #include "sdk/android/native_api/stacktrace/stacktrace.h" +@@ -238,7 +242,11 @@ ScopedJavaLocalRef CreatePeerConnectionFactoryForJava( + network_controller_factory, + std::unique_ptr + network_state_predictor_factory, +- std::unique_ptr neteq_factory) { ++ std::unique_ptr neteq_factory, ++ // Proxy support ++ std::string proxyAddress, ++ jint proxyPort, ++ std::string userAgent) { + // talk/ assumes pretty widely that the current Thread is ThreadManager'd, but + // ThreadManager only WrapCurrentThread()s the thread where it is first + // created. Since the semantics around when auto-wrapping happens in +@@ -280,6 +288,10 @@ ScopedJavaLocalRef CreatePeerConnectionFactoryForJava( + dependencies.network_monitor_factory = + std::make_unique(); + } ++ // Proxy support ++ if ((!proxyAddress.empty() && proxyPort != 0) || !userAgent.empty()) { ++ dependencies.packet_socket_factory = std::make_unique(socket_server.get(), rtc::SocketAddress(proxyAddress, proxyPort), userAgent); ++ } + + dependencies.adm = std::move(audio_device_module); + dependencies.audio_encoder_factory = std::move(audio_encoder_factory); +@@ -320,7 +332,10 @@ JNI_PeerConnectionFactory_CreatePeerConnectionFactory( + jlong native_fec_controller_factory, + jlong native_network_controller_factory, + jlong native_network_state_predictor_factory, +- jlong native_neteq_factory) { ++ jlong native_neteq_factory, ++ const JavaParamRef& proxyAddress, // Proxy support ++ jint proxyPort, ++ const JavaParamRef& userAgent) { + rtc::scoped_refptr audio_processor( + reinterpret_cast(native_audio_processor)); + return CreatePeerConnectionFactoryForJava( +@@ -337,7 +352,11 @@ JNI_PeerConnectionFactory_CreatePeerConnectionFactory( + native_network_controller_factory), + TakeOwnershipOfUniquePtr( + native_network_state_predictor_factory), +- TakeOwnershipOfUniquePtr(native_neteq_factory)); ++ TakeOwnershipOfUniquePtr(native_neteq_factory), ++ // Proxy support ++ proxyAddress.is_null() ? "" : JavaToStdString(jni, proxyAddress), ++ proxyPort, ++ userAgent.is_null() ? "" : JavaToStdString(jni, userAgent)); + } + + static void JNI_PeerConnectionFactory_FreeFactory(JNIEnv*, jlong j_p) {