Merge branch 'dev' of https://github.com/metaphore/session-android into background-polling

This commit is contained in:
Anton Chekulaev 2020-10-27 12:23:48 +11:00
commit 8101ad665e
54 changed files with 595 additions and 301 deletions

View File

@ -144,6 +144,7 @@
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity" android:name="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity"
android:label="@string/activity_edit_closed_group_title"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity <activity
android:name="org.thoughtcrime.securesms.loki.activities.JoinPublicChatActivity" android:name="org.thoughtcrime.securesms.loki.activities.JoinPublicChatActivity"

View File

@ -182,8 +182,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 99 def canonicalVersionCode = 110
def canonicalVersionName = "1.5.4" def canonicalVersionName = "1.6.1"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<!-- Add half of the medium_profile_picture_size padding on the right to better work with the group icons. -->
<item
android:id="@android:id/mask"
android:right="24dp">
<shape>
<corners
android:bottomLeftRadius="@dimen/medium_profile_picture_size"
android:topLeftRadius="@dimen/medium_profile_picture_size" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.67,3.87L9.9,2.1 0,12l9.9,9.9 1.77,-1.77L3.54,12z"/>
</vector>

View File

@ -5,34 +5,40 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <ScrollView
android:id="@+id/mainContentContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content" >
android:orientation="vertical">
<EditText <LinearLayout
style="@style/SessionEditText" android:id="@+id/mainContentContainer"
android:id="@+id/nameEditText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing" android:orientation="vertical">
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/medium_spacing"
android:hint="@string/activity_create_closed_group_edit_text_hint" />
<View <EditText
android:layout_width="match_parent" style="@style/SessionEditText"
android:layout_height="1px" android:id="@+id/nameEditText"
android:background="?android:dividerHorizontal" /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/medium_spacing"
android:hint="@string/activity_create_closed_group_edit_text_hint" />
<androidx.recyclerview.widget.RecyclerView <View
android:id="@+id/recyclerView" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="1px"
android:layout_height="match_parent" /> android:background="?android:dividerHorizontal" />
</LinearLayout> <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</ScrollView>
<LinearLayout <LinearLayout
android:id="@+id/emptyStateContainer" android:id="@+id/emptyStateContainer"
@ -51,7 +57,7 @@
<Button <Button
style="@style/Widget.Session.Button.Common.ProminentOutline" style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/btnCreateNewPrivateChat" android:id="@+id/createNewPrivateChatButton"
android:layout_width="196dp" android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height" android:layout_height="@dimen/medium_button_height"
android:layout_marginTop="@dimen/medium_spacing" android:layout_marginTop="@dimen/medium_spacing"

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity"> tools:context="org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity">
<LinearLayout <LinearLayout
@ -146,4 +146,22 @@
</LinearLayout> </LinearLayout>
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:visibility="gone"
android:alpha="0">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_centerInParent="true"
app:SpinKit_Color="@android:color/white" />
</RelativeLayout>
</RelativeLayout> </RelativeLayout>

View File

@ -123,7 +123,7 @@
<Button <Button
style="@style/Widget.Session.Button.Common.ProminentOutline" style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/btnCreateNewPrivateChat" android:id="@+id/createNewPrivateChatButton"
android:layout_width="196dp" android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height" android:layout_height="@dimen/medium_button_height"
android:layout_marginTop="@dimen/medium_spacing" android:layout_marginTop="@dimen/medium_spacing"

View File

@ -26,6 +26,7 @@
android:layout_centerInParent="true"> android:layout_centerInParent="true">
<TextView <TextView
android:id="@+id/emptyStateMessageTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="@dimen/medium_font_size" android:textSize="@dimen/medium_font_size"

View File

@ -16,4 +16,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/camera_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/small_spacing"
android:padding="@dimen/small_spacing"
android:background="@drawable/circle_touch_highlight_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/ic_baseline_clear_24"
android:tint="@android:color/white"/>
</FrameLayout>
</FrameLayout> </FrameLayout>

View File

@ -11,19 +11,39 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:contentInsetLeft="24dp" android:clipChildren="false"
app:contentInsetRight="20dp"> app:contentInsetStart="4dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:clipChildren="false">
<org.thoughtcrime.securesms.loki.views.ProfilePictureView <LinearLayout
android:id="@+id/profilePictureView" android:id="@+id/homeButtonContainer"
android:layout_width="@dimen/medium_profile_picture_size" android:layout_width="wrap_content"
android:layout_height="@dimen/medium_profile_picture_size" /> android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="@drawable/conversation_home_touch_highlight"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:src="@drawable/ic_baseline_arrow_back_compact_24"
android:layout_marginRight="-2dp"
android:layout_marginLeft="8dp" />
<org.thoughtcrime.securesms.loki.views.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -153,4 +153,22 @@
</org.thoughtcrime.securesms.components.InputAwareLayout> </org.thoughtcrime.securesms.components.InputAwareLayout>
<FrameLayout
android:id="@+id/mediasend_close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/small_spacing"
android:padding="@dimen/small_spacing"
android:background="@drawable/circle_touch_highlight_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/ic_baseline_clear_24"
android:tint="@android:color/white"/>
</FrameLayout>
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?actionBarSize">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/session_logo"/>
<FrameLayout
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_margin="@dimen/small_spacing"
android:padding="@dimen/small_spacing"
android:background="@drawable/circle_touch_highlight_background"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_baseline_arrow_back_24"
android:alpha="0.5"/>
</FrameLayout>
</RelativeLayout>

View File

@ -1386,7 +1386,7 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangen</s
<string name="activity_create_closed_group_empty_state_button_title">Session starten</string> <string name="activity_create_closed_group_empty_state_button_title">Session starten</string>
<string name="activity_create_closed_group_group_name_missing_error">Bitte geben Sie einen Gruppennamen ein.</string> <string name="activity_create_closed_group_group_name_missing_error">Bitte geben Sie einen Gruppennamen ein.</string>
<string name="activity_create_closed_group_group_name_too_long_error">Bitte geben Sie einen kürzeren Gruppennamen ein.</string> <string name="activity_create_closed_group_group_name_too_long_error">Bitte geben Sie einen kürzeren Gruppennamen ein.</string>
<string name="activity_create_closed_group_too_many_group_members_error">Eine geschlossene Gruppe kann maximal zehn Mitglieder haben.</string> <string name="activity_create_closed_group_too_many_group_members_error">Eine geschlossene Gruppe kann maximal 20 Mitglieder haben.</string>
<string name="activity_create_closed_group_invalid_session_id_error">Ein Mitglied Ihrer Gruppe hat eine ungültige Session ID.</string> <string name="activity_create_closed_group_invalid_session_id_error">Ein Mitglied Ihrer Gruppe hat eine ungültige Session ID.</string>
<string name="activity_join_public_chat_title">Offener Gruppe beitreten</string> <string name="activity_join_public_chat_title">Offener Gruppe beitreten</string>

View File

@ -1398,7 +1398,7 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del
<string name="activity_create_closed_group_empty_state_button_title">Empezar una Session</string> <string name="activity_create_closed_group_empty_state_button_title">Empezar una Session</string>
<string name="activity_create_closed_group_group_name_missing_error">Por favor, ingresa un nombre de grupo</string> <string name="activity_create_closed_group_group_name_missing_error">Por favor, ingresa un nombre de grupo</string>
<string name="activity_create_closed_group_group_name_too_long_error">Por favor, ingresa un nombre de grupo más corto</string> <string name="activity_create_closed_group_group_name_too_long_error">Por favor, ingresa un nombre de grupo más corto</string>
<string name="activity_create_closed_group_too_many_group_members_error">Un grupo cerrado no puede tener más de 10 miembros</string> <string name="activity_create_closed_group_too_many_group_members_error">Un grupo cerrado no puede tener más de 20 miembros</string>
<string name="activity_create_closed_group_invalid_session_id_error">Uno de los miembros de tu grupo tiene un ID de Session no válido</string> <string name="activity_create_closed_group_invalid_session_id_error">Uno de los miembros de tu grupo tiene un ID de Session no válido</string>
<string name="activity_join_public_chat_title">Únete al grupo abierto</string> <string name="activity_join_public_chat_title">Únete al grupo abierto</string>

View File

@ -1312,7 +1312,7 @@
<string name="activity_create_closed_group_empty_state_button_title">شروع Session</string> <string name="activity_create_closed_group_empty_state_button_title">شروع Session</string>
<string name="activity_create_closed_group_group_name_missing_error">لطفا یک نام گروه وارد کنید</string> <string name="activity_create_closed_group_group_name_missing_error">لطفا یک نام گروه وارد کنید</string>
<string name="activity_create_closed_group_group_name_too_long_error">لطفا نام گروه کوتاه‌تری وارد کنید</string> <string name="activity_create_closed_group_group_name_too_long_error">لطفا نام گروه کوتاه‌تری وارد کنید</string>
<string name="activity_create_closed_group_too_many_group_members_error">یک گروه خصوصی نمی‌تواند بیش از ۱۰ عضو داشته باشد</string> <string name="activity_create_closed_group_too_many_group_members_error">یک گروه خصوصی نمی‌تواند بیش از بیست عضو داشته باشد</string>
<string name="activity_create_closed_group_invalid_session_id_error">یکی از اعضای گروه شما دارای شناسه نامعتبر است</string> <string name="activity_create_closed_group_invalid_session_id_error">یکی از اعضای گروه شما دارای شناسه نامعتبر است</string>
<string name="activity_join_public_chat_title">به گروه باز بپیوندید</string> <string name="activity_join_public_chat_title">به گروه باز بپیوندید</string>

View File

@ -1393,7 +1393,7 @@ Vous avez reçu un message déchange de clés pour une version de protocole i
<string name="activity_create_closed_group_empty_state_button_title">Démarrer une session</string> <string name="activity_create_closed_group_empty_state_button_title">Démarrer une session</string>
<string name="activity_create_closed_group_group_name_missing_error">Veuillez saisir un nom de groupe</string> <string name="activity_create_closed_group_group_name_missing_error">Veuillez saisir un nom de groupe</string>
<string name="activity_create_closed_group_group_name_too_long_error">Veuillez saisir un nom de groupe plus court</string> <string name="activity_create_closed_group_group_name_too_long_error">Veuillez saisir un nom de groupe plus court</string>
<string name="activity_create_closed_group_too_many_group_members_error">Un groupe privé ne peut pas avoir plus de 10 membres</string> <string name="activity_create_closed_group_too_many_group_members_error">Un groupe privé ne peut pas avoir plus de 20 membres</string>
<string name="activity_create_closed_group_invalid_session_id_error">Un des membres de votre groupe a un Session ID non valide</string> <string name="activity_create_closed_group_invalid_session_id_error">Un des membres de votre groupe a un Session ID non valide</string>
<string name="activity_join_public_chat_title">Joindre un groupe public</string> <string name="activity_join_public_chat_title">Joindre un groupe public</string>

View File

@ -1351,7 +1351,7 @@ Diterima pesan pertukaran kunci untuk versi protokol yang tidak valid.
<string name="activity_create_closed_group_group_name_missing_error">Masukkan nama grup</string> <string name="activity_create_closed_group_group_name_missing_error">Masukkan nama grup</string>
<string name="activity_create_closed_group_group_name_too_long_error">Masukkan nama grup yang lebih pendek</string> <string name="activity_create_closed_group_group_name_too_long_error">Masukkan nama grup yang lebih pendek</string>
<string name="activity_create_closed_group_not_enough_group_members_error">Pilih setidaknya 2 anggota grup</string> <string name="activity_create_closed_group_not_enough_group_members_error">Pilih setidaknya 2 anggota grup</string>
<string name="activity_create_closed_group_too_many_group_members_error">Grup tertutup maksimal berisi 10 anggota</string> <string name="activity_create_closed_group_too_many_group_members_error">Grup tertutup maksimal berisi 20 anggota</string>
<string name="activity_create_closed_group_invalid_session_id_error">Salah satu anggota di grup memiliki Session ID yang salah</string> <string name="activity_create_closed_group_invalid_session_id_error">Salah satu anggota di grup memiliki Session ID yang salah</string>
<string name="activity_join_public_chat_title">Gabung ke grup terbuka</string> <string name="activity_join_public_chat_title">Gabung ke grup terbuka</string>

View File

@ -1394,7 +1394,7 @@ Ricevuto un messaggio di scambio chiavi per una versione di protocollo non valid
<string name="activity_create_closed_group_empty_state_button_title">Inizia una sessione</string> <string name="activity_create_closed_group_empty_state_button_title">Inizia una sessione</string>
<string name="activity_create_closed_group_group_name_missing_error">Inserisci un nome per il gruppo</string> <string name="activity_create_closed_group_group_name_missing_error">Inserisci un nome per il gruppo</string>
<string name="activity_create_closed_group_group_name_too_long_error">Inserisci un nome gruppo più breve</string> <string name="activity_create_closed_group_group_name_too_long_error">Inserisci un nome gruppo più breve</string>
<string name="activity_create_closed_group_too_many_group_members_error">Un gruppo chiuso non può avere più di 10 membri</string> <string name="activity_create_closed_group_too_many_group_members_error">Un gruppo chiuso non può avere più di 20 membri</string>
<string name="activity_create_closed_group_invalid_session_id_error">Uno dei membri del tuo gruppo ha una Sessione ID non valido</string> <string name="activity_create_closed_group_invalid_session_id_error">Uno dei membri del tuo gruppo ha una Sessione ID non valido</string>
<string name="activity_join_public_chat_title">Unisciti a un gruppo aperto</string> <string name="activity_join_public_chat_title">Unisciti a un gruppo aperto</string>

View File

@ -1357,7 +1357,7 @@
<string name="activity_create_closed_group_group_name_missing_error">グループ名を入力してください</string> <string name="activity_create_closed_group_group_name_missing_error">グループ名を入力してください</string>
<string name="activity_create_closed_group_group_name_too_long_error">短いグループ名を入力してください</string> <string name="activity_create_closed_group_group_name_too_long_error">短いグループ名を入力してください</string>
<string name="activity_create_closed_group_not_enough_group_members_error">グループメンバーを少なくとも 2 人選択してください</string> <string name="activity_create_closed_group_not_enough_group_members_error">グループメンバーを少なくとも 2 人選択してください</string>
<string name="activity_create_closed_group_too_many_group_members_error">閉じたグループは 10 人を超えるメンバーを抱えることはできません</string> <string name="activity_create_closed_group_too_many_group_members_error">閉じたグループは 20 人を超えるメンバーを抱えることはできません</string>
<string name="activity_create_closed_group_invalid_session_id_error">グループのメンバーの 1 人の Session ID が無効です</string> <string name="activity_create_closed_group_invalid_session_id_error">グループのメンバーの 1 人の Session ID が無効です</string>
<string name="activity_join_public_chat_title">オープングループに参加する</string> <string name="activity_join_public_chat_title">オープングループに参加する</string>

View File

@ -1449,7 +1449,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
<string name="activity_create_closed_group_empty_state_button_title">Rozpocznij sesję</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> <string name="activity_create_closed_group_group_name_missing_error">Wpisz nazwę grupy</string>
<string name="activity_create_closed_group_group_name_too_long_error">Wprowadź krótszą nazwę grupy</string> <string name="activity_create_closed_group_group_name_too_long_error">Wprowadź krótszą nazwę grupy</string>
<string name="activity_create_closed_group_too_many_group_members_error">Grupa zamknięta nie może mieć więcej niż 10 członków</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_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 Open Group</string>

View File

@ -1397,7 +1397,7 @@
<string name="activity_create_closed_group_empty_state_button_title">Iniciar uma sessão</string> <string name="activity_create_closed_group_empty_state_button_title">Iniciar uma sessão</string>
<string name="activity_create_closed_group_group_name_missing_error">Digite um nome de grupo</string> <string name="activity_create_closed_group_group_name_missing_error">Digite um nome de grupo</string>
<string name="activity_create_closed_group_group_name_too_long_error">Digite um nome de grupo mais curto</string> <string name="activity_create_closed_group_group_name_too_long_error">Digite um nome de grupo mais curto</string>
<string name="activity_create_closed_group_too_many_group_members_error">Um grupo fechado não pode ter mais de 10 membros</string> <string name="activity_create_closed_group_too_many_group_members_error">Um grupo fechado não pode ter mais de 20 membros</string>
<string name="activity_create_closed_group_invalid_session_id_error">Um dos membros do seu grupo tem um ID Session inválido</string> <string name="activity_create_closed_group_invalid_session_id_error">Um dos membros do seu grupo tem um ID Session inválido</string>
<string name="activity_join_public_chat_title">Participar em grupo aberto</string> <string name="activity_join_public_chat_title">Participar em grupo aberto</string>

View File

@ -1451,7 +1451,7 @@
<string name="activity_create_closed_group_empty_state_button_title">Начать Сессию</string> <string name="activity_create_closed_group_empty_state_button_title">Начать Сессию</string>
<string name="activity_create_closed_group_group_name_missing_error">Пожалуйста, введите название группы</string> <string name="activity_create_closed_group_group_name_missing_error">Пожалуйста, введите название группы</string>
<string name="activity_create_closed_group_group_name_too_long_error">Пожалуйста, введите более короткое имя группы</string> <string name="activity_create_closed_group_group_name_too_long_error">Пожалуйста, введите более короткое имя группы</string>
<string name="activity_create_closed_group_too_many_group_members_error">В закрытой группе не может быть больше 10 участников</string> <string name="activity_create_closed_group_too_many_group_members_error">В закрытой группе не может быть больше 20 участников</string>
<string name="activity_create_closed_group_invalid_session_id_error">Один из участников вашей группы имеет недопустимый Session ID</string> <string name="activity_create_closed_group_invalid_session_id_error">Один из участников вашей группы имеет недопустимый Session ID</string>
<string name="activity_join_public_chat_title">Присоединиться к открытой группе</string> <string name="activity_join_public_chat_title">Присоединиться к открытой группе</string>

View File

@ -845,7 +845,7 @@ Các tin nhắn và cuộc gọi riêng tư miễn phí đến người dùng Si
<string name="activity_create_closed_group_group_name_missing_error">Vui lòng nhập tên nhóm</string> <string name="activity_create_closed_group_group_name_missing_error">Vui lòng nhập tên nhóm</string>
<string name="activity_create_closed_group_group_name_too_long_error">Vui lòng nhập một tên nhóm ngắn hơn </string> <string name="activity_create_closed_group_group_name_too_long_error">Vui lòng nhập một tên nhóm ngắn hơn </string>
<string name="activity_create_closed_group_not_enough_group_members_error">Vui lòng chọn ít nhất 2 thành viên trong nhóm </string> <string name="activity_create_closed_group_not_enough_group_members_error">Vui lòng chọn ít nhất 2 thành viên trong nhóm </string>
<string name="activity_create_closed_group_too_many_group_members_error">Một nhóm kín không thể có nhiều hơn 10 thành viên </string> <string name="activity_create_closed_group_too_many_group_members_error">Một nhóm kín không thể có nhiều hơn 20 thành viên </string>
<string name="activity_create_closed_group_invalid_session_id_error">Một trong các thành viên trong nhóm của bạn có Session ID không hợp lệ </string> <string name="activity_create_closed_group_invalid_session_id_error">Một trong các thành viên trong nhóm của bạn có Session ID không hợp lệ </string>
<string name="activity_join_public_chat_title">Tham gia nhóm mở</string> <string name="activity_join_public_chat_title">Tham gia nhóm mở</string>

View File

@ -1364,7 +1364,7 @@
<string name="activity_create_closed_group_empty_state_button_title">开始对话</string> <string name="activity_create_closed_group_empty_state_button_title">开始对话</string>
<string name="activity_create_closed_group_group_name_missing_error">请输入群组名称</string> <string name="activity_create_closed_group_group_name_missing_error">请输入群组名称</string>
<string name="activity_create_closed_group_group_name_too_long_error">请输入较短的群组名称</string> <string name="activity_create_closed_group_group_name_too_long_error">请输入较短的群组名称</string>
<string name="activity_create_closed_group_too_many_group_members_error">私密群组成员不得超过10个</string> <string name="activity_create_closed_group_too_many_group_members_error">私密群组成员不得超过20个</string>
<string name="activity_create_closed_group_invalid_session_id_error">您群组中的一位成员的Session ID无效</string> <string name="activity_create_closed_group_invalid_session_id_error">您群组中的一位成员的Session ID无效</string>
<string name="activity_join_public_chat_title">加入公开群组</string> <string name="activity_join_public_chat_title">加入公开群组</string>

View File

@ -1748,7 +1748,7 @@
<string name="activity_create_closed_group_group_name_missing_error">Please enter a group name</string> <string name="activity_create_closed_group_group_name_missing_error">Please enter a group name</string>
<string name="activity_create_closed_group_group_name_too_long_error">Please enter a shorter group name</string> <string name="activity_create_closed_group_group_name_too_long_error">Please enter a shorter group name</string>
<string name="activity_create_closed_group_not_enough_group_members_error">Please pick at least 1 group member</string> <string name="activity_create_closed_group_not_enough_group_members_error">Please pick at least 1 group member</string>
<string name="activity_create_closed_group_too_many_group_members_error">A closed group cannot have more than 10 members</string> <string name="activity_create_closed_group_too_many_group_members_error">A closed group cannot have more than 20 members</string>
<string name="activity_create_closed_group_invalid_session_id_error">One of the members of your group has an invalid Session ID</string> <string name="activity_create_closed_group_invalid_session_id_error">One of the members of your group has an invalid Session ID</string>
<string name="activity_join_public_chat_title">Join Open Group</string> <string name="activity_join_public_chat_title">Join Open Group</string>

View File

@ -18,8 +18,8 @@
<item name="android:textSize">@dimen/very_large_font_size</item> <item name="android:textSize">@dimen/very_large_font_size</item>
</style> </style>
<style name="TextSecure.BaseDarkTheme.SearchView" parent="@style/Widget.AppCompat.SearchView"> <style name="Widget.Session.SearchView" parent="@style/Widget.AppCompat.SearchView">
<item name="closeIcon">@drawable/ic_baseline_clear_24</item> <item name="closeIcon">@drawable/ic_clear</item>
</style> </style>
<style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert"> <style name="ThemeOverlay.Session.AlertDialog" parent="ThemeOverlay.AppCompat.Dialog.Alert">

View File

@ -41,6 +41,8 @@
<item name="dividerVertical">@color/separator</item> <item name="dividerVertical">@color/separator</item>
<item name="dividerHorizontal">?dividerVertical</item> <item name="dividerHorizontal">?dividerVertical</item>
<item name="searchViewStyle">@style/Widget.Session.SearchView</item>
<!-- App specific attributes --> <!-- App specific attributes -->
<item name="ic_visibility_on">@drawable/ic_baseline_visibility_24</item> <item name="ic_visibility_on">@drawable/ic_baseline_visibility_24</item>
<item name="ic_visibility_off">@drawable/ic_baseline_visibility_off_24</item> <item name="ic_visibility_off">@drawable/ic_baseline_visibility_off_24</item>
@ -50,8 +52,7 @@
<item name="media_overview_toolbar_background">@color/transparent</item> <item name="media_overview_toolbar_background">@color/transparent</item>
<item name="media_overview_header_foreground">@color/text</item> <item name="media_overview_header_foreground">@color/text</item>
<item name="media_keyboard_button_color">@color/core_grey_25</item> <item name="media_keyboard_button_color">@color/core_grey_25</item>\
<item name="searchViewStyle">@style/TextSecure.BaseDarkTheme.SearchView</item>
<item name="attachment_type_selector_background">?android:windowBackground</item> <item name="attachment_type_selector_background">?android:windowBackground</item>
<item name="attachment_type_selector_hide_button_background">@color/gray50</item> <item name="attachment_type_selector_hide_button_background">@color/gray50</item>
@ -161,7 +162,6 @@
<item name="media_overview_header_foreground">@color/text</item> <item name="media_overview_header_foreground">@color/text</item>
<item name="theme_type">dark</item> <item name="theme_type">dark</item>
<item name="searchViewStyle">@style/TextSecure.BaseDarkTheme.SearchView</item>
<item name="android:navigationBarColor">@color/compose_view_background</item> <item name="android:navigationBarColor">@color/compose_view_background</item>
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item> <item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>

View File

@ -12,6 +12,7 @@ import android.os.Build;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -42,6 +43,13 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
if (BaseActivity.isMenuWorkaroundRequired()) { if (BaseActivity.isMenuWorkaroundRequired()) {
forceOverflowMenu(); forceOverflowMenu();
} }
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
@ -69,6 +77,14 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter("unexpectedDeviceLinkRequestReceived")); LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter("unexpectedDeviceLinkRequestReceived"));
} }
@Override
public boolean onSupportNavigateUp() {
if (super.onSupportNavigateUp()) return true;
onBackPressed();
return true;
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);

View File

@ -34,6 +34,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode; import androidx.appcompat.view.ActionMode;
@ -136,9 +137,12 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
private void initializeToolbar() { private void initializeToolbar() {
setSupportActionBar(this.toolbar); setSupportActionBar(this.toolbar);
getSupportActionBar().setTitle(recipient.toShortString()); ActionBar actionBar = getSupportActionBar();
actionBar.setTitle(recipient.toShortString());
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
this.recipient.addListener(recipient -> { this.recipient.addListener(recipient -> {
Util.runOnMain(() -> getSupportActionBar().setTitle(recipient.toShortString())); Util.runOnMain(() -> actionBar.setTitle(recipient.toShortString()));
}); });
} }

View File

@ -19,7 +19,8 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import androidx.lifecycle.ViewModelProviders; import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
@ -122,7 +123,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
protected void onCreate(Bundle bundle, boolean ready) { protected void onCreate(Bundle bundle, boolean ready) {
dynamicLanguage.onCreate(this); dynamicLanguage.onCreate(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); viewModel = new ViewModelProvider(this).get(MediaPreviewViewModel.class);
setContentView(R.layout.media_preview_activity); setContentView(R.layout.media_preview_activity);
@ -224,8 +225,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
captionContainer = findViewById(R.id.media_preview_caption_container); captionContainer = findViewById(R.id.media_preview_caption_container);
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container); playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.toolbar));
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
} }
private void initializeResources() { private void initializeResources() {

View File

@ -32,6 +32,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.SearchToolbar;
@ -151,6 +152,9 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
private void initializeToolbar() { private void initializeToolbar() {
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
} }
private void initializeResources() { private void initializeResources() {

View File

@ -3,8 +3,6 @@ package org.thoughtcrime.securesms.components;
import android.animation.Animator; import android.animation.Animator;
import android.content.Context; import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.MenuItem; import android.view.MenuItem;
@ -49,11 +47,8 @@ public class SearchToolbar extends LinearLayout {
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
Drawable drawable = getContext().getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); toolbar.setNavigationIcon(
drawable.mutate(); getContext().getResources().getDrawable(R.drawable.ic_baseline_clear_24));
drawable.setColorFilter(getContext().getResources().getColor(R.color.grey_700), PorterDuff.Mode.SRC_IN);
toolbar.setNavigationIcon(drawable);
toolbar.inflateMenu(R.menu.conversation_list_search); toolbar.inflateMenu(R.menu.conversation_list_search);
this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search); this.searchItem = toolbar.getMenu().findItem(R.id.action_filter_search);
@ -122,7 +117,6 @@ public class SearchToolbar extends LinearLayout {
private void hide() { private void hide() {
if (getVisibility() == View.VISIBLE) { if (getVisibility() == View.VISIBLE) {
if (listener != null) listener.onSearchClosed(); if (listener != null) listener.onSearchClosed();
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {

View File

@ -323,6 +323,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private ProgressBar messageStatusProgressBar; private ProgressBar messageStatusProgressBar;
private ImageView muteIndicatorImageView; private ImageView muteIndicatorImageView;
private TextView subtitleTextView; private TextView subtitleTextView;
private View homeButtonContainer;
private AttachmentTypeSelector attachmentTypeSelector; private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager; private AttachmentManager attachmentManager;
@ -474,6 +475,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
collapsedKeyboardHeight = Math.min(collapsedKeyboardHeight, height); collapsedKeyboardHeight = Math.min(collapsedKeyboardHeight, height);
keyboardHeight = expandedKeyboardHeight - collapsedKeyboardHeight; keyboardHeight = expandedKeyboardHeight - collapsedKeyboardHeight;
// Use 300dp if the keyboard wasn't opened yet.
if (keyboardHeight == 0) {
keyboardHeight = (int)(300f * getResources().getDisplayMetrics().density);
}
}); });
} }
@ -1691,6 +1697,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar); messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar);
muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView); muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView);
subtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView); subtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView);
homeButtonContainer = ViewUtil.findById(this, R.id.homeButtonContainer);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button); ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@ -1745,6 +1752,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
searchNav.setEventListener(this); searchNav.setEventListener(this);
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment()); inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
homeButtonContainer.setOnClickListener(v -> onSupportNavigateUp());
} }
protected void initializeActionBar() { protected void initializeActionBar() {
@ -2278,7 +2287,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else { } else {
MarkReadReceiver.process(context, messageIds); MarkReadReceiver.process(context, messageIds);
} }
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context);
return null; return null;
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
@ -2431,28 +2440,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage); final long id = fragment.stageOutgoingMessage(outgoingMessage);
new AsyncTask<Void, Void, Long>() { if (initiating) {
@Override DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
protected Long doInBackground(Void... param) { }
if (initiating) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
if (!recipient.isGroupRecipient()) { if (!recipient.isGroupRecipient()) {
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
} }
return result; sendComplete(result);
} future.set(null);
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
future.set(null);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return future; return future;
} }
@ -2480,27 +2479,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
silentlySetComposeText(""); silentlySetComposeText("");
final long id = fragment.stageOutgoingMessage(message); final long id = fragment.stageOutgoingMessage(message);
new AsyncTask<OutgoingTextMessage, Void, Long>() { if (initiatingConversation) {
@Override DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
protected Long doInBackground(OutgoingTextMessage... messages) { }
if (initiatingConversation) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
long result = MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); long result = MessageSender.send(context, message, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
if (!recipient.isGroupRecipient()) { if (!recipient.isGroupRecipient()) {
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
} }
return result; sendComplete(result);
}
@Override
protected void onPostExecute(Long result) {
sendComplete(result);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
} }
private void showDefaultSmsPrompt() { private void showDefaultSmsPrompt() {
@ -3113,8 +3102,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void updateProfilePicture() { private void updateProfilePicture() {
profilePictureView.glide = GlideApp.with(this); try {
profilePictureView.update(recipient, threadId); profilePictureView.glide = GlideApp.with(this);
profilePictureView.update(recipient, threadId);
} catch (Exception exception) {
// Do nothing
}
} }
private void updateSubtitleTextView() { private void updateSubtitleTextView() {

View File

@ -71,8 +71,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setHomeButtonEnabled(true);
} }
private void initializeResources() { private void initializeResources() {

View File

@ -564,9 +564,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional.absent(), Optional.absent(),
Optional.absent()); Optional.absent());
database.insertSecureDecryptedMessageInbox(mediaMessage, -1); database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, message.getExpiresInSeconds()); DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient, message.getExpiresInSeconds());
if (smsMessageId.isPresent()) { if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get()); DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());

View File

@ -32,11 +32,10 @@ import org.whispersystems.libsignal.util.guava.Optional
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() }
private var members = listOf<String>() private var members = listOf<String>()
set(value) { set(value) { field = value; selectContactsAdapter.members = value }
field = value
selectContactsAdapter.members = value
}
private val selectContactsAdapter by lazy { private val selectContactsAdapter by lazy {
SelectContactsAdapter(this, GlideApp.with(this)) SelectContactsAdapter(this, GlideApp.with(this))
@ -49,21 +48,17 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_create_closed_group) setContentView(R.layout.activity_create_closed_group)
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title) supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
recyclerView.adapter = this.selectContactsAdapter recyclerView.adapter = this.selectContactsAdapter
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
btnCreateNewPrivateChat.setOnClickListener { createNewPrivateChat() }
LoaderManager.getInstance(this).initLoader(0, null, this) LoaderManager.getInstance(this).initLoader(0, null, this)
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_done, menu) menuInflater.inflate(R.menu.menu_done, menu)
return members.isNotEmpty() return members.isNotEmpty() && !isLoading
} }
// endregion // endregion
@ -91,7 +86,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
// region Interaction // region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) { when(item.itemId) {
R.id.doneButton -> createClosedGroup() R.id.doneButton -> if (!isLoading) { createClosedGroup() }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@ -125,9 +120,11 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this)
isLoading = true
loader.fadeIn() loader.fadeIn()
ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
loader.fadeOut() loader.fadeOut()
isLoading = false
val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
if (!isFinishing) { if (!isFinishing) {
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))

View File

@ -8,9 +8,7 @@ import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter import androidx.fragment.app.FragmentPagerAdapter
import android.text.InputType import android.text.InputType
import android.view.LayoutInflater import android.view.*
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
@ -43,9 +41,21 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
viewPager.adapter = adapter viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager) tabLayout.setupWithViewPager(viewPager)
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_done, menu)
return true
}
// endregion // endregion
// region Interaction // region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
R.id.doneButton -> adapter.enterPublicKeyFragment.createPrivateChatIfPossible()
}
return super.onOptionsItemSelected(item)
}
override fun handleQRCodeScanned(hexEncodedPublicKey: String) { override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
createPrivateChatIfPossible(hexEncodedPublicKey) createPrivateChatIfPossible(hexEncodedPublicKey)
} }
@ -71,6 +81,7 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC
// region Adapter // region Adapter
private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) { private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
val enterPublicKeyFragment = EnterPublicKeyFragment()
override fun getCount(): Int { override fun getCount(): Int {
return 2 return 2
@ -78,7 +89,7 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc
override fun getItem(index: Int): Fragment { override fun getItem(index: Int): Fragment {
return when (index) { return when (index) {
0 -> EnterPublicKeyFragment() 0 -> enterPublicKeyFragment
1 -> { 1 -> {
val result = ScanQRCodeWrapperFragment() val result = ScanQRCodeWrapperFragment()
result.delegate = activity result.delegate = activity
@ -104,8 +115,8 @@ class EnterPublicKeyFragment : Fragment() {
private val hexEncodedPublicKey: String private val hexEncodedPublicKey: String
get() { get() {
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context!!) val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(requireContext())
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context!!) val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(requireContext())
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
} }
@ -148,7 +159,7 @@ class EnterPublicKeyFragment : Fragment() {
startActivity(intent) startActivity(intent)
} }
private fun createPrivateChatIfPossible() { fun createPrivateChatIfPossible() {
val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() ?: "" val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() ?: ""
(requireActivity() as CreatePrivateChatActivity).createPrivateChatIfPossible(hexEncodedPublicKey) (requireActivity() as CreatePrivateChatActivity).createPrivateChatIfPossible(hexEncodedPublicKey)
} }

View File

@ -12,21 +12,29 @@ import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_edit_closed_group.* import kotlinx.android.synthetic.main.activity_edit_closed_group.*
import kotlinx.android.synthetic.main.activity_edit_closed_group.loader
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ThemeUtil
import org.whispersystems.signalservice.loki.utilities.toHexString import org.whispersystems.signalservice.loki.utilities.toHexString
import java.io.IOException import java.io.IOException
@ -34,6 +42,8 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>() private val originalMembers = HashSet<String>()
private val members = HashSet<String>() private val members = HashSet<String>()
private var hasNameChanged = false private var hasNameChanged = false
private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() }
private lateinit var groupID: String private lateinit var groupID: String
private lateinit var originalName: String private lateinit var originalName: String
@ -60,11 +70,12 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
// region Lifecycle // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady) super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_edit_closed_group) setContentView(R.layout.activity_edit_closed_group)
supportActionBar!!.title = resources.getString(R.string.activity_edit_closed_group_title)
groupID = intent.getStringExtra(Companion.groupIDKey) supportActionBar!!.setHomeAsUpIndicator(
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!!
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title
name = originalName name = originalName
@ -88,7 +99,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
} }
} }
LoaderManager.getInstance(this).initLoader(Companion.loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> { LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID) return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
@ -97,7 +108,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) { override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
// We no longer need any subsequent loading events // We no longer need any subsequent loading events
// (they will occur on every activity resume). // (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(Companion.loaderID) LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
originalMembers.clear() originalMembers.clear()
originalMembers.addAll(members.toHashSet()) originalMembers.addAll(members.toHashSet())
@ -112,7 +123,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu) menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return members.isNotEmpty() return members.isNotEmpty() && !isLoading
} }
// endregion // endregion
@ -120,7 +131,7 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
Companion.addUsersRequestCode -> { addUsersRequestCode -> {
if (resultCode != RESULT_OK) return if (resultCode != RESULT_OK) return
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
@ -162,8 +173,8 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
// region Interaction // region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) { when (item.itemId) {
R.id.action_apply -> commitChanges() R.id.action_apply -> if (!isLoading) { commitChanges() }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@ -180,8 +191,9 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private fun onAddMembersClick() { private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java) val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.Companion.usersToExcludeKey, members.toTypedArray()) intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.toTypedArray())
startActivityForResult(intent, Companion.addUsersRequestCode) intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode)
} }
private fun saveName() { private fun saveName() {
@ -227,17 +239,26 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
} }
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else Companion.legacyGroupSizeLimit val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) { if (members.size >= maxGroupMembers) {
// TODO: Update copy for SSK based closed groups // TODO: Update copy for SSK based closed groups
return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
} }
if (isSSKBasedClosedGroup) { if (isSSKBasedClosedGroup) {
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name) isLoading = true
loader.fadeIn()
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name).successUi {
loader.fadeOut()
isLoading = false
finish()
}.failUi { exception ->
val message = if (exception is ClosedGroupsProtocol.Error) exception.description else "An error occurred"
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
isLoading = false
}
} else { } else {
GroupManager.updateGroup(this, groupID, members, null, name, admins) GroupManager.updateGroup(this, groupID, members, null, name, admins)
} }
finish()
} }
} }

View File

@ -125,7 +125,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
recyclerView.adapter = homeAdapter recyclerView.adapter = homeAdapter
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)
// Set up empty state view // Set up empty state view
btnCreateNewPrivateChat.setOnClickListener { createNewPrivateChat() } createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will) // This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> { LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {

View File

@ -40,7 +40,7 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_landing) setContentView(R.layout.activity_landing)
setUpActionBarSessionLogo() setUpActionBarSessionLogo(true)
fakeChatView.startAnimating() fakeChatView.startAnimating()
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }

View File

@ -10,7 +10,10 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import kotlinx.android.synthetic.main.activity_create_closed_group.* import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import kotlinx.android.synthetic.main.activity_select_contacts.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
@ -26,6 +29,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
companion object { companion object {
val usersToExcludeKey = "usersToExcludeKey" val usersToExcludeKey = "usersToExcludeKey"
val emptyStateTextKey = "emptyStateTextKey"
val selectedContactsKey = "selectedContactsKey" val selectedContactsKey = "selectedContactsKey"
} }
@ -37,6 +41,10 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana
supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title)
usersToExclude = intent.getStringArrayExtra(Companion.usersToExcludeKey)?.toSet() ?: setOf() usersToExclude = intent.getStringArrayExtra(Companion.usersToExcludeKey)?.toSet() ?: setOf()
val emptyStateText = intent.getStringExtra(Companion.emptyStateTextKey)
if (emptyStateText != null) {
emptyStateMessageTextView.text = emptyStateText
}
recyclerView.adapter = selectContactsAdapter recyclerView.adapter = selectContactsAdapter
recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.layoutManager = LinearLayoutManager(this)

View File

@ -9,8 +9,10 @@ import org.whispersystems.libsignal.logging.Log
import org.whispersystems.signalservice.internal.util.JsonUtil import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.loki.api.PushNotificationAPI import org.whispersystems.signalservice.loki.api.PushNotificationAPI
import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI import org.whispersystems.signalservice.loki.api.onionrequests.OnionRequestAPI
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
object LokiPushNotificationManager { object LokiPushNotificationManager {
private val maxRetryCount = 4
private val tokenExpirationInterval = 12 * 60 * 60 * 1000 private val tokenExpirationInterval = 12 * 60 * 60 * 1000
private val server by lazy { private val server by lazy {
@ -38,15 +40,17 @@ object LokiPushNotificationManager {
val url = "$server/unregister" val url = "$server/unregister"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json -> retryIfNeeded(maxRetryCount) {
val code = json["code"] as? Int OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json ->
if (code != null && code != 0) { val code = json["code"] as? Int
TextSecurePreferences.setIsUsingFCM(context, false) if (code != null && code != 0) {
} else { TextSecurePreferences.setIsUsingFCM(context, false)
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.") } else {
Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
} }
}.fail { exception ->
Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.")
} }
// Unsubscribe from all closed groups // Unsubscribe from all closed groups
val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
@ -65,17 +69,19 @@ object LokiPushNotificationManager {
val url = "$server/register" val url = "$server/register"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json -> retryIfNeeded(maxRetryCount) {
val code = json["code"] as? Int OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json ->
if (code != null && code != 0) { val code = json["code"] as? Int
TextSecurePreferences.setIsUsingFCM(context, true) if (code != null && code != 0) {
TextSecurePreferences.setFCMToken(context, token) TextSecurePreferences.setIsUsingFCM(context, true)
TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) TextSecurePreferences.setFCMToken(context, token)
} else { TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis())
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.") } else {
Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
} }
}.fail { exception ->
Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.")
} }
// Subscribe to all closed groups // Subscribe to all closed groups
val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
@ -91,13 +97,15 @@ object LokiPushNotificationManager {
val url = "$server/${operation.rawValue}" val url = "$server/${operation.rawValue}"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body) val request = Request.Builder().url(url).post(body)
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json -> retryIfNeeded(maxRetryCount) {
val code = json["code"] as? Int OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey).map { json ->
if (code == null || code == 0) { val code = json["code"] as? Int
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")
}
}.fail { exception ->
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
} }
}.fail { exception ->
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.")
} }
} }
} }

View File

@ -153,7 +153,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters
"New" -> { "New" -> {
val name = data.getString("name") val name = data.getString("name")
val groupPrivateKey = data.getByteArray("groupPrivateKey") val groupPrivateKey = data.getByteArray("groupPrivateKey")
val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } val senderKeys = data.getStringOrDefault("senderKeys", "").split(" - ").mapNotNull { ClosedGroupSenderKey.fromJSON(it) } // Can be empty
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins) kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins)

View File

@ -25,6 +25,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
import org.whispersystems.signalservice.internal.push.SignalServiceProtos import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext 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.ClosedGroupRatchet
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
@ -36,9 +37,15 @@ import java.util.*
import kotlin.jvm.Throws import kotlin.jvm.Throws
object ClosedGroupsProtocol { object ClosedGroupsProtocol {
val isSharedSenderKeysEnabled = false val isSharedSenderKeysEnabled = true
val groupSizeLimit = 10 val groupSizeLimit = 20
sealed class Error(val description: String) : Exception() {
object NoThread : Error("Couldn't find a thread associated with the given group public key")
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
object InvalidUpdate : Error("Invalid group update.")
}
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> { public fun createClosedGroup(context: Context, name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>() val deferred = deferred<String, Exception>()
Thread { Thread {
@ -98,101 +105,128 @@ object ClosedGroupsProtocol {
val name = group.title val name = group.title
val oldMembers = group.members.map { it.serialize() }.toSet() val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = oldMembers.minus(userPublicKey) val newMembers = oldMembers.minus(userPublicKey)
update(context, groupPublicKey, newMembers, name) return update(context, groupPublicKey, newMembers, name).get()
} }
public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String) { public fun update(context: Context, groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) val deferred = deferred<Unit, Exception>()
val sskDatabase = DatabaseFactory.getSSKDatabase(context) Thread {
val groupDB = DatabaseFactory.getGroupDatabase(context) val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val sskDatabase = DatabaseFactory.getSSKDatabase(context)
val group = groupDB.getGroup(groupID).orNull() val groupDB = DatabaseFactory.getGroupDatabase(context)
if (group == null) { val groupID = doubleEncodeGroupID(groupPublicKey)
Log.d("Loki", "Can't update nonexistent closed group.") val group = groupDB.getGroup(groupID).orNull()
return if (group == null) {
} Log.d("Loki", "Can't update nonexistent closed group.")
val oldMembers = group.members.map { it.serialize() }.toSet() return@Thread deferred.reject(Error.NoThread)
val membersAsData = members.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
if (groupPrivateKey == null) {
Log.d("Loki", "Couldn't get private key for closed group.")
return
}
val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
val removedMembers = oldMembers.minus(members)
val isUserLeaving = removedMembers.contains(userPublicKey)
if (wasAnyUserRemoved) {
if (isUserLeaving && removedMembers.count() != 1) {
Log.d("Loki", "Can't remove self and others simultaneously.")
return
} }
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) val oldMembers = group.members.map { it.serialize() }.toSet()
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), val newMembers = members.minus(oldMembers)
name, setOf(), membersAsData, adminsAsData) val membersAsData = members.map { Hex.fromStringCondensed(it) }
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) val admins = group.admins.map { it.serialize() }
job.setContext(context) val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
job.onRun() // Run the job immediately val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
// Delete all ratchets (it's important that this happens * after * sending out the update) if (groupPrivateKey == null) {
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) Log.d("Loki", "Couldn't get private key for closed group.")
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and return@Thread deferred.reject(Error.NoPrivateKey)
// send it out to all members (minus the removed ones) using established channels. }
if (isUserLeaving) { val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) val removedMembers = oldMembers.minus(members)
groupDB.setActive(groupID, false) val isUserLeaving = removedMembers.contains(userPublicKey)
groupDB.remove(groupID, Address.fromSerialized(userPublicKey)) var newSenderKeys = listOf<ClosedGroupSenderKey>()
// Notify the PN server if (wasAnyUserRemoved) {
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) if (isUserLeaving && removedMembers.count() != 1) {
} else { Log.d("Loki", "Can't remove self and others simultaneously.")
return@Thread deferred.reject(Error.InvalidUpdate)
}
// Establish sessions if needed // Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, members) establishSessionsWithMembersIfNeeded(context, members)
// Send out the user's new ratchet to all members (minus the removed ones) using established channels // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) for (member in oldMembers) {
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) {
if (member == userPublicKey) { continue }
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
name, setOf(), membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
job.setContext(context)
job.onRun() // Run the job immediately
}
// Delete all ratchets (it's important that this happens * after * sending out the update)
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
// send it out to all members (minus the removed ones) using established channels.
if (isUserLeaving) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
groupDB.setActive(groupID, false)
groupDB.remove(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
} else {
// Send closed group update messages to any new members using established channels
for (member in newMembers) {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
Hex.fromStringCondensed(groupPrivateKey), listOf(), membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) {
if (member == userPublicKey) { continue }
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
} else if (newMembers.isNotEmpty()) {
// Generate ratchets for any new members
newSenderKeys = newMembers.map { publicKey ->
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
}
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
newSenderKeys, membersAsData, adminsAsData)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
// Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, newMembers)
// Send closed group update messages to the new members using established channels
var allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
allSenderKeys = allSenderKeys.union(newSenderKeys)
for (member in newMembers) {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
} }
} } else {
} else { val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey);
// Generate ratchets for any new members val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
val newMembers = members.minus(oldMembers) allSenderKeys, membersAsData, adminsAsData)
val newSenderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey -> val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
}
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
newSenderKeys, membersAsData, adminsAsData)
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
// Establish sessions if needed
establishSessionsWithMembersIfNeeded(context, newMembers)
// Send closed group update messages to the new members using established channels
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + newSenderKeys
for (member in newMembers) {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
} }
} // Update the group
// Update the group groupDB.updateTitle(groupID, name)
groupDB.updateTitle(groupID, name) if (!isUserLeaving) {
if (!isUserLeaving) { // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
// The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) }
} // Notify the user
// 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).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID)
deferred.resolve(Unit)
}.start()
return deferred.promise
} }
@JvmStatic @JvmStatic
@ -225,7 +259,7 @@ object ClosedGroupsProtocol {
when (closedGroupUpdate.type) { when (closedGroupUpdate.type) {
SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> {
return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.groupPrivateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.groupPrivateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
&& closedGroupUpdate.senderKeysCount > 0 && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty
} }
SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> { SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> {
return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty
@ -255,10 +289,35 @@ object ClosedGroupsProtocol {
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet)
} }
// Sort out any discrepancies between the provided sender keys and what's required
val missingSenderKeys = members.toSet().subtract(senderKeys.map { Hex.toStringCondensed(it.publicKey) })
if (missingSenderKeys.contains(userPublicKey)) {
establishSessionsWithMembersIfNeeded(context, members)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
for (member in members) {
if (member == userPublicKey) { continue }
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
for (publicKey in missingSenderKeys.minus(userPublicKey)) {
requestSenderKey(context, groupPublicKey, publicKey)
}
// Create the group // Create the group
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }), val groupDB = DatabaseFactory.getGroupDatabase(context)
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) })) if (groupDB.getGroup(groupID).orNull() != null) {
// Update the group
groupDB.updateTitle(groupID, name)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else {
groupDB.create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Add the group to the user's set of public keys to poll for // Add the group to the user's set of public keys to poll for
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
@ -297,7 +356,6 @@ object ClosedGroupsProtocol {
} }
// Store the ratchets for any new members (it's important that this happens before the code below) // Store the ratchets for any new members (it's important that this happens before the code below)
senderKeys.forEach { senderKey -> senderKeys.forEach { senderKey ->
if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach }
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet)
} }
@ -358,7 +416,8 @@ object ClosedGroupsProtocol {
// Respond to the request // Respond to the request
Log.d("Loki", "Responding to sender key request from: $senderPublicKey.") Log.d("Loki", "Responding to sender key request from: $senderPublicKey.")
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val userRatchet = DatabaseFactory.getSSKDatabase(context).getClosedGroupRatchet(groupPublicKey, userPublicKey)
?: SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind)
@ -369,24 +428,12 @@ object ClosedGroupsProtocol {
// Prepare // Prepare
val sskDatabase = DatabaseFactory.getSSKDatabase(context) val sskDatabase = DatabaseFactory.getSSKDatabase(context)
val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString()
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group sender key for nonexistent group.")
return
}
val senderKeyProto = closedGroupUpdate.senderKeysList.firstOrNull() val senderKeyProto = closedGroupUpdate.senderKeysList.firstOrNull()
if (senderKeyProto == null) { if (senderKeyProto == null) {
Log.d("Loki", "Ignoring invalid closed group sender key.") Log.d("Loki", "Ignoring invalid closed group sender key.")
return return
} }
val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray()) val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray())
// Check that the sending user is a member of the group
if (!group.members.map { it.serialize() }.contains(senderPublicKey)) {
Log.d("Loki", "Ignoring closed group sender key from non-member.")
return
}
if (senderKeyProto.publicKey.toByteArray().toHexString() != senderPublicKey) { if (senderKeyProto.publicKey.toByteArray().toHexString() != senderPublicKey) {
Log.d("Loki", "Ignoring invalid closed group sender key.") Log.d("Loki", "Ignoring invalid closed group sender key.")
return return

View File

@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit
class NullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) { class NullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) {
companion object { companion object {
const val KEY = "PushNullMessageSendJob" const val KEY = "NullMessageSendJob"
} }
constructor(publicKey: String) : this(Parameters.Builder() constructor(publicKey: String) : this(Parameters.Builder()

View File

@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit
class SessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) { class SessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) {
companion object { companion object {
const val KEY = "PushSessionRequestMessageSendJob" const val KEY = "SessionRequestMessageSendJob"
} }
constructor(publicKey: String, timestamp: Long) : this(Parameters.Builder() constructor(publicKey: String, timestamp: Long) : this(Parameters.Builder()

View File

@ -1,24 +1,36 @@
package org.thoughtcrime.securesms.loki.utilities package org.thoughtcrime.securesms.loki.utilities
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.ActionBar import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.view.Gravity import androidx.appcompat.widget.Toolbar
import android.widget.ImageView
import android.widget.RelativeLayout
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity
fun AppCompatActivity.setUpActionBarSessionLogo() { fun BaseActionBarActivity.setUpActionBarSessionLogo(hideBackButton: Boolean = false) {
supportActionBar!!.setDisplayShowHomeEnabled(false) val actionbar = supportActionBar!!
supportActionBar!!.setDisplayShowTitleEnabled(false)
val logoImageView = ImageView(this) actionbar.setDisplayShowHomeEnabled(false)
logoImageView.setImageResource(R.drawable.session_logo) actionbar.setDisplayShowTitleEnabled(false)
val logoImageViewContainer = RelativeLayout(this) actionbar.setDisplayHomeAsUpEnabled(false)
logoImageViewContainer.addView(logoImageView) actionbar.setHomeButtonEnabled(false)
logoImageViewContainer.gravity = Gravity.CENTER
val logoImageViewContainerLayoutParams = ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.WRAP_CONTENT) actionbar.setCustomView(R.layout.session_logo_action_bar_content)
supportActionBar!!.setCustomView(logoImageViewContainer, logoImageViewContainerLayoutParams) actionbar.setDisplayShowCustomEnabled(true)
supportActionBar!!.setDisplayShowCustomEnabled(true)
val rootView: Toolbar = actionbar.customView!!.parent as Toolbar
rootView.setPadding(0,0,0,0)
rootView.setContentInsetsAbsolute(0,0);
val backButton = actionbar.customView!!.findViewById<View>(R.id.back_button)
if (hideBackButton) {
backButton.visibility = View.GONE
} else {
backButton.visibility = View.VISIBLE
backButton.setOnClickListener {
onSupportNavigateUp()
}
}
} }
val AppCompatActivity.defaultSessionRequestCode: Int val AppCompatActivity.defaultSessionRequestCode: Int

View File

@ -27,6 +27,7 @@ import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation; import android.view.animation.RotateAnimation;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView;
import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.Transformation;
@ -51,6 +52,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
private TextureView cameraPreview; private TextureView cameraPreview;
private ViewGroup controlsContainer; private ViewGroup controlsContainer;
private View cameraCloseButton;
private ImageButton flipButton; private ImageButton flipButton;
private Button captureButton; private Button captureButton;
private Camera1Controller camera; private Camera1Controller camera;
@ -95,6 +97,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
cameraPreview = view.findViewById(R.id.camera_preview); cameraPreview = view.findViewById(R.id.camera_preview);
controlsContainer = view.findViewById(R.id.camera_controls_container); controlsContainer = view.findViewById(R.id.camera_controls_container);
cameraCloseButton = view.findViewById(R.id.camera_close_button);
onOrientationChanged(getResources().getConfiguration().orientation); onOrientationChanged(getResources().getConfiguration().orientation);
@ -102,6 +105,8 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
GestureDetector gestureDetector = new GestureDetector(flipGestureListener); GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event)); cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
cameraCloseButton.setOnClickListener(v -> requireActivity().onBackPressed());
} }
@Override @Override

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
@ -53,7 +55,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
recipientName = getArguments().getString(KEY_RECIPIENT_NAME); recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
} }
@Override @Override
@ -85,7 +87,7 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
viewModel.getFolders(requireContext()).observe(this, adapter::setFolders); viewModel.getFolders(requireContext()).observe(getViewLifecycleOwner(), adapter::setFolders);
initToolbar(view.findViewById(R.id.mediapicker_toolbar)); initToolbar(view.findViewById(R.id.mediapicker_toolbar));
} }
@ -107,7 +109,10 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
private void initToolbar(Toolbar toolbar) { private void initToolbar(Toolbar toolbar) {
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(getString(R.string.MediaPickerActivity_send_to, recipientName)); ActionBar actionBar = ((AppCompatActivity) requireActivity()).getSupportActionBar();
actionBar.setTitle(getString(R.string.MediaPickerActivity_send_to, recipientName));
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed()); toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import androidx.appcompat.app.ActionBar;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
@ -166,8 +167,12 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
} }
private void initToolbar(Toolbar toolbar) { private void initToolbar(Toolbar toolbar) {
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); AppCompatActivity activity = (AppCompatActivity) requireActivity();
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(folderTitle); activity.setSupportActionBar(toolbar);
ActionBar actionBar = activity.getSupportActionBar();
actionBar.setTitle(folderTitle);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed()); toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
} }

View File

@ -1,16 +1,10 @@
package org.thoughtcrime.securesms.mediasend; package org.thoughtcrime.securesms.mediasend;
import android.Manifest; import android.Manifest;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import android.view.View; import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator; import android.view.animation.AccelerateInterpolator;
@ -21,8 +15,13 @@ import android.view.animation.ScaleAnimation;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@ -41,6 +40,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import network.loki.messenger.R;
/** /**
* Encompasses the entire flow of sending media, starting from the selection process to the actual * Encompasses the entire flow of sending media, starting from the selection process to the actual
* captioning and editing of the content. * captioning and editing of the content.
@ -124,6 +125,8 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
@Override @Override
protected void onCreate(Bundle savedInstanceState, boolean ready) { protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.mediasend_activity); setContentView(R.layout.mediasend_activity);
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);

View File

@ -91,6 +91,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
private Stub<MediaKeyboard> emojiDrawer; private Stub<MediaKeyboard> emojiDrawer;
private ViewGroup playbackControlsContainer; private ViewGroup playbackControlsContainer;
private TextView charactersLeft; private TextView charactersLeft;
private View closeButton;
private ControllableViewPager fragmentPager; private ControllableViewPager fragmentPager;
private MediaSendFragmentPagerAdapter fragmentPagerAdapter; private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
@ -154,6 +155,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
mediaRail = view.findViewById(R.id.mediasend_media_rail); mediaRail = view.findViewById(R.id.mediasend_media_rail);
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container); playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
charactersLeft = view.findViewById(R.id.mediasend_characters_left); charactersLeft = view.findViewById(R.id.mediasend_characters_left);
closeButton = view.findViewById(R.id.mediasend_close_button);
View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg); View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg);
@ -227,6 +229,8 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
} else { } else {
emojiToggle.setOnClickListener(this::onEmojiToggleClicked); emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
} }
closeButton.setOnClickListener(v -> requireActivity().onBackPressed());
} }
@Override @Override

View File

@ -140,7 +140,12 @@ class SaveAttachmentTask : ProgressDialogAsyncTask<SaveAttachmentTask.Attachment
} }
} }
val mediaFileUri = resolver.insert(collectionUri, mediaRecord) var mediaFileUri: Uri?
try {
mediaFileUri = resolver.insert(collectionUri, mediaRecord)
} catch (exception: Exception) {
return null
}
if (mediaFileUri == null) return null if (mediaFileUri == null) return null
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri) val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri)