mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
Merge branch 'refactor' into backup-restore
This commit is contained in:
commit
58e68f968c
@ -688,7 +688,7 @@
|
||||
</receiver>
|
||||
<!-- Session -->
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.loki.api.BackgroundPollListener"
|
||||
android:name="org.thoughtcrime.securesms.loki.api.BackgroundPollWorker$BootBroadcastReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
@ -706,13 +706,6 @@
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.AlarmManagerScheduler$RetryReceiver"
|
||||
android:enabled="@bool/enable_alarm_manager" /> <!-- Probably don't need this one -->
|
||||
<receiver
|
||||
android:name="org.thoughtcrime.securesms.jobmanager.BootReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
|
@ -90,8 +90,11 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.1.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation "androidx.work:work-runtime-ktx:2.4.0"
|
||||
|
||||
implementation ("com.google.firebase:firebase-messaging:18.0.0") {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
@ -259,6 +262,10 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'LICENSE.txt'
|
||||
exclude 'LICENSE'
|
||||
|
@ -79,10 +79,10 @@
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:src="@drawable/ic_close_white_18dp"
|
||||
android:tint="@color/gray70"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="@color/gray70"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
|
@ -1103,7 +1103,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="preferences__chats">Rozmowy i multimedia</string>
|
||||
<string name="preferences__conversation_length_limit">Limit długości konwersacji</string>
|
||||
<string name="preferences__trim_all_conversations_now">Przytnij wszystkie konwersacje teraz</string>
|
||||
<string name="preferences__scan_through_all_conversations_and_enforce_conversation_length_limits">Przeskanuj wszystkie konwersacje i przytnij to określonej długości</string>
|
||||
<string name="preferences__scan_through_all_conversations_and_enforce_conversation_length_limits">Przeskanuj wszystkie konwersacje i przytnij do określonej długości</string>
|
||||
<string name="preferences__linked_devices">Połączone urządzenia</string>
|
||||
<string name="preferences__light_theme">Jasny</string>
|
||||
<string name="preferences__dark_theme">Ciemny</string>
|
||||
@ -1360,29 +1360,29 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="continue_2">Kontyntynuj</string>
|
||||
<string name="copy">Kopiuj</string>
|
||||
<string name="invalid_url">nieprawidłowy URL</string>
|
||||
<string name="copied_to_clipboard">Skopiowane do schowka</string>
|
||||
<string name="device_linking_failed">Nie można połączyć urządzenia.</string>
|
||||
<string name="next">Kolejny</string>
|
||||
<string name="share">Dzielić</string>
|
||||
<string name="copied_to_clipboard">Skopiowano do schowka</string>
|
||||
<string name="device_linking_failed">Nie można podłączyć urządzenia.</string>
|
||||
<string name="next">Dalej</string>
|
||||
<string name="share">Udostępnij</string>
|
||||
<string name="invalid_session_id">Nieprawidłowy identyfikator Session</string>
|
||||
<string name="cancel">Anuluj</string>
|
||||
<string name="your_session_id">Twój identyfikator Session</string>
|
||||
|
||||
<string name="activity_landing_title_2">Twoja Session zaczyna się tutaj...</string>
|
||||
<string name="activity_landing_title_2">Twoja sesja zaczyna się tutaj...</string>
|
||||
<string name="activity_landing_register_button_title">Utwórz identyfikator Session</string>
|
||||
<string name="activity_landing_restore_button_title">Kontynuuj swoją sesję</string>
|
||||
<string name="activity_landing_link_button_title">Połącz z istniejącym kontem</string>
|
||||
<string name="activity_landing_device_unlinked_dialog_title">Twoje urządzenie zostało rozłączone pomyślnie</string>
|
||||
|
||||
<string name="view_fake_chat_bubble_1">Jaka jest Session</string>
|
||||
<string name="view_fake_chat_bubble_1">Jaki jest Session</string>
|
||||
<string name="view_fake_chat_bubble_2">To zdecentralizowana, szyfrowana aplikacja do przesyłania wiadomości</string>
|
||||
<string name="view_fake_chat_bubble_3">Więc nie zbiera moich danych osobowych ani metadanych z mojej rozmowy? Jak to działa?</string>
|
||||
<string name="view_fake_chat_bubble_4">Wykorzystując połączenie zaawansowanych anonimowych tras i technologii szyfrowania end-to-end.</string>
|
||||
<string name="view_fake_chat_bubble_5">Znajomi nie pozwalają znajomym korzystać z zainfekowanych komunikatorów. Nie ma za co.</string>
|
||||
|
||||
<string name="activity_register_title">Przywitaj się z identyfikatorem Session</string>
|
||||
<string name="activity_register_explanation">Twój identyfikator Session to unikalny adres, za pomocą którego można się z Tobą kontaktować w Sesji. Bez połączenia z twoją prawdziwą tożsamością, identyfikator Session jest z założenia całkowicie anonimowy i prywatny.</string>
|
||||
<string name="activity_register_public_key_copied_message">Skopiowane do schowka</string>
|
||||
<string name="activity_register_explanation">Twój identyfikator Session to unikalny adres, za pomocą którego można się z Tobą kontaktować w Session. Bez połączenia z twoją prawdziwą tożsamością, identyfikator Session jest z założenia całkowicie anonimowy i prywatny.</string>
|
||||
<string name="activity_register_public_key_copied_message">Skopiowano do schowka</string>
|
||||
|
||||
<string name="activity_restore_title">Przywróć swoje konto</string>
|
||||
<string name="activity_restore_explanation">Wprowadź frazę odzyskiwania, która została Ci przekazana podczas rejestracji w celu przywrócenia konta.</string>
|
||||
@ -1398,11 +1398,11 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="fragment_enter_session_id_edit_text_hint">Wpisz swój identyfikator Session</string>
|
||||
|
||||
<string name="activity_display_name_title_2">Wybierz swoją nazwę wyświetlaną</string>
|
||||
<string name="activity_display_name_explanation">To będzie twoje imię, kiedy będziesz używać Sesji. Może to być twoje prawdziwe imię, alias lub cokolwiek innego, co lubisz.</string>
|
||||
<string name="activity_display_name_explanation">To będzie twoje imię, kiedy będziesz używać Session. Może to być twoje prawdziwe imię, pseudonim lub cokolwiek innego, co lubisz.</string>
|
||||
<string name="activity_display_name_edit_text_hint">Wprowadź wyświetlaną nazwe</string>
|
||||
<string name="activity_display_name_display_name_missing_error">Wybierz wyświetlaną nazwę</string>
|
||||
<string name="activity_display_name_display_name_invalid_error">Wybierz wyświetlaną nazwę, która składa się tylko z znaków az, AZ, 0–9 i _</string>
|
||||
<string name="activity_display_name_display_name_too_long_error">Wybierz krótszą nazwę wyświetlaną</string>
|
||||
<string name="activity_display_name_display_name_too_long_error">Wybierz krótszą wyświetlaną nazwę</string>
|
||||
|
||||
<string name="activity_pn_mode_recommended_option_tag">Zalecana</string>
|
||||
<string name="activity_pn_mode_no_option_picked_dialog_title">Wybierz opcję</string>
|
||||
@ -1416,35 +1416,35 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
|
||||
<string name="activity_seed_title">Twoja fraza odzyskiwania</string>
|
||||
<string name="activity_seed_title_2">Poznaj swoją frazę odzyskiwania</string>
|
||||
<string name="activity_seed_explanation">Twoja fraza odzyskiwania jest kluczem głównym do identyfikatora Session - możesz go użyć do przywrócenia identyfikatora Session, jeśli stracisz dostęp do urządzenia. Przechowuj swoją frazę odzyskiwania w bezpiecznym miejscu i nikomu jej nie udostępniaj.</string>
|
||||
<string name="activity_seed_explanation">Twoja fraza odzyskiwania jest głównym kluczem do identyfikatora Session - możesz jej użyć do przywrócenia identyfikatora Session, jeśli stracisz dostęp do urządzenia. Przechowuj swoją frazę odzyskiwania w bezpiecznym miejscu i nikomu jej nie udostępniaj.</string>
|
||||
<string name="activity_seed_reveal_button_title">Przytrzymaj, aby odsłonić</string>
|
||||
|
||||
<string name="view_seed_reminder_subtitle_1">Zabezpiecz swoje konto, zapisując frazę odzyskiwania</string>
|
||||
<string name="view_seed_reminder_subtitle_2">Stuknij i przytrzymaj zredagowane słowa, aby odsłonić frazę odzyskiwania, a następnie przechowuj ją bezpiecznie, aby zabezpieczyć identyfikator Session.</string>
|
||||
<string name="view_seed_reminder_subtitle_2">Stuknij i przytrzymaj zredagowane słowa, aby odsłonić frazę odzyskiwania, a następnie przechowuj ją w bezpiecznym miejscu, aby zabezpieczyć identyfikator Session.</string>
|
||||
<string name="view_seed_reminder_subtitle_3">Pamiętaj, aby przechowywać frazę odzyskiwania w bezpiecznym miejscu</string>
|
||||
|
||||
<string name="activity_path_title">Ścieżka</string>
|
||||
<string name="activity_path_explanation">Sesja ukrywa Twój adres IP, odbijając wiadomości przez kilka węzłów usług w zdecentralizowanej sieci Session. Oto kraje, w których obecnie Twoje połączenie jest odbijane:</string>
|
||||
<string name="activity_path_explanation">Session ukrywa Twój adres IP, odbijając wiadomości przez kilka węzłów usług w zdecentralizowanej sieci Session. Oto kraje, w których obecnie Twoje połączenie jest odbijane:</string>
|
||||
<string name="activity_path_device_row_title">ty</string>
|
||||
<string name="activity_path_guard_node_row_title">Węzeł wejścia</string>
|
||||
<string name="activity_path_service_node_row_title">Węzeł serwisowy</string>
|
||||
<string name="activity_path_destination_row_title">Miejsce docelowe</string>
|
||||
<string name="activity_path_learn_more_button_title">Ucz się więcej</string>
|
||||
<string name="activity_path_learn_more_button_title">Dowiedz się więcej</string>
|
||||
|
||||
<string name="activity_create_private_chat_title">Nowa Session</string>
|
||||
<string name="activity_create_private_chat_title">Nowa sesja</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Wpisz identyfikator Session</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">Skanowania QR code</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">Zeskanuj kod QR użytkownika, aby rozpocząć sesję. Kody QR można znaleźć, dotykając ikony kodu QR w ustawieniach konta.</string>
|
||||
|
||||
<string name="fragment_enter_public_key_edit_text_hint">Wprowadź identyfikator Session odbiorcy</string>
|
||||
<string name="fragment_enter_public_key_explanation">Użytkownicy mogą udostępnić swój identyfikator Session, przechodząc do ustawień konta i stukając opcję Udostępnij identyfikator Session lub udostępniając kod QR.</string>
|
||||
<string name="fragment_enter_public_key_explanation">Użytkownicy mogą udostępnić swój identyfikator Session, przechodząc do ustawień konta i klikając opcję Udostępnij identyfikator Session lub udostępniając kod QR.</string>
|
||||
|
||||
<string name="fragment_scan_qr_code_camera_access_explanation">Sesja wymaga dostępu do kamery, aby skanować kody QR</string>
|
||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session wymaga dostępu do kamery, aby skanować kody QR</string>
|
||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Udziel dostępu do kamery</string>
|
||||
|
||||
<string name="activity_create_closed_group_title">Nowa grupa zamknięta</string>
|
||||
<string name="activity_create_closed_group_edit_text_hint">Wpisz nazwę grupy</string>
|
||||
<string name="activity_create_closed_group_explanation">Grupy zamknięte obsługują do 10 członków i zapewniają taką samą ochronę prywatności jak sesje jeden na jednego.</string>
|
||||
<string name="activity_create_closed_group_explanation">Grupy zamknięte obsługują do 10 członków i zapewniają taką samą ochronę prywatności jak sesje jeden do jednego.</string>
|
||||
<string name="activity_create_closed_group_empty_state_message">Nie masz jeszcze żadnych kontaktów</string>
|
||||
<string name="activity_create_closed_group_empty_state_button_title">Rozpocznij sesję</string>
|
||||
<string name="activity_create_closed_group_group_name_missing_error">Wpisz nazwę grupy</string>
|
||||
@ -1452,7 +1452,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="activity_create_closed_group_too_many_group_members_error">Grupa zamknięta nie może mieć więcej niż 20 członków</string>
|
||||
<string name="activity_create_closed_group_invalid_session_id_error">Jeden z członków Twojej grupy ma nieprawidłowy identyfikator Session</string>
|
||||
|
||||
<string name="activity_join_public_chat_title">Dołącz do Open Group</string>
|
||||
<string name="activity_join_public_chat_title">Dołącz do otwartej grupy</string>
|
||||
<string name="activity_join_public_chat_error">Nie można dołączyć do grupy</string>
|
||||
<string name="activity_join_public_chat_enter_group_url_tab_title">Otwórz adres URL grupy</string>
|
||||
<string name="activity_join_public_chat_scan_qr_code_tab_title">Skanowania QR code</string>
|
||||
@ -1463,13 +1463,13 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="activity_settings_title">Ustawienia</string>
|
||||
<string name="activity_settings_display_name_edit_text_hint">Wprowadź wyświetlaną nazwe</string>
|
||||
<string name="activity_settings_display_name_missing_error">Wybierz wyświetlaną nazwę</string>
|
||||
<string name="activity_settings_invalid_display_name_error">Wybierz wyświetlaną nazwę, która składa się tylko z znaków az, AZ, 0–9 i _</string>
|
||||
<string name="activity_settings_display_name_too_long_error">Wybierz krótszą nazwę wyświetlaną</string>
|
||||
<string name="activity_settings_invalid_display_name_error">Wybierz wyświetlaną nazwę, która składa się tylko ze znaków az, AZ, 0–9 i _</string>
|
||||
<string name="activity_settings_display_name_too_long_error">Wybierz krótszą wyświetlaną nazwę</string>
|
||||
<string name="activity_settings_privacy_button_title">Prywatność</string>
|
||||
<string name="activity_settings_notifications_button_title">Powiadomienia</string>
|
||||
<string name="activity_settings_chats_button_title">Czaty</string>
|
||||
<string name="activity_settings_devices_button_title">Urządzenia</string>
|
||||
<string name="activity_settings_recovery_phrase_button_title">Zwrot odzyskiwania</string>
|
||||
<string name="activity_settings_recovery_phrase_button_title">Fraza odzyskiwania</string>
|
||||
<string name="activity_settings_clear_all_data_button_title">Wyczyść dane</string>
|
||||
|
||||
<string name="activity_notification_settings_title">Powiadomienia</string>
|
||||
@ -1482,9 +1482,9 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
|
||||
<string name="activity_linked_devices_title">Urządzenia</string>
|
||||
<string name="activity_linked_devices_multi_device_limit_reached_dialog_title">Osiągnięto limit urządzeń</string>
|
||||
<string name="activity_linked_devices_multi_device_limit_reached_dialog_explanation">Obecnie nie można łączyć więcej niż jednego urządzenia.</string>
|
||||
<string name="activity_linked_devices_multi_device_limit_reached_dialog_explanation">Obecnie nie można podłączyć więcej niż jednego urządzenia.</string>
|
||||
<string name="activity_linked_devices_unlinking_failed_message">Nie można odłączyć urządzenia.</string>
|
||||
<string name="activity_linked_devices_unlinking_successful_message">Twoje urządzenie zostało rozłączone pomyślnie</string>
|
||||
<string name="activity_linked_devices_unlinking_successful_message">Twoje urządzenie zostało odłączone pomyślnie</string>
|
||||
<string name="activity_linked_devices_linking_failed_message">Nie można połączyć urządzenia.</string>
|
||||
<string name="activity_linked_devices_empty_state_message">Nie podłączyłeś jeszcze żadnych urządzeń</string>
|
||||
<string name="activity_linked_devices_empty_state_button_title">Połącz urządzenie (Beta)</string>
|
||||
@ -1502,7 +1502,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="dialog_link_device_master_mode_explanation_1">Pobierz Sesję na drugie urządzenie i dotknij Połącz z istniejącym kontem u dołu ekranu docelowego. Jeśli masz już konto na drugim urządzeniu, najpierw musisz je usunąć.</string>
|
||||
<string name="dialog_link_device_master_mode_explanation_2">Sprawdź, czy poniższe słowa odpowiadają słowom wyświetlanym na drugim urządzeniu.</string>
|
||||
<string name="dialog_link_device_master_mode_explanation_3">Poczekaj, aż łącze urządzenia zostanie utworzone. Może to potrwać do minuty.</string>
|
||||
<string name="dialog_link_device_master_mode_authorize_button_title">Autoryzować</string>
|
||||
<string name="dialog_link_device_master_mode_authorize_button_title">Autoryzuj</string>
|
||||
|
||||
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Zmień nazwę</string>
|
||||
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Odłącz urządzenie</string>
|
||||
@ -1510,10 +1510,10 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="dialog_edit_device_name_edit_text_hint">Wpisz imię</string>
|
||||
|
||||
<string name="dialog_seed_title">Twoja fraza odzyskiwania</string>
|
||||
<string name="dialog_seed_explanation">To jest twoja fraza odzyskiwania. Dzięki niemu możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie.</string>
|
||||
<string name="dialog_seed_explanation">To jest twoja fraza odzyskiwania. Dzięki niej możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie.</string>
|
||||
|
||||
<string name="dialog_clear_all_data_title">Wyczyść wszystkie dane</string>
|
||||
<string name="dialog_clear_all_data_explanation">Spowoduje to trwałe usunięcie wiadomości, Session i kontaktów.</string>
|
||||
<string name="dialog_clear_all_data_explanation">Spowoduje to trwałe usunięcie wiadomości, sesji i kontaktów.</string>
|
||||
|
||||
<string name="activity_qr_code_title">Kod QR</string>
|
||||
<string name="activity_qr_code_view_my_qr_code_tab_title">Wyświetl mój kod QR</string>
|
||||
@ -1524,8 +1524,8 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
|
||||
<string name="fragment_view_my_qr_code_share_title">Udostępnij kod QR</string>
|
||||
|
||||
<string name="session_reset_banner_message">Czy chcesz przywrócić sesję za pomocą %s?</string>
|
||||
<string name="session_reset_banner_dismiss_button_title">Oddalić</string>
|
||||
<string name="session_reset_banner_restore_button_title">Przywracać</string>
|
||||
<string name="session_reset_banner_dismiss_button_title">Anuluj</string>
|
||||
<string name="session_reset_banner_restore_button_title">Przywróć</string>
|
||||
|
||||
<string name="fragment_contact_selection_contacts_title">Łączność</string>
|
||||
<string name="fragment_contact_selection_closed_groups_title">Grupy zamknięte</string>
|
||||
|
@ -62,7 +62,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollListener;
|
||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
|
||||
import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller;
|
||||
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
|
||||
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
|
||||
@ -390,7 +390,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
LocalBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
BackgroundPollListener.schedule(this); // Loki
|
||||
BackgroundPollWorker.schedulePeriodic(this); // Loki
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
|
@ -348,7 +348,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true, Collections.singletonList(local));
|
||||
Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getOrCreateThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
@ -767,7 +767,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
.setBlocked(recipient, blocked);
|
||||
|
||||
if (recipient.isGroupRecipient() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.getAddress().toGroupString())) {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
Optional<OutgoingGroupMediaMessage> leaveMessage = GroupUtil.createGroupLeaveMessage(context, recipient);
|
||||
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
@ -776,7 +776,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
String groupId = recipient.getAddress().toGroupString();
|
||||
groupDatabase.setActive(groupId, false);
|
||||
groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
groupDatabase.removeMember(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
} else {
|
||||
Log.w(TAG, "Failed to leave group. Can't block.");
|
||||
Toast.makeText(context, R.string.RecipientPreferenceActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
|
@ -100,6 +100,16 @@ public class LinkPreviewView extends FrameLayout {
|
||||
site.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
|
||||
setLinkPreview(glideRequests, linkPreview, showThumbnail);
|
||||
if (showCloseButton) {
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
} else {
|
||||
closeButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
@ -107,6 +117,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
|
||||
|
@ -197,7 +197,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
|
||||
|
||||
String quoteeDisplayName = author.toShortString();
|
||||
|
||||
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(conversationRecipient);
|
||||
String senderHexEncodedPublicKey = author.getAddress().serialize();
|
||||
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
|
||||
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
|
||||
|
@ -90,7 +90,7 @@ public class TypingStatusSender {
|
||||
Set<String> linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.getAddress().serialize());
|
||||
for (String device : linkedDevices) {
|
||||
Recipient deviceAsRecipient = Recipient.from(context, Address.fromSerialized(device), false);
|
||||
long deviceThreadID = threadDatabase.getThreadIdFor(deviceAsRecipient);
|
||||
long deviceThreadID = threadDatabase.getOrCreateThreadIdFor(deviceAsRecipient);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +156,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity;
|
||||
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
|
||||
import org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabaseDelegate;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
@ -163,6 +164,7 @@ import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
|
||||
import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
|
||||
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
|
||||
import org.thoughtcrime.securesms.loki.views.SessionRestoreBannerView;
|
||||
@ -462,20 +464,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||
if (publicChat != null) {
|
||||
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(this).getPublicChatAPI();
|
||||
publicChatAPI.getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success(info -> {
|
||||
String groupId = GroupUtil.getEncodedOpenGroupId(publicChat.getId().getBytes());
|
||||
|
||||
publicChatAPI.updateProfileIfNeeded(
|
||||
publicChat.getChannel(),
|
||||
publicChat.getServer(),
|
||||
groupId,
|
||||
info,
|
||||
false);
|
||||
|
||||
runOnUiThread(ConversationActivity.this::updateSubtitleTextView);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
// Request open group info update and handle the successful result in #onOpenGroupInfoUpdated().
|
||||
PublicChatInfoUpdateWorker.scheduleInstant(this, publicChat.getServer(), publicChat.getChannel());
|
||||
}
|
||||
|
||||
View rootView = findViewById(R.id.rootView);
|
||||
@ -1940,6 +1930,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onOpenGroupInfoUpdated(OpenGroupUtilities.GroupInfoUpdatedEvent event) {
|
||||
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
|
||||
if (publicChat != null &&
|
||||
publicChat.getChannel() == event.getChannel() &&
|
||||
publicChat.getServer().equals(event.getUrl())) {
|
||||
this.updateSubtitleTextView();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeReceivers() {
|
||||
securityUpdateReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@ -2095,7 +2095,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
long threadId = params[0];
|
||||
|
||||
if (drafts.size() > 0) {
|
||||
if (threadId == -1) threadId = threadDatabase.getThreadIdFor(getRecipient(), thisDistributionType);
|
||||
if (threadId == -1) threadId = threadDatabase.getOrCreateThreadIdFor(getRecipient(), thisDistributionType);
|
||||
|
||||
draftDatabase.insertDrafts(threadId, drafts);
|
||||
threadDatabase.updateSnippet(threadId, drafts.getSnippet(ConversationActivity.this),
|
||||
@ -2370,7 +2370,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient.getAddress().isEmail() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
LinkPreviewUtil.isWhitelistedMediaUrl(message) || // Loki - Send GIFs as media messages
|
||||
LinkPreviewUtil.isValidMediaUrl(message) || // Loki - Send GIFs as media messages
|
||||
needsSplit;
|
||||
|
||||
Log.i(TAG, "isManual Selection: " + sendButton.isManualSelection());
|
||||
|
@ -500,7 +500,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
private void adjustMarginsIfNeeded(MessageRecord messageRecord) {
|
||||
LinearLayout.LayoutParams bodyTextLayoutParams = (LinearLayout.LayoutParams)bodyText.getLayoutParams();
|
||||
bodyTextLayoutParams.topMargin = 0;
|
||||
if (hasOnlyThumbnail(messageRecord)) {
|
||||
if (hasOnlyThumbnail(messageRecord) || hasLinkPreview(messageRecord)) {
|
||||
int topPadding = 0;
|
||||
if (groupSenderHolder.getVisibility() == VISIBLE) {
|
||||
topPadding = (int)getResources().getDimension(R.dimen.medium_spacing);
|
||||
@ -583,7 +583,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false, false);
|
||||
|
||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, true);
|
||||
@ -591,7 +591,7 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParams(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
} else {
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true);
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, false);
|
||||
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
|
||||
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
@ -601,7 +601,6 @@ public class ConversationItem extends TapJackingProofLinearLayout
|
||||
linkPreviewStub.get().setOnClickListener(linkPreviewClickListener);
|
||||
linkPreviewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
|
@ -218,6 +218,18 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public boolean delete(@NonNull String groupId) {
|
||||
int result = databaseHelper.getWritableDatabase().delete(TABLE_NAME, GROUP_ID + " = ?", new String[]{groupId});
|
||||
|
||||
if (result > 0) {
|
||||
Recipient.removeCached(Address.fromSerialized(groupId));
|
||||
notifyConversationListListeners();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(String groupId, String title, SignalServiceAttachmentPointer avatar) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
if (title != null) contentValues.put(TITLE, title);
|
||||
@ -262,7 +274,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
long avatarId;
|
||||
|
||||
if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong());
|
||||
else avatarId = 0;
|
||||
else avatarId = 0;
|
||||
|
||||
|
||||
ContentValues contentValues = new ContentValues(2);
|
||||
@ -300,7 +312,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?", new String[] {groupId});
|
||||
}
|
||||
|
||||
public void remove(String groupId, Address source) {
|
||||
public void removeMember(String groupId, Address source) {
|
||||
List<Address> currentMembers = getCurrentMembers(groupId);
|
||||
currentMembers.remove(source);
|
||||
|
||||
@ -352,13 +364,21 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
||||
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId});
|
||||
}
|
||||
|
||||
|
||||
public byte[] allocateGroupId() {
|
||||
byte[] groupId = new byte[16];
|
||||
new SecureRandom().nextBytes(groupId);
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean hasGroup(@NonNull String groupId) {
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(
|
||||
"SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1",
|
||||
new String[]{groupId}
|
||||
)) {
|
||||
return cursor.getCount() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Reader implements Closeable {
|
||||
|
||||
private final Cursor cursor;
|
||||
|
@ -321,10 +321,10 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
private long getThreadIdFor(IncomingMediaMessage retrieved) throws RecipientFormattingException, MmsException {
|
||||
if (retrieved.getGroupId() != null) {
|
||||
Recipient groupRecipients = Recipient.from(context, retrieved.getGroupId(), true);
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
|
||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipients);
|
||||
} else {
|
||||
Recipient sender = Recipient.from(context, retrieved.getFrom(), true);
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender);
|
||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(sender);
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,7 +333,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
? Util.toIsoString(notification.getFrom().getTextString())
|
||||
: "";
|
||||
Recipient recipient = Recipient.from(context, Address.fromExternal(context, fromString), false);
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
}
|
||||
|
||||
private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
|
||||
|
@ -556,7 +556,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
private @NonNull Pair<Long, Long> insertCallLog(@NonNull Address address, long type, boolean unread) {
|
||||
Recipient recipient = Recipient.from(context, address, true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
|
||||
ContentValues values = new ContentValues(6);
|
||||
values.put(ADDRESS, address.serialize());
|
||||
@ -620,8 +620,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
long threadId;
|
||||
|
||||
if (groupRecipient == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
if (groupRecipient == null) threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
else threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
|
||||
|
||||
ContentValues values = new ContentValues(6);
|
||||
values.put(ADDRESS, message.getSender().serialize());
|
||||
|
@ -209,7 +209,7 @@ public class SmsMigrator {
|
||||
|
||||
if (ourRecipients != null) {
|
||||
if (ourRecipients.size() == 1) {
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourRecipients.iterator().next());
|
||||
long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourRecipients.iterator().next());
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
} else if (ourRecipients.size() > 1) {
|
||||
ourRecipients.add(Recipient.from(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), true));
|
||||
@ -222,7 +222,7 @@ public class SmsMigrator {
|
||||
|
||||
String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true, null);
|
||||
Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true);
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
long ourThreadId = threadDatabase.getOrCreateThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
||||
migrateConversation(context, listener, progress, theirThreadId, ourThreadId);
|
||||
}
|
||||
|
@ -515,11 +515,11 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public long getThreadIdFor(Recipient recipient) {
|
||||
return getThreadIdFor(recipient, DistributionTypes.DEFAULT);
|
||||
public long getOrCreateThreadIdFor(Recipient recipient) {
|
||||
return getOrCreateThreadIdFor(recipient, DistributionTypes.DEFAULT);
|
||||
}
|
||||
|
||||
public long getThreadIdFor(Recipient recipient, int distributionType) {
|
||||
public long getOrCreateThreadIdFor(Recipient recipient, int distributionType) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = ADDRESS + " = ?";
|
||||
String[] recipientsArg = new String[]{recipient.getAddress().serialize()};
|
||||
|
@ -94,8 +94,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV15 = 36;
|
||||
private static final int lokiV16 = 37;
|
||||
private static final int lokiV17 = 38;
|
||||
private static final int lokiV18_CLEAR_BG_POLL_JOBS = 39;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV17;
|
||||
private static final int DATABASE_VERSION = lokiV18_CLEAR_BG_POLL_JOBS; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -634,7 +635,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
if (oldVersion < lokiV15) {
|
||||
db.execSQL(SharedSenderKeysDatabase.getCreateOldClosedGroupRatchetTableCommand());
|
||||
}
|
||||
|
||||
|
||||
if (oldVersion < lokiV16) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
|
||||
}
|
||||
@ -644,6 +645,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN audio_duration INTEGER");
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV18_CLEAR_BG_POLL_JOBS) {
|
||||
// BackgroundPollJob was replaced with BackgroundPollWorker. Clear all the scheduled job records.
|
||||
db.execSQL("DELETE FROM job_spec WHERE factory_key = 'BackgroundPollJob'");
|
||||
db.execSQL("DELETE FROM constraint_spec WHERE factory_key = 'BackgroundPollJob'");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -62,7 +62,7 @@ public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMed
|
||||
@Override
|
||||
public BucketedThreadMedia loadInBackground() {
|
||||
BucketedThreadMedia result = new BucketedThreadMedia(getContext());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.from(getContext(), address, true));
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(Recipient.from(getContext(), address, true));
|
||||
|
||||
DatabaseFactory.getMediaDatabase(getContext()).subscribeToMediaChanges(observer);
|
||||
try (Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId)) {
|
||||
|
@ -34,7 +34,7 @@ public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
|
||||
@Nullable
|
||||
@Override
|
||||
public Pair<Cursor, Integer> loadInBackground() {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(recipient);
|
||||
Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
|
@ -23,7 +23,7 @@ public class ThreadMediaLoader extends AbstractCursorLoader {
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.from(getContext(), address, true));
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getOrCreateThreadIdFor(Recipient.from(getContext(), address, true));
|
||||
|
||||
if (gallery) return DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
|
||||
else return DatabaseFactory.getMediaDatabase(getContext()).getDocumentMediaForThread(threadId);
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@ -89,7 +90,8 @@ public class GroupManager {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
|
||||
} else {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(
|
||||
groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
}
|
||||
@ -127,10 +129,30 @@ public class GroupManager {
|
||||
|
||||
groupDatabase.updateProfilePicture(groupId, avatarBytes);
|
||||
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(
|
||||
groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadID);
|
||||
}
|
||||
|
||||
public static boolean deleteGroup(@NonNull String groupId,
|
||||
@NonNull Context context)
|
||||
{
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
|
||||
|
||||
if (!groupDatabase.getGroup(groupId).isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long threadId = threadDatabase.getThreadIdIfExistsFor(groupRecipient);
|
||||
if (threadId != -1L) {
|
||||
threadDatabase.deleteConversation(threadId);
|
||||
}
|
||||
|
||||
return groupDatabase.delete(groupId);
|
||||
}
|
||||
|
||||
public static GroupActionResult updateGroup(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull Set<Recipient> members,
|
||||
@ -154,7 +176,7 @@ public class GroupManager {
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ public class GroupMessageProcessor {
|
||||
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
|
||||
if (masterDevice == null) { masterDevice = content.getSender(); }
|
||||
if (members.contains(Address.fromExternal(context, masterDevice))) {
|
||||
database.remove(id, Address.fromExternal(context, masterDevice));
|
||||
database.removeMember(id, Address.fromExternal(context, masterDevice));
|
||||
if (outgoing) database.setActive(id, false);
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
@ -260,7 +260,7 @@ public class GroupMessageProcessor {
|
||||
Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
mmsDatabase.markAsSent(messageId, true);
|
||||
|
@ -1,17 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = BootReceiver.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.i(TAG, "Boot received. Application is created, kickstarting JobManager.");
|
||||
}
|
||||
}
|
@ -23,6 +23,10 @@ import java.util.concurrent.TimeUnit;
|
||||
* {@link #serialize()}. Your job is then recreated using a {@link Factory} that you register in
|
||||
* {@link JobManager.Configuration.Builder#setJobFactories(Map)}, which is given the saved
|
||||
* {@link Data} bundle.
|
||||
*
|
||||
* @deprecated
|
||||
* use <a href="https://developer.android.com/reference/androidx/work/WorkManager">WorkManager</a>
|
||||
* API instead.
|
||||
*/
|
||||
public abstract class Job {
|
||||
|
||||
|
@ -7,7 +7,6 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.DefaultExecutorFactory;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||
import org.thoughtcrime.securesms.jobmanager.migration.WorkManagerMigrator;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
@ -52,11 +51,6 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
this::onEmptyQueue);
|
||||
|
||||
executor.execute(() -> {
|
||||
if (WorkManagerMigrator.needsMigration(application)) {
|
||||
Log.i(TAG, "Detected an old WorkManager database. Migrating.");
|
||||
WorkManagerMigrator.migrate(application, configuration.getJobStorage(), configuration.getDataSerializer());
|
||||
}
|
||||
|
||||
jobController.init();
|
||||
|
||||
for (int i = 0; i < jobRunners.length; i++) {
|
||||
|
@ -1,101 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class WorkManagerDatabase extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = WorkManagerDatabase.class.getSimpleName();
|
||||
|
||||
static final String DB_NAME = "androidx.work.workdb";
|
||||
|
||||
WorkManagerDatabase(@NonNull Context context) {
|
||||
super(context, DB_NAME, null, 5);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
throw new UnsupportedOperationException("We should never be creating this database, only migrating an existing one!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// There's a chance that a user who hasn't upgraded in > 6 months could hit this onUpgrade path,
|
||||
// but we don't use any of the columns that were added in any migrations they could hit, so we
|
||||
// can ignore this.
|
||||
Log.w(TAG, "Hit onUpgrade path from " + oldVersion + " to " + newVersion);
|
||||
}
|
||||
|
||||
@NonNull List<FullSpec> getAllJobs(@NonNull Data.Serializer dataSerializer) {
|
||||
SQLiteDatabase db = getReadableDatabase();
|
||||
String[] columns = new String[] { "id", "worker_class_name", "input", "required_network_type"};
|
||||
List<FullSpec> fullSpecs = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query("WorkSpec", columns, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String factoryName = WorkManagerFactoryMappings.getFactoryKey(cursor.getString(cursor.getColumnIndexOrThrow("worker_class_name")));
|
||||
|
||||
if (factoryName != null) {
|
||||
String id = cursor.getString(cursor.getColumnIndexOrThrow("id"));
|
||||
byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow("input"));
|
||||
|
||||
List<ConstraintSpec> constraints = new LinkedList<>();
|
||||
JobSpec jobSpec = new JobSpec(id,
|
||||
factoryName,
|
||||
getQueueKey(id),
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
0,
|
||||
Job.Parameters.UNLIMITED,
|
||||
TimeUnit.SECONDS.toMillis(30),
|
||||
TimeUnit.DAYS.toMillis(1),
|
||||
Job.Parameters.UNLIMITED,
|
||||
dataSerializer.serialize(DataMigrator.convert(data)),
|
||||
false);
|
||||
|
||||
|
||||
|
||||
if (cursor.getInt(cursor.getColumnIndexOrThrow("required_network_type")) != 0) {
|
||||
constraints.add(new ConstraintSpec(id, NetworkConstraint.KEY));
|
||||
}
|
||||
|
||||
fullSpecs.add(new FullSpec(jobSpec, constraints, Collections.emptyList()));
|
||||
} else {
|
||||
Log.w(TAG, "Failed to find a matching factory for worker class: " + factoryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fullSpecs;
|
||||
}
|
||||
|
||||
private @Nullable String getQueueKey(@NonNull String jobId) {
|
||||
String query = "work_spec_id = ?";
|
||||
String[] args = new String[] { jobId };
|
||||
|
||||
try (Cursor cursor = getReadableDatabase().query("WorkName", null, query, args, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow("name"));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
@ -61,7 +60,6 @@ public class WorkManagerFactoryMappings {
|
||||
put(AttachmentDownloadJob.class.getName(), AttachmentDownloadJob.KEY);
|
||||
put(AttachmentUploadJob.class.getName(), AttachmentUploadJob.KEY);
|
||||
put(AvatarDownloadJob.class.getName(), AvatarDownloadJob.KEY);
|
||||
put(BackgroundPollJob.class.getName(), BackgroundPollJob.KEY);
|
||||
put(CleanPreKeysJob.class.getName(), CleanPreKeysJob.KEY);
|
||||
put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY);
|
||||
put(CreateSignedPreKeyJob.class.getName(), CreateSignedPreKeyJob.KEY);
|
||||
|
@ -1,45 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.migration;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobStorage;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WorkManagerMigrator {
|
||||
|
||||
private static final String TAG = Log.tag(WorkManagerMigrator.class);
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@WorkerThread
|
||||
public static synchronized void migrate(@NonNull Context context,
|
||||
@NonNull JobStorage jobStorage,
|
||||
@NonNull Data.Serializer dataSerializer)
|
||||
{
|
||||
long startTime = System.currentTimeMillis();
|
||||
Log.i(TAG, "Beginning WorkManager migration.");
|
||||
|
||||
WorkManagerDatabase database = new WorkManagerDatabase(context);
|
||||
List<FullSpec> fullSpecs = database.getAllJobs(dataSerializer);
|
||||
|
||||
for (FullSpec fullSpec : fullSpecs) {
|
||||
Log.i(TAG, String.format("Migrating job with key '%s' and %d constraint(s).", fullSpec.getJobSpec().getFactoryKey(), fullSpec.getConstraintSpecs().size()));
|
||||
}
|
||||
|
||||
jobStorage.insertJobs(fullSpecs);
|
||||
|
||||
context.deleteDatabase(WorkManagerDatabase.DB_NAME);
|
||||
Log.i(TAG, String.format("WorkManager migration finished. Migrated %d job(s) in %d ms.", fullSpecs.size(), System.currentTimeMillis() - startTime));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static synchronized boolean needsMigration(@NonNull Context context) {
|
||||
return context.getDatabasePath(WorkManagerDatabase.DB_NAME).exists();
|
||||
}
|
||||
}
|
@ -6,6 +6,11 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use <a href="https://developer.android.com/reference/androidx/work/WorkManager">WorkManager</a>
|
||||
* API instead.
|
||||
*/
|
||||
public abstract class BaseJob extends Job {
|
||||
|
||||
private static final String TAG = BaseJob.class.getSimpleName();
|
||||
|
@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.loki.api.BackgroundPollJob;
|
||||
import org.thoughtcrime.securesms.loki.api.PrepareAttachmentAudioExtrasJob;
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
@ -34,7 +33,6 @@ public final class JobManagerFactories {
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
|
||||
put(BackgroundPollJob.KEY, new BackgroundPollJob.Factory());
|
||||
put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory());
|
||||
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
|
||||
|
@ -498,7 +498,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1);
|
||||
OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
|
||||
if (!recipient.isGroupRecipient()) {
|
||||
// TODO: Handle session reset on sync messages
|
||||
@ -808,7 +808,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (result.getMessageId() > -1) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
||||
}
|
||||
}
|
||||
@ -822,7 +822,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
message.getTimestamp(),
|
||||
message.getMessage().getExpiresInSeconds() * 1000L);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
@ -864,7 +864,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
}
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipients);
|
||||
|
||||
database.beginTransaction();
|
||||
|
||||
@ -995,7 +995,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (result.getMessageId() > -1) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
||||
}
|
||||
}
|
||||
@ -1018,7 +1018,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
}
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
boolean isGroup = recipient.getAddress().isGroup();
|
||||
|
||||
MessagingDatabase database;
|
||||
@ -1102,7 +1102,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (canRecoverAutomatically(e)) {
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false);
|
||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(context);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
threadDB.addSessionRestoreDevice(threadID, sender);
|
||||
SessionManagementProtocol.startSessionReset(context, sender);
|
||||
} else {
|
||||
@ -1249,7 +1249,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
} else {
|
||||
// See if we need to redirect the message
|
||||
author = getMessageMasterDestination(content.getSender());
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author);
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(author);
|
||||
}
|
||||
|
||||
if (threadId <= 0) {
|
||||
@ -1368,7 +1368,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Optional<String> title = Optional.fromNullable(preview.getTitle());
|
||||
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
|
||||
boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
|
||||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
|
||||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidLinkUrl(url.get());
|
||||
|
||||
if (hasContent && presentInBody && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail);
|
||||
@ -1459,7 +1459,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) {
|
||||
Recipient author = Recipient.from(context, Address.fromSerialized(sender), false);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(conversationRecipient);
|
||||
|
||||
if (threadId > 0) {
|
||||
Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message.");
|
||||
|
@ -1,50 +0,0 @@
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class LinkPreviewDomains {
|
||||
public static final String STICKERS = "signal.org";
|
||||
|
||||
public static final Set<String> LINKS = new HashSet<>(Arrays.asList(
|
||||
// YouTube
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"youtu.be",
|
||||
// Reddit
|
||||
"reddit.com",
|
||||
"www.reddit.com",
|
||||
"m.reddit.com",
|
||||
// Imgur
|
||||
"imgur.com",
|
||||
"www.imgur.com",
|
||||
"m.imgur.com",
|
||||
// Instagram
|
||||
"instagram.com",
|
||||
"www.instagram.com",
|
||||
"m.instagram.com",
|
||||
// Pinterest
|
||||
"pinterest.com",
|
||||
"www.pinterest.com",
|
||||
"pin.it",
|
||||
// Giphy
|
||||
"giphy.com",
|
||||
"media.giphy.com",
|
||||
"media1.giphy.com",
|
||||
"media2.giphy.com",
|
||||
"media3.giphy.com",
|
||||
"gph.is"
|
||||
));
|
||||
|
||||
public static final Set<String> IMAGES = new HashSet<>(Arrays.asList(
|
||||
"ytimg.com",
|
||||
"cdninstagram.com",
|
||||
"fbcdn.net",
|
||||
"redd.it",
|
||||
"imgur.com",
|
||||
"pinimg.com",
|
||||
"giphy.com"
|
||||
));
|
||||
}
|
@ -2,20 +2,21 @@ package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.google.android.gms.common.util.IOUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.net.CallRequestController;
|
||||
@ -34,9 +35,11 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest.StickerInfo;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil.OpenGraph;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
@ -73,7 +76,7 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
RequestController getLinkPreview(@NonNull Context context, @NonNull String url, @NonNull Callback<Optional<LinkPreview>> callback) {
|
||||
CompositeRequestController compositeController = new CompositeRequestController();
|
||||
|
||||
if (!LinkPreviewUtil.isWhitelistedLinkUrl(url)) {
|
||||
if (!LinkPreviewUtil.isValidLinkUrl(url)) {
|
||||
Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain.");
|
||||
callback.onComplete(Optional.absent());
|
||||
return compositeController;
|
||||
@ -112,7 +115,8 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
}
|
||||
|
||||
private @NonNull RequestController fetchMetadata(@NonNull String url, Callback<Metadata> callback) {
|
||||
Call call = client.newCall(new Request.Builder().url(url).cacheControl(NO_CACHE).build());
|
||||
Call call = client.newCall(new Request.Builder().url(url).removeHeader("User-Agent").addHeader("User-Agent",
|
||||
"WhatsApp").cacheControl(NO_CACHE).build());
|
||||
|
||||
call.enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
@ -134,14 +138,20 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
}
|
||||
|
||||
String body = response.body().string();
|
||||
Optional<String> title = getProperty(body, "title");
|
||||
Optional<String> imageUrl = getProperty(body, "image");
|
||||
OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body);
|
||||
Optional<String> title = openGraph.getTitle();
|
||||
Optional<String> imageUrl = openGraph.getImageUrl();
|
||||
|
||||
if (imageUrl.isPresent() && !LinkPreviewUtil.isWhitelistedMediaUrl(imageUrl.get())) {
|
||||
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidMediaUrl(imageUrl.get())) {
|
||||
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
|
||||
imageUrl = Optional.absent();
|
||||
}
|
||||
|
||||
if (imageUrl.isPresent() && !LinkPreviewUtil.isVaildMimeType(imageUrl.get())) {
|
||||
Log.i(TAG, "Image URL was invalid mime type. Skipping.");
|
||||
imageUrl = Optional.absent();
|
||||
}
|
||||
|
||||
callback.onComplete(new Metadata(title, imageUrl));
|
||||
}
|
||||
});
|
||||
@ -150,60 +160,36 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
}
|
||||
|
||||
private @NonNull RequestController fetchThumbnail(@NonNull Context context, @NonNull String imageUrl, @NonNull Callback<Optional<Attachment>> callback) {
|
||||
FutureTarget<Bitmap> bitmapFuture = GlideApp.with(context).asBitmap()
|
||||
.load(new ChunkedImageUrl(imageUrl))
|
||||
.skipMemoryCache(true)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.centerInside()
|
||||
.submit(1024, 1024);
|
||||
|
||||
RequestController controller = () -> bitmapFuture.cancel(false);
|
||||
Call call = client.newCall(new Request.Builder().url(imageUrl).build());
|
||||
CallRequestController controller = new CallRequestController(call);
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
Bitmap bitmap = bitmapFuture.get();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Response response = call.execute();
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
controller.cancel();
|
||||
callback.onComplete(Optional.absent());
|
||||
return;
|
||||
}
|
||||
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
|
||||
InputStream bodyStream = response.body().byteStream();
|
||||
controller.setStream(bodyStream);
|
||||
|
||||
byte[] bytes = baos.toByteArray();
|
||||
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
|
||||
Optional<Attachment> thumbnail = Optional.of(new UriAttachment(uri,
|
||||
uri,
|
||||
MediaUtil.IMAGE_JPEG,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
bytes.length,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null));
|
||||
byte[] data = IOUtils.readInputStreamFully(bodyStream);
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
Optional<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.JPEG, MediaUtil.IMAGE_JPEG);
|
||||
|
||||
if (bitmap != null) bitmap.recycle();
|
||||
|
||||
callback.onComplete(thumbnail);
|
||||
} catch (CancellationException | ExecutionException | InterruptedException e) {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Exception during link preview image retrieval.", e);
|
||||
controller.cancel();
|
||||
callback.onComplete(Optional.absent());
|
||||
} finally {
|
||||
bitmapFuture.cancel(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () -> bitmapFuture.cancel(true);
|
||||
}
|
||||
|
||||
private @NonNull Optional<String> getProperty(@NonNull String searchText, @NonNull String property) {
|
||||
Pattern pattern = Pattern.compile("<\\s*meta\\s+property\\s*=\\s*\"\\s*og:" + property + "\\s*\"\\s+[^>]*content\\s*=\\s*\"(.*?)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
|
||||
Matcher matcher = pattern.matcher(searchText);
|
||||
|
||||
if (matcher.find()) {
|
||||
String text = Html.fromHtml(matcher.group(1)).toString();
|
||||
return TextUtils.isEmpty(text) ? Optional.absent() : Optional.of(text);
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
return controller;
|
||||
}
|
||||
|
||||
private RequestController fetchStickerPackLinkPreview(@NonNull Context context,
|
||||
@ -266,6 +252,38 @@ public class LinkPreviewRepository implements InjectableType {
|
||||
return () -> Log.i(TAG, "Cancelled sticker pack link preview fetch -- no effect.");
|
||||
}
|
||||
|
||||
private static Optional<Attachment> bitmapToAttachment(@Nullable Bitmap bitmap,
|
||||
@NonNull Bitmap.CompressFormat format,
|
||||
@NonNull String contentType)
|
||||
{
|
||||
if (bitmap == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
bitmap.compress(format, 80, baos);
|
||||
|
||||
byte[] bytes = baos.toByteArray();
|
||||
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
|
||||
|
||||
return Optional.of(new UriAttachment(uri,
|
||||
uri,
|
||||
contentType,
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_STARTED,
|
||||
bytes.length,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null));
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class Metadata {
|
||||
private final Optional<String> title;
|
||||
private final Optional<String> imageUrl;
|
||||
|
@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.URLSpan;
|
||||
@ -10,9 +13,15 @@ import android.text.util.Linkify;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ -20,10 +29,15 @@ import okhttp3.HttpUrl;
|
||||
|
||||
public final class LinkPreviewUtil {
|
||||
|
||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
|
||||
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
|
||||
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
|
||||
private static final Pattern STICKER_URL_PATTERN = Pattern.compile("^.*#pack_id=(.*)&pack_key=(.*)$");
|
||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* @return All whitelisted URLs in the source text.
|
||||
@ -38,14 +52,14 @@ public final class LinkPreviewUtil {
|
||||
|
||||
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
|
||||
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
|
||||
.filter(link -> isWhitelistedLinkUrl(link.getUrl()))
|
||||
.filter(link -> isValidLinkUrl(link.getUrl()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the host is present in the link whitelist.
|
||||
* @return True if the host is valid.
|
||||
*/
|
||||
public static boolean isWhitelistedLinkUrl(@Nullable String linkUrl) {
|
||||
public static boolean isValidLinkUrl(@Nullable String linkUrl) {
|
||||
if (linkUrl == null) return false;
|
||||
if (StickerUrl.isValidShareLink(linkUrl)) return true;
|
||||
|
||||
@ -53,21 +67,19 @@ public final class LinkPreviewUtil {
|
||||
return url != null &&
|
||||
!TextUtils.isEmpty(url.scheme()) &&
|
||||
"https".equals(url.scheme()) &&
|
||||
LinkPreviewDomains.LINKS.contains(url.host()) &&
|
||||
isLegalUrl(linkUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the top-level domain is present in the media whitelist.
|
||||
* @return True if the top-level domain is valid.
|
||||
*/
|
||||
public static boolean isWhitelistedMediaUrl(@Nullable String mediaUrl) {
|
||||
public static boolean isValidMediaUrl(@Nullable String mediaUrl) {
|
||||
if (mediaUrl == null) return false;
|
||||
|
||||
HttpUrl url = HttpUrl.parse(mediaUrl);
|
||||
return url != null &&
|
||||
!TextUtils.isEmpty(url.scheme()) &&
|
||||
"https".equals(url.scheme()) &&
|
||||
LinkPreviewDomains.IMAGES.contains(url.topPrivateDomain()) &&
|
||||
isLegalUrl(mediaUrl);
|
||||
}
|
||||
|
||||
@ -84,4 +96,135 @@ public final class LinkPreviewUtil {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isVaildMimeType(@NonNull String url) {
|
||||
String[] vaildMimeType = {"jpg", "png", "gif", "jpeg"};
|
||||
if (url.contains(".")) {
|
||||
String extenstion = url.substring(url.lastIndexOf(".") + 1).toLowerCase();
|
||||
return Arrays.asList(vaildMimeType).contains(extenstion);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
|
||||
return parseOpenGraphFields(html, text -> Html.fromHtml(text).toString());
|
||||
}
|
||||
|
||||
static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html, @NonNull HtmlDecoder htmlDecoder) {
|
||||
if (html == null) {
|
||||
return new OpenGraph(Collections.emptyMap(), null, null);
|
||||
}
|
||||
|
||||
Map<String, String> openGraphTags = new HashMap<>();
|
||||
Matcher openGraphMatcher = OPEN_GRAPH_TAG_PATTERN.matcher(html);
|
||||
|
||||
while (openGraphMatcher.find()) {
|
||||
String tag = openGraphMatcher.group();
|
||||
String property = openGraphMatcher.groupCount() > 0 ? openGraphMatcher.group(1) : null;
|
||||
|
||||
if (property != null) {
|
||||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||
openGraphTags.put(property.toLowerCase(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
|
||||
|
||||
while (articleMatcher.find()) {
|
||||
String tag = articleMatcher.group();
|
||||
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
|
||||
|
||||
if (property != null) {
|
||||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||
openGraphTags.put(property.toLowerCase(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String htmlTitle = "";
|
||||
String faviconUrl = "";
|
||||
|
||||
Matcher titleMatcher = TITLE_PATTERN.matcher(html);
|
||||
if (titleMatcher.find() && titleMatcher.groupCount() > 0) {
|
||||
htmlTitle = htmlDecoder.fromEncoded(titleMatcher.group(1));
|
||||
}
|
||||
|
||||
Matcher faviconMatcher = FAVICON_PATTERN.matcher(html);
|
||||
if (faviconMatcher.find()) {
|
||||
Matcher faviconHrefMatcher = FAVICON_HREF_PATTERN.matcher(faviconMatcher.group());
|
||||
if (faviconHrefMatcher.find() && faviconHrefMatcher.groupCount() > 0) {
|
||||
faviconUrl = faviconHrefMatcher.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
|
||||
}
|
||||
|
||||
private static @Nullable String parseTopLevelDomain(@NonNull String domain) {
|
||||
int periodIndex = domain.lastIndexOf(".");
|
||||
|
||||
if (periodIndex >= 0 && periodIndex < domain.length() - 1) {
|
||||
return domain.substring(periodIndex + 1);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final class OpenGraph {
|
||||
|
||||
private final Map<String, String> values;
|
||||
|
||||
private final @Nullable String htmlTitle;
|
||||
private final @Nullable String faviconUrl;
|
||||
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_DESCRIPTION_URL = "description";
|
||||
private static final String KEY_IMAGE_URL = "image";
|
||||
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
|
||||
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
|
||||
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
|
||||
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
|
||||
|
||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||
this.values = values;
|
||||
this.htmlTitle = htmlTitle;
|
||||
this.faviconUrl = faviconUrl;
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getTitle() {
|
||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_TITLE), htmlTitle));
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getImageUrl() {
|
||||
return Optional.of(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
public long getDate() {
|
||||
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
|
||||
values.get(KEY_PUBLISHED_TIME_2),
|
||||
values.get(KEY_MODIFIED_TIME_1),
|
||||
values.get(KEY_MODIFIED_TIME_2))
|
||||
.map(DateUtils::parseIso8601)
|
||||
.filter(time -> time > 0)
|
||||
.findFirst()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public @NonNull
|
||||
Optional<String> getDescription() {
|
||||
return Optional.of(values.get(KEY_DESCRIPTION_URL));
|
||||
}
|
||||
}
|
||||
|
||||
public interface HtmlDecoder {
|
||||
@NonNull String fromEncoded(@NonNull String html);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
loader.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
val threadID = DatabaseFactory.getThreadDatabase(this).getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
if (!isFinishing) {
|
||||
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
finish()
|
||||
|
@ -9,7 +9,6 @@ import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
@ -18,11 +17,15 @@ import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
@ -31,6 +34,7 @@ import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
|
||||
import org.thoughtcrime.securesms.loki.api.ResetThreadSessionJob
|
||||
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
|
||||
@ -100,24 +104,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Process any outstanding deletes
|
||||
val threadDatabase = DatabaseFactory.getThreadDatabase(this)
|
||||
val archivedConversationCount = threadDatabase.archivedConversationListCount
|
||||
if (archivedConversationCount > 0) {
|
||||
val archivedConversations = threadDatabase.archivedConversationList
|
||||
archivedConversations.moveToFirst()
|
||||
fun deleteThreadAtCurrentPosition() {
|
||||
val threadID = archivedConversations.getLong(archivedConversations.getColumnIndex(ThreadDatabase.ID))
|
||||
AsyncTask.execute {
|
||||
threadDatabase.deleteConversation(threadID)
|
||||
(applicationContext as ApplicationContext).messageNotifier.updateNotification(this)
|
||||
}
|
||||
}
|
||||
deleteThreadAtCurrentPosition()
|
||||
while (archivedConversations.moveToNext()) {
|
||||
deleteThreadAtCurrentPosition()
|
||||
}
|
||||
}
|
||||
// Double check that the long poller is up
|
||||
(applicationContext as ApplicationContext).startPollingIfNeeded()
|
||||
// Set content view
|
||||
@ -373,53 +359,56 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
val threadID = thread.threadId
|
||||
val recipient = thread.recipient
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(this)
|
||||
val deleteThread = Runnable {
|
||||
AsyncTask.execute {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID)
|
||||
if (publicChat != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity)
|
||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||
ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server)
|
||||
}
|
||||
threadDB.deleteConversation(threadID)
|
||||
ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity)
|
||||
}
|
||||
}
|
||||
val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
dialog.setMessage(dialogMessage)
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ ->
|
||||
dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) {
|
||||
val context = this@HomeActivity as Context
|
||||
|
||||
val isClosedGroup = recipient.address.isClosedGroup
|
||||
// Send a leave group message if this is an active closed group
|
||||
if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) {
|
||||
if (isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) {
|
||||
var isSSKBasedClosedGroup: Boolean
|
||||
var groupPublicKey: String?
|
||||
try {
|
||||
groupPublicKey = ClosedGroupsProtocol.doubleDecodeGroupID(recipient.address.toString()).toHexString()
|
||||
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey)
|
||||
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)
|
||||
} catch (e: IOException) {
|
||||
groupPublicKey = null
|
||||
isSSKBasedClosedGroup = false
|
||||
}
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocol.leave(this, groupPublicKey!!)
|
||||
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) {
|
||||
Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@setPositiveButton
|
||||
ClosedGroupsProtocol.leave(context, groupPublicKey!!)
|
||||
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) {
|
||||
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
// Archive the conversation and then delete it after 10 seconds (the case where the
|
||||
// app was closed before the conversation could be deleted is handled in onCreate)
|
||||
threadDB.archiveConversation(threadID)
|
||||
val delay = if (isClosedGroup) 10000L else 1000L
|
||||
val handler = Handler()
|
||||
handler.postDelayed(deleteThread, delay)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
//TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager
|
||||
if (publicChat != null) {
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatAPI!!
|
||||
.leave(publicChat.channel, publicChat.server)
|
||||
|
||||
ApplicationContext.getInstance(context).publicChatManager
|
||||
.removeChat(publicChat.server, publicChat.channel)
|
||||
} else {
|
||||
threadDB.deleteConversation(threadID)
|
||||
}
|
||||
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
|
||||
}
|
||||
|
||||
// Notify the user
|
||||
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
|
||||
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show()
|
||||
}}
|
||||
dialog.setNegativeButton(R.string.no) { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
|
@ -11,8 +11,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
@ -22,6 +26,7 @@ import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import java.lang.Exception
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
@ -67,13 +72,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
|
||||
}
|
||||
showLoader()
|
||||
val channel: Long = 1
|
||||
OpenGroupUtilities.addGroup(this, url, channel).success {
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoader()
|
||||
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
SyncMessagesProtocol.syncAllOpenGroups(this@JoinPublicChatActivity)
|
||||
}.successUi {
|
||||
finish()
|
||||
}.failUi {
|
||||
hideLoader()
|
||||
Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
withContext(Dispatchers.Main) { finish() }
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@ -123,13 +134,13 @@ class EnterChatURLFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = context!!.getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||
var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
|
||||
if (!chatURL.toLowerCase().startsWith("https")) {
|
||||
chatURL = "https://$chatURL"
|
||||
}
|
||||
(activity!! as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
}
|
||||
// endregion
|
@ -1,87 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BackgroundPollJob private constructor(parameters: Parameters) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "BackgroundPollJob"
|
||||
}
|
||||
|
||||
constructor(context: Context) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build()) {
|
||||
setContext(context)
|
||||
}
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.EMPTY
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
public override fun onRun() {
|
||||
try {
|
||||
Log.d("Loki", "Performing background poll.")
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val promises = mutableListOf<Promise<Unit, Exception>>()
|
||||
if (!TextSecurePreferences.isUsingFCM(context)) {
|
||||
Log.d("Loki", "Not using FCM; polling for contacts and closed groups.")
|
||||
val promise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes ->
|
||||
envelopes.forEach {
|
||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
||||
}
|
||||
}
|
||||
promises.add(promise)
|
||||
promises.addAll(ClosedGroupPoller.shared.pollOnce())
|
||||
}
|
||||
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value }
|
||||
for (openGroup in openGroups) {
|
||||
val poller = PublicChatPoller(context, openGroup)
|
||||
poller.stop()
|
||||
promises.add(poller.pollForNewMessages())
|
||||
}
|
||||
all(promises).get()
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Background poll failed due to error: $exception.")
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCanceled() { }
|
||||
|
||||
class Factory : Job.Factory<BackgroundPollJob> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): BackgroundPollJob {
|
||||
return BackgroundPollJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
||||
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackgroundPollListener : PersistentAlarmManagerListener() {
|
||||
|
||||
companion object {
|
||||
private val pollInterval = TimeUnit.MINUTES.toMillis(15)
|
||||
|
||||
@JvmStatic
|
||||
fun schedule(context: Context) {
|
||||
BackgroundPollListener().onReceive(context, Intent())
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNextScheduledExecutionTime(context: Context): Long {
|
||||
return TextSecurePreferences.getBackgroundPollTime(context)
|
||||
}
|
||||
|
||||
override fun onAlarm(context: Context, scheduledTime: Long): Long {
|
||||
ApplicationContext.getInstance(context).jobManager.add(BackgroundPollJob(context))
|
||||
val nextTime = System.currentTimeMillis() + pollInterval
|
||||
TextSecurePreferences.setBackgroundPollTime(context, nextTime)
|
||||
return nextTime
|
||||
}
|
||||
}
|
111
src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt
Normal file
111
src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt
Normal file
@ -0,0 +1,111 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.work.*
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "BackgroundPollWorker"
|
||||
|
||||
private const val RETRY_ATTEMPTS = 3
|
||||
|
||||
@JvmStatic
|
||||
fun scheduleInstant(context: Context) {
|
||||
val workRequest = OneTimeWorkRequestBuilder<BackgroundPollWorker>()
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueue(workRequest)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun schedulePeriodic(context: Context) {
|
||||
Log.v(TAG, "Scheduling periodic work.")
|
||||
val workRequest = PeriodicWorkRequestBuilder<BackgroundPollWorker>(15, TimeUnit.MINUTES)
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(
|
||||
TAG,
|
||||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
if (TextSecurePreferences.getLocalNumber(context) == null) {
|
||||
Log.v(TAG, "Background poll is canceled due to the Session user is not set up yet.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
try {
|
||||
Log.v(TAG, "Performing background poll.")
|
||||
val promises = mutableListOf<Promise<Unit, Exception>>()
|
||||
|
||||
// Private chats
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val privateChatsPromise = SnodeAPI.shared.getMessages(userPublicKey).map { envelopes ->
|
||||
envelopes.forEach {
|
||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
||||
}
|
||||
}
|
||||
promises.add(privateChatsPromise)
|
||||
|
||||
// Closed groups
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
ClosedGroupPoller.configureIfNeeded(context, sskDatabase)
|
||||
promises.addAll(ClosedGroupPoller.shared.pollOnce())
|
||||
|
||||
// Open Groups
|
||||
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value }
|
||||
for (openGroup in openGroups) {
|
||||
val poller = PublicChatPoller(context, openGroup)
|
||||
promises.add(poller.pollForNewMessages())
|
||||
}
|
||||
|
||||
// Wait till all the promises get resolved
|
||||
all(promises).get()
|
||||
|
||||
return Result.success()
|
||||
} catch (exception: Exception) {
|
||||
Log.v(TAG, "Background poll failed due to error: ${exception.message}.", exception)
|
||||
|
||||
return if (runAttemptCount < RETRY_ATTEMPTS) Result.retry() else Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
class BootBroadcastReceiver: BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
Log.v(TAG, "Boot broadcast caught.")
|
||||
BackgroundPollWorker.scheduleInstant(context)
|
||||
BackgroundPollWorker.schedulePeriodic(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ import org.whispersystems.signalservice.loki.utilities.getRandomElementOrNull
|
||||
|
||||
class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) {
|
||||
private var isPolling = false
|
||||
private val handler = Handler()
|
||||
private val handler: Handler by lazy { Handler() }
|
||||
|
||||
private val task = object : Runnable {
|
||||
|
||||
|
@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.*
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
|
||||
|
||||
/**
|
||||
* Delegates the [OpenGroupUtilities.updateGroupInfo] call to the work manager.
|
||||
*/
|
||||
class PublicChatInfoUpdateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "PublicChatInfoUpdateWorker"
|
||||
|
||||
private const val DATA_KEY_SERVER_URL = "server_uRL"
|
||||
private const val DATA_KEY_CHANNEL = "channel"
|
||||
|
||||
@JvmStatic
|
||||
fun scheduleInstant(context: Context, serverURL: String, channel: Long) {
|
||||
val workRequest = OneTimeWorkRequestBuilder<PublicChatInfoUpdateWorker>()
|
||||
.setConstraints(Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
)
|
||||
.setInputData(workDataOf(
|
||||
DATA_KEY_SERVER_URL to serverURL,
|
||||
DATA_KEY_CHANNEL to channel
|
||||
))
|
||||
.build()
|
||||
|
||||
WorkManager
|
||||
.getInstance(context)
|
||||
.enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doWork(): Result {
|
||||
val serverUrl = inputData.getString(DATA_KEY_SERVER_URL)!!
|
||||
val channel = inputData.getLong(DATA_KEY_CHANNEL, -1)
|
||||
|
||||
val publicChatId = PublicChat.getId(channel, serverUrl)
|
||||
|
||||
return try {
|
||||
Log.v(TAG, "Updating open group info for $publicChatId.")
|
||||
OpenGroupUtilities.updateGroupInfo(context, serverUrl, channel)
|
||||
Log.v(TAG, "Open group info was successfully updated for $publicChatId.")
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to update open group info for $publicChatId", e)
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,7 @@ import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.graphics.Bitmap
|
||||
import android.text.TextUtils
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
@ -16,6 +14,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatInfo
|
||||
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PublicChatManager(private val context: Context) {
|
||||
private var chats = mutableMapOf<Long, PublicChat>()
|
||||
@ -23,7 +22,7 @@ class PublicChatManager(private val context: Context) {
|
||||
private val observers = mutableMapOf<Long, ContentObserver>()
|
||||
private var isPolling = false
|
||||
|
||||
public fun areAllCaughtUp():Boolean {
|
||||
public fun areAllCaughtUp(): Boolean {
|
||||
var areAllCaughtUp = true
|
||||
refreshChatsAndPollers()
|
||||
for ((threadID, chat) in chats) {
|
||||
@ -58,19 +57,24 @@ class PublicChatManager(private val context: Context) {
|
||||
isPolling = false
|
||||
}
|
||||
|
||||
public fun addChat(server: String, channel: Long): Promise<PublicChat, Exception> {
|
||||
//TODO Declare a specific type of checked exception instead of "Exception".
|
||||
@WorkerThread
|
||||
@Throws(java.lang.Exception::class)
|
||||
public fun addChat(server: String, channel: Long): PublicChat {
|
||||
val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
||||
?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!"))
|
||||
return groupChatAPI.getAuthToken(server).bind {
|
||||
groupChatAPI.getChannelInfo(channel, server)
|
||||
}.map {
|
||||
addChat(server, channel, it)
|
||||
}
|
||||
?: throw IllegalStateException("LokiPublicChatAPI is not set!")
|
||||
|
||||
// Ensure the auth token is acquired.
|
||||
groupChatAPI.getAuthToken(server).get()
|
||||
|
||||
val channelInfo = groupChatAPI.getChannelInfo(channel, server).get()
|
||||
return addChat(server, channel, channelInfo)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public fun addChat(server: String, channel: Long, info: PublicChatInfo): PublicChat {
|
||||
val chat = PublicChat(channel, server, info.displayName, true)
|
||||
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
|
||||
var profilePicture: Bitmap? = null
|
||||
// Create the group if we don't have one
|
||||
if (threadID < 0) {
|
||||
@ -89,11 +93,21 @@ class PublicChatManager(private val context: Context) {
|
||||
ApplicationContext.getInstance(context).publicChatAPI?.setDisplayName(displayName, server)
|
||||
}
|
||||
// Start polling
|
||||
Util.runOnMain{ startPollersIfNeeded() }
|
||||
Util.runOnMain { startPollersIfNeeded() }
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
public fun removeChat(server: String, channel: Long) {
|
||||
val threadDB = DatabaseFactory.getThreadDatabase(context)
|
||||
val groupId = PublicChat.getId(channel, server)
|
||||
val threadId = GroupManager.getOpenGroupThreadID(groupId, context)
|
||||
val groupAddress = threadDB.getRecipientForThreadId(threadId)!!.address.serialize()
|
||||
GroupManager.deleteGroup(groupAddress, context)
|
||||
|
||||
Util.runOnMain { startPollersIfNeeded() }
|
||||
}
|
||||
|
||||
private fun refreshChatsAndPollers() {
|
||||
val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats()
|
||||
val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) }
|
||||
@ -105,7 +119,7 @@ class PublicChatManager(private val context: Context) {
|
||||
|
||||
private fun listenToThreadDeletion(threadID: Long) {
|
||||
if (threadID < 0 || observers[threadID] != null) { return }
|
||||
val observer = createDeletionObserver(threadID, Runnable {
|
||||
val observer = createDeletionObserver(threadID) {
|
||||
val chat = chats[threadID]
|
||||
|
||||
// Reset last message cache
|
||||
@ -119,7 +133,7 @@ class PublicChatManager(private val context: Context) {
|
||||
pollers.remove(threadID)?.stop()
|
||||
observers.remove(threadID)
|
||||
startPollersIfNeeded()
|
||||
})
|
||||
}
|
||||
observers[threadID] = observer
|
||||
|
||||
context.applicationContext.contentResolver.registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer)
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.api
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
@ -30,9 +31,10 @@ import org.whispersystems.signalservice.loki.api.opengroups.PublicChatMessage
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class PublicChatPoller(private val context: Context, private val group: PublicChat) {
|
||||
private val handler = Handler()
|
||||
private val handler by lazy { Handler() }
|
||||
private var hasStarted = false
|
||||
private var isPollOngoing = false
|
||||
public var isCaughtUp = false
|
||||
@ -191,8 +193,8 @@ class PublicChatPoller(private val context: Context, private val group: PublicCh
|
||||
val messageID = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID)
|
||||
var isDuplicate = false
|
||||
if (messageID != null) {
|
||||
isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) > 0
|
||||
|| DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) > 0
|
||||
isDuplicate = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageID) >= 0
|
||||
|| DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) >= 0
|
||||
}
|
||||
if (isDuplicate) { return }
|
||||
if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return }
|
||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.loki.database
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.util.Log
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
@ -34,7 +33,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
override fun getThreadID(hexEncodedPublicKey: String): Long {
|
||||
val address = Address.fromSerialized(hexEncodedPublicKey)
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
|
||||
fun getThreadID(messageID: Long): Long {
|
||||
|
@ -25,7 +25,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
|
||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
@ -82,7 +81,7 @@ object ClosedGroupsProtocol {
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
|
||||
@ -166,7 +165,7 @@ object ClosedGroupsProtocol {
|
||||
if (isUserLeaving) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
} else {
|
||||
@ -230,7 +229,7 @@ object ClosedGroupsProtocol {
|
||||
}
|
||||
// Notify the user
|
||||
val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
|
||||
deferred.resolve(Unit)
|
||||
}.start()
|
||||
@ -385,7 +384,7 @@ object ClosedGroupsProtocol {
|
||||
if (wasCurrentUserRemoved) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
groupDB.setActive(groupID, false)
|
||||
groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
|
||||
groupDB.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
// Notify the PN server
|
||||
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
|
||||
} else {
|
||||
@ -510,7 +509,7 @@ object ClosedGroupsProtocol {
|
||||
@JvmStatic
|
||||
fun leaveLegacyGroup(context: Context, recipient: Recipient): Boolean {
|
||||
if (!recipient.address.isClosedGroup) { return true }
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
||||
val message = GroupUtil.createGroupLeaveMessage(context, recipient).orNull()
|
||||
if (threadID < 0 || message == null) { return false }
|
||||
MessageSender.send(context, message, threadID, false, null)
|
||||
@ -522,7 +521,7 @@ object ClosedGroupsProtocol {
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = recipient.address.toGroupString()
|
||||
groupDatabase.setActive(groupID, false)
|
||||
groupDatabase.remove(groupID, Address.fromSerialized(userPublicKey))
|
||||
groupDatabase.removeMember(groupID, Address.fromSerialized(userPublicKey))
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import android.os.AsyncTask
|
||||
import android.util.Log
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
@ -11,7 +10,6 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.CleanPreKeysJob
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
|
||||
@ -28,7 +26,7 @@ object SessionManagementProtocol {
|
||||
val recipient = recipient(context, publicKey)
|
||||
if (recipient.isGroupRecipient) { return }
|
||||
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
||||
val devices = lokiThreadDB.getSessionRestoreDevices(threadID)
|
||||
for (device in devices) {
|
||||
val endSessionMessage = OutgoingEndSessionMessage(OutgoingTextMessage(recipient, "TERMINATE", 0, -1))
|
||||
@ -106,7 +104,7 @@ object SessionManagementProtocol {
|
||||
if (TextSecurePreferences.getRestorationTime(context) > errorTimestamp) {
|
||||
return ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(publicKey)
|
||||
}
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(masterDeviceAsRecipient)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(masterDeviceAsRecipient)
|
||||
DatabaseFactory.getLokiThreadDatabase(context).addSessionRestoreDevice(threadID, publicKey)
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ class SessionResetImplementation(private val context: Context) : SessionResetPro
|
||||
}
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient)
|
||||
val infoMessage = OutgoingTextMessage(recipient, "", 0, 0)
|
||||
val infoMessageID = smsDB.insertMessageOutbox(threadID, infoMessage, false, System.currentTimeMillis(), null)
|
||||
if (infoMessageID > -1) {
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol.shelved
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData
|
||||
@ -132,6 +133,7 @@ object SyncMessagesProtocol {
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun handleOpenGroupSyncMessage(context: Context, content: SignalServiceContent, openGroups: List<PublicChat>) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
|
||||
|
@ -6,7 +6,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, context: Context): String {
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(threadRecipient)
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
val publicKey = recipient.address.toString()
|
||||
val displayName = if (publicChat != null) {
|
||||
|
@ -1,28 +1,42 @@
|
||||
package org.thoughtcrime.securesms.loki.utilities
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.then
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
|
||||
import java.lang.Exception
|
||||
import java.lang.IllegalStateException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
//TODO Refactor so methods declare specific type of checked exceptions and not generalized Exception.
|
||||
object OpenGroupUtilities {
|
||||
|
||||
@JvmStatic fun addGroup(context: Context, url: String, channel: Long): Promise<PublicChat, Exception> {
|
||||
// Check for an existing group
|
||||
val groupID = PublicChat.getId(channel, url)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
if (openGroup != null) { return Promise.of(openGroup) }
|
||||
// Add the new group
|
||||
val application = ApplicationContext.getInstance(context)
|
||||
val displayName = TextSecurePreferences.getProfileName(context)
|
||||
val lokiPublicChatAPI = application.publicChatAPI ?: throw Error("LokiPublicChatAPI is not initialized.")
|
||||
return application.publicChatManager.addChat(url, channel).then { group ->
|
||||
private const val TAG = "OpenGroupUtilities"
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
@Throws(Exception::class)
|
||||
fun addGroup(context: Context, url: String, channel: Long): PublicChat {
|
||||
// Check for an existing group.
|
||||
val groupID = PublicChat.getId(channel, url)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
|
||||
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
if (openGroup != null) return openGroup
|
||||
|
||||
// Add the new group.
|
||||
val application = ApplicationContext.getInstance(context)
|
||||
val displayName = TextSecurePreferences.getProfileName(context)
|
||||
val lokiPublicChatAPI = application.publicChatAPI
|
||||
?: throw IllegalStateException("LokiPublicChatAPI is not initialized.")
|
||||
|
||||
val group = application.publicChatManager.addChat(url, channel)
|
||||
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
|
||||
lokiPublicChatAPI.getMessages(channel, url)
|
||||
@ -31,7 +45,34 @@ object OpenGroupUtilities {
|
||||
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(context)
|
||||
val profileUrl: String? = TextSecurePreferences.getProfilePictureURL(context)
|
||||
lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl)
|
||||
group
|
||||
return group
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls the general public chat data from the server and updates related records.
|
||||
* Fires [GroupInfoUpdatedEvent] on [EventBus] upon success.
|
||||
*
|
||||
* Consider using [org.thoughtcrime.securesms.loki.api.PublicChatInfoUpdateWorker] for lazy approach.
|
||||
*/
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
@Throws(Exception::class)
|
||||
fun updateGroupInfo(context: Context, url: String, channel: Long) {
|
||||
val publicChatAPI = ApplicationContext.getInstance(context).publicChatAPI
|
||||
?: throw IllegalStateException("Public chat API is not initialized!")
|
||||
|
||||
// Check if open group has a related DB record.
|
||||
val groupId = GroupUtil.getEncodedOpenGroupId(PublicChat.getId(channel, url).toByteArray())
|
||||
if (!DatabaseFactory.getGroupDatabase(context).hasGroup(groupId)) {
|
||||
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
|
||||
}
|
||||
|
||||
val info = publicChatAPI.getChannelInfo(channel, url).get()
|
||||
|
||||
publicChatAPI.updateProfileIfNeeded(channel, url, groupId, info, false)
|
||||
|
||||
EventBus.getDefault().post(GroupInfoUpdatedEvent(url, channel))
|
||||
}
|
||||
|
||||
data class GroupInfoUpdatedEvent(val url: String, val channel: Long)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.loki.views
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -10,11 +9,9 @@ import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
|
||||
import kotlinx.android.synthetic.main.view_user.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
|
||||
|
||||
class UserView : LinearLayout {
|
||||
var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly
|
||||
@ -63,7 +60,7 @@ class UserView : LinearLayout {
|
||||
return result ?: publicKey
|
||||
}
|
||||
}
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(user)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(user)
|
||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this
|
||||
val address = user.address.serialize()
|
||||
profilePictureView.glide = glide
|
||||
|
@ -31,7 +31,7 @@ public class ContentProxySafetyInterceptor implements Interceptor {
|
||||
Response response = chain.proceed(chain.request());
|
||||
|
||||
if (response.isRedirect()) {
|
||||
if (isWhitelisted(response.header("Location"))) {
|
||||
if (isWhitelisted(response.header("location")) || isWhitelisted(response.header("Location"))) {
|
||||
return response;
|
||||
} else {
|
||||
Log.w(TAG, "Tried to redirect to a non-whitelisted domain!");
|
||||
@ -53,6 +53,6 @@ public class ContentProxySafetyInterceptor implements Interceptor {
|
||||
}
|
||||
|
||||
private static boolean isWhitelisted(@Nullable String url) {
|
||||
return LinkPreviewUtil.isWhitelistedLinkUrl(url) || LinkPreviewUtil.isWhitelistedMediaUrl(url);
|
||||
return LinkPreviewUtil.isValidLinkUrl(url) || LinkPreviewUtil.isValidMediaUrl(url);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,9 @@
|
||||
package org.thoughtcrime.securesms.net;
|
||||
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewDomains;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
@ -27,8 +23,7 @@ public class ContentProxySelector extends ProxySelector {
|
||||
|
||||
private static final Set<String> WHITELISTED_DOMAINS = new HashSet<>();
|
||||
static {
|
||||
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.LINKS);
|
||||
WHITELISTED_DOMAINS.addAll(LinkPreviewDomains.IMAGES);
|
||||
WHITELISTED_DOMAINS.add("giphy.com");
|
||||
}
|
||||
|
||||
private final List<Proxy> CONTENT = new ArrayList<Proxy>(1) {{
|
||||
|
@ -121,6 +121,10 @@ public class Recipient implements RecipientModifiedListener {
|
||||
if (recipient.isPresent()) consumer.accept(recipient.get());
|
||||
}
|
||||
|
||||
public static boolean removeCached(@NonNull Address address) {
|
||||
return provider.removeCached(address);
|
||||
}
|
||||
|
||||
Recipient(@NonNull Context context,
|
||||
@NonNull Address address,
|
||||
@Nullable Recipient stale,
|
||||
|
@ -79,6 +79,10 @@ class RecipientProvider {
|
||||
return Optional.fromNullable(recipientCache.get(address));
|
||||
}
|
||||
|
||||
boolean removeCached(@NonNull Address address) {
|
||||
return recipientCache.remove(address);
|
||||
}
|
||||
|
||||
private @NonNull Optional<RecipientDetails> createPrefetchedRecipientDetails(@NonNull Context context, @NonNull Address address,
|
||||
@NonNull Optional<RecipientSettings> settings,
|
||||
@NonNull Optional<GroupRecord> groupRecord)
|
||||
@ -230,6 +234,10 @@ class RecipientProvider {
|
||||
cache.put(address, recipient);
|
||||
}
|
||||
|
||||
public synchronized boolean remove(Address address) {
|
||||
return cache.remove(address) != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -69,7 +69,7 @@ public class MessageSender {
|
||||
long allocatedThreadId;
|
||||
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
@ -94,7 +94,7 @@ public class MessageSender {
|
||||
long allocatedThreadId;
|
||||
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
allocatedThreadId = threadDatabase.getOrCreateThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ public class CommunicationActions {
|
||||
new AsyncTask<Void, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(Void... voids) {
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
return DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -16,10 +16,17 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
@ -142,4 +149,33 @@ public class DateUtils extends android.text.format.DateUtils {
|
||||
private static String getLocalizedPattern(String template, Locale locale) {
|
||||
return DateFormat.getBestDateTimePattern(locale, template);
|
||||
}
|
||||
|
||||
/**
|
||||
* e.g. 2020-09-04T19:17:51Z
|
||||
* https://www.iso.org/iso-8601-date-and-time-format.html
|
||||
*
|
||||
* Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences.
|
||||
*
|
||||
* @return The timestamp if able to be parsed, otherwise -1.
|
||||
*/
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
public static long parseIso8601(@Nullable String date) {
|
||||
SimpleDateFormat format;
|
||||
if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
|
||||
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
|
||||
} else {
|
||||
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||
}
|
||||
|
||||
if (date.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
return format.parse(date).getTime();
|
||||
} catch (ParseException e) {
|
||||
Log.w(TAG, "Failed to parse date.", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ public class IdentityUtil {
|
||||
smsDatabase.insertMessageInbox(incoming);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(group.getGroupId(), false)), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(groupRecipient);
|
||||
OutgoingTextMessage outgoing ;
|
||||
|
||||
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient);
|
||||
@ -112,7 +112,7 @@ public class IdentityUtil {
|
||||
if (verified) outgoing = new OutgoingIdentityVerifiedMessage(recipient);
|
||||
else outgoing = new OutgoingIdentityDefaultMessage(recipient);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
|
||||
|
||||
Log.i(TAG, "Inserting verified outbox...");
|
||||
DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoing, false, time, null);
|
||||
|
Loading…
Reference in New Issue
Block a user