Copione merged onto master

master
blallo 2020-09-24 00:01:07 +02:00
commit 1dbdf06ca4
285 changed files with 5663 additions and 2433 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ obj/
jni/libspeex/.deps/ jni/libspeex/.deps/
*.sh *.sh
pkcs11.password pkcs11.password
dev.keystore

View File

@ -80,8 +80,8 @@ protobuf {
} }
} }
def canonicalVersionCode = 708 def canonicalVersionCode = 709
def canonicalVersionName = "4.71.5" def canonicalVersionName = "4.72.0"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['universal' : 0, def abiPostFix = ['universal' : 0,
@ -100,6 +100,15 @@ android {
javaMaxHeapSize "4g" javaMaxHeapSize "4g"
} }
signingConfigs {
staging {
storeFile file("${project.rootDir}/dev.keystore")
storePassword 'android'
keyAlias 'staging'
keyPassword 'android'
}
}
defaultConfig { defaultConfig {
versionCode canonicalVersionCode * postFixSize versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName versionName canonicalVersionName
@ -195,6 +204,8 @@ android {
} }
staging { staging {
initWith debug initWith debug
applicationIdSuffix ".staging"
signingConfig signingConfigs.staging
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\"" buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\"" buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
@ -310,7 +321,7 @@ dependencies {
implementation 'org.signal:argon2:13.1@aar' implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.5.1' implementation 'org.signal:ringrtc-android:2.7.0'
implementation "me.leolin:ShortcutBadger:1.1.16" implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@ -5,5 +5,12 @@
<application <application
android:name=".FlipperApplicationContext" android:name=".FlipperApplicationContext"
tools:replace="android:name"/> tools:replace="android:name">
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest> </manifest>

View File

@ -5,7 +5,7 @@
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" /> <uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS" <permission android:name="${applicationId}.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets" android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" /> android:protectionLevel="signature" />
@ -113,7 +113,7 @@
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" /> <meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" /> <meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<activity android:name="org.thoughtcrime.securesms.WebRtcCallActivity" <activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.LightTheme.WebRTCCall" android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@ -127,25 +127,25 @@
android:theme="@style/TextSecure.DarkNoActionBar" android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:noHistory="true" android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".InviteActivity" <activity android:name=".InviteActivity"
android:theme="@style/Signal.Light.NoActionBar.Invite" android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" /> android:value=".MainActivity" />
</activity> </activity>
<activity android:name=".PromptMmsActivity" <activity android:name=".PromptMmsActivity"
android:label="Configure MMS Settings" android:label="Configure MMS Settings"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceProvisioningActivity" <activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -155,7 +155,7 @@
</activity> </activity>
<activity android:name=".preferences.MmsPreferencesActivity" <activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.ShareActivity" <activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
@ -164,7 +164,7 @@
android:taskAffinity="" android:taskAffinity=""
android:noHistory="true" android:noHistory="true"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
@ -195,7 +195,7 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:noHistory="true" android:noHistory="true"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -242,7 +242,7 @@
<activity android:name=".conversation.ConversationActivity" <activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity"> android:parentActivityName=".MainActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
@ -257,16 +257,16 @@
android:taskAffinity="" android:taskAffinity=""
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:theme="@style/TextSecure.LightTheme.Popup" android:theme="@style/TextSecure.LightTheme.Popup"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" /> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".messagedetails.MessageDetailsActivity" <activity android:name=".messagedetails.MessageDetailsActivity"
android:label="@string/AndroidManifest__message_details" android:label="@string/AndroidManifest__message_details"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity" <activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" /> android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity" <activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
@ -275,64 +275,64 @@
<activity android:name=".groups.ui.managegroup.ManageGroupActivity" <activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity" <activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity" <activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar" android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity" <activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar" android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseCreateActivity" <activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase" android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphrasePromptActivity" <activity android:name=".PassphrasePromptActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/TextSecure.LightIntroTheme" android:theme="@style/TextSecure.LightIntroTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".NewConversationActivity" <activity android:name=".NewConversationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible" android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity" <activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts" android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".giph.ui.GiphyActivity" <activity android:name=".giph.ui.GiphyActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity" <activity android:name=".mediasend.MediaSendActivity"
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity" <activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase" android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyIdentityActivity" <activity android:name=".VerifyIdentityActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ApplicationPreferencesActivity" <activity android:name=".ApplicationPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" /> <category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
@ -343,45 +343,45 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.ViewOnceMessageActivity" <activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".stickers.StickerManagementActivity" <activity android:name=".stickers.StickerManagementActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateUnchanged" android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity" <activity android:name=".DeviceActivity"
android:label="@string/AndroidManifest__linked_devices" android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".logsubmit.SubmitDebugLogActivity" <activity android:name=".logsubmit.SubmitDebugLogActivity"
android:label="@string/AndroidManifest__log_submit" android:label="@string/AndroidManifest__log_submit"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity" <activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview" android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".AvatarPreviewActivity" <activity android:name=".AvatarPreviewActivity"
android:label="@string/AndroidManifest__media_preview" android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediaoverview.MediaOverviewActivity" <activity android:name=".mediaoverview.MediaOverviewActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DummyActivity" <activity android:name=".DummyActivity"
android:theme="@android:style/Theme.NoDisplay" android:theme="@android:style/Theme.NoDisplay"
@ -396,7 +396,7 @@
<activity android:name=".PlayServicesProblemActivity" <activity android:name=".PlayServicesProblemActivity"
android:theme="@style/TextSecure.DialogActivity" android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SmsSendtoActivity"> <activity android:name=".SmsSendtoActivity">
<intent-filter> <intent-filter>
@ -420,7 +420,7 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:theme="@style/NoAnimation.Theme.BlackScreen" android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -432,15 +432,15 @@
<activity android:name=".mediasend.AvatarSelectionActivity" <activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".BlockedContactsActivity" <activity android:name=".BlockedContactsActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity" <activity android:name=".scribbles.ImageEditorStickerSelectActivity"
android:theme="@style/TextSecure.DarkTheme" android:theme="@style/TextSecure.DarkTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".profiles.edit.EditProfileActivity" <activity android:name=".profiles.edit.EditProfileActivity"
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
@ -449,16 +449,16 @@
<activity android:name=".lock.v2.CreateKbsPinActivity" <activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".lock.v2.KbsMigrationActivity" <activity android:name=".lock.v2.KbsMigrationActivity"
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearProfileAvatarActivity" <activity android:name=".ClearProfileAvatarActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert" android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:icon="@drawable/clear_profile_avatar" android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo"> android:label="@string/AndroidManifest_remove_photo">
@ -474,39 +474,39 @@
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity" <activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactShareEditActivity" <activity android:name=".contactshare.ContactShareEditActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactNameEditActivity" <activity android:name=".contactshare.ContactNameEditActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.SharedContactDetailsActivity" <activity android:name=".contactshare.SharedContactDetailsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShortcutLauncherActivity" <activity android:name=".ShortcutLauncherActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true" android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity <activity
android:name=".maps.PlacePickerActivity" android:name=".maps.PlacePickerActivity"
android:label="@string/PlacePickerActivity_title" android:label="@string/PlacePickerActivity_title"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MainActivity" <activity android:name=".MainActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity" <activity android:name=".pin.PinRestoreActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity" <activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" /> android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
@ -521,14 +521,14 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar" /> android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity" <activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".megaphone.ClientDeprecatedActivity" <activity android:name=".megaphone.ClientDeprecatedActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/> <service android:enabled="true" android:name=".service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/> <service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/> <service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/> <service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
@ -650,15 +650,15 @@
<provider android:name=".providers.PartProvider" <provider android:name=".providers.PartProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:exported="false" android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms" /> android:authorities="${applicationId}.part" />
<provider android:name=".providers.MmsBodyProvider" <provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:exported="false" android:exported="false"
android:authorities="org.thoughtcrime.provider.securesms.mms" /> android:authorities="${applicationId}.mms" />
<provider android:name="androidx.core.content.FileProvider" <provider android:name="androidx.core.content.FileProvider"
android:authorities="org.thoughtcrime.securesms.fileprovider" android:authorities="${applicationId}.fileprovider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
@ -667,23 +667,23 @@
</provider> </provider>
<provider android:name=".database.DatabaseContentProviders$Conversation" <provider android:name=".database.DatabaseContentProviders$Conversation"
android:authorities="org.thoughtcrime.securesms.database.conversation" android:authorities="${applicationId}.database.conversation"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$ConversationList" <provider android:name=".database.DatabaseContentProviders$ConversationList"
android:authorities="org.thoughtcrime.securesms.database.conversationlist" android:authorities="${applicationId}.database.conversationlist"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment" <provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="org.thoughtcrime.securesms.database.attachment" android:authorities="${applicationId}.database.attachment"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Sticker" <provider android:name=".database.DatabaseContentProviders$Sticker"
android:authorities="org.thoughtcrime.securesms.database.sticker" android:authorities="${applicationId}.database.sticker"
android:exported="false" /> android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$StickerPack" <provider android:name=".database.DatabaseContentProviders$StickerPack"
android:authorities="org.thoughtcrime.securesms.database.stickerpack" android:authorities="${applicationId}.database.stickerpack"
android:exported="false" /> android:exported="false" />
<receiver android:name=".service.BootReceiver"> <receiver android:name=".service.BootReceiver">

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
public final class AppCapabilities { public final class AppCapabilities {
@ -9,12 +8,13 @@ public final class AppCapabilities {
} }
private static final boolean UUID_CAPABLE = false; private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
/** /**
* @param storageCapable Whether or not the user can use storage service. This is another way of * @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not. * asking if the user has set a Signal PIN or not.
*/ */
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) { public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable); return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
} }
} }

View File

@ -137,7 +137,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
NotificationChannels.create(this); NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary(); RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync(); StorageSyncHelper.scheduleRoutineSync();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
RegistrationUtil.maybeMarkRegistrationComplete(this); RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
@ -155,6 +154,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
Log.i(TAG, "App is now visible."); Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary(); FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp(); ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
executePendingContactSync(); executePendingContactSync();
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin(); ApplicationDependencies.getFrameRateTracker().begin();

View File

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -10,12 +9,7 @@ import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.transition.TransitionInflater; import android.transition.TransitionInflater;
import android.view.DisplayCutout;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -40,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
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.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
/** /**
* Activity for displaying avatars full screen. * Activity for displaying avatars full screen.
@ -81,17 +76,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Build.VERSION.SDK_INT >= 28) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new DisplayCutoutAdjuster(toolbar, findViewById(R.id.toolbar_cutout_spacer)));
}
showSystemUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Context context = getApplicationContext(); Context context = getApplicationContext();
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA)); RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
@ -140,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
toolbar.setTitle(recipient.getDisplayName(context)); toolbar.setTitle(recipient.getDisplayName(context));
}); });
avatar.setOnClickListener(v -> toggleUiVisibility()); FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout)); findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
}
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) { fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
for (View view : views) { fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
} }
@Override @Override
@ -188,36 +139,4 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
onBackPressed(); onBackPressed();
return true; return true;
} }
/**
* Adjust a spacer for the toolbar when a display cutout is detected. Runs within
* a layout listener because the activity delays view attachment due to the transitions
* and needs to update on device rotation.
*/
@TargetApi(28)
private static class DisplayCutoutAdjuster implements ViewTreeObserver.OnGlobalLayoutListener {
private final View view;
private final View spacer;
private DisplayCutoutAdjuster(@NonNull View view, @NonNull View spacer) {
this.view = view;
this.spacer = spacer;
}
@Override
public void onGlobalLayout() {
if (view.getRootWindowInsets() == null) {
return;
}
DisplayCutout cutout = view.getRootWindowInsets().getDisplayCutout();
if (cutout != null) {
ViewGroup.LayoutParams params = spacer.getLayoutParams();
params.height = cutout.getSafeInsetTop();
spacer.setLayoutParams(params);
spacer.setVisibility(View.VISIBLE);
}
}
}
} }

View File

@ -4,6 +4,7 @@ import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.conversation.ConversationMessage;
@ -22,7 +23,8 @@ import java.util.Locale;
import java.util.Set; import java.util.Set;
public interface BindableConversationItem extends Unbindable { public interface BindableConversationItem extends Unbindable {
void bind(@NonNull ConversationMessage messageRecord, void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,

View File

@ -31,8 +31,6 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -69,6 +67,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity; import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
@ -119,6 +118,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private boolean cameFromAllMedia; private boolean cameFromAllMedia;
private boolean showThread; private boolean showThread;
private MediaDatabase.Sorting sorting; private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private @Nullable Cursor cursor = null; private @Nullable Cursor cursor = null;
@ -133,7 +133,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize()); intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption()); intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent); intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType()); intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent; return intent;
} }
@ -147,10 +147,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, fullscreenHelper = new FullscreenHelper(this);
WindowManager.LayoutParams.FLAG_FULLSCREEN);
showSystemUI();
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -273,9 +270,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer); anchorMarginsToBottomInsets(detailsContainer);
anchorMarginsToTopInsets(toolbarLayout); fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout); fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
} }
private void initializeResources() { private void initializeResources() {
@ -546,7 +543,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@Override @Override
public boolean singleTapOnMedia() { public boolean singleTapOnMedia() {
toggleUiVisibility(); fullscreenHelper.toggleUiVisibility();
return true; return true;
} }
@ -556,32 +553,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish(); finish();
} }
private void toggleUiVisibility() {
int systemUiVisibility = getWindow().getDecorView().getSystemUiVisibility();
if ((systemUiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN );
}
private void showSystemUI() {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN );
}
private class ViewPagerListener extends ExtendedOnPageChangedListener { private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override @Override
@ -697,33 +668,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}); });
} }
private static void anchorMarginsToTopInsets(@NonNull View viewToAnchor) {
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
layoutParams.bottomMargin);
view.setLayoutParams(layoutParams);
return insets;
});
}
private static void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
for (View view : views) {
view.animate()
.alpha(hide ? 0 : 1)
.start();
}
});
}
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter { private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
@ -801,7 +745,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return new MediaItem(Recipient.live(recipientId).get(), return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(), Recipient.live(threadRecipientId).get(),
attachment, attachment,
Objects.requireNonNull(attachment.getDataUri()), Objects.requireNonNull(attachment.getUri()),
mediaRecord.getContentType(), mediaRecord.getContentType(),
mediaRecord.getDate(), mediaRecord.getDate(),
mediaRecord.isOutgoing()); mediaRecord.isOutgoing());

View File

@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -65,16 +66,17 @@ public class NewConversationActivity extends ContactSelectionActivity
launch(Recipient.resolved(recipientId.get())); launch(Recipient.resolved(recipientId.get()));
} else { } else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient."); Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (FeatureFlags.cds() && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] CDS enabled. Doing contact refresh."); if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this); AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> { SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number); Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered()) { if (!resolved.isRegistered() || !resolved.hasUuid()) {
Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh."); Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try { try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false); DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId()); resolved = Recipient.resolved(resolved.getId());
@ -102,7 +104,7 @@ public class NewConversationActivity extends ContactSelectionActivity
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)); intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
intent.setDataAndType(getIntent().getData(), getIntent().getType()); intent.setDataAndType(getIntent().getData(), getIntent().getType());
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread); intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT); intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);

View File

@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else { } else {
Recipient recipient = Recipient.external(this, destination.getDestination()); Recipient recipient = Recipient.external(this, destination.getDestination());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient); long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
nextIntent = new Intent(this, ConversationActivity.class); nextIntent = new Intent(this, ConversationActivity.class);
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody()); nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());

View File

@ -19,8 +19,6 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.app.PictureInPictureParams; import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
@ -41,9 +39,11 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput; import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@ -52,11 +52,12 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback { public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
@ -85,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity); setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide(); getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@ -132,11 +134,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
super.onStop(); super.onStop();
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
}
@Override CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
public void onConfigurationChanged(Configuration newConfiguration) { if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
super.onConfigurationChanged(newConfiguration); Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
}
} }
@Override @Override
@ -162,7 +166,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private boolean enterPipModeIfPossible() { private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder() PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16)) .setAspectRatio(new Rational(9, 16))
.build(); .build();
@ -196,21 +200,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
private void initializeResources() { private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen); callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener()); callScreen.setControlsListener(new ControlsListener());
} }
private void initializeViewModel() { private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class); viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode()); viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls); viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent); viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime); viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard); viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
} }
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@ -375,19 +376,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
startService(intent); startService(intent);
} }
private void handleIncomingCall(@NonNull WebRtcViewModel event) { private void handleOutgoingCall() {
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling)); callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
} }
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) { private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name()); Log.i(TAG, "handleTerminate called: " + hangupType.name());
callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType); callScreen.setStatusFromHangupType(hangupType);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
@ -398,62 +393,47 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
delayedFinish(); delayedFinish();
} }
private void handleCallRinging(@NonNull WebRtcViewModel event) { private void handleCallRinging() {
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_ringing)); callScreen.setStatus(getString(R.string.RedPhone_ringing));
} }
private void handleCallBusy(@NonNull WebRtcViewModel event) { private void handleCallBusy() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy)); callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH); delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
} }
private void handleCallConnected(@NonNull WebRtcViewModel event) { private void handleCallConnected() {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES); getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setRecipient(event.getRecipient());
} }
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) { private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable)); callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish(); delayedFinish();
} }
private void handleServerFailure(@NonNull WebRtcViewModel event) { private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed)); callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish(); delayedFinish();
} }
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) { private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
AlertDialog.Builder dialog = new AlertDialog.Builder(this); new AlertDialog.Builder(this)
dialog.setTitle(R.string.RedPhone_number_not_registered); .setTitle(R.string.RedPhone_number_not_registered)
dialog.setIconAttribute(R.attr.dialog_alert_icon); .setIconAttribute(R.attr.dialog_alert_icon)
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice); .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
dialog.setCancelable(true); .setCancelable(true)
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() { .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
@Override .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
public void onClick(DialogInterface dialog, int which) { .show();
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.show();
} }
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirKey = event.getIdentityKey(); final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
final Recipient recipient = event.getRecipient(); final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) { if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL); handleTerminate(recipient, HangupMessage.Type.NORMAL);
@ -493,32 +473,29 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN) @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(final WebRtcViewModel event) { public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event); Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient()); viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) { switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break; case CALL_CONNECTED: handleCallConnected(); break;
case NETWORK_FAILURE: handleServerFailure(event); break; case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(event); break; case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break; case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break; case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break; case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break; case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break; case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER: handleNoSuchUser(event); break; case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break; case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
case CALL_INCOMING: handleIncomingCall(event); break; case CALL_OUTGOING: handleOutgoingCall(); break;
case CALL_OUTGOING: handleOutgoingCall(event); break; case CALL_BUSY: handleCallBusy(); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break; case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
} }
callScreen.setLocalRenderer(event.getLocalRenderer()); boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
callScreen.setRemoteRenderer(event.getRemoteRenderer());
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
viewModel.updateFromWebRtcViewModel(event, enableVideo); viewModel.updateFromWebRtcViewModel(event, enableVideo);
@ -530,6 +507,24 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private final class ControlsListener implements WebRtcCallView.ControlsListener { private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onStartCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()))
.putExtra(WebRtcCallService.EXTRA_OFFER_TYPE, (isVideoCall ? OfferMessage.Type.VIDEO_CALL : OfferMessage.Type.AUDIO_CALL).getCode());
startService(intent);
MessageSender.onMessageSent();
}
@Override
public void onCancelStartCall() {
finish();
}
@Override @Override
public void onControlsFadeOut() { public void onControlsFadeOut() {
if (videoTooltip != null) { if (videoTooltip != null) {
@ -594,8 +589,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
} }
@Override @Override
public void onDownCaretPressed() { public void onShowParticipantsList() {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
@Override
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
} }
} }

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.NonNull;
public class ResizeAnimation extends Animation {
private final View target;
private final int targetWidthPx;
private final int targetHeightPx;
private int startWidth;
private int startHeight;
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
this.target = target;
this.targetWidthPx = targetWidthPx;
this.targetHeightPx = targetHeightPx;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
ViewGroup.LayoutParams params = target.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
target.setLayoutParams(params);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
this.startWidth = width;
this.startHeight = height;
}
@Override
public boolean willChangeBounds() {
return true;
}
}

View File

@ -106,10 +106,7 @@ public abstract class Attachment {
} }
@Nullable @Nullable
public abstract Uri getDataUri(); public abstract Uri getUri();
@Nullable
public abstract Uri getThumbnailUri();
public int getTransferState() { public int getTransferState() {
return transferState; return transferState;

View File

@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
@Override @Override
@Nullable @Nullable
public Uri getDataUri() { public Uri getUri() {
if (hasData) { if (hasData) {
return PartAuthority.getAttachmentDataUri(attachmentId); return PartAuthority.getAttachmentDataUri(attachmentId);
} else { } else {
@ -65,16 +65,6 @@ public class DatabaseAttachment extends Attachment {
} }
} }
@Override
@Nullable
public Uri getThumbnailUri() {
if (hasThumbnail) {
return PartAuthority.getAttachmentThumbnailUri(attachmentId);
} else {
return null;
}
}
public AttachmentId getAttachmentId() { public AttachmentId getAttachmentId() {
return attachmentId; return attachmentId;
} }

View File

@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
@Nullable @Nullable
@Override @Override
public Uri getDataUri() { public Uri getUri() {
return null;
}
@Nullable
@Override
public Uri getThumbnailUri() {
return null; return null;
} }

View File

@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
@Nullable @Nullable
@Override @Override
public Uri getDataUri() { public Uri getUri() {
return null; return null;
} }
@Nullable
@Override
public Uri getThumbnailUri() {
return null;
}
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) { public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
List<Attachment> results = new LinkedList<>(); List<Attachment> results = new LinkedList<>();

View File

@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
} }
@Override @Override
public @Nullable Uri getDataUri() { public @Nullable Uri getUri() {
return null;
}
@Override
public @Nullable Uri getThumbnailUri() {
return null; return null;
} }
} }

View File

@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
public class UriAttachment extends Attachment { public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri; private final @NonNull Uri dataUri;
private final @Nullable Uri thumbnailUri;
public UriAttachment(@NonNull Uri uri, public UriAttachment(@NonNull Uri uri,
@NonNull String contentType, @NonNull String contentType,
@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash, @Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties) @Nullable TransformProperties transformProperties)
{ {
this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties); this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
} }
public UriAttachment(@NonNull Uri dataUri, public UriAttachment(@NonNull Uri dataUri,
@Nullable Uri thumbnailUri,
@NonNull String contentType, @NonNull String contentType,
int transferState, int transferState,
long size, long size,
@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties transformProperties) @Nullable TransformProperties transformProperties)
{ {
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties); super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri; this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
} }
@Override @Override
@NonNull @NonNull
public Uri getDataUri() { public Uri getUri() {
return dataUri; return dataUri;
} }
@Override
@Nullable
public Uri getThumbnailUri() {
return thumbnailUri;
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri); return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);

View File

@ -138,13 +138,11 @@ public class FullBackupImporter extends FullBackupBase {
inputStream.readAttachmentTo(output.second, attachment.getLength()); inputStream.readAttachmentTo(output.second, attachment.getLength());
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath()); contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
contentValues.put(AttachmentDatabase.THUMBNAIL, (String)null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first); contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
} catch (BadMacException e) { } catch (BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e); Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
dataFile.delete(); dataFile.delete();
contentValues.put(AttachmentDatabase.DATA, (String) null); contentValues.put(AttachmentDatabase.DATA, (String) null);
contentValues.put(AttachmentDatabase.THUMBNAIL, (String) null);
contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null); contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null);
} }

View File

@ -54,7 +54,7 @@ public class BorderlessImageView extends FrameLayout {
} }
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getDataUri() == null; boolean showControls = slide.asAttachment().getUri() == null;
if (slide.hasSticker()) { if (slide.hasSticker()) {
image.setFit(new CenterInside()); image.setFit(new CenterInside());

View File

@ -0,0 +1,51 @@
package org.thoughtcrime.securesms.components;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
/**
* Base dialog fragment for rendering as a full screen dialog with animation
* transitions.
*/
public abstract class FullScreenDialogFragment extends DialogFragment {
protected Toolbar toolbar;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
: R.style.TextSecure_LightTheme_FullScreenDialog);
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
toolbar.setTitle(getTitle());
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
return view;
}
protected void onNavigateUp() {
dismissAllowingStateLoss();
}
protected abstract @StringRes int getTitle();
protected abstract @LayoutRes int getDialogLayoutResource();
}

View File

@ -242,14 +242,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
if (!viewOnceSlides.isEmpty()) { if (!viewOnceSlides.isEmpty()) {
thumbnailView.setVisibility(GONE); thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE); attachmentContainerView.setVisibility(GONE);
} else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getThumbnailUri() != null) { } else if (!imageVideoSlides.isEmpty() && imageVideoSlides.get(0).getUri() != null) {
thumbnailView.setVisibility(VISIBLE); thumbnailView.setVisibility(VISIBLE);
attachmentContainerView.setVisibility(GONE); attachmentContainerView.setVisibility(GONE);
dismissView.setBackgroundResource(R.drawable.dismiss_background); dismissView.setBackgroundResource(R.drawable.dismiss_background);
if (imageVideoSlides.get(0).hasVideo()) { if (imageVideoSlides.get(0).hasVideo()) {
attachmentVideoOverlayView.setVisibility(VISIBLE); attachmentVideoOverlayView.setVisibility(VISIBLE);
} }
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri())) glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
.centerCrop() .centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size)) .override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)

View File

@ -119,7 +119,7 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
this.activeRecipients.clear(); this.activeRecipients.clear();
presentContact(contact); presentContact(contact);
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null); presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
presentActionButtons(ContactUtil.getRecipients(getContext(), contact)); presentActionButtons(ContactUtil.getRecipients(getContext(), contact));
for (LiveRecipient recipient : activeRecipients.values()) { for (LiveRecipient recipient : activeRecipients.values()) {

View File

@ -279,7 +279,7 @@ public class ThumbnailView extends FrameLayout {
getTransferControls().setVisibility(View.GONE); getTransferControls().setVisibility(View.GONE);
} }
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() && if (slide.getUri() != null && slide.hasPlayOverlay() &&
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview)) (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
{ {
this.playOverlay.setVisibility(View.VISIBLE); this.playOverlay.setVisibility(View.VISIBLE);
@ -288,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
} }
if (Util.equals(slide, this.slide)) { if (Util.equals(slide, this.slide)) {
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
return new SettableFuture<>(false); return new SettableFuture<>(false);
} }
if (this.slide != null && this.slide.getFastPreflightId() != null && if (this.slide != null && this.slide.getFastPreflightId() != null &&
(!slide.hasVideo() || Util.equals(this.slide.getThumbnailUri(), slide.getThumbnailUri())) && (!slide.hasVideo() || Util.equals(this.slide.getUri(), slide.getUri())) &&
Util.equals(this.slide.getFastPreflightId(), slide.getFastPreflightId())) Util.equals(this.slide.getFastPreflightId(), slide.getFastPreflightId()))
{ {
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId()); Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
@ -301,7 +301,7 @@ public class ThumbnailView extends FrameLayout {
return new SettableFuture<>(false); return new SettableFuture<>(false);
} }
Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri() Log.i(TAG, "loading part with id " + slide.asAttachment().getUri()
+ ", progress " + slide.getTransferState() + ", fast preflight id: " + + ", progress " + slide.getTransferState() + ", fast preflight id: " +
slide.asAttachment().getFastPreflightId()); slide.asAttachment().getFastPreflightId());
@ -327,7 +327,7 @@ public class ThumbnailView extends FrameLayout {
blurhash.setImageDrawable(null); blurhash.setImageDrawable(null);
} }
if (slide.getThumbnailUri() != null) { if (slide.getUri() != null) {
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) { if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>(); SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
thumbnailFuture.deferTo(result); thumbnailFuture.deferTo(result);
@ -412,7 +412,7 @@ public class ThumbnailView extends FrameLayout {
} }
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getUri()))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transition(withCrossFade()), fit); .transition(withCrossFade()), fit);
@ -469,10 +469,10 @@ public class ThumbnailView extends FrameLayout {
private class ThumbnailClickDispatcher implements View.OnClickListener { private class ThumbnailClickDispatcher implements View.OnClickListener {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
if (thumbnailClickListener != null && if (thumbnailClickListener != null &&
slide != null && slide != null &&
slide.asAttachment().getDataUri() != null && slide.asAttachment().getUri() != null &&
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
{ {
thumbnailClickListener.onClick(view, slide); thumbnailClickListener.onClick(view, slide);
} else if (parentClickListener != null) { } else if (parentClickListener != null) {

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.webrtc.EglBase;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.WeakHashMap;
public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
}
public @Nullable EglBase getEglBase() {
return eglBase;
}
public void addSink(@NonNull VideoSink sink) {
sinks.put(sink, true);
}
public void removeSink(@NonNull VideoSink sink) {
sinks.remove(sink);
}
@Override
public void onFrame(@NonNull VideoFrame videoFrame) {
for (VideoSink sink : sinks.keySet()) {
sink.onFrame(videoFrame);
}
}
}

View File

@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import java.util.Objects;
/**
* Encapsulates views needed to show a call participant including their
* avatar in full screen or pip mode, and their video feed.
*/
public class CallParticipantView extends ConstraintLayout {
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private RecipientId recipientId;
private AvatarImageView avatar;
private TextureViewRenderer renderer;
private ImageView pipAvatar;
private ContactPhoto contactPhoto;
public CallParticipantView(@NonNull Context context) {
super(context);
onFinishInflate();
}
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CallParticipantView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
}
void setCallParticipant(@NonNull CallParticipant participant) {
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
recipientId = participant.getRecipient().getId();
renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
if (participant.isVideoEnabled()) {
if (participant.getVideoSink().getEglBase() != null) {
renderer.init(participant.getVideoSink().getEglBase());
}
renderer.attachBroadcastVideoSink(participant.getVideoSink());
} else {
renderer.attachBroadcastVideoSink(null);
}
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
avatar.setAvatar(participant.getRecipient());
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this);
setPipAvatar(participant.getRecipient());
contactPhoto = participant.getRecipient().getContactPhoto();
}
}
void setRenderInPip(boolean shouldRenderInPip) {
avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
}
private void setPipAvatar(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
GlideApp.with(this)
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(getContext()))
.error(fallbackPhoto.asCallCard(getContext()))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(pipAvatar);
pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
}
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
@Override
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
ResourceContactPhoto photo = new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
return photo;
}
}
}

View File

@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.flexbox.AlignItems;
import com.google.android.flexbox.FlexboxLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.Util;
import java.util.Collections;
import java.util.List;
/**
* Can dynamically render a collection of call participants, adjusting their
* sizing and layout depending on the total number of participants.
*/
public class CallParticipantsLayout extends FlexboxLayout {
private List<CallParticipant> callParticipants = Collections.emptyList();
private boolean shouldRenderInPip;
public CallParticipantsLayout(@NonNull Context context) {
super(context);
}
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CallParticipantsLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
void update(@NonNull List<CallParticipant> callParticipants, boolean shouldRenderInPip) {
this.callParticipants = callParticipants;
this.shouldRenderInPip = shouldRenderInPip;
updateLayout();
}
private void updateLayout() {
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
updateChildrenCount(1);
update(0, callParticipants.get(0));
} else {
int count = callParticipants.size();
updateChildrenCount(count);
for (int i = 0; i < callParticipants.size(); i++) {
update(i, callParticipants.get(i));
}
}
}
private void updateChildrenCount(int count) {
int childCount = getChildCount();
if (childCount < count) {
for (int i = childCount; i < count; i++) {
addCallParticipantView();
}
} else if (childCount > count) {
for (int i = count; i < childCount; i++) {
removeViewAt(count);
}
}
}
private void update(int index, @NonNull CallParticipant participant) {
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(shouldRenderInPip);
setChildLayoutParams(callParticipantView, index, getChildCount());
}
private void addCallParticipantView() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
params.setAlignSelf(AlignItems.STRETCH);
view.setLayoutParams(params);
addView(view);
}
private void setChildLayoutParams(@NonNull View child, int childPosition, int childCount) {
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) child.getLayoutParams();
if (childCount < 3) {
params.setFlexBasisPercent(1f);
} else {
if ((childCount % 2) != 0 && childPosition == childCount - 1) {
params.setFlexBasisPercent(1f);
} else {
params.setFlexBasisPercent(0.5f);
}
}
child.setLayoutParams(params);
}
}

View File

@ -0,0 +1,215 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents the state of all participants, remote and local, combined with view state
* needed to properly render the participants. The view state primarily consists of
* if we are in System PIP mode and if we should show our video for an outgoing call.
*/
public final class CallParticipantsState {
private static final int SMALL_GROUP_MAX = 6;
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
private final WebRtcViewModel.State callState;
private final List<CallParticipant> remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@NonNull WebRtcLocalRenderState localRenderState,
boolean isInPipMode,
boolean showVideoForOutgoing,
boolean isViewingFocusedParticipant)
{
this.callState = callState;
this.remoteParticipants = remoteParticipants;
this.localParticipant = localParticipant;
this.localRenderState = localRenderState;
this.focusedParticipant = focusedParticipant;
this.isInPipMode = isInPipMode;
this.showVideoForOutgoing = showVideoForOutgoing;
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
}
public @NonNull WebRtcViewModel.State getCallState() {
return callState;
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
} else {
return getAllRemoteParticipants();
}
}
public @NonNull List<CallParticipant> getListParticipants() {
List<CallParticipant> listParticipants = new ArrayList<>();
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()));
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
} else {
return Collections.emptyList();
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
}
public @NonNull CallParticipant getLocalParticipant() {
return localParticipant;
}
public @Nullable CallParticipant getFocusedParticipant() {
return focusedParticipant;
}
public @NonNull WebRtcLocalRenderState getLocalRenderState() {
return localRenderState;
}
public boolean isLargeVideoGroup() {
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
}
public boolean isInPipMode() {
return isInPipMode;
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
@NonNull WebRtcViewModel webRtcViewModel,
boolean enableVideo)
{
boolean newShowVideoForOutgoing = oldState.showVideoForOutgoing;
if (enableVideo) {
newShowVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
newShowVideoForOutgoing = false;
}
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(webRtcViewModel.getLocalParticipant(),
oldState.isInPipMode,
newShowVideoForOutgoing,
webRtcViewModel.getState(),
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getRemoteParticipants(),
webRtcViewModel.getLocalParticipant(),
focused,
localRenderState,
oldState.isInPipMode,
newShowVideoForOutgoing,
oldState.isViewingFocusedParticipant);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
isInPip,
oldState.showVideoForOutgoing,
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(oldState.callState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
localRenderState,
isInPip,
oldState.showVideoForOutgoing,
oldState.isViewingFocusedParticipant);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.callState,
oldState.getAllRemoteParticipants().size(),
selectedPage == SelectedPage.FOCUSED);
return new CallParticipantsState(oldState.callState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
selectedPage == SelectedPage.FOCUSED);
}
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
boolean isInPip,
boolean showVideoForOutgoing,
@NonNull WebRtcViewModel.State callState,
int numberOfRemoteParticipants,
boolean isViewingFocusedParticipant)
{
boolean displayLocal = !isInPip && localParticipant.isVideoEnabled();
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
} else {
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
}
} else if (callState != WebRtcViewModel.State.CALL_DISCONNECTED) {
localRenderState = WebRtcLocalRenderState.LARGE;
}
} else if (callState == WebRtcViewModel.State.CALL_PRE_JOIN) {
localRenderState = WebRtcLocalRenderState.LARGE_NO_VIDEO;
}
return localRenderState;
}
public enum SelectedPage {
GRID,
FOCUSED
}
}

View File

@ -9,6 +9,7 @@ import android.view.VelocityTracker;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -16,21 +17,26 @@ import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout; import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener { public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
private static final float DECELERATION_RATE = 0.99f; private static final float DECELERATION_RATE = 0.99f;
private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
private final ViewGroup parent; private final ViewGroup parent;
private final View child; private final View child;
private final int framePadding; private final int framePadding;
private final int pipWidth; private final Queue<Runnable> runAfterFling;
private final int pipHeight;
private int pipWidth;
private int pipHeight;
private int activePointerId = MotionEvent.INVALID_POINTER_ID; private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX; private float lastTouchX;
private float lastTouchY; private float lastTouchY;
@ -42,6 +48,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private double projectionY; private double projectionY;
private VelocityTracker velocityTracker; private VelocityTracker velocityTracker;
private int maximumFlingVelocity; private int maximumFlingVelocity;
private boolean isLockedToBottomEnd;
private Interpolator interpolator;
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) { public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
@ -95,6 +103,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width); this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width);
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height); this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity(); this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
this.runAfterFling = new LinkedList<>();
this.interpolator = ADJUST_INTERPOLATOR;
} }
public void clearVerticalBoundaries() { public void clearVerticalBoundaries() {
@ -105,11 +115,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
extraPaddingTop = topBoundary - parent.getTop(); extraPaddingTop = topBoundary - parent.getTop();
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary; extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
if (isAnimating) { adjustPip();
fling();
} else if (!isDragging) {
onFling(null, null, 0, 0);
}
} }
private boolean onGestureFinished(MotionEvent e) { private boolean onGestureFinished(MotionEvent e) {
@ -123,12 +129,46 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return false; return false;
} }
public void adjustPip() {
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
if (isAnimating) {
interpolator = ADJUST_INTERPOLATOR;
fling();
} else if (!isDragging) {
interpolator = ADJUST_INTERPOLATOR;
onFling(null, null, 0, 0);
}
}
public void lockToBottomEnd() {
isLockedToBottomEnd = true;
}
public void enableCorners() {
isLockedToBottomEnd = false;
}
public void performAfterFling(@NonNull Runnable runnable) {
if (isAnimating) {
runAfterFling.add(runnable);
} else {
runnable.run();
}
}
@Override @Override
public boolean onDown(MotionEvent e) { public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0); activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX(); lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY(); lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true; isDragging = true;
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
interpolator = FLING_INTERPOLATOR;
return true; return true;
} }
@ -167,6 +207,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
return true; return true;
} }
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
return true;
}
private void fling() { private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY); Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection); Point nearestCornerPosition = findNearestCornerPosition(projection);
@ -178,17 +225,30 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
.translationX(getTranslationXForPoint(nearestCornerPosition)) .translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition)) .translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250) .setDuration(250)
.setInterpolator(new ViscousFluidInterpolator()) .setInterpolator(interpolator)
.setListener(new AnimationCompleteListener() { .setListener(new AnimationCompleteListener() {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
isAnimating = false; isAnimating = false;
Iterator<Runnable> afterFlingRunnables = runAfterFling.iterator();
while (afterFlingRunnables.hasNext()) {
Runnable runnable = afterFlingRunnables.next();
runnable.run();
afterFlingRunnables.remove();
}
} }
}) })
.start(); .start();
} }
private Point findNearestCornerPosition(Point projection) { private Point findNearestCornerPosition(Point projection) {
if (isLockedToBottomEnd) {
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
: calculateBottomLeftCoordinates(parent);
}
Point maxPoint = null; Point maxPoint = null;
double maxDistance = Double.MAX_VALUE; double maxDistance = Double.MAX_VALUE;

View File

@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
private boolean enableFixedSize; private boolean enableFixedSize;
private int surfaceWidth; private int surfaceWidth;
private int surfaceHeight; private int surfaceHeight;
private boolean isInitialized;
private BroadcastVideoSink attachedVideoSink;
public TextureViewRenderer(@NonNull Context context) { public TextureViewRenderer(@NonNull Context context) {
super(context); super(context);
@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.setSurfaceTextureListener(this); this.setSurfaceTextureListener(this);
} }
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) { public void init(@NonNull EglBase eglBase) {
this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer()); if (isInitialized) return;
isInitialized = true;
this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
} }
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) { public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
@ -63,6 +69,30 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
this.eglRenderer.init(sharedContext, this, configAttributes, drawer); this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
} }
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
if (attachedVideoSink == videoSink) {
return;
}
if (attachedVideoSink != null) {
attachedVideoSink.removeSink(this);
}
if (videoSink != null) {
videoSink.addSink(this);
} else {
clearImage();
}
attachedVideoSink = videoSink;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
release();
}
public void release() { public void release() {
eglRenderer.release(); eglRenderer.release();
} }
@ -125,6 +155,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
protected void onMeasure(int widthSpec, int heightSpec) { protected void onMeasure(int widthSpec, int heightSpec) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
widthSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, widthSpec, 0), MeasureSpec.AT_MOST);
heightSpec = MeasureSpec.makeMeasureSpec(resolveSizeAndState(0, heightSpec, 0), MeasureSpec.AT_MOST);
Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight); Point size = videoLayoutMeasure.measure(widthSpec, heightSpec, this.rotatedFrameWidth, this.rotatedFrameHeight);
setMeasuredDimension(size.x, size.y); setMeasuredDimension(size.x, size.y);

View File

@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.events.CallParticipant;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
class WebRtcCallParticipantsPage {
private final List<CallParticipant> callParticipants;
private final boolean isSpeaker;
private final boolean isRenderInPip;
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
}
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
}
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
boolean isSpeaker,
boolean isRenderInPip)
{
this.callParticipants = callParticipants;
this.isSpeaker = isSpeaker;
this.isRenderInPip = isRenderInPip;
}
public @NonNull List<CallParticipant> getCallParticipants() {
return callParticipants;
}
public boolean isRenderInPip() {
return isRenderInPip;
}
public boolean isSpeaker() {
return isSpeaker;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
return isSpeaker == that.isSpeaker &&
isRenderInPip == that.isRenderInPip &&
callParticipants.equals(that.callParticipants);
}
@Override
public int hashCode() {
return Objects.hash(callParticipants, isSpeaker);
}
}

View File

@ -0,0 +1,127 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
private static final int VIEW_TYPE_MULTI = 0;
private static final int VIEW_TYPE_SINGLE = 1;
private final Runnable onPageClicked;
WebRtcCallParticipantsPagerAdapter(@NonNull Runnable onPageClicked) {
super(new DiffCallback());
this.onPageClicked = onPageClicked;
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final ViewHolder viewHolder;
switch (viewType) {
case VIEW_TYPE_SINGLE:
viewHolder = new SingleParticipantViewHolder((CallParticipantView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.call_participant_item,
parent,
false));
break;
case VIEW_TYPE_MULTI:
viewHolder = new MultipleParticipantViewHolder((CallParticipantsLayout) LayoutInflater.from(parent.getContext())
.inflate(R.layout.webrtc_call_participants_layout,
parent,
false));
break;
default:
throw new IllegalArgumentException("Unsupported viewType: " + viewType);
}
viewHolder.itemView.setOnClickListener(unused -> onPageClicked.run());
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override
public int getItemViewType(int position) {
return getItem(position).isSpeaker() ? VIEW_TYPE_SINGLE : VIEW_TYPE_MULTI;
}
static abstract class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(@NonNull View itemView) {
super(itemView);
}
abstract void bind(WebRtcCallParticipantsPage page);
}
private static class MultipleParticipantViewHolder extends ViewHolder {
private final CallParticipantsLayout callParticipantsLayout;
private MultipleParticipantViewHolder(@NonNull CallParticipantsLayout callParticipantsLayout) {
super(callParticipantsLayout);
this.callParticipantsLayout = callParticipantsLayout;
}
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.isRenderInPip());
}
}
private static class SingleParticipantViewHolder extends ViewHolder {
private final CallParticipantView callParticipantView;
private SingleParticipantViewHolder(CallParticipantView callParticipantView) {
super(callParticipantView);
this.callParticipantView = callParticipantView;
ViewGroup.LayoutParams params = callParticipantView.getLayoutParams();
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
callParticipantView.setLayoutParams(params);
}
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
callParticipantView.setRenderInPip(page.isRenderInPip());
}
}
private static final class DiffCallback extends DiffUtil.ItemCallback<WebRtcCallParticipantsPage> {
@Override
public boolean areItemsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
return oldItem.isSpeaker() == newItem.isSpeaker();
}
@Override
public boolean areContentsTheSame(@NonNull WebRtcCallParticipantsPage oldItem, @NonNull WebRtcCallParticipantsPage newItem) {
return oldItem.equals(newItem);
}
}
}

View File

@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
private static final int PARTICIPANT = 0;
private static final int EMPTY = 1;
protected WebRtcCallParticipantsRecyclerAdapter() {
super(new DiffCallback());
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == PARTICIPANT) {
return new ParticipantViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
} else {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_empty_item, parent, false));
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override
public int getItemViewType(int position) {
return getItem(position) == CallParticipant.EMPTY ? EMPTY : PARTICIPANT;
}
static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(@NonNull View itemView) {
super(itemView);
}
void bind(@NonNull CallParticipant callParticipant) {}
}
private static class ParticipantViewHolder extends ViewHolder {
private final CallParticipantView callParticipantView;
ParticipantViewHolder(@NonNull View itemView) {
super(itemView);
callParticipantView = itemView.findViewById(R.id.call_participant);
}
@Override
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
}
}
private static class DiffCallback extends DiffUtil.ItemCallback<CallParticipant> {
@Override
public boolean areItemsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
return oldItem.getRecipient().equals(newItem.getRecipient());
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipant oldItem, @NonNull CallParticipant newItem) {
return oldItem.equals(newItem);
}
}
}

View File

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.components.webrtc; package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context; import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.animation.Animation;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -13,67 +15,85 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.Guideline; import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition; import androidx.transition.AutoTransition;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition; import androidx.transition.Transition;
import androidx.transition.TransitionManager; import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton; import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
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.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon; import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
public class WebRtcCallView extends FrameLayout { public class WebRtcCallView extends FrameLayout {
private static final long TRANSITION_DURATION_MILLIS = 250; private static final long TRANSITION_DURATION_MILLIS = 250;
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8; private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16; private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
public static final int FADE_OUT_DELAY = 5000; public static final int FADE_OUT_DELAY = 5000;
public static final int PIP_RESIZE_DURATION = 300;
public static final int CONTROLS_HEIGHT = 98;
private TextureViewRenderer localRenderer;
private WebRtcAudioOutputToggleButton audioToggle; private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle; private AccessibleToggleButton videoToggle;
private AccessibleToggleButton micToggle; private AccessibleToggleButton micToggle;
private ViewGroup largeLocalRenderContainer; private ViewGroup smallLocalRenderFrame;
private ViewGroup localRenderPipFrame; private TextureViewRenderer smallLocalRender;
private ViewGroup smallLocalRenderContainer; private View largeLocalRenderFrame;
private ViewGroup remoteRenderContainer; private TextureViewRenderer largeLocalRender;
private View largeLocalRenderNoVideo;
private ImageView largeLocalRenderNoVideoAvatar;
private TextView recipientName; private TextView recipientName;
private TextView status; private TextView status;
private ConstraintLayout parent; private ConstraintLayout parent;
private AvatarImageView avatar; private ConstraintLayout participantsParent;
private ImageView avatarCard;
private ControlsListener controlsListener; private ControlsListener controlsListener;
private RecipientId recipientId; private RecipientId recipientId;
private CameraState.Direction cameraDirection;
private ImageView answer; private ImageView answer;
private ImageView cameraDirectionToggle; private ImageView cameraDirectionToggle;
private PictureInPictureGestureHelper pictureInPictureGestureHelper; private PictureInPictureGestureHelper pictureInPictureGestureHelper;
private ImageView hangup; private ImageView hangup;
private View answerWithAudio; private View answerWithAudio;
private View answerWithAudioLabel; private View answerWithAudioLabel;
private View ongoingFooterGradient; private View footerGradient;
private View startCallControls;
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
private final Set<View> incomingCallViews = new HashSet<>(); private final Set<View> incomingCallViews = new HashSet<>();
private final Set<View> topViews = new HashSet<>(); private final Set<View> topViews = new HashSet<>();
@ -82,7 +102,8 @@ public class WebRtcCallView extends FrameLayout {
private WebRtcControls controls = WebRtcControls.NONE; private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> { private final Runnable fadeOutRunnable = () -> {
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); }; if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
};
public WebRtcCallView(@NonNull Context context) { public WebRtcCallView(@NonNull Context context) {
this(context, null); this(context, null);
@ -99,42 +120,61 @@ public class WebRtcCallView extends FrameLayout {
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
audioToggle = findViewById(R.id.call_screen_speaker_toggle); audioToggle = findViewById(R.id.call_screen_speaker_toggle);
videoToggle = findViewById(R.id.call_screen_video_toggle); videoToggle = findViewById(R.id.call_screen_video_toggle);
micToggle = findViewById(R.id.call_screen_audio_mic_toggle); micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
localRenderPipFrame = findViewById(R.id.call_screen_pip); smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder); smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder); largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder); largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
recipientName = findViewById(R.id.call_screen_recipient_name); largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
status = findViewById(R.id.call_screen_status); largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
parent = findViewById(R.id.call_screen); recipientName = findViewById(R.id.call_screen_recipient_name);
avatar = findViewById(R.id.call_screen_recipient_avatar); status = findViewById(R.id.call_screen_status);
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card); parent = findViewById(R.id.call_screen);
answer = findViewById(R.id.call_screen_answer_call); participantsParent = findViewById(R.id.call_screen_participants_parent);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle); answer = findViewById(R.id.call_screen_answer_call);
hangup = findViewById(R.id.call_screen_end_call); cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio); hangup = findViewById(R.id.call_screen_end_call);
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label); answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient); answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
footerGradient = findViewById(R.id.call_screen_footer_gradient);
startCallControls = findViewById(R.id.call_screen_start_call_controls);
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
View topGradient = findViewById(R.id.call_screen_header_gradient); View topGradient = findViewById(R.id.call_screen_header_gradient);
View downCaret = findViewById(R.id.call_screen_down_arrow);
View decline = findViewById(R.id.call_screen_decline_call); View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label); View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label); View declineLabel = findViewById(R.id.call_screen_decline_call_label);
View incomingFooterGradient = findViewById(R.id.call_screen_incoming_footer_gradient);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline); Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View startCall = findViewById(R.id.call_screen_start_call_start_call);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
topViews.add(status); callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
pagerAdapter = new WebRtcCallParticipantsPagerAdapter(this::toggleControls);
recyclerAdapter = new WebRtcCallParticipantsRecyclerAdapter();
callParticipantsPager.setAdapter(pagerAdapter);
callParticipantsRecycler.setAdapter(recyclerAdapter);
callParticipantsPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
}
});
topViews.add(toolbar);
topViews.add(topGradient); topViews.add(topGradient);
topViews.add(recipientName);
incomingCallViews.add(answer); incomingCallViews.add(answer);
incomingCallViews.add(answerLabel); incomingCallViews.add(answerLabel);
incomingCallViews.add(decline); incomingCallViews.add(decline);
incomingCallViews.add(declineLabel); incomingCallViews.add(declineLabel);
incomingCallViews.add(incomingFooterGradient); incomingCallViews.add(footerGradient);
adjustableMarginsSet.add(micToggle); adjustableMarginsSet.add(micToggle);
adjustableMarginsSet.add(cameraDirectionToggle); adjustableMarginsSet.add(cameraDirectionToggle);
@ -158,15 +198,18 @@ public class WebRtcCallView extends FrameLayout {
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed)); hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed)); decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed)); answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed)); answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
setOnClickListener(v -> toggleControls()); pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
avatar.setOnClickListener(v -> toggleControls());
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(localRenderPipFrame); startCall.setOnClickListener(v -> runIfNonNull(controlsListener, listener -> listener.onStartCall(videoToggle.isChecked())));
cancelStartCall.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCancelStartCall));
ColorMatrix greyScaleMatrix = new ColorMatrix();
greyScaleMatrix.setSaturation(0);
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this); int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight); statusBarGuideline.setGuidelineBegin(statusBarHeight);
@ -195,67 +238,99 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false); micToggle.setChecked(isMicEnabled, false);
} }
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) { public void updateCallParticipants(@NonNull CallParticipantsState state) {
if (isRemoteVideoEnabled) { List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
remoteRenderContainer.setVisibility(View.VISIBLE);
if (!state.getGridParticipants().isEmpty()) {
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
}
if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
if (state.isLargeVideoGroup()) {
layoutParticipantsForLargeCount();
} else { } else {
remoteRenderContainer.setVisibility(View.GONE); layoutParticipantsForSmallCount();
} }
} }
public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) { public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
if (localRenderer == surfaceViewRenderer) { smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
return; largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
largeLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
if (localCallParticipant.getVideoSink().getEglBase() != null) {
smallLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
} }
localRenderer = surfaceViewRenderer; switch (state) {
if (surfaceViewRenderer == null) {
setRenderer(largeLocalRenderContainer, null);
setRenderer(smallLocalRenderContainer, null);
} else {
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
}
}
public void setRemoteRenderer(@Nullable TextureViewRenderer remoteRenderer) {
setRenderer(remoteRenderContainer, remoteRenderer);
}
public void setLocalRenderState(WebRtcLocalRenderState localRenderState) {
videoToggle.setChecked(localRenderState != WebRtcLocalRenderState.GONE, false);
switch (localRenderState) {
case GONE: case GONE:
localRenderPipFrame.setVisibility(View.GONE); largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderContainer.setVisibility(View.GONE); largeLocalRenderFrame.setVisibility(View.GONE);
setRenderer(largeLocalRenderContainer, null); smallLocalRender.attachBroadcastVideoSink(null);
setRenderer(smallLocalRenderContainer, null); smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
case SMALL_RECTANGLE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToRectangle();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break;
case SMALL_SQUARE:
smallLocalRenderFrame.setVisibility(View.VISIBLE);
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
animatePipToSquare();
largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break; break;
case LARGE: case LARGE:
localRenderPipFrame.setVisibility(View.GONE); largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
largeLocalRenderContainer.setVisibility(View.VISIBLE); largeLocalRenderFrame.setVisibility(View.VISIBLE);
if (largeLocalRenderContainer.getChildCount() == 0) {
setRenderer(largeLocalRenderContainer, localRenderer); largeLocalRenderNoVideo.setVisibility(View.GONE);
} largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
smallLocalRender.attachBroadcastVideoSink(null);
smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(true, false);
break; break;
case SMALL: case LARGE_NO_VIDEO:
localRenderPipFrame.setVisibility(View.VISIBLE); largeLocalRender.attachBroadcastVideoSink(null);
largeLocalRenderContainer.setVisibility(View.GONE); largeLocalRenderFrame.setVisibility(View.VISIBLE);
if (smallLocalRenderContainer.getChildCount() == 0) { largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
setRenderer(smallLocalRenderContainer, localRenderer); largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
}
}
}
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) { GlideApp.with(getContext().getApplicationContext())
this.cameraDirection = cameraDirection; .load(new ProfileContactPhoto(localCallParticipant.getRecipient(), localCallParticipant.getRecipient().getProfileAvatar()))
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(largeLocalRenderNoVideoAvatar);
if (localRenderer != null) { smallLocalRender.attachBroadcastVideoSink(null);
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT); smallLocalRenderFrame.setVisibility(View.GONE);
videoToggle.setChecked(false, false);
break;
} }
} }
@ -265,17 +340,16 @@ public class WebRtcCallView extends FrameLayout {
} }
recipientId = recipient.getId(); recipientId = recipient.getId();
recipientName.setText(recipient.getDisplayName(getContext()));
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
avatar.setAvatar(GlideApp.with(this), recipient, false);
AvatarUtil.loadBlurredIconIntoViewBackground(recipient, this);
setRecipientCallCard(recipient); if (recipient.isGroup()) {
} recipientName.setText(R.string.WebRtcCallView__group_call);
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
public void showCallCard(boolean showCallCard) { toolbar.inflateMenu(R.menu.group_call);
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE); toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
avatar.setVisibility(showCallCard ? GONE : VISIBLE); }
} else {
recipientName.setText(recipient.getDisplayName(getContext()));
}
} }
public void setStatus(@NonNull String status) { public void setStatus(@NonNull String status) {
@ -302,11 +376,16 @@ public class WebRtcCallView extends FrameLayout {
} }
} }
public void setWebRtcControls(WebRtcControls webRtcControls) { public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet); Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
visibleViewSet.clear(); visibleViewSet.clear();
if (webRtcControls.displayStartCallControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(startCallControls);
}
if (webRtcControls.displayTopViews()) { if (webRtcControls.displayTopViews()) {
visibleViewSet.addAll(topViews); visibleViewSet.addAll(topViews);
} }
@ -341,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
if (webRtcControls.displayEndCall()) { if (webRtcControls.displayEndCall()) {
visibleViewSet.add(hangup); visibleViewSet.add(hangup);
visibleViewSet.add(ongoingFooterGradient); visibleViewSet.add(footerGradient);
} }
if (webRtcControls.displayMuteAudio()) { if (webRtcControls.displayMuteAudio()) {
@ -358,6 +437,12 @@ public class WebRtcCallView extends FrameLayout {
updateButtonStateForLargeButtons(); updateButtonStateForLargeButtons();
} }
if (webRtcControls.displayRemoteVideoRecycler()) {
callParticipantsRecycler.setVisibility(View.VISIBLE);
} else {
callParticipantsRecycler.setVisibility(View.GONE);
}
if (webRtcControls.isFadeOutEnabled()) { if (webRtcControls.isFadeOutEnabled()) {
if (!controls.isFadeOutEnabled()) { if (!controls.isFadeOutEnabled()) {
scheduleFadeOut(); scheduleFadeOut();
@ -378,8 +463,39 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle; return videoToggle;
} }
private void animatePipToRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
pictureInPictureGestureHelper.enableCorners();
pictureInPictureGestureHelper.adjustPip();
}
});
smallLocalRenderFrame.startAnimation(animation);
}
private void animatePipToSquare() {
pictureInPictureGestureHelper.lockToBottomEnd();
pictureInPictureGestureHelper.performAfterFling(() -> {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
animation.setDuration(PIP_RESIZE_DURATION);
animation.setAnimationListener(new SimpleAnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
pictureInPictureGestureHelper.adjustPip();
}
});
smallLocalRenderFrame.startAnimation(animation);
});
}
private void toggleControls() { private void toggleControls() {
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) { if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
fadeOutControls(); fadeOutControls();
} else { } else {
fadeInControls(); fadeInControls();
@ -399,9 +515,44 @@ public class WebRtcCallView extends FrameLayout {
scheduleFadeOut(); scheduleFadeOut();
} }
private void fadeControls(int visibility) { private void layoutParticipantsForSmallCount() {
pagerBottomMarginDp = 0;
layoutParticipants();
}
private void layoutParticipantsForLargeCount() {
pagerBottomMarginDp = 104;
layoutParticipants();
}
private int withControlsHeight(int margin) {
if (margin == 0) {
return 0;
}
return controlsVisible ? margin + CONTROLS_HEIGHT : margin;
}
private void layoutParticipants() {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS); Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(participantsParent, transition);
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(participantsParent);
constraintSet.setMargin(R.id.call_screen_participants_pager, ConstraintSet.BOTTOM, ViewUtil.dpToPx(withControlsHeight(pagerBottomMarginDp)));
constraintSet.applyTo(participantsParent);
}
private void fadeControls(int visibility) {
controlsVisible = visibility == VISIBLE;
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(parent, transition); TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet(); ConstraintSet constraintSet = new ConstraintSet();
@ -412,6 +563,8 @@ public class WebRtcCallView extends FrameLayout {
} }
constraintSet.applyTo(parent); constraintSet.applyTo(parent);
layoutParticipants();
} }
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) { private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
@ -458,40 +611,6 @@ public class WebRtcCallView extends FrameLayout {
} }
} }
private static void setRenderer(@NonNull ViewGroup container, @Nullable View renderer) {
if (renderer == null) {
container.removeAllViews();
return;
}
ViewParent parent = renderer.getParent();
if (parent != null && parent != container) {
((ViewGroup) parent).removeAllViews();
}
if (parent == container) {
return;
}
container.addView(renderer);
}
private void setRecipientCallCard(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
GlideApp.with(this).load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(getContext()))
.error(fallbackPhoto.asCallCard(getContext()))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(this.avatarCard);
if (contactPhoto == null) this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
else this.avatarCard.setScaleType(ImageView.ScaleType.CENTER_CROP);
this.avatarCard.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
}
private void updateButtonStateForLargeButtons() { private void updateButtonStateForLargeButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle); cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup); hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
@ -508,14 +627,14 @@ public class WebRtcCallView extends FrameLayout {
audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small); audioToggle.setImageResource(R.drawable.webrtc_call_screen_speaker_toggle_small);
} }
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider { private boolean showParticipantsList() {
@Override controlsListener.onShowParticipantsList();
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() { return true;
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
}
} }
public interface ControlsListener { public interface ControlsListener {
void onStartCall(boolean isVideoCall);
void onCancelStartCall();
void onControlsFadeOut(); void onControlsFadeOut();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput); void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled); void onVideoChanged(boolean isVideoEnabled);
@ -525,6 +644,7 @@ public class WebRtcCallView extends FrameLayout {
void onDenyCallPressed(); void onDenyCallPressed();
void onAcceptCallWithVoiceOnlyPressed(); void onAcceptCallWithVoiceOnlyPressed();
void onAcceptCallPressed(); void onAcceptCallPressed();
void onDownCaretPressed(); void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
} }
} }

View File

@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
public class WebRtcCallViewModel extends ViewModel { public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false); private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true); private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE); private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false); private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false); private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT); private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b); private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState); private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> ellapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private boolean showVideoForOutgoing = false;
private long callConnectedTime = -1;
private Handler ellapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
private Runnable ellapsedTimeRunnable = this::handleTick;
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
private long callConnectedTime = -1;
private Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository(); private final WebRtcCallRepository repository = new WebRtcCallRepository();
public LiveData<Boolean> getRemoteVideoEnabled() {
return Transformations.distinctUntilChanged(remoteVideoEnabled);
}
public LiveData<Boolean> getMicrophoneEnabled() { public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled); return Transformations.distinctUntilChanged(microphoneEnabled);
} }
public LiveData<CameraState.Direction> getCameraDirection() {
return Transformations.distinctUntilChanged(cameraDirection);
}
public LiveData<Boolean> displaySquareCallCard() {
return isInPipMode;
}
public LiveData<WebRtcLocalRenderState> getLocalRenderState() {
return realLocalRenderState;
}
public LiveData<WebRtcControls> getWebRtcControls() { public LiveData<WebRtcControls> getWebRtcControls() {
return realWebRtcControls; return realWebRtcControls;
} }
@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
} }
public LiveData<Long> getCallTime() { public LiveData<Long> getCallTime() {
return Transformations.map(ellapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall); return Transformations.map(elapsed, timeInCall -> callConnectedTime == -1 ? -1 : timeInCall);
}
public LiveData<CallParticipantsState> getCallParticipantsState() {
return participantsState;
}
public boolean canEnterPipMode() {
return canEnterPipMode;
} }
public boolean isAnswerWithVideoAvailable() { public boolean isAnswerWithVideoAvailable() {
@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread @MainThread
public void setIsInPipMode(boolean isInPipMode) { public void setIsInPipMode(boolean isInPipMode) {
this.isInPipMode.setValue(isInPipMode); this.isInPipMode.setValue(isInPipMode);
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), isInPipMode));
}
@MainThread
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
} }
public void onDismissedVideoTooltip() { public void onDismissedVideoTooltip() {
@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread @MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) { public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled()); canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) { CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
}
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled()); microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
if (enableVideo) { //noinspection ConstantConditions
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING; participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
showVideoForOutgoing = false;
}
updateLocalRenderState(webRtcViewModel.getState());
updateWebRtcControls(webRtcViewModel.getState(), updateWebRtcControls(webRtcViewModel.getState(),
webRtcViewModel.getLocalCameraState().isEnabled(), localParticipant.getCameraState().isEnabled(),
webRtcViewModel.isRemoteVideoEnabled(), webRtcViewModel.isRemoteVideoEnabled(),
webRtcViewModel.isRemoteVideoOffer(), webRtcViewModel.isRemoteVideoOffer(),
webRtcViewModel.getLocalCameraState().getCameraCount() > 1, localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(), webRtcViewModel.isBluetoothAvailable(),
repository.getAudioOutput()); repository.getAudioOutput());
@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
callConnectedTime = -1; callConnectedTime = -1;
} }
if (webRtcViewModel.getLocalCameraState().isEnabled()) { if (localParticipant.getCameraState().isEnabled()) {
canDisplayTooltipIfNeeded = false; canDisplayTooltipIfNeeded = false;
hasEnabledLocalVideo = true; hasEnabledLocalVideo = true;
events.setValue(Event.DISMISS_VIDEO_TOOLTIP); events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
} }
@ -144,34 +132,36 @@ public class WebRtcCallViewModel extends ViewModel {
} }
} }
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) { private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
return direction == CameraState.Direction.FRONT || direction == CameraState.Direction.BACK;
}
private void updateLocalRenderState(WebRtcViewModel.State state) {
if (state == WebRtcViewModel.State.CALL_CONNECTED) {
localRenderState.setValue(WebRtcLocalRenderState.SMALL);
} else {
localRenderState.setValue(WebRtcLocalRenderState.LARGE);
}
}
private void updateWebRtcControls(WebRtcViewModel.State state,
boolean isLocalVideoEnabled, boolean isLocalVideoEnabled,
boolean isRemoteVideoEnabled, boolean isRemoteVideoEnabled,
boolean isRemoteVideoOffer, boolean isRemoteVideoOffer,
boolean isMoreThanOneCameraAvailable, boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable, boolean isBluetoothAvailable,
WebRtcAudioOutput audioOutput) @NonNull WebRtcAudioOutput audioOutput)
{ {
final WebRtcControls.CallState callState; final WebRtcControls.CallState callState;
switch (state) { switch (state) {
case CALL_PRE_JOIN:
callState = WebRtcControls.CallState.PRE_JOIN;
break;
case CALL_INCOMING: case CALL_INCOMING:
callState = WebRtcControls.CallState.INCOMING; callState = WebRtcControls.CallState.INCOMING;
answerWithVideoAvailable = isRemoteVideoOffer; answerWithVideoAvailable = isRemoteVideoOffer;
break; break;
case CALL_OUTGOING:
case CALL_RINGING:
callState = WebRtcControls.CallState.OUTGOING;
break;
case CALL_ACCEPTED_ELSEWHERE:
case CALL_DECLINED_ELSEWHERE:
case CALL_ONGOING_ELSEWHERE:
case CALL_NEEDS_PERMISSION:
case CALL_BUSY:
case CALL_DISCONNECTED:
callState = WebRtcControls.CallState.ENDING;
break;
default: default:
callState = WebRtcControls.CallState.ONGOING; callState = WebRtcControls.CallState.ONGOING;
} }
@ -180,25 +170,19 @@ public class WebRtcCallViewModel extends ViewModel {
isRemoteVideoEnabled || isRemoteVideoOffer, isRemoteVideoEnabled || isRemoteVideoOffer,
isMoreThanOneCameraAvailable, isMoreThanOneCameraAvailable,
isBluetoothAvailable, isBluetoothAvailable,
isInPipMode.getValue() == Boolean.TRUE, Boolean.TRUE.equals(isInPipMode.getValue()),
callState, callState,
audioOutput)); audioOutput));
} }
private @NonNull WebRtcLocalRenderState getRealLocalRenderState(boolean shouldDisplayLocalVideo, @NonNull WebRtcLocalRenderState state) {
if (shouldDisplayLocalVideo || showVideoForOutgoing) return state;
else return WebRtcLocalRenderState.GONE;
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) { private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
if (isInPipMode) return WebRtcControls.PIP; return isInPipMode ? WebRtcControls.PIP : controls;
else return controls;
} }
private void startTimer() { private void startTimer() {
cancelTimer(); cancelTimer();
ellapsedTimeHandler.post(ellapsedTimeRunnable); elapsedTimeHandler.post(elapsedTimeRunnable);
} }
private void handleTick() { private void handleTick() {
@ -208,13 +192,13 @@ public class WebRtcCallViewModel extends ViewModel {
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000; long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
ellapsed.postValue(newValue); elapsed.postValue(newValue);
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000); elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
} }
private void cancelTimer() { private void cancelTimer() {
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable); elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
} }
@Override @Override

View File

@ -36,24 +36,32 @@ public final class WebRtcControls {
this.audioOutput = audioOutput; this.audioOutput = audioOutput;
} }
boolean displayStartCallControls() {
return isPreJoin();
}
boolean displayEndCall() { boolean displayEndCall() {
return isOngoing(); return isAtLeastOutgoing();
} }
boolean displayMuteAudio() { boolean displayMuteAudio() {
return isOngoing(); return isPreJoin() || isAtLeastOutgoing();
} }
boolean displayVideoToggle() { boolean displayVideoToggle() {
return isOngoing(); return isPreJoin() || isAtLeastOutgoing();
} }
boolean displayAudioToggle() { boolean displayAudioToggle() {
return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable); return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
} }
boolean displayCameraToggle() { boolean displayCameraToggle() {
return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable; return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
}
boolean displayRemoteVideoRecycler() {
return isOngoing();
} }
boolean displayAnswerWithAudio() { boolean displayAnswerWithAudio() {
@ -73,25 +81,29 @@ public final class WebRtcControls {
} }
boolean isFadeOutEnabled() { boolean isFadeOutEnabled() {
return isOngoing() && isRemoteVideoEnabled; return isAtLeastOutgoing() && isRemoteVideoEnabled;
} }
boolean displaySmallOngoingCallButtons() { boolean displaySmallOngoingCallButtons() {
return isOngoing() && displayAudioToggle() && displayCameraToggle(); return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle();
} }
boolean displayLargeOngoingCallButtons() { boolean displayLargeOngoingCallButtons() {
return isOngoing() && !(displayAudioToggle() && displayCameraToggle()); return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle());
} }
boolean displayTopViews() { boolean displayTopViews() {
return !isInPipMode; return !isInPipMode;
} }
WebRtcAudioOutput getAudioOutput() { @NonNull WebRtcAudioOutput getAudioOutput() {
return audioOutput; return audioOutput;
} }
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
private boolean isOngoing() { private boolean isOngoing() {
return callState == CallState.ONGOING; return callState == CallState.ONGOING;
} }
@ -100,9 +112,20 @@ public final class WebRtcControls {
return callState == CallState.INCOMING; return callState == CallState.INCOMING;
} }
private boolean isAtLeastOutgoing() {
return callState.isAtLeast(CallState.OUTGOING);
}
public enum CallState { public enum CallState {
NONE, NONE,
PRE_JOIN,
INCOMING, INCOMING,
ONGOING OUTGOING,
ONGOING,
ENDING;
boolean isAtLeast(@NonNull CallState other) {
return compareTo(other) >= 0;
}
} }
} }

View File

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.webrtc;
public enum WebRtcLocalRenderState { public enum WebRtcLocalRenderState {
GONE, GONE,
SMALL, SMALL_RECTANGLE,
LARGE SMALL_SQUARE,
LARGE,
LARGE_NO_VIDEO
} }

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.view.View;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
public CallParticipantViewHolder(@NonNull View itemView) {
super(itemView, null);
}
@Override
public void bind(@NonNull CallParticipantViewState model) {
super.bind(model);
}
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
public final class CallParticipantViewState extends RecipientMappingModel<CallParticipantViewState> {
private final CallParticipant callParticipant;
CallParticipantViewState(@NonNull CallParticipant callParticipant) {
this.callParticipant = callParticipant;
}
@Override
public @NonNull Recipient getRecipient() {
return callParticipant.getRecipient();
}
}

View File

@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter;
public class CallParticipantsListAdapter extends MappingAdapter {
CallParticipantsListAdapter() {
registerFactory(CallParticipantsListHeader.class, new LayoutFactory<>(CallParticipantsListHeaderViewHolder::new, R.layout.call_participants_list_header));
registerFactory(CallParticipantViewState.class, new LayoutFactory<>(CallParticipantViewHolder::new, R.layout.call_participants_list_item));
}
}

View File

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.os.Bundle;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.MappingModel;
import java.util.ArrayList;
import java.util.List;
public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private RecyclerView participantList;
private CallParticipantsListAdapter adapter;
public static void show(@NonNull FragmentManager manager) {
CallParticipantsListDialog fragment = new CallParticipantsListDialog();
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet);
super.onCreate(savedInstanceState);
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
ContextThemeWrapper contextThemeWrapper = new ContextThemeWrapper(inflater.getContext(), R.style.TextSecure_DarkTheme);
LayoutInflater themedInflater = LayoutInflater.from(contextThemeWrapper);
participantList = (RecyclerView) themedInflater.inflate(R.layout.call_participants_list_dialog, container, false);
return participantList;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class);
initializeList();
viewModel.getCallParticipantsState().observe(getViewLifecycleOwner(), this::updateList);
}
private void initializeList() {
adapter = new CallParticipantsListAdapter();
participantList.setLayoutManager(new LinearLayoutManager(requireContext()));
participantList.setAdapter(adapter);
}
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
adapter.submitList(items);
}
}

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingModel;
public class CallParticipantsListHeader implements MappingModel<CallParticipantsListHeader> {
private int participantCount;
public CallParticipantsListHeader(int participantCount) {
this.participantCount = participantCount;
}
@NonNull String getHeader(@NonNull Context context) {
return context.getResources().getQuantityString(R.plurals.CallParticipantsListDialog_in_this_call_d_people, participantCount, participantCount);
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantsListHeader newItem) {
return true;
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipantsListHeader newItem) {
return participantCount == newItem.participantCount;
}
}

View File

@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.components.webrtc.participantslist;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingViewHolder;
public class CallParticipantsListHeaderViewHolder extends MappingViewHolder<CallParticipantsListHeader> {
private final TextView headerText;
public CallParticipantsListHeaderViewHolder(@NonNull View itemView) {
super(itemView);
headerText = findViewById(R.id.call_participants_list_header);
}
@Override
public void bind(@NonNull CallParticipantsListHeader model) {
headerText.setText(model.getHeader(getContext()));
}
}

View File

@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
private final int smallResourceId; private final int smallResourceId;
private final int callCardResourceId; private final int callCardResourceId;
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
public ResourceContactPhoto(@DrawableRes int resourceId) { public ResourceContactPhoto(@DrawableRes int resourceId) {
this(resourceId, resourceId, resourceId); this(resourceId, resourceId, resourceId);
} }
@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
this.smallResourceId = smallResourceId; this.smallResourceId = smallResourceId;
} }
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
this.scaleType = scaleType;
}
@Override @Override
public Drawable asDrawable(Context context, int color) { public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
return asDrawable(context, color, false); return asDrawable(context, color, false);
} }
@Override @Override
public Drawable asDrawable(Context context, int color, boolean inverted) { public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, resourceId, color, inverted); return buildDrawable(context, resourceId, color, inverted);
} }
@Override @Override
public Drawable asSmallDrawable(Context context, int color, boolean inverted) { public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) {
return buildDrawable(context, smallResourceId, color, inverted); return buildDrawable(context, smallResourceId, color, inverted);
} }
private Drawable buildDrawable(Context context, int resourceId, int color, boolean inverted) { private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) {
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
foreground.setScaleType(ImageView.ScaleType.CENTER); //noinspection ConstantConditions
foreground.setScaleType(scaleType);
if (inverted) { if (inverted) {
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
} }
@Override @Override
public Drawable asCallCard(Context context) { public @Nullable Drawable asCallCard(@NonNull Context context) {
return AppCompatResources.getDrawable(context, callCardResourceId); return AppCompatResources.getDrawable(context, callCardResourceId);
} }
private static class ExpandingLayerDrawable extends LayerDrawable { private static class ExpandingLayerDrawable extends LayerDrawable {
public ExpandingLayerDrawable(Drawable[] layers) { public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
super(layers); super(layers);
} }

View File

@ -1,58 +0,0 @@
package org.thoughtcrime.securesms.contacts.sync;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper.DirectoryResult;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.SetUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
class ContactDiscoveryV1 {
private static final String TAG = ContactDiscoveryV1.class.getSimpleName();
static @NonNull DirectoryResult getDirectoryResult(@NonNull Set<String> databaseNumbers,
@NonNull Set<String> systemNumbers)
throws IOException
{
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
FuzzyPhoneNumberHelper.InputResult inputResult = FuzzyPhoneNumberHelper.generateInput(allNumbers, databaseNumbers);
List<ContactTokenDetails> activeTokens = getTokens(inputResult.getNumbers());
Set<String> activeNumbers = Stream.of(activeTokens).map(ContactTokenDetails::getNumber).collect(Collectors.toSet());
FuzzyPhoneNumberHelper.OutputResult outputResult = FuzzyPhoneNumberHelper.generateOutput(activeNumbers, inputResult);
HashMap<String, UUID> uuids = new HashMap<>();
for (String number : outputResult.getNumbers()) {
uuids.put(number, null);
}
return new DirectoryResult(uuids, outputResult.getRewrites());
}
static @NonNull DirectoryResult getDirectoryResult(@NonNull String number) throws IOException {
return getDirectoryResult(Collections.singleton(number), Collections.singleton(number));
}
private static @NonNull List<ContactTokenDetails> getTokens(@NonNull Set<String> numbers) throws IOException {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
if (numbers.size() == 1) {
Optional<ContactTokenDetails> details = accountManager.getContact(numbers.iterator().next());
return details.isPresent() ? Collections.singletonList(details.get()) : Collections.emptyList();
} else {
return accountManager.getContacts(numbers);
}
}
}

View File

@ -31,6 +31,9 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
/**
* Uses CDS to map E164's to UUIDs.
*/
class ContactDiscoveryV2 { class ContactDiscoveryV2 {
private static final String TAG = Log.tag(ContactDiscoveryV2.class); private static final String TAG = Log.tag(ContactDiscoveryV2.class);

View File

@ -25,20 +25,21 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactsDatabase; import org.thoughtcrime.securesms.contacts.ContactsDatabase;
import org.thoughtcrime.securesms.crypto.SessionUtil; import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle; import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.RecipientDetails;
import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -50,10 +51,13 @@ import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import java.io.IOException; import java.io.IOException;
import java.util.Calendar; import java.util.Calendar;
@ -154,13 +158,7 @@ public class DirectoryHelper {
return RegisteredState.NOT_REGISTERED; return RegisteredState.NOT_REGISTERED;
} }
DirectoryResult result; DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
} else {
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
}
stopwatch.split("e164-network"); stopwatch.split("e164-network");
@ -179,6 +177,13 @@ public class DirectoryHelper {
} else { } else {
recipientDatabase.markRegistered(recipient.getId()); recipientDatabase.markRegistered(recipient.getId());
} }
} else if (recipient.hasUuid() && recipient.isRegistered() && hasCommunicatedWith(context, recipient)) {
if (isUuidRegistered(context, recipient)) {
recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
} else {
recipientDatabase.markUnregistered(recipient.getId());
}
stopwatch.split("e164-unlisted-network");
} else { } else {
recipientDatabase.markUnregistered(recipient.getId()); recipientDatabase.markUnregistered(recipient.getId());
} }
@ -218,13 +223,7 @@ public class DirectoryHelper {
Stopwatch stopwatch = new Stopwatch("refresh"); Stopwatch stopwatch = new Stopwatch("refresh");
DirectoryResult result; DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
if (FeatureFlags.cds()) {
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
} else {
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
}
stopwatch.split("network"); stopwatch.split("network");
@ -244,6 +243,17 @@ public class DirectoryHelper {
stopwatch.split("process-cds"); stopwatch.split("process-cds");
UnlistedResult unlistedResult = filterForUnlistedUsers(context, inactiveIds);
inactiveIds.removeAll(unlistedResult.getPossiblyActive());
if (unlistedResult.getRetries().size() > 0) {
Log.i(TAG, "Some profile fetches failed to resolve. Assuming not-inactive for now and scheduling a retry.");
RetrieveProfileJob.enqueue(unlistedResult.getRetries());
}
stopwatch.split("handle-unlisted");
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds); recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
stopwatch.split("update-registered"); stopwatch.split("update-registered");
@ -275,16 +285,10 @@ public class DirectoryHelper {
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException { private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
try { try {
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS); ProfileUtil.retrieveProfileSync(context, recipient, SignalServiceProfile.RequestType.PROFILE);
return true; return true;
} catch (ExecutionException e) { } catch (NotFoundException e) {
if (e.getCause() instanceof NotFoundException) { return false;
return false;
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new IOException(e);
} }
} }
@ -420,6 +424,50 @@ public class DirectoryHelper {
}).collect(Collectors.toSet()); }).collect(Collectors.toSet());
} }
/**
* Users can mark themselves as 'unlisted' in CDS, meaning that even if CDS says they're
* unregistered, they might actually be registered. We need to double-check users who we already
* have UUIDs for. Also, we only want to bother doing this for users we have conversations for,
* so we will also only check for users that have a thread.
*/
private static UnlistedResult filterForUnlistedUsers(@NonNull Context context, @NonNull Set<RecipientId> inactiveIds) {
List<Recipient> possiblyUnlisted = Stream.of(inactiveIds)
.map(Recipient::resolved)
.filter(Recipient::isRegistered)
.filter(Recipient::hasUuid)
.filter(r -> hasCommunicatedWith(context, r))
.toList();
List<Pair<Recipient, ListenableFuture<ProfileAndCredential>>> futures = Stream.of(possiblyUnlisted)
.map(r -> new Pair<>(r, ProfileUtil.retrieveProfile(context, r, SignalServiceProfile.RequestType.PROFILE)))
.toList();
Set<RecipientId> potentiallyActiveIds = new HashSet<>();
Set<RecipientId> retries = new HashSet<>();
Stream.of(futures)
.forEach(pair -> {
try {
pair.second().get(5, TimeUnit.SECONDS);
potentiallyActiveIds.add(pair.first().getId());
} catch (InterruptedException | TimeoutException e) {
retries.add(pair.first().getId());
potentiallyActiveIds.add(pair.first().getId());
} catch (ExecutionException e) {
if (!(e.getCause() instanceof NotFoundException)) {
retries.add(pair.first().getId());
potentiallyActiveIds.add(pair.first().getId());
}
}
});
return new UnlistedResult(potentiallyActiveIds, retries);
}
private static boolean hasCommunicatedWith(@NonNull Context context, @NonNull Recipient recipient) {
return DatabaseFactory.getThreadDatabase(context).hasThread(recipient.getId()) ||
DatabaseFactory.getSessionDatabase(context).hasSessionFor(recipient.getId());
}
static class DirectoryResult { static class DirectoryResult {
private final Map<String, UUID> registeredNumbers; private final Map<String, UUID> registeredNumbers;
private final Map<String, String> numberRewrites; private final Map<String, String> numberRewrites;
@ -441,6 +489,24 @@ public class DirectoryHelper {
} }
} }
private static class UnlistedResult {
private final Set<RecipientId> possiblyActive;
private final Set<RecipientId> retries;
private UnlistedResult(@NonNull Set<RecipientId> possiblyActive, @NonNull Set<RecipientId> retries) {
this.possiblyActive = possiblyActive;
this.retries = retries;
}
@NonNull Set<RecipientId> getPossiblyActive() {
return possiblyActive;
}
@NonNull Set<RecipientId> getRetries() {
return retries;
}
}
private static class AccountHolder { private static class AccountHolder {
private final boolean fresh; private final boolean fresh;
private final Account account; private final Account account;

View File

@ -14,7 +14,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
@ -648,7 +647,7 @@ public class Contact implements Parcelable {
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(attachment != null ? attachment.getDataUri() : null, flags); dest.writeParcelable(attachment != null ? attachment.getUri() : null, flags);
dest.writeByte((byte) (isProfile ? 1 : 0)); dest.writeByte((byte) (isProfile ? 1 : 0));
} }

View File

@ -215,7 +215,7 @@ class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.Conta
Field(@NonNull Avatar avatar) { Field(@NonNull Avatar avatar) {
this.value = ""; this.value = "";
this.iconResId = R.drawable.baseline_account_circle_white_24; this.iconResId = R.drawable.baseline_account_circle_white_24;
this.iconUri = avatar.getAttachment() != null ? avatar.getAttachment().getDataUri() : null; this.iconUri = avatar.getAttachment() != null ? avatar.getAttachment().getUri() : null;
this.maxLines = 1; this.maxLines = 1;
this.selectable = avatar; this.selectable = avatar;
this.label = ""; this.label = "";

View File

@ -186,11 +186,11 @@ public final class ContactUtil {
intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, getSystemType(contact.getPostalAddresses().get(0).getType())); intent.putExtra(ContactsContract.Intents.Insert.POSTAL_TYPE, getSystemType(contact.getPostalAddresses().get(0).getType()));
} }
if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getDataUri() != null) { if (contact.getAvatarAttachment() != null && contact.getAvatarAttachment().getUri() != null) {
try { try {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getDataUri()))); values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, Util.readFully(PartAuthority.getAttachmentStream(context, contact.getAvatarAttachment().getUri())));
ArrayList<ContentValues> valuesArray = new ArrayList<>(1); ArrayList<ContentValues> valuesArray = new ArrayList<>(1);
valuesArray.add(values); valuesArray.add(values);

View File

@ -96,7 +96,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
presentContact(contact); presentContact(contact);
presentActionButtons(ContactUtil.getRecipients(this, contact)); presentActionButtons(ContactUtil.getRecipients(this, contact));
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null); presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
for (LiveRecipient recipient : activeRecipients.values()) { for (LiveRecipient recipient : activeRecipients.values()) {
recipient.observe(this, r -> presentActionButtons(Collections.singletonList(r.getId()))); recipient.observe(this, r -> presentActionButtons(Collections.singletonList(r.getId())));

View File

@ -93,66 +93,7 @@ public class SharedContactRepository {
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) { try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
VCard vcard = Ezvcard.parse(stream).first(); VCard vcard = Ezvcard.parse(stream).first();
contact = VCardUtil.getContactFromVcard(vcard);
ezvcard.property.StructuredName vName = vcard.getStructuredName();
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
List<ezvcard.property.Email> vEmails = vcard.getEmails();
List<ezvcard.property.Address> vPostalAddresses = vcard.getAddresses();
String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
if (displayName == null && vName != null) {
displayName = vName.getGiven();
}
if (displayName == null && vcard.getOrganization() != null) {
displayName = organization;
}
if (displayName == null) {
throw new IOException("No valid name.");
}
Name name = new Name(displayName,
vName != null ? vName.getGiven() : null,
vName != null ? vName.getFamily() : null,
vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
null);
List<Phone> phoneNumbers = new ArrayList<>(vPhones.size());
for (ezvcard.property.Telephone vEmail : vPhones) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
// Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
String phoneNumberFromText = vEmail.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
phoneNumbers.add(new Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
}
List<Email> emails = new ArrayList<>(vEmails.size());
for (ezvcard.property.Email vEmail : vEmails) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
emails.add(new Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
}
List<PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
postalAddresses.add(new PostalAddress(postalAddressTypeFromVcardType(label),
label,
vPostalAddress.getStreetAddress(),
vPostalAddress.getPoBox(),
null,
vPostalAddress.getLocality(),
vPostalAddress.getRegion(),
vPostalAddress.getPostalCode(),
vPostalAddress.getCountry()));
}
contact = new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to parse the vcard.", e); Log.w(TAG, "Failed to parse the vcard.", e);
} }
@ -201,7 +142,7 @@ public class SharedContactRepository {
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber); String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
Phone existing = numberMap.get(number); Phone existing = numberMap.get(number);
Phone candidate = new Phone(number, phoneTypeFromContactType(cursorType), cursorLabel); Phone candidate = new Phone(number, VCardUtil.phoneTypeFromContactType(cursorType), cursorLabel);
if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) { if (existing == null || (existing.getType() == Phone.Type.CUSTOM && existing.getLabel() == null)) {
numberMap.put(number, candidate); numberMap.put(number, candidate);
@ -224,7 +165,7 @@ public class SharedContactRepository {
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE)); int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL)); String cursorLabel = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL));
emails.add(new Email(cursorEmail, emailTypeFromContactType(cursorType), cursorLabel)); emails.add(new Email(cursorEmail, VCardUtil.emailTypeFromContactType(cursorType), cursorLabel));
} }
} }
@ -247,7 +188,7 @@ public class SharedContactRepository {
String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)); String cursorPostal = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE));
String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)); String cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType), postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType),
cursorLabel, cursorLabel,
cursorStreet, cursorStreet,
cursorPoBox, cursorPoBox,
@ -304,70 +245,6 @@ public class SharedContactRepository {
return null; return null;
} }
private Phone.Type phoneTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
return Phone.Type.HOME;
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
return Phone.Type.MOBILE;
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
return Phone.Type.WORK;
}
return Phone.Type.CUSTOM;
}
private Phone.Type phoneTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Phone.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Phone.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Phone.Type.WORK;
else return Phone.Type.CUSTOM;
}
private Email.Type emailTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
return Email.Type.HOME;
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
return Email.Type.MOBILE;
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
return Email.Type.WORK;
}
return Email.Type.CUSTOM;
}
private Email.Type emailTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Email.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Email.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Email.Type.WORK;
else return Email.Type.CUSTOM;
}
private PostalAddress.Type postalAddressTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
return PostalAddress.Type.HOME;
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
return PostalAddress.Type.WORK;
}
return PostalAddress.Type.CUSTOM;
}
private PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return PostalAddress.Type.HOME;
else if ("work".equalsIgnoreCase(type)) return PostalAddress.Type.WORK;
else return PostalAddress.Type.CUSTOM;
}
private String getCleanedVcardType(@Nullable String type) {
if (TextUtils.isEmpty(type)) return "";
if (type.startsWith("x-") && type.length() > 2) {
return type.substring(2);
}
return type;
}
interface ValueCallback<T> { interface ValueCallback<T> {
void onComplete(@NonNull T value); void onComplete(@NonNull T value);
} }

View File

@ -0,0 +1,160 @@
package org.thoughtcrime.securesms.contactshare;
import android.provider.ContactsContract;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.logging.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import ezvcard.Ezvcard;
import ezvcard.VCard;
public final class VCardUtil {
private VCardUtil(){}
private static final String TAG = VCardUtil.class.getSimpleName();
public static List<Contact> parseContacts(@NonNull String vCardData) {
List<VCard> vContacts = Ezvcard.parse(vCardData).all();
List<Contact> contacts = new LinkedList<>();
for (VCard vCard: vContacts){
contacts.add(getContactFromVcard(vCard));
}
return contacts;
}
static @Nullable Contact getContactFromVcard(@NonNull VCard vcard) {
ezvcard.property.StructuredName vName = vcard.getStructuredName();
List<ezvcard.property.Telephone> vPhones = vcard.getTelephoneNumbers();
List<ezvcard.property.Email> vEmails = vcard.getEmails();
List<ezvcard.property.Address> vPostalAddresses = vcard.getAddresses();
String organization = vcard.getOrganization() != null && !vcard.getOrganization().getValues().isEmpty() ? vcard.getOrganization().getValues().get(0) : null;
String displayName = vcard.getFormattedName() != null ? vcard.getFormattedName().getValue() : null;
if (displayName == null && vName != null) {
displayName = vName.getGiven();
}
if (displayName == null && vcard.getOrganization() != null) {
displayName = organization;
}
if (displayName == null) {
Log.w(TAG, "Failed to parse the vcard: No valid name.");
return null;
}
Contact.Name name = new Contact.Name(displayName,
vName != null ? vName.getGiven() : null,
vName != null ? vName.getFamily() : null,
vName != null && !vName.getPrefixes().isEmpty() ? vName.getPrefixes().get(0) : null,
vName != null && !vName.getSuffixes().isEmpty() ? vName.getSuffixes().get(0) : null,
null);
List<Contact.Phone> phoneNumbers = new ArrayList<>(vPhones.size());
for (ezvcard.property.Telephone vEmail : vPhones) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
// Phone number is stored in the uri field in v4.0 only. In other versions, it is in the text field.
String phoneNumberFromText = vEmail.getText();
String extractedPhoneNumber = phoneNumberFromText == null ? vEmail.getUri().getNumber() : phoneNumberFromText;
phoneNumbers.add(new Contact.Phone(extractedPhoneNumber, phoneTypeFromVcardType(label), label));
}
List<Contact.Email> emails = new ArrayList<>(vEmails.size());
for (ezvcard.property.Email vEmail : vEmails) {
String label = !vEmail.getTypes().isEmpty() ? getCleanedVcardType(vEmail.getTypes().get(0).getValue()) : null;
emails.add(new Contact.Email(vEmail.getValue(), emailTypeFromVcardType(label), label));
}
List<Contact.PostalAddress> postalAddresses = new ArrayList<>(vPostalAddresses.size());
for (ezvcard.property.Address vPostalAddress : vPostalAddresses) {
String label = !vPostalAddress.getTypes().isEmpty() ? getCleanedVcardType(vPostalAddress.getTypes().get(0).getValue()) : null;
postalAddresses.add(new Contact.PostalAddress(postalAddressTypeFromVcardType(label),
label,
vPostalAddress.getStreetAddress(),
vPostalAddress.getPoBox(),
null,
vPostalAddress.getLocality(),
vPostalAddress.getRegion(),
vPostalAddress.getPostalCode(),
vPostalAddress.getCountry()));
}
return new Contact(name, organization, phoneNumbers, emails, postalAddresses, null);
}
static Contact.Phone.Type phoneTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
return Contact.Phone.Type.HOME;
case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
return Contact.Phone.Type.MOBILE;
case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
return Contact.Phone.Type.WORK;
}
return Contact.Phone.Type.CUSTOM;
}
private static Contact.Phone.Type phoneTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.Phone.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Contact.Phone.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Contact.Phone.Type.WORK;
else return Contact.Phone.Type.CUSTOM;
}
static Contact.Email.Type emailTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
return Contact.Email.Type.HOME;
case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
return Contact.Email.Type.MOBILE;
case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
return Contact.Email.Type.WORK;
}
return Contact.Email.Type.CUSTOM;
}
private static Contact.Email.Type emailTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.Email.Type.HOME;
else if ("cell".equalsIgnoreCase(type)) return Contact.Email.Type.MOBILE;
else if ("work".equalsIgnoreCase(type)) return Contact.Email.Type.WORK;
else return Contact.Email.Type.CUSTOM;
}
static Contact.PostalAddress.Type postalAddressTypeFromContactType(int type) {
switch (type) {
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
return Contact.PostalAddress.Type.HOME;
case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
return Contact.PostalAddress.Type.WORK;
}
return Contact.PostalAddress.Type.CUSTOM;
}
private static Contact.PostalAddress.Type postalAddressTypeFromVcardType(@Nullable String type) {
if ("home".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.HOME;
else if ("work".equalsIgnoreCase(type)) return Contact.PostalAddress.Type.WORK;
else return Contact.PostalAddress.Type.CUSTOM;
}
private static String getCleanedVcardType(@Nullable String type) {
if (TextUtils.isEmpty(type)) return "";
if (type.startsWith("x-") && type.length() > 2) {
return type.substring(2);
}
return type;
}
}

View File

@ -676,11 +676,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
for (Media mediaItem : result.getNonUploadedMedia()) { for (Media mediaItem : result.getNonUploadedMedia()) {
if (MediaUtil.isVideoType(mediaItem.getMimeType())) { if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull())); slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), mediaItem.getTransformProperties().orNull()));
} else if (MediaUtil.isGif(mediaItem.getMimeType())) { } else if (MediaUtil.isGif(mediaItem.getMimeType())) {
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull())); slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull()));
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) { } else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null)); slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orNull(), null));
} else { } else {
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping."); Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
} }
@ -1913,13 +1913,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (previewState == null) return; if (previewState == null) return;
if (previewState.isLoading()) { if (previewState.isLoading()) {
Log.d(TAG, "Loading link preview.");
inputPanel.setLinkPreviewLoading(); inputPanel.setLinkPreviewLoading();
} else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) { } else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
Log.d(TAG, "No preview found.");
inputPanel.setLinkPreviewNoPreview(previewState.getError()); inputPanel.setLinkPreviewNoPreview(previewState.getError());
} else { } else {
Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview()); inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
} }

View File

@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.paging.PagedList; import androidx.paging.PagedList;
import androidx.paging.PagedListAdapter; import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.DiffUtil;
@ -84,6 +85,7 @@ public class ConversationAdapter
private static final long FOOTER_ID = Long.MIN_VALUE + 1; private static final long FOOTER_ID = Long.MIN_VALUE + 1;
private final ItemClickListener clickListener; private final ItemClickListener clickListener;
private final LifecycleOwner lifecycleOwner;
private final GlideRequests glideRequests; private final GlideRequests glideRequests;
private final Locale locale; private final Locale locale;
private final Recipient recipient; private final Recipient recipient;
@ -99,12 +101,14 @@ public class ConversationAdapter
private View headerView; private View headerView;
private View footerView; private View footerView;
ConversationAdapter(@NonNull GlideRequests glideRequests, ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@NonNull Locale locale, @NonNull Locale locale,
@Nullable ItemClickListener clickListener, @Nullable ItemClickListener clickListener,
@NonNull Recipient recipient) @NonNull Recipient recipient)
{ {
super(new DiffCallback()); super(new DiffCallback());
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.locale = locale; this.locale = locale;
@ -170,8 +174,6 @@ public class ConversationAdapter
case MESSAGE_TYPE_OUTGOING_TEXT: case MESSAGE_TYPE_OUTGOING_TEXT:
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
case MESSAGE_TYPE_UPDATE: case MESSAGE_TYPE_UPDATE:
long start = System.currentTimeMillis();
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false); View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
BindableConversationItem bindable = (BindableConversationItem) itemView; BindableConversationItem bindable = (BindableConversationItem) itemView;
@ -190,7 +192,6 @@ public class ConversationAdapter
bindable.setEventListener(clickListener); bindable.setEventListener(clickListener);
Log.d(TAG, String.format(Locale.US, "Inflate time: %d ms for View type: %d", System.currentTimeMillis() - start, viewType));
return new ConversationViewHolder(itemView); return new ConversationViewHolder(itemView);
case MESSAGE_TYPE_PLACEHOLDER: case MESSAGE_TYPE_PLACEHOLDER:
View v = new FrameLayout(parent.getContext()); View v = new FrameLayout(parent.getContext());
@ -219,7 +220,8 @@ public class ConversationAdapter
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
conversationViewHolder.getBindable().bind(conversationMessage, conversationViewHolder.getBindable().bind(lifecycleOwner,
conversationMessage,
Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null), Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null),
Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null), Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null),
glideRequests, glideRequests,

View File

@ -479,7 +479,7 @@ public class ConversationFragment extends LoggingFragment {
private void initializeListAdapter() { private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) { if (this.recipient != null && this.threadId != -1) {
Log.d(TAG, "Initializing adapter for " + recipient.getId()); Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get()); ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
list.setAdapter(adapter); list.setAdapter(adapter);
setStickyHeaderDecoration(adapter); setStickyHeaderDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool()); ConversationAdapter.initializePool(list.getRecycledViewPool());
@ -810,7 +810,7 @@ public class ConversationFragment extends LoggingFragment {
.toList(); .toList();
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri(); Uri uri = attachment.getUri();
if (uri != null) { if (uri != null) {
mediaList.add(new Media(uri, mediaList.add(new Media(uri,
@ -1424,7 +1424,10 @@ public class ConversationFragment extends LoggingFragment {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView, public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
@Nullable ScrollRequestValidator scrollRequestValidator) @Nullable ScrollRequestValidator scrollRequestValidator)
{ {
super(recyclerView, scrollRequestValidator); super(recyclerView, scrollRequestValidator, () -> {
list.scrollToPosition(0);
list.post(ConversationFragment.this::postMarkAsReadRequest);
});
} }
@Override @Override

View File

@ -54,6 +54,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -79,8 +80,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -116,9 +115,9 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil; import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.UrlClickHandler;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} }
@Override @Override
public void bind(@NonNull ConversationMessage conversationMessage, public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@ -336,6 +336,16 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} }
} }
if (hasSharedContact(messageRecord)) {
int contactWidth = sharedContactStub.get().getMeasuredWidth();
int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get());
if (contactWidth != availableWidth) {
sharedContactStub.get().getLayoutParams().width = availableWidth;
needsMeasure = true;
}
}
ConversationItemFooter activeFooter = getActiveFooter(messageRecord); ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer); int availableWidth = getAvailableMessageBubbleWidth(footer);
@ -892,12 +902,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
} }
private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) { private void setSharedContactCorners(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) { if (TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))){
sharedContactStub.get().setSingularStyle(); if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
} else if (current.isOutgoing()) { sharedContactStub.get().setSingularStyle();
sharedContactStub.get().setClusteredOutgoingStyle(); } else if (current.isOutgoing()) {
} else { sharedContactStub.get().setClusteredOutgoingStyle();
sharedContactStub.get().setClusteredIncomingStyle(); } else {
sharedContactStub.get().setClusteredIncomingStyle();
}
} }
} }
@ -1075,7 +1087,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) { private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) { if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
return stickerFooter; return stickerFooter;
} else if (hasSharedContact(messageRecord)) { } else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return sharedContactStub.get().getFooter(); return sharedContactStub.get().getFooter();
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) { } else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return mediaThumbnailStub.get().getFooter(); return mediaThumbnailStub.get().getFooter();
@ -1442,7 +1454,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
Log.i(TAG, "Public URI: " + publicUri); Log.i(TAG, "Public URI: " + publicUri);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType()); intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), Intent.normalizeMimeType(slide.getContentType()));
try { try {
context.startActivity(intent); context.startActivity(intent);
} catch (ActivityNotFoundException anfe) { } catch (ActivityNotFoundException anfe) {

View File

@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations; import androidx.lifecycle.Transformations;
@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale; import java.util.Locale;
@ -44,9 +44,7 @@ import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public final class ConversationUpdateItem extends LinearLayout public final class ConversationUpdateItem extends LinearLayout
implements RecipientForeverObserver, implements BindableConversationItem
BindableConversationItem,
Observer<SpannableString>
{ {
private static final String TAG = ConversationUpdateItem.class.getSimpleName(); private static final String TAG = ConversationUpdateItem.class.getSimpleName();
@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout
private Locale locale; private Locale locale;
private LiveData<SpannableString> displayBody; private LiveData<SpannableString> displayBody;
private final Debouncer bodyClearDebouncer = new Debouncer(150); private final UpdateObserver updateObserver = new UpdateObserver();
private final SenderObserver senderObserver = new SenderObserver();
public ConversationUpdateItem(Context context) { public ConversationUpdateItem(Context context) {
super(context); super(context);
@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout
} }
@Override @Override
public void bind(@NonNull ConversationMessage conversationMessage, public void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout
{ {
this.batchSelected = batchSelected; this.batchSelected = batchSelected;
bind(conversationMessage, locale); bind(lifecycleOwner, conversationMessage, locale);
}
@Override
protected void onDetachedFromWindow() {
unbind();
super.onDetachedFromWindow();
} }
@Override @Override
@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout
return conversationMessage; return conversationMessage;
} }
private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) { private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
if (this.sender != null) {
this.sender.removeForeverObserver(this);
}
observeDisplayBody(null);
setBodyText(null);
this.conversationMessage = conversationMessage; this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord(); this.messageRecord = conversationMessage.getMessageRecord();
this.sender = messageRecord.getIndividualRecipient().live();
this.locale = locale; this.locale = locale;
this.sender.observeForever(this); observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext())); UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription); LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new); LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
present(conversationMessage); present(conversationMessage);
observeDisplayBody(spannableStringMessage); observeDisplayBody(lifecycleOwner, spannableStringMessage);
} }
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) { /** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */
private @NonNull LiveData<String> loading(@NonNull LiveData<String> string) {
return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading)));
}
private static LiveData<SpannableString> toSpannable(LiveData<String> loading) {
return Transformations.map(loading, source -> source == null ? null : new SpannableString(source));
}
@Override
public void unbind() {
}
private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
if (sender != null) {
sender.getLiveData().removeObserver(senderObserver);
}
if (recipient != null) {
sender = recipient.live();
sender.getLiveData().observe(lifecycleOwner, senderObserver);
} else {
sender = null;
}
}
private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != displayBody) { if (this.displayBody != displayBody) {
if (this.displayBody != null) { if (this.displayBody != null) {
this.displayBody.removeObserver(this); this.displayBody.removeObserver(updateObserver);
} }
this.displayBody = displayBody; this.displayBody = displayBody;
if (this.displayBody != null) { if (this.displayBody != null) {
this.displayBody.observeForever(this); this.displayBody.observe(lifecycleOwner, updateObserver);
} }
} }
} }
private void setBodyText(@Nullable CharSequence text) { private void setBodyText(@Nullable CharSequence text) {
if (text == null) { if (text == null) {
bodyClearDebouncer.publish(() -> body.setText(null)); body.setVisibility(INVISIBLE);
} else { } else {
bodyClearDebouncer.clear();
body.setText(text); body.setText(text);
body.setVisibility(VISIBLE); body.setVisibility(VISIBLE);
} }
@ -186,7 +197,7 @@ public final class ConversationUpdateItem extends LinearLayout
else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp); else if (messageRecord.isOutgoingCall()) icon.setImageResource(R.drawable.ic_call_made_grey600_24dp);
else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp); else icon.setImageResource(R.drawable.ic_call_missed_grey600_24dp);
date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateReceived())); date.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getDateSent()));
title.setVisibility(GONE); title.setVisibility(GONE);
date.setVisibility(View.VISIBLE); date.setVisibility(View.VISIBLE);
@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout
icon.setColorFilter(getIconTintFilter()); icon.setColorFilter(getIconTintFilter());
} }
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
present(conversationMessage);
}
@Override @Override
public void setOnClickListener(View.OnClickListener l) { public void setOnClickListener(View.OnClickListener l) {
super.setOnClickListener(new InternalClickListener(l)); super.setOnClickListener(new InternalClickListener(l));
} }
@Override private final class SenderObserver implements Observer<Recipient> {
public void unbind() {
if (sender != null) {
sender.removeForeverObserver(this);
}
observeDisplayBody(null); @Override
public void onChanged(Recipient recipient) {
present(conversationMessage);
}
} }
@Override private final class UpdateObserver implements Observer<SpannableString> {
public void onChanged(SpannableString update) {
setBodyText(update); @Override
public void onChanged(SpannableString update) {
setBodyText(update);
}
} }
private class InternalClickListener implements View.OnClickListener { private class InternalClickListener implements View.OnClickListener {

View File

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingViewHolder;
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
private final AvatarImageView avatar;
private final TextView name;
@Nullable private final MentionEventsListener mentionEventsListener;
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
super(itemView);
this.mentionEventsListener = mentionEventsListener;
avatar = findViewById(R.id.mention_recipient_avatar);
name = findViewById(R.id.mention_recipient_name);
}
@Override
public void bind(@NonNull MentionViewState model) {
avatar.setRecipient(model.getRecipient());
name.setText(model.getName(context));
itemView.setOnClickListener(v -> {
if (mentionEventsListener != null) {
mentionEventsListener.onMentionClicked(model.getRecipient());
}
});
}
public interface MentionEventsListener {
void onMentionClicked(@NonNull Recipient recipient);
}
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_picker_recipient_list_item);
}
}

View File

@ -1,17 +1,11 @@
package org.thoughtcrime.securesms.conversation.ui.mentions; package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects; public final class MentionViewState extends RecipientMappingModel<MentionViewState> {
public final class MentionViewState implements MappingModel<MentionViewState> {
private final Recipient recipient; private final Recipient recipient;
@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
this.recipient = recipient; this.recipient = recipient;
} }
@NonNull String getName(@NonNull Context context) { @Override
return recipient.getDisplayName(context); public @NonNull Recipient getRecipient() {
}
@NonNull Recipient getRecipient() {
return recipient; return recipient;
} }
@Override
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
return recipient.getId().equals(newItem.recipient.getId());
}
@Override
public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
Context context = ApplicationDependencies.getApplication();
return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
}
} }

View File

@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter; import org.thoughtcrime.securesms.util.MappingAdapter;
import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder.EventListener;
import java.util.List; import java.util.List;
public class MentionsPickerAdapter extends MappingAdapter { public class MentionsPickerAdapter extends MappingAdapter {
private final Runnable currentListChangedListener; private final Runnable currentListChangedListener;
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) { public MentionsPickerAdapter(@Nullable EventListener<MentionViewState> listener, @NonNull Runnable currentListChangedListener) {
this.currentListChangedListener = currentListChangedListener; this.currentListChangedListener = currentListChangedListener;
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener)); registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
} }
@Override @Override

View File

@ -12,8 +12,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery; import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
private final MutableLiveData<LiveRecipient> liveRecipient; private final MutableLiveData<LiveRecipient> liveRecipient;
private final MutableLiveData<Query> liveQuery; private final MutableLiveData<Query> liveQuery;
private final MutableLiveData<Boolean> isShowing; private final MutableLiveData<Boolean> isShowing;
private final MegaphoneRepository megaphoneRepository;
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository, MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
@NonNull MegaphoneRepository megaphoneRepository)
{
this.megaphoneRepository = megaphoneRepository;
this.liveRecipient = new MutableLiveData<>(); this.liveRecipient = new MutableLiveData<>();
this.liveQuery = new MutableLiveData<>(); this.liveQuery = new MutableLiveData<>();
this.selectedRecipient = new SingleLiveEvent<>(); this.selectedRecipient = new SingleLiveEvent<>();
@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
void onSelectionChange(@NonNull Recipient recipient) { void onSelectionChange(@NonNull Recipient recipient) {
selectedRecipient.setValue(recipient); selectedRecipient.setValue(recipient);
megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
} }
void setIsShowing(boolean isShowing) { void setIsShowing(boolean isShowing) {
@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
@Override @Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) { public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions //noinspection ConstantConditions
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()), return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
ApplicationDependencies.getMegaphoneRepository()));
} }
} }
} }

View File

@ -123,11 +123,13 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@Override @Override
protected void onItemSwiped(long threadId, int unreadCount) { protected void onItemSwiped(long threadId, int unreadCount) {
new SnackbarAsyncTask<Long>(getView(), new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1), requireView(),
getString(R.string.ConversationListFragment_undo), getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
getResources().getColor(R.color.amber_500), getString(R.string.ConversationListFragment_undo),
Snackbar.LENGTH_LONG, false) getResources().getColor(R.color.amber_500),
Snackbar.LENGTH_LONG,
false)
{ {
@Override @Override
protected void executeAction(@Nullable Long parameter) { protected void executeAction(@Nullable Long parameter) {

View File

@ -55,6 +55,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.TooltipCompat; import androidx.appcompat.widget.TooltipCompat;
import androidx.core.content.res.ResourcesCompat; import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@ -223,7 +224,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
list.setItemAnimator(new DeleteItemAnimator()); list.setItemAnimator(new DeleteItemAnimator());
list.addOnScrollListener(new ScrollListener()); list.addOnScrollListener(new ScrollListener());
snapToTopDataObserver = new SnapToTopDataObserver(list, null); snapToTopDataObserver = new SnapToTopDataObserver(list);
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list); new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
@ -367,7 +368,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override @Override
public void onContactClicked(@NonNull Recipient contact) { public void onContactClicked(@NonNull Recipient contact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact); return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact.getId());
}, threadId -> { }, threadId -> {
hideKeyboard(); hideKeyboard();
getNavigator().goToConversation(contact.getId(), getNavigator().goToConversation(contact.getId(),
@ -423,6 +424,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
viewModel.onMegaphoneCompleted(event); viewModel.onMegaphoneCompleted(event);
} }
@Override
public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
}
private void onReminderAction(@IdRes int reminderActionId) { private void onReminderAction(@IdRes int reminderActionId) {
if (reminderActionId == R.id.reminder_action_update_now) { if (reminderActionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
@ -679,7 +685,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
int count = selectedConversations.size(); int count = selectedConversations.size();
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count); String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
new SnackbarAsyncTask<Void>(getView(), new SnackbarAsyncTask<Void>(getViewLifecycleOwner().getLifecycle(),
requireView(),
snackBarTitle, snackBarTitle,
getString(R.string.ConversationListFragment_undo), getString(R.string.ConversationListFragment_undo),
getResources().getColor(R.color.amber_500), getResources().getColor(R.color.amber_500),
@ -1002,11 +1009,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
protected void onItemSwiped(long threadId, int unreadCount) { protected void onItemSwiped(long threadId, int unreadCount) {
new SnackbarAsyncTask<Long>(getView(), new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1), requireView(),
getString(R.string.ConversationListFragment_undo), getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
getResources().getColor(R.color.amber_500), getString(R.string.ConversationListFragment_undo),
Snackbar.LENGTH_LONG, false) getResources().getColor(R.color.amber_500),
Snackbar.LENGTH_LONG,
false)
{ {
@Override @Override
protected void executeAction(@Nullable Long parameter) { protected void executeAction(@Nullable Long parameter) {

View File

@ -453,11 +453,13 @@ public final class ConversationListItem extends RelativeLayout
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds); String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time)); return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) { } else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
if (thread.getRecipient().isGroup()) { return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed)); if (r.isGroup()) {
} else { return context.getString(R.string.ThreadRecord_safety_number_changed);
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)))); } else {
} return context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context));
}
}));
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) { } else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified)); return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) { } else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {

View File

@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.CertificateValidator;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKey;
@ -27,6 +29,11 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UnidentifiedAccessUtil { public class UnidentifiedAccessUtil {
@ -42,34 +49,64 @@ public class UnidentifiedAccessUtil {
} }
@WorkerThread @WorkerThread
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, @NonNull Recipient recipient) {
@NonNull Recipient recipient) return getAccessFor(context, recipient, true);
{ }
try {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient);
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { @WorkerThread
ourUnidentifiedAccessKey = Util.getSecretBytes(16); public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, @NonNull Recipient recipient, boolean log) {
} return getAccessFor(context, Collections.singletonList(recipient), log).get(0);
}
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + @WorkerThread
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) + public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients) {
" | UUID certificate supported? " + recipient.isUuidSupported()); return getAccessFor(context, recipients, true);
}
@WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean log) {
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
}
List<Optional<UnidentifiedAccessPair>> access = new ArrayList<>(recipients.size());
Map<CertificateType, Integer> typeCounts = new HashMap<>();
for (Recipient recipient : recipients) {
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
CertificateType certificateType = getUnidentifiedAccessCertificateType(recipient);
byte[] ourUnidentifiedAccessCertificate = SignalStore.certificateValues().getUnidentifiedAccessCertificate(certificateType);
int typeCount = Util.getOrDefault(typeCounts, certificateType, 0);
typeCount++;
typeCounts.put(certificateType, typeCount);
if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey, try {
ourUnidentifiedAccessCertificate), access.add(Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
new UnidentifiedAccess(ourUnidentifiedAccessKey, ourUnidentifiedAccessCertificate),
ourUnidentifiedAccessCertificate))); new UnidentifiedAccess(ourUnidentifiedAccessKey,
ourUnidentifiedAccessCertificate))));
} catch (InvalidCertificateException e) {
Log.w(TAG, e);
access.add(Optional.absent());
}
} else {
access.add(Optional.absent());
} }
return Optional.absent();
} catch (InvalidCertificateException e) {
Log.w(TAG, e);
return Optional.absent();
} }
int unidentifiedCount = Stream.of(access).filter(Optional::isPresent).toList().size();
int otherCount = access.size() - unidentifiedCount;
if (log) {
Log.i(TAG, "Unidentified: " + unidentifiedCount + ", Other: " + otherCount + ". Types: " + typeCounts);
}
return access;
} }
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) { public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
@ -95,21 +132,20 @@ public class UnidentifiedAccessUtil {
} }
} }
private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) { private static @NonNull CertificateType getUnidentifiedAccessCertificateType(@NonNull Recipient recipient) {
CertificateType certificateType;
PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode(); PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode();
switch (sendPhoneNumberTo) { switch (sendPhoneNumberTo) {
case EVERYONE: certificateType = CertificateType.UUID_AND_E164; break; case EVERYONE: return CertificateType.UUID_AND_E164;
case CONTACTS: certificateType = recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY; break; case CONTACTS: return recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY;
case NOBODY : certificateType = CertificateType.UUID_ONLY; break; case NOBODY : return CertificateType.UUID_ONLY;
default : throw new AssertionError(); default : throw new AssertionError();
} }
}
Log.i(TAG, String.format("Certificate type for %s with setting %s -> %s", recipient.getId(), sendPhoneNumberTo, certificateType)); private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
return SignalStore.certificateValues() return SignalStore.certificateValues()
.getUnidentifiedAccessCertificate(certificateType); .getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType(recipient));
} }
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {

View File

@ -19,11 +19,8 @@ package org.thoughtcrime.securesms.database;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap;
import android.media.MediaDataSource; import android.media.MediaDataSource;
import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
@ -58,14 +55,10 @@ import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -74,7 +67,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.JsonUtil; import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -90,9 +82,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
public class AttachmentDatabase extends Database { public class AttachmentDatabase extends Database {
@ -111,8 +100,6 @@ public class AttachmentDatabase extends Database {
private static final String TRANSFER_FILE = "transfer_file"; private static final String TRANSFER_FILE = "transfer_file";
public static final String SIZE = "data_size"; public static final String SIZE = "data_size";
static final String FILE_NAME = "file_name"; static final String FILE_NAME = "file_name";
public static final String THUMBNAIL = "thumbnail";
static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio";
public static final String UNIQUE_ID = "unique_id"; public static final String UNIQUE_ID = "unique_id";
static final String DIGEST = "digest"; static final String DIGEST = "digest";
static final String VOICE_NOTE = "voice_note"; static final String VOICE_NOTE = "voice_note";
@ -124,7 +111,6 @@ public class AttachmentDatabase extends Database {
static final String STICKER_EMOJI = "sticker_emoji"; static final String STICKER_EMOJI = "sticker_emoji";
static final String FAST_PREFLIGHT_ID = "fast_preflight_id"; static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
public static final String DATA_RANDOM = "data_random"; public static final String DATA_RANDOM = "data_random";
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
static final String WIDTH = "width"; static final String WIDTH = "width";
static final String HEIGHT = "height"; static final String HEIGHT = "height";
static final String CAPTION = "caption"; static final String CAPTION = "caption";
@ -149,11 +135,10 @@ public class AttachmentDatabase extends Database {
private static final String[] PROJECTION = new String[] {ROW_ID, private static final String[] PROJECTION = new String[] {ROW_ID,
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL, CDN_NUMBER, CONTENT_LOCATION, DATA,
TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL, TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST,
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM, FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM,
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH, STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH,
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER, TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
UPLOAD_TIMESTAMP }; UPLOAD_TIMESTAMP };
@ -175,15 +160,12 @@ public class AttachmentDatabase extends Database {
DATA + " TEXT, " + DATA + " TEXT, " +
SIZE + " INTEGER, " + SIZE + " INTEGER, " +
FILE_NAME + " TEXT, " + FILE_NAME + " TEXT, " +
THUMBNAIL + " TEXT, " +
THUMBNAIL_ASPECT_RATIO + " REAL, " +
UNIQUE_ID + " INTEGER NOT NULL, " + UNIQUE_ID + " INTEGER NOT NULL, " +
DIGEST + " BLOB, " + DIGEST + " BLOB, " +
FAST_PREFLIGHT_ID + " TEXT, " + FAST_PREFLIGHT_ID + " TEXT, " +
VOICE_NOTE + " INTEGER DEFAULT 0, " + VOICE_NOTE + " INTEGER DEFAULT 0, " +
BORDERLESS + " INTEGER DEFAULT 0, " + BORDERLESS + " INTEGER DEFAULT 0, " +
DATA_RANDOM + " BLOB, " + DATA_RANDOM + " BLOB, " +
THUMBNAIL_RANDOM + " BLOB, " +
QUOTE + " INTEGER DEFAULT 0, " + QUOTE + " INTEGER DEFAULT 0, " +
WIDTH + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " +
HEIGHT + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " +
@ -208,10 +190,6 @@ public class AttachmentDatabase extends Database {
"CREATE INDEX IF NOT EXISTS part_data_index ON " + TABLE_NAME + " (" + DATA + ");" "CREATE INDEX IF NOT EXISTS part_data_index ON " + TABLE_NAME + " (" + DATA + ");"
}; };
private static final long STANDARD_THUMB_TIME = 1000;
private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
private final AttachmentSecret attachmentSecret; private final AttachmentSecret attachmentSecret;
public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) { public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
@ -228,29 +206,6 @@ public class AttachmentDatabase extends Database {
else return dataStream; else return dataStream;
} }
public @NonNull InputStream getThumbnailStream(@NonNull AttachmentId attachmentId)
throws IOException
{
Log.d(TAG, "getThumbnailStream(" + attachmentId + ")");
InputStream dataStream = getDataStream(attachmentId, THUMBNAIL, 0);
if (dataStream != null) {
return dataStream;
}
try {
InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME)).get();
if (generatedStream == null) throw new FileNotFoundException("No thumbnail stream available: " + attachmentId);
else return generatedStream;
} catch (InterruptedException ie) {
throw new AssertionError("interrupted");
} catch (ExecutionException ee) {
Log.w(TAG, ee);
throw new IOException(ee);
}
}
public boolean containsStickerPackId(@NonNull String stickerPackId) { public boolean containsStickerPackId(@NonNull String stickerPackId) {
String selection = STICKER_PACK_ID + " = ?"; String selection = STICKER_PACK_ID + " = ?";
String[] args = new String[] { stickerPackId }; String[] args = new String[] { stickerPackId };
@ -365,12 +320,11 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?", cursor = database.query(TABLE_NAME, new String[] {DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
new String[] {mmsId+""}, null, null, null); new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)), deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)), cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)), new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID)))); cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
@ -418,12 +372,11 @@ public class AttachmentDatabase extends Database {
Cursor cursor = null; Cursor cursor = null;
try { try {
cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?", cursor = database.query(TABLE_NAME, new String[] {DATA, CONTENT_TYPE, ROW_ID, UNIQUE_ID}, MMS_ID + " = ?",
new String[] {mmsId+""}, null, null, null); new String[] {mmsId+""}, null, null, null);
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)), deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)), cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)), new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID)))); cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
@ -437,8 +390,6 @@ public class AttachmentDatabase extends Database {
values.put(DATA, (String) null); values.put(DATA, (String) null);
values.put(DATA_RANDOM, (byte[]) null); values.put(DATA_RANDOM, (byte[]) null);
values.put(DATA_HASH, (String) null); values.put(DATA_HASH, (String) null);
values.put(THUMBNAIL, (String) null);
values.put(THUMBNAIL_RANDOM, (byte[]) null);
values.put(FILE_NAME, (String) null); values.put(FILE_NAME, (String) null);
values.put(CAPTION, (String) null); values.put(CAPTION, (String) null);
values.put(SIZE, 0); values.put(SIZE, 0);
@ -463,7 +414,7 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
try (Cursor cursor = database.query(TABLE_NAME, try (Cursor cursor = database.query(TABLE_NAME,
new String[]{DATA, THUMBNAIL, CONTENT_TYPE}, new String[]{DATA, CONTENT_TYPE},
PART_ID_WHERE, PART_ID_WHERE,
id.toStrings(), id.toStrings(),
null, null,
@ -475,11 +426,10 @@ public class AttachmentDatabase extends Database {
return; return;
} }
String data = cursor.getString(cursor.getColumnIndex(DATA)); String data = cursor.getString(cursor.getColumnIndex(DATA));
String thumbnail = cursor.getString(cursor.getColumnIndex(THUMBNAIL));
String contentType = cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)); String contentType = cursor.getString(cursor.getColumnIndex(CONTENT_TYPE));
database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings()); database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings());
deleteAttachmentOnDisk(data, thumbnail, contentType, id); deleteAttachmentOnDisk(data, contentType, id);
notifyAttachmentListeners(); notifyAttachmentListeners();
} }
} }
@ -502,10 +452,9 @@ public class AttachmentDatabase extends Database {
filesOnDisk.add(file.getAbsolutePath()); filesOnDisk.add(file.getAbsolutePath());
} }
try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA, THUMBNAIL }, null, null, null, null, null, null)) { try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA }, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
filesInDb.add(CursorUtil.requireString(cursor, DATA)); filesInDb.add(CursorUtil.requireString(cursor, DATA));
filesInDb.add(CursorUtil.requireString(cursor, THUMBNAIL));
} }
} }
@ -530,7 +479,6 @@ public class AttachmentDatabase extends Database {
} }
private void deleteAttachmentOnDisk(@Nullable String data, private void deleteAttachmentOnDisk(@Nullable String data,
@Nullable String thumbnail,
@Nullable String contentType, @Nullable String contentType,
@NonNull AttachmentId attachmentId) @NonNull AttachmentId attachmentId)
{ {
@ -561,8 +509,6 @@ public class AttachmentDatabase extends Database {
values.putNull(DATA); values.putNull(DATA);
values.putNull(DATA_RANDOM); values.putNull(DATA_RANDOM);
values.putNull(DATA_HASH); values.putNull(DATA_HASH);
values.putNull(THUMBNAIL);
values.putNull(THUMBNAIL_RANDOM);
deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings()); deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings());
} }
database.setTransactionSuccessful(); database.setTransactionSuccessful();
@ -581,15 +527,7 @@ public class AttachmentDatabase extends Database {
} }
} }
if (!TextUtils.isEmpty(thumbnail)) { if (MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)) {
if (new File(thumbnail).delete()) {
Log.i(TAG, "[deleteAttachmentOnDisk] Deleted thumbnail. " + data + " " + attachmentId);
} else {
Log.w(TAG, "[deleteAttachmentOnDisk] Failed to delete attachment. " + data + " " + attachmentId);
}
}
if (MediaUtil.isImageType(contentType) || thumbnail != null) {
Glide.get(context).clearDiskCache(); Glide.get(context).clearDiskCache();
} }
} }
@ -623,22 +561,17 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
DataInfo oldInfo = getAttachmentDataFileInfo(attachmentId, DATA); DataInfo oldInfo = getAttachmentDataFileInfo(attachmentId, DATA);
DataInfo dataInfo = setAttachmentData(inputStream, false, attachmentId); DataInfo dataInfo = setAttachmentData(inputStream, attachmentId);
File transferFile = getTransferFile(databaseHelper.getReadableDatabase(), attachmentId); File transferFile = getTransferFile(databaseHelper.getReadableDatabase(), attachmentId);
if (oldInfo != null) { if (oldInfo != null) {
updateAttachmentDataHash(database, oldInfo.hash, dataInfo); updateAttachmentDataHash(database, oldInfo.hash, dataInfo);
} }
if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) { values.put(DATA, dataInfo.file.getAbsolutePath());
values.put(THUMBNAIL, dataInfo.file.getAbsolutePath()); values.put(SIZE, dataInfo.length);
values.put(THUMBNAIL_RANDOM, dataInfo.random); values.put(DATA_RANDOM, dataInfo.random);
} else { values.put(DATA_HASH, dataInfo.hash);
values.put(DATA, dataInfo.file.getAbsolutePath());
values.put(SIZE, dataInfo.length);
values.put(DATA_RANDOM, dataInfo.random);
values.put(DATA_HASH, dataInfo.hash);
}
String visualHashString = getVisualHashStringOrNull(placeholder); String visualHashString = getVisualHashStringOrNull(placeholder);
if (visualHashString != null) { if (visualHashString != null) {
@ -662,8 +595,6 @@ public class AttachmentDatabase extends Database {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
transferFile.delete(); transferFile.delete();
} }
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
} }
private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) { private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
@ -846,7 +777,6 @@ public class AttachmentDatabase extends Database {
DataInfo dataInfo = setAttachmentData(destination, DataInfo dataInfo = setAttachmentData(destination,
mediaStream.getStream(), mediaStream.getStream(),
false,
databaseAttachment.getAttachmentId()); databaseAttachment.getAttachmentId());
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
@ -1055,16 +985,8 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null; Cursor cursor = null;
String randomColumn;
switch (dataType) {
case DATA: randomColumn = DATA_RANDOM; break;
case THUMBNAIL: randomColumn = THUMBNAIL_RANDOM; break;
default:throw new AssertionError("Unknown data type: " + dataType);
}
try { try {
cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, randomColumn, DATA_HASH}, PART_ID_WHERE, attachmentId.toStrings(), cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, DATA_RANDOM, DATA_HASH}, PART_ID_WHERE, attachmentId.toStrings(),
null, null, null); null, null, null);
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
@ -1074,7 +996,7 @@ public class AttachmentDatabase extends Database {
return new DataInfo(new File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))), return new DataInfo(new File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
cursor.getBlob(cursor.getColumnIndexOrThrow(randomColumn)), cursor.getBlob(cursor.getColumnIndexOrThrow(DATA_RANDOM)),
cursor.getString(cursor.getColumnIndexOrThrow(DATA_HASH))); cursor.getString(cursor.getColumnIndexOrThrow(DATA_HASH)));
} else { } else {
return null; return null;
@ -1087,26 +1009,24 @@ public class AttachmentDatabase extends Database {
} }
private @NonNull DataInfo setAttachmentData(@NonNull Uri uri, private @NonNull DataInfo setAttachmentData(@NonNull Uri uri,
boolean isThumbnail,
@Nullable AttachmentId attachmentId) @Nullable AttachmentId attachmentId)
throws MmsException throws MmsException
{ {
try { try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri); InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
return setAttachmentData(inputStream, isThumbnail, attachmentId); return setAttachmentData(inputStream, attachmentId);
} catch (IOException e) { } catch (IOException e) {
throw new MmsException(e); throw new MmsException(e);
} }
} }
private @NonNull DataInfo setAttachmentData(@NonNull InputStream in, private @NonNull DataInfo setAttachmentData(@NonNull InputStream in,
boolean isThumbnail,
@Nullable AttachmentId attachmentId) @Nullable AttachmentId attachmentId)
throws MmsException throws MmsException
{ {
try { try {
File dataFile = newFile(); File dataFile = newFile();
return setAttachmentData(dataFile, in, isThumbnail, attachmentId); return setAttachmentData(dataFile, in, attachmentId);
} catch (IOException e) { } catch (IOException e) {
throw new MmsException(e); throw new MmsException(e);
} }
@ -1119,7 +1039,6 @@ public class AttachmentDatabase extends Database {
private @NonNull DataInfo setAttachmentData(@NonNull File destination, private @NonNull DataInfo setAttachmentData(@NonNull File destination,
@NonNull InputStream in, @NonNull InputStream in,
boolean isThumbnail,
@Nullable AttachmentId attachmentId) @Nullable AttachmentId attachmentId)
throws MmsException throws MmsException
{ {
@ -1130,18 +1049,16 @@ public class AttachmentDatabase extends Database {
long length = Util.copy(digestInputStream, out.second); long length = Util.copy(digestInputStream, out.second);
String hash = Base64.encodeBytes(digestInputStream.getMessageDigest().digest()); String hash = Base64.encodeBytes(digestInputStream.getMessageDigest().digest());
if (!isThumbnail) { SQLiteDatabase database = databaseHelper.getWritableDatabase();
SQLiteDatabase database = databaseHelper.getWritableDatabase(); Optional<DataInfo> sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId);
Optional<DataInfo> sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId); if (sharedDataInfo.isPresent()) {
if (sharedDataInfo.isPresent()) { Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath());
Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath()); if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) {
if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) { Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
}
return sharedDataInfo.get();
} else {
Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
} }
return sharedDataInfo.get();
} else {
Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
} }
return new DataInfo(destination, length, out.first, hash); return new DataInfo(destination, length, out.first, hash);
@ -1216,7 +1133,7 @@ public class AttachmentDatabase extends Database {
result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)), result.add(new DatabaseAttachment(new AttachmentId(object.getLong(ROW_ID), object.getLong(UNIQUE_ID)),
object.getLong(MMS_ID), object.getLong(MMS_ID),
!TextUtils.isEmpty(object.getString(DATA)), !TextUtils.isEmpty(object.getString(DATA)),
!TextUtils.isEmpty(object.getString(THUMBNAIL)), MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType, contentType,
object.getInt(TRANSFER_STATE), object.getInt(TRANSFER_STATE),
object.getLong(SIZE), object.getLong(SIZE),
@ -1254,7 +1171,7 @@ public class AttachmentDatabase extends Database {
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)), MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
contentType, contentType,
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
@ -1296,10 +1213,9 @@ public class AttachmentDatabase extends Database {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
DataInfo dataInfo = null; DataInfo dataInfo = null;
long uniqueId = System.currentTimeMillis(); long uniqueId = System.currentTimeMillis();
long thumbnailTimeUs;
if (attachment.getDataUri() != null) { if (attachment.getUri() != null) {
dataInfo = setAttachmentData(attachment.getDataUri(), false, null); dataInfo = setAttachmentData(attachment.getUri(), null);
Log.d(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath()); Log.d(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath());
} }
@ -1342,11 +1258,9 @@ public class AttachmentDatabase extends Database {
if (attachment.getTransformProperties().isVideoEdited()) { if (attachment.getTransformProperties().isVideoEdited()) {
contentValues.putNull(VISUAL_HASH); contentValues.putNull(VISUAL_HASH);
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize()); contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
} else { } else {
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template)); contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize()); contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
thumbnailTimeUs = STANDARD_THUMB_TIME;
} }
if (attachment.isSticker()) { if (attachment.isSticker()) {
@ -1370,38 +1284,6 @@ public class AttachmentDatabase extends Database {
boolean notifyPacks = attachment.isSticker() && !hasStickerAttachments(); boolean notifyPacks = attachment.isSticker() && !hasStickerAttachments();
long rowId = database.insert(TABLE_NAME, null, contentValues); long rowId = database.insert(TABLE_NAME, null, contentValues);
AttachmentId attachmentId = new AttachmentId(rowId, uniqueId); AttachmentId attachmentId = new AttachmentId(rowId, uniqueId);
Uri thumbnailUri = attachment.getThumbnailUri();
boolean hasThumbnail = false;
if (thumbnailUri != null) {
try (InputStream attachmentStream = PartAuthority.getAttachmentStream(context, thumbnailUri)) {
Pair<Integer, Integer> dimens = BitmapUtil.getDimensions(attachmentStream);
updateAttachmentThumbnail(attachmentId,
PartAuthority.getAttachmentStream(context, thumbnailUri),
(float) dimens.first / (float) dimens.second);
hasThumbnail = true;
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, "Failed to save existing thumbnail.", e);
}
}
if (!hasThumbnail && dataInfo != null) {
if (MediaUtil.hasVideoThumbnail(attachment.getDataUri()) && thumbnailTimeUs == STANDARD_THUMB_TIME) {
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, attachment.getDataUri(), thumbnailTimeUs);
if (bitmap != null) {
try (ThumbnailData thumbnailData = new ThumbnailData(bitmap)) {
updateAttachmentThumbnail(attachmentId, thumbnailData.toDataStream(), thumbnailData.getAspectRatio());
}
} else {
Log.w(TAG, "Retrieving video thumbnail failed, submitting thumbnail generation job...");
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
}
} else {
Log.i(TAG, "Submitting thumbnail generation job...");
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, thumbnailTimeUs));
}
}
if (notifyPacks) { if (notifyPacks) {
notifyStickerPackListeners(); notifyStickerPackListeners();
@ -1423,35 +1305,6 @@ public class AttachmentDatabase extends Database {
return null; return null;
} }
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
protected void updateAttachmentThumbnail(AttachmentId attachmentId, InputStream in, float aspectRatio)
throws MmsException
{
Log.i(TAG, "updating part thumbnail for #" + attachmentId);
DataInfo thumbnailFile = setAttachmentData(in, true, attachmentId);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues(2);
values.put(THUMBNAIL, thumbnailFile.file.getAbsolutePath());
values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio);
values.put(THUMBNAIL_RANDOM, thumbnailFile.random);
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
Cursor cursor = database.query(TABLE_NAME, new String[] {MMS_ID}, PART_ID_WHERE, attachmentId.toStrings(), null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID))));
}
} finally {
if (cursor != null) cursor.close();
}
}
@WorkerThread @WorkerThread
public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) { public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) {
Log.i(TAG, "updating part audio wave form for #" + attachmentId); Log.i(TAG, "updating part audio wave form for #" + attachmentId);
@ -1468,66 +1321,6 @@ public class AttachmentDatabase extends Database {
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
} }
@VisibleForTesting
class ThumbnailFetchCallable implements Callable<InputStream> {
private final AttachmentId attachmentId;
private final long timeUs;
ThumbnailFetchCallable(AttachmentId attachmentId, long timeUs) {
this.attachmentId = attachmentId;
this.timeUs = timeUs;
}
@Override
public @Nullable InputStream call() throws Exception {
Log.d(TAG, "Executing thumbnail job...");
final InputStream stream = getDataStream(attachmentId, THUMBNAIL, 0);
if (stream != null) {
return stream;
}
DatabaseAttachment attachment = getAttachment(attachmentId);
if (attachment == null || !attachment.hasData()) {
return null;
}
if (MediaUtil.isVideoType(attachment.getContentType())) {
try (ThumbnailData data = generateVideoThumbnail(attachmentId, timeUs)) {
if (data != null) {
updateAttachmentThumbnail(attachmentId, data.toDataStream(), data.getAspectRatio());
return getDataStream(attachmentId, THUMBNAIL, 0);
}
}
}
return null;
}
private ThumbnailData generateVideoThumbnail(AttachmentId attachmentId, long timeUs) throws IOException {
if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported...");
return null;
}
try (MediaDataSource dataSource = mediaDataSourceFor(attachmentId)) {
if (dataSource == null) return null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource);
Bitmap bitmap = retriever.getFrameAtTime(timeUs);
Log.i(TAG, "Generated video thumbnail...");
return bitmap != null ? new ThumbnailData(bitmap) : null;
}
}
}
@RequiresApi(23) @RequiresApi(23)
public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId) { public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId) {

View File

@ -4,9 +4,12 @@ import android.content.ContentProvider;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BuildConfig;
/** /**
* Starting in API 26, a {@link ContentProvider} needs to be defined for each authority you wish to * Starting in API 26, a {@link ContentProvider} needs to be defined for each authority you wish to
* observe changes on. These classes essentially do nothing except exist so Android doesn't complain. * observe changes on. These classes essentially do nothing except exist so Android doesn't complain.
@ -14,11 +17,15 @@ import androidx.annotation.Nullable;
public class DatabaseContentProviders { public class DatabaseContentProviders {
public static class ConversationList extends NoopContentProvider { public static class ConversationList extends NoopContentProvider {
public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.conversationlist"); private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversationlist";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
} }
public static class Conversation extends NoopContentProvider { public static class Conversation extends NoopContentProvider {
private static final String CONTENT_URI_STRING = "content://org.thoughtcrime.securesms.database.conversation/"; private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversation";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/";
public static Uri getUriForThread(long threadId) { public static Uri getUriForThread(long threadId) {
return Uri.parse(CONTENT_URI_STRING + threadId); return Uri.parse(CONTENT_URI_STRING + threadId);
@ -34,15 +41,24 @@ public class DatabaseContentProviders {
} }
public static class Attachment extends NoopContentProvider { public static class Attachment extends NoopContentProvider {
public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.attachment"); private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.attachment";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
} }
public static class Sticker extends NoopContentProvider { public static class Sticker extends NoopContentProvider {
public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.sticker"); private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.sticker";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
} }
public static class StickerPack extends NoopContentProvider { public static class StickerPack extends NoopContentProvider {
public static final Uri CONTENT_URI = Uri.parse("content://org.thoughtcrime.securesms.database.stickerpack"); private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.stickerpack";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
} }
private static abstract class NoopContentProvider extends ContentProvider { private static abstract class NoopContentProvider extends ContentProvider {

View File

@ -22,8 +22,6 @@ public class EarlyReceiptCache {
} }
public synchronized void increment(long timestamp, @NonNull RecipientId origin) { public synchronized void increment(long timestamp, @NonNull RecipientId origin) {
Log.i(TAG, String.format(Locale.US, "[%s] Timestamp: %d, Recipient: %s", name, timestamp, origin.serialize()));
Map<RecipientId, Long> receipts = cache.get(timestamp); Map<RecipientId, Long> receipts = cache.get(timestamp);
if (receipts == null) { if (receipts == null) {
@ -43,10 +41,6 @@ public class EarlyReceiptCache {
public synchronized Map<RecipientId, Long> remove(long timestamp) { public synchronized Map<RecipientId, Long> remove(long timestamp) {
Map<RecipientId, Long> receipts = cache.remove(timestamp); Map<RecipientId, Long> receipts = cache.remove(timestamp);
Log.i(TAG, this+"");
Log.i(TAG, String.format(Locale.US, "Checking early receipts (%d): %d", timestamp, receipts == null ? 0 : receipts.size()));
return receipts != null ? receipts : new HashMap<>(); return receipts != null ? receipts : new HashMap<>();
} }
} }

View File

@ -23,14 +23,12 @@ public class MediaDatabase extends Database {
private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", " private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", "
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", "

View File

@ -126,7 +126,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address); public abstract @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address);
public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address); public abstract @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address);
public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address); public abstract @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type); public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type);
public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message); public abstract Optional<InsertResult> insertMessageInbox(IncomingTextMessage message);

View File

@ -213,7 +213,6 @@ public class MmsDatabase extends MessageDatabase {
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
@ -390,7 +389,7 @@ public class MmsDatabase extends MessageDatabase {
} }
@Override @Override
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address) { public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -411,7 +411,6 @@ public class MmsSmsDatabase extends Database {
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + "'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + "'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +

View File

@ -10,7 +10,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.gms.common.util.ArrayUtils;
import net.sqlcipher.database.SQLiteConstraintException; import net.sqlcipher.database.SQLiteConstraintException;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
@ -152,14 +151,14 @@ public class RecipientDatabase extends Database {
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat( private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
new String[] { TABLE_NAME + "." + ID, new String[] { TABLE_NAME + "." + ID,
TABLE_NAME + "." + STORAGE_PROTO }, TABLE_NAME + "." + STORAGE_PROTO },
TYPED_RECIPIENT_PROJECTION, TYPED_RECIPIENT_PROJECTION,
new String[] { new String[] {
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS, IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
}); }).flatMap(Stream::of).toArray(String[]::new);
public static final String[] CREATE_INDEXS = new String[] { public static final String[] CREATE_INDEXS = new String[] {
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");", "CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
@ -395,10 +394,6 @@ public class RecipientDatabase extends Database {
throw new IllegalArgumentException("Must provide a UUID or E164!"); throw new IllegalArgumentException("Must provide a UUID or E164!");
} }
if (!FeatureFlags.cds()) {
highTrust = true;
}
RecipientId recipientNeedingRefresh = null; RecipientId recipientNeedingRefresh = null;
Pair<RecipientId, RecipientId> remapped = null; Pair<RecipientId, RecipientId> remapped = null;
boolean transactionSuccessful = false; boolean transactionSuccessful = false;
@ -1000,7 +995,7 @@ public class RecipientDatabase extends Database {
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw())); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
values.put(DIRTY, DirtyState.CLEAN.getId()); values.put(DIRTY, DirtyState.CLEAN.getId());
if (contact.isProfileSharingEnabled() && isInsert) { if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) {
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize()); values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
} }
@ -1055,8 +1050,8 @@ public class RecipientDatabase extends Database {
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID; + " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
List<RecipientSettings> out = new ArrayList<>(); List<RecipientSettings> out = new ArrayList<>();
String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION, String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }); new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY }).flatMap(Stream::of).toArray(String[]::new);
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) { try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
@ -1080,7 +1075,7 @@ public class RecipientDatabase extends Database {
public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() { public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ? AND " + GROUP_TYPE + " != ?"; String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ? AND " + GROUP_TYPE + " != ?";
String[] args = { String.valueOf(DirtyState.DELETE), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) }; String[] args = { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) };
Map<RecipientId, StorageId> out = new HashMap<>(); Map<RecipientId, StorageId> out = new HashMap<>();
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) { try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) {
@ -1802,6 +1797,12 @@ public class RecipientDatabase extends Database {
} }
} }
/**
* Handles inserts the (e164, UUID) pairs, which could result in merges. Does not mark users as
* registered.
*
* @return A mapping of (RecipientId, UUID)
*/
public @NonNull Map<RecipientId, String> bulkProcessCdsResult(@NonNull Map<String, UUID> mapping) { public @NonNull Map<RecipientId, String> bulkProcessCdsResult(@NonNull Map<String, UUID> mapping) {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
HashMap<RecipientId, String> uuidMap = new HashMap<>(); HashMap<RecipientId, String> uuidMap = new HashMap<>();

View File

@ -12,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -145,6 +146,16 @@ public class SessionDatabase extends Database {
database.delete(TABLE_NAME, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()}); database.delete(TABLE_NAME, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()});
} }
public boolean hasSessionFor(@NonNull RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String query = RECIPIENT_ID + " = ?";
String[] args = SqlUtil.buildArgs(recipientId);
try (Cursor cursor = database.query(TABLE_NAME, new String[] { ID }, query, args, null, null, null, "1")) {
return cursor != null && cursor.moveToFirst();
}
}
public static final class SessionRow { public static final class SessionRow {
private final RecipientId recipientId; private final RecipientId recipientId;
private final int deviceId; private final int deviceId;

View File

@ -644,20 +644,20 @@ public class SmsDatabase extends MessageDatabase {
@Override @Override
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) { public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address) {
return insertCallLog(address, Types.INCOMING_CALL_TYPE, false); return insertCallLog(address, Types.INCOMING_CALL_TYPE, false, System.currentTimeMillis());
} }
@Override @Override
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) { public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address) {
return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false); return insertCallLog(address, Types.OUTGOING_CALL_TYPE, false, System.currentTimeMillis());
} }
@Override @Override
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address) { public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
return insertCallLog(address, Types.MISSED_CALL_TYPE, true); return insertCallLog(address, Types.MISSED_CALL_TYPE, true, timestamp);
} }
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread) { private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
Recipient recipient = Recipient.resolved(recipientId); Recipient recipient = Recipient.resolved(recipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
@ -665,7 +665,7 @@ public class SmsDatabase extends MessageDatabase {
values.put(RECIPIENT_ID, recipientId.serialize()); values.put(RECIPIENT_ID, recipientId.serialize());
values.put(ADDRESS_DEVICE_ID, 1); values.put(ADDRESS_DEVICE_ID, 1);
values.put(DATE_RECEIVED, System.currentTimeMillis()); values.put(DATE_RECEIVED, System.currentTimeMillis());
values.put(DATE_SENT, System.currentTimeMillis()); values.put(DATE_SENT, timestamp);
values.put(READ, unread ? 0 : 1); values.put(READ, unread ? 0 : 1);
values.put(TYPE, type); values.put(TYPE, type);
values.put(THREAD_ID, threadId); values.put(THREAD_ID, threadId);

View File

@ -69,10 +69,8 @@ public final class ThreadBodyUtil {
} else if (hasImage) { } else if (hasImage) {
return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo); return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo);
} else if (TextUtils.isEmpty(record.getBody())) { } else if (TextUtils.isEmpty(record.getBody())) {
Log.w(TAG, "Got a media message without a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
return context.getString(R.string.ThreadRecord_media_message); return context.getString(R.string.ThreadRecord_media_message);
} else { } else {
Log.w(TAG, "Got a media message with a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
return getBody(context, record); return getBody(context, record);
} }
} }

View File

@ -872,22 +872,17 @@ public class ThreadDatabase extends Database {
deleteAllThreads(); deleteAllThreads();
} }
public long getThreadIdIfExistsFor(Recipient recipient) { public long getThreadIdIfExistsFor(@NonNull RecipientId recipientId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase(); SQLiteDatabase db = databaseHelper.getReadableDatabase();
String where = RECIPIENT_ID + " = ?"; String where = RECIPIENT_ID + " = ?";
String[] recipientsArg = new String[] {recipient.getId().serialize()}; String[] recipientsArg = new String[] {recipientId.serialize()};
Cursor cursor = null;
try { try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID }, where, recipientsArg, null, null, null, "1")) {
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null); if (cursor != null && cursor.moveToFirst()) {
return CursorUtil.requireLong(cursor, ID);
if (cursor != null && cursor.moveToFirst()) } else {
return cursor.getLong(cursor.getColumnIndexOrThrow(ID)); return -1;
else }
return -1L;
} finally {
if (cursor != null)
cursor.close();
} }
} }
@ -950,6 +945,10 @@ public class ThreadDatabase extends Database {
return Recipient.resolved(id); return Recipient.resolved(id);
} }
public boolean hasThread(@NonNull RecipientId recipientId) {
return getThreadIdIfExistsFor(recipientId) > -1;
}
public void setHasSent(long threadId, boolean hasSent) { public void setHasSent(long threadId, boolean hasSent) {
ContentValues contentValues = new ContentValues(1); ContentValues contentValues = new ContentValues(1);
contentValues.put(HAS_SENT, hasSent ? 1 : 0); contentValues.put(HAS_SENT, hasSent ? 1 : 0);
@ -1095,7 +1094,7 @@ public class ThreadDatabase extends Database {
Slide thumbnail = Optional.fromNullable(slideDeck.getThumbnailSlide()).or(Optional.fromNullable(slideDeck.getStickerSlide())).orNull(); Slide thumbnail = Optional.fromNullable(slideDeck.getThumbnailSlide()).or(Optional.fromNullable(slideDeck.getStickerSlide())).orNull();
if (thumbnail != null && !((MmsMessageRecord) record).isViewOnce()) { if (thumbnail != null && !((MmsMessageRecord) record).isViewOnce()) {
return thumbnail.getThumbnailUri(); return thumbnail.getUri();
} }
return null; return null;

View File

@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.SqlUtil;
@ -146,8 +147,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int UNKNOWN_STORAGE_FIELDS = 71; private static final int UNKNOWN_STORAGE_FIELDS = 71;
private static final int STICKER_CONTENT_TYPE = 72; private static final int STICKER_CONTENT_TYPE = 72;
private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73; private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73;
private static final int THUMBNAIL_CLEANUP = 74;
private static final int STICKER_CONTENT_TYPE_CLEANUP = 75;
private static final int DATABASE_VERSION = 73; private static final int DATABASE_VERSION = 75;
private static final String DATABASE_NAME = "signal.db"; private static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -1023,6 +1026,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN sticker_emoji TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE part ADD COLUMN sticker_emoji TEXT DEFAULT NULL");
} }
if (oldVersion < THUMBNAIL_CLEANUP) {
int total = 0;
int deleted = 0;
try (Cursor cursor = db.rawQuery("SELECT thumbnail FROM part WHERE thumbnail NOT NULL", null)) {
if (cursor != null) {
total = cursor.getCount();
Log.w(TAG, "Found " + total + " thumbnails to delete.");
}
while (cursor != null && cursor.moveToNext()) {
File file = new File(CursorUtil.requireString(cursor, "thumbnail"));
if (file.delete()) {
deleted++;
} else {
Log.w(TAG, "Failed to delete file! " + file.getAbsolutePath());
}
}
}
Log.w(TAG, "Deleted " + deleted + "/" + total + " thumbnail files.");
}
if (oldVersion < STICKER_CONTENT_TYPE_CLEANUP) {
ContentValues values = new ContentValues();
values.put("ct", "image/webp");
String query = "sticker_id NOT NULL AND (ct IS NULL OR ct = '')";
int rows = db.update("part", values, query, null);
Log.i(TAG, "Updated " + rows + " sticker attachment content types.");
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -270,7 +270,7 @@ public abstract class MessageRecord extends DisplayRecord {
} }
public long getTimestamp() { public long getTimestamp() {
if (isPush() && getDateSent() < getDateReceived()) { if ((isPush() || isCallLog()) && getDateSent() < getDateReceived()) {
return getDateSent(); return getDateSent();
} }
return getDateReceived(); return getDateReceived();

View File

@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects; import java.util.Objects;
@ -68,7 +69,7 @@ public final class StickerRecord {
} }
public @NonNull String getContentType() { public @NonNull String getContentType() {
return contentType == null ? MediaUtil.IMAGE_WEBP : contentType; return Util.isEmpty(contentType) ? MediaUtil.IMAGE_WEBP : contentType;
} }
public long getSize() { public long getSize() {

View File

@ -138,8 +138,7 @@ public class ApplicationDependencies {
messageSender.update( messageSender.update(
IncomingMessageObserver.getPipe(), IncomingMessageObserver.getPipe(),
IncomingMessageObserver.getUnidentifiedPipe(), IncomingMessageObserver.getUnidentifiedPipe(),
TextSecurePreferences.isMultiDevice(application), TextSecurePreferences.isMultiDevice(application));
FeatureFlags.attachmentsV3());
} }
return messageSender; return messageSender;

View File

@ -95,7 +95,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
new SignalProtocolStoreImpl(context), new SignalProtocolStoreImpl(context),
BuildConfig.SIGNAL_AGENT, BuildConfig.SIGNAL_AGENT,
TextSecurePreferences.isMultiDevice(context), TextSecurePreferences.isMultiDevice(context),
FeatureFlags.attachmentsV3(),
Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.fromNullable(IncomingMessageObserver.getPipe()),
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()), Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)), Optional.of(new SecurityEventListener(context)),

View File

@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.whispersystems.libsignal.IdentityKey;
import java.util.Objects;
public class CallParticipant {
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false);
private final @NonNull CameraState cameraState;
private final @NonNull Recipient recipient;
private final @Nullable IdentityKey identityKey;
private final @NonNull BroadcastVideoSink videoSink;
private final boolean videoEnabled;
private final boolean microphoneEnabled;
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
@NonNull BroadcastVideoSink renderer,
boolean microphoneEnabled)
{
return new CallParticipant(Recipient.self(),
null,
renderer,
cameraState,
cameraState.isEnabled() && cameraState.getCameraCount() > 0,
microphoneEnabled);
}
public static @NonNull CallParticipant createRemote(@NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
@NonNull BroadcastVideoSink renderer,
boolean videoEnabled)
{
return new CallParticipant(recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, true);
}
private CallParticipant(@NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
@NonNull BroadcastVideoSink videoSink,
@NonNull CameraState cameraState,
boolean videoEnabled,
boolean microphoneEnabled)
{
this.recipient = recipient;
this.identityKey = identityKey;
this.videoSink = videoSink;
this.cameraState = cameraState;
this.videoEnabled = videoEnabled;
this.microphoneEnabled = microphoneEnabled;
}
public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
}
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
return new CallParticipant(recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled);
}
public @NonNull Recipient getRecipient() {
return recipient;
}
public @Nullable IdentityKey getIdentityKey() {
return identityKey;
}
public @NonNull BroadcastVideoSink getVideoSink() {
return videoSink;
}
public @NonNull CameraState getCameraState() {
return cameraState;
}
public boolean isVideoEnabled() {
return videoEnabled;
}
public boolean isMicrophoneEnabled() {
return microphoneEnabled;
}
public @NonNull CameraState.Direction getCameraDirection() {
if (cameraState.getActiveDirection() == CameraState.Direction.BACK) {
return cameraState.getActiveDirection();
}
return CameraState.Direction.FRONT;
}
public boolean isMoreThanOneCameraAvailable() {
return cameraState.getCameraCount() > 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CallParticipant that = (CallParticipant) o;
return videoEnabled == that.videoEnabled &&
microphoneEnabled == that.microphoneEnabled &&
cameraState.equals(that.cameraState) &&
recipient.equals(that.recipient) &&
Objects.equals(identityKey, that.identityKey) &&
Objects.equals(videoSink, that.videoSink);
}
@Override
public int hashCode() {
return Objects.hash(cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled);
}
}

View File

@ -1,18 +1,20 @@
package org.thoughtcrime.securesms.events; package org.thoughtcrime.securesms.events;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey; import java.util.List;
public class WebRtcViewModel { public class WebRtcViewModel {
public enum State { public enum State {
// Normal states // Normal states
CALL_PRE_JOIN,
CALL_INCOMING, CALL_INCOMING,
CALL_OUTGOING, CALL_OUTGOING,
CALL_CONNECTED, CALL_CONNECTED,
@ -33,70 +35,34 @@ public class WebRtcViewModel {
CALL_ONGOING_ELSEWHERE CALL_ONGOING_ELSEWHERE
} }
private final @NonNull State state;
private final @NonNull State state; private final @NonNull Recipient recipient;
private final @NonNull Recipient recipient;
private final @Nullable IdentityKey identityKey;
private final boolean remoteVideoEnabled;
private final boolean isBluetoothAvailable; private final boolean isBluetoothAvailable;
private final boolean isMicrophoneEnabled;
private final boolean isRemoteVideoOffer; private final boolean isRemoteVideoOffer;
private final long callConnectedTime;
private final CameraState localCameraState; private final CallParticipant localParticipant;
private final TextureViewRenderer localRenderer; private final List<CallParticipant> remoteParticipants;
private final TextureViewRenderer remoteRenderer;
private final long callConnectedTime; public WebRtcViewModel(@NonNull State state,
@NonNull Recipient recipient,
public WebRtcViewModel(@NonNull State state, @NonNull CameraState localCameraState,
@NonNull Recipient recipient, @NonNull BroadcastVideoSink localSink,
@NonNull CameraState localCameraState, boolean isBluetoothAvailable,
@NonNull TextureViewRenderer localRenderer, boolean isMicrophoneEnabled,
@NonNull TextureViewRenderer remoteRenderer, boolean isRemoteVideoOffer,
boolean remoteVideoEnabled, long callConnectedTime,
boolean isBluetoothAvailable, @NonNull List<CallParticipant> remoteParticipants)
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer,
long callConnectedTime)
{
this(state,
recipient,
null,
localCameraState,
localRenderer,
remoteRenderer,
remoteVideoEnabled,
isBluetoothAvailable,
isMicrophoneEnabled,
isRemoteVideoOffer,
callConnectedTime);
}
public WebRtcViewModel(@NonNull State state,
@NonNull Recipient recipient,
@Nullable IdentityKey identityKey,
@NonNull CameraState localCameraState,
@NonNull TextureViewRenderer localRenderer,
@NonNull TextureViewRenderer remoteRenderer,
boolean remoteVideoEnabled,
boolean isBluetoothAvailable,
boolean isMicrophoneEnabled,
boolean isRemoteVideoOffer,
long callConnectedTime)
{ {
this.state = state; this.state = state;
this.recipient = recipient; this.recipient = recipient;
this.localCameraState = localCameraState;
this.localRenderer = localRenderer;
this.remoteRenderer = remoteRenderer;
this.identityKey = identityKey;
this.remoteVideoEnabled = remoteVideoEnabled;
this.isBluetoothAvailable = isBluetoothAvailable; this.isBluetoothAvailable = isBluetoothAvailable;
this.isMicrophoneEnabled = isMicrophoneEnabled;
this.isRemoteVideoOffer = isRemoteVideoOffer; this.isRemoteVideoOffer = isRemoteVideoOffer;
this.callConnectedTime = callConnectedTime; this.callConnectedTime = callConnectedTime;
this.remoteParticipants = remoteParticipants;
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
} }
public @NonNull State getState() { public @NonNull State getState() {
@ -107,50 +73,28 @@ public class WebRtcViewModel {
return recipient; return recipient;
} }
public @NonNull CameraState getLocalCameraState() {
return localCameraState;
}
public @Nullable IdentityKey getIdentityKey() {
return identityKey;
}
public boolean isRemoteVideoEnabled() { public boolean isRemoteVideoEnabled() {
return remoteVideoEnabled; return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
} }
public boolean isBluetoothAvailable() { public boolean isBluetoothAvailable() {
return isBluetoothAvailable; return isBluetoothAvailable;
} }
public boolean isMicrophoneEnabled() {
return isMicrophoneEnabled;
}
public boolean isRemoteVideoOffer() { public boolean isRemoteVideoOffer() {
return isRemoteVideoOffer; return isRemoteVideoOffer;
} }
public TextureViewRenderer getLocalRenderer() {
return localRenderer;
}
public TextureViewRenderer getRemoteRenderer() {
return remoteRenderer;
}
public long getCallConnectedTime() { public long getCallConnectedTime() {
return callConnectedTime; return callConnectedTime;
} }
public @NonNull String toString() { public @NonNull CallParticipant getLocalParticipant() {
return "[State: " + state + return localParticipant;
", recipient: " + recipient.getId().serialize() +
", identity: " + identityKey +
", remoteVideo: " + remoteVideoEnabled +
", localVideo: " + localCameraState.isEnabled() +
", isRemoteVideoOffer: " + isRemoteVideoOffer +
", callConnectedTime: " + callConnectedTime +
"]";
} }
public @NonNull List<CallParticipant> getRemoteParticipants() {
return remoteParticipants;
}
} }

View File

@ -58,7 +58,7 @@ class EncryptedCoder {
} }
} }
InputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException { CipherInputStream createEncryptedInputStream(@NonNull byte[] masterKey, @NonNull File file) throws IOException {
try { try {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(masterKey, "HmacSHA256")); mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));

View File

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -152,9 +153,12 @@ public class CreateGroupActivity extends ContactSelectionActivity {
stopwatch.split("registered"); stopwatch.split("registered");
List<Recipient> recipientsAndSelf = new ArrayList<>(resolved);
recipientsAndSelf.add(Recipient.self().resolve());
if (FeatureFlags.groupsV2create()) { if (FeatureFlags.groupsV2create()) {
try { try {
GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(resolved); GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(recipientsAndSelf);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to refresh all recipient capabilities.", e); Log.w(TAG, "Failed to refresh all recipient capabilities.", e);
} }
@ -164,8 +168,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
resolved = Recipient.resolvedList(ids); resolved = Recipient.resolvedList(ids);
if (Stream.of(resolved).anyMatch(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) && boolean gv2 = Stream.of(recipientsAndSelf).allMatch(r -> r.getGroupsV2Capability() == Recipient.Capability.SUPPORTED);
Stream.of(resolved).anyMatch(r -> !r.hasE164())) if (!gv2 && Stream.of(resolved).anyMatch(r -> !r.hasE164()))
{ {
Log.w(TAG, "Invalid GV1 group..."); Log.w(TAG, "Invalid GV1 group...");
ids = Collections.emptyList(); ids = Collections.emptyList();

View File

@ -24,7 +24,9 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl; import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.BottomSheetUtil;
@ -113,6 +115,13 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
}); });
groupJoinButton.setVisibility(View.VISIBLE); groupJoinButton.setVisibility(View.VISIBLE);
break; break;
case UPDATE_LINKED_DEVICE_TO_JOIN:
groupJoinExplain.setText(R.string.GroupJoinUpdateRequiredBottomSheetDialogFragment_update_linked_device_message);
groupCancelButton.setText(android.R.string.ok);
groupJoinButton.setVisibility(View.GONE);
ApplicationDependencies.getJobManager()
.add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
break;
case LOCAL_CAN_JOIN: case LOCAL_CAN_JOIN:
groupJoinExplain.setText(details.joinRequiresAdminApproval() ? R.string.GroupJoinBottomSheetDialogFragment_admin_approval_needed groupJoinExplain.setText(details.joinRequiresAdminApproval() ? R.string.GroupJoinBottomSheetDialogFragment_admin_approval_needed
: R.string.GroupJoinBottomSheetDialogFragment_direct_join); : R.string.GroupJoinBottomSheetDialogFragment_direct_join);
@ -151,19 +160,21 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
); );
} }
private static FeatureFlags.GroupJoinStatus getGroupJoinStatus() { private static ExtendedGroupJoinStatus getGroupJoinStatus() {
FeatureFlags.GroupJoinStatus groupJoinStatus = FeatureFlags.clientLocalGroupJoinStatus(); FeatureFlags.GroupJoinStatus groupJoinStatus = FeatureFlags.clientLocalGroupJoinStatus();
if (groupJoinStatus == FeatureFlags.GroupJoinStatus.LOCAL_CAN_JOIN) { switch (groupJoinStatus) {
if (!FeatureFlags.groupsV2() || Recipient.self().getGroupsV2Capability() == Recipient.Capability.NOT_SUPPORTED) { case COMING_SOON : return ExtendedGroupJoinStatus.COMING_SOON;
// TODO [Alan] GV2 additional copy could be presented in these cases case UPDATE_TO_JOIN: return ExtendedGroupJoinStatus.UPDATE_TO_JOIN;
return FeatureFlags.GroupJoinStatus.UPDATE_TO_JOIN; case LOCAL_CAN_JOIN: {
} if (Recipient.self().getGroupsV2Capability() != Recipient.Capability.SUPPORTED) {
return ExtendedGroupJoinStatus.UPDATE_LINKED_DEVICE_TO_JOIN;
return groupJoinStatus; }
}
return groupJoinStatus; return ExtendedGroupJoinStatus.LOCAL_CAN_JOIN;
}
default: throw new AssertionError();
}
} }
private @NonNull String errorToMessage(@NonNull FetchGroupDetailsError error) { private @NonNull String errorToMessage(@NonNull FetchGroupDetailsError error) {
@ -201,4 +212,18 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
return new ResourceContactPhoto(R.drawable.ic_group_outline_48); return new ResourceContactPhoto(R.drawable.ic_group_outline_48);
} }
} }
public enum ExtendedGroupJoinStatus {
/** No version of the client that can join V2 groups by link is in production. */
COMING_SOON,
/** A newer version of the client is in production that will allow joining via GV2 group links. */
UPDATE_TO_JOIN,
/** Locally we're using a version that can use group links, but one or more linked devices needs updating for GV2. */
UPDATE_LINKED_DEVICE_TO_JOIN,
/** This version of the client allows joining via GV2 group links. */
LOCAL_CAN_JOIN
}
} }

View File

@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.AvatarPreviewActivity; import org.thoughtcrime.securesms.AvatarPreviewActivity;
import org.thoughtcrime.securesms.InviteActivity;
import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.MediaPreviewActivity;
@ -78,7 +79,7 @@ public class ManageGroupFragment extends LoggingFragment {
private TextView pendingAndRequestingCount; private TextView pendingAndRequestingCount;
private Toolbar toolbar; private Toolbar toolbar;
private TextView groupName; private TextView groupName;
private LearnMoreTextView groupV1Indicator; private LearnMoreTextView groupInfoText;
private TextView memberCountUnderAvatar; private TextView memberCountUnderAvatar;
private TextView memberCountAboveList; private TextView memberCountAboveList;
private AvatarImageView avatar; private AvatarImageView avatar;
@ -139,7 +140,7 @@ public class ManageGroupFragment extends LoggingFragment {
avatar = view.findViewById(R.id.group_avatar); avatar = view.findViewById(R.id.group_avatar);
toolbar = view.findViewById(R.id.toolbar); toolbar = view.findViewById(R.id.toolbar);
groupName = view.findViewById(R.id.name); groupName = view.findViewById(R.id.name);
groupV1Indicator = view.findViewById(R.id.manage_group_group_v1_indicator); groupInfoText = view.findViewById(R.id.manage_group_info_text);
memberCountUnderAvatar = view.findViewById(R.id.member_count); memberCountUnderAvatar = view.findViewById(R.id.member_count);
memberCountAboveList = view.findViewById(R.id.member_count_2); memberCountAboveList = view.findViewById(R.id.member_count_2);
groupMemberList = view.findViewById(R.id.group_members); groupMemberList = view.findViewById(R.id.group_members);
@ -176,9 +177,6 @@ public class ManageGroupFragment extends LoggingFragment {
groupLinkRow = view.findViewById(R.id.group_link_row); groupLinkRow = view.findViewById(R.id.group_link_row);
groupLinkButton = view.findViewById(R.id.group_link_button); groupLinkButton = view.findViewById(R.id.group_link_button);
groupV1Indicator.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
groupV1Indicator.setLearnMoreVisible(true);
return view; return view;
} }
@ -249,7 +247,6 @@ public class ManageGroupFragment extends LoggingFragment {
viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::setText); viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::setText);
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText); viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
viewModel.getShowLegacyIndicator().observe(getViewLifecycleOwner(), showLegacyIndicators -> groupV1Indicator.setVisibility(showLegacyIndicators ? View.VISIBLE : View.GONE));
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText); viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> { viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
avatar.setRecipient(groupRecipient); avatar.setRecipient(groupRecipient);
@ -376,6 +373,26 @@ public class ManageGroupFragment extends LoggingFragment {
blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE); blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE);
unblockGroup.setVisibility(canBlock ? View.GONE : View.VISIBLE); unblockGroup.setVisibility(canBlock ? View.GONE : View.VISIBLE);
}); });
viewModel.getGroupInfoMessage().observe(getViewLifecycleOwner(), message -> {
switch (message) {
case LEGACY_GROUP_LEARN_MORE:
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more);
groupInfoText.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
groupInfoText.setLearnMoreVisible(true);
groupInfoText.setVisibility(View.VISIBLE);
break;
case MMS_WARNING:
groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group);
groupInfoText.setOnLinkClickListener(v -> startActivity(new Intent(requireContext(), InviteActivity.class)));
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now);
groupInfoText.setVisibility(View.VISIBLE);
break;
default:
groupInfoText.setVisibility(View.GONE);
break;
}
});
} }
private static int booleanToOnOff(boolean isOn) { private static int booleanToOnOff(boolean isOn) {

View File

@ -80,6 +80,7 @@ public class ManageGroupViewModel extends ViewModel {
private final LiveData<Boolean> showLegacyIndicator; private final LiveData<Boolean> showLegacyIndicator;
private final LiveData<String> mentionSetting; private final LiveData<String> mentionSetting;
private final LiveData<Boolean> groupLinkOn; private final LiveData<Boolean> groupLinkOn;
private final LiveData<GroupInfoMessage> groupInfoMessage;
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) { private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
this.context = context; this.context = context;
@ -123,6 +124,16 @@ public class ManageGroupViewModel extends ViewModel {
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient, this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting()))); recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled); this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
this.groupInfoMessage = Transformations.map(this.showLegacyIndicator,
showLegacyInfo -> {
if (showLegacyInfo) {
return GroupInfoMessage.LEGACY_GROUP_LEARN_MORE;
} else if (groupId.isMms()) {
return GroupInfoMessage.MMS_WARNING;
} else {
return GroupInfoMessage.NONE;
}
});
} }
@WorkerThread @WorkerThread
@ -152,10 +163,6 @@ public class ManageGroupViewModel extends ViewModel {
return fullMemberCountSummary; return fullMemberCountSummary;
} }
LiveData<Boolean> getShowLegacyIndicator() {
return showLegacyIndicator;
}
LiveData<Recipient> getGroupRecipient() { LiveData<Recipient> getGroupRecipient() {
return groupRecipient; return groupRecipient;
} }
@ -228,6 +235,10 @@ public class ManageGroupViewModel extends ViewModel {
return groupLinkOn; return groupLinkOn;
} }
LiveData<GroupInfoMessage> getGroupInfoMessage() {
return groupInfoMessage;
}
void handleExpirationSelection() { void handleExpirationSelection() {
manageGroupRepository.getRecipient(groupRecipient -> manageGroupRepository.getRecipient(groupRecipient ->
ExpirationDialog.show(context, ExpirationDialog.show(context,
@ -397,6 +408,12 @@ public class ManageGroupViewModel extends ViewModel {
} }
} }
enum GroupInfoMessage {
NONE,
LEGACY_GROUP_LEARN_MORE,
MMS_WARNING
}
private enum CollapseState { private enum CollapseState {
OPEN, OPEN,
COLLAPSED COLLAPSED

View File

@ -11,7 +11,6 @@ import android.content.SharedPreferences;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@ -36,7 +35,15 @@ public class JobSchedulerScheduler implements Scheduler {
@RequiresApi(26) @RequiresApi(26)
@Override @Override
public void schedule(long delay, @NonNull List<Constraint> constraints) { public void schedule(long delay, @NonNull List<Constraint> constraints) {
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getNextId(), new ComponentName(application, SystemService.class)) JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
int currentId = getCurrentId();
if (constraints.isEmpty() && jobScheduler.getPendingJob(currentId) != null) {
Log.d(TAG, "Skipping JobScheduler enqueue because we have no constraints and there's already one pending.");
return;
}
JobInfo.Builder jobInfoBuilder = new JobInfo.Builder(getAndUpdateNextId(), new ComponentName(application, SystemService.class))
.setMinimumLatency(delay) .setMinimumLatency(delay)
.setPersisted(true); .setPersisted(true);
@ -44,12 +51,15 @@ public class JobSchedulerScheduler implements Scheduler {
constraint.applyToJobInfo(jobInfoBuilder); constraint.applyToJobInfo(jobInfoBuilder);
} }
Log.i(TAG, "Scheduling a run in " + delay + " ms.");
JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
jobScheduler.schedule(jobInfoBuilder.build()); jobScheduler.schedule(jobInfoBuilder.build());
} }
private int getNextId() { private int getCurrentId() {
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return prefs.getInt(PREF_NEXT_ID, 0);
}
private int getAndUpdateNextId() {
SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); SharedPreferences prefs = application.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int returnedId = prefs.getInt(PREF_NEXT_ID, 0); int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1; int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
@ -64,8 +74,6 @@ public class JobSchedulerScheduler implements Scheduler {
@Override @Override
public boolean onStartJob(JobParameters params) { public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob()");
JobManager jobManager = ApplicationDependencies.getJobManager(); JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() { jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
@ -73,7 +81,6 @@ public class JobSchedulerScheduler implements Scheduler {
public void onQueueEmpty() { public void onQueueEmpty() {
jobManager.removeOnEmptyQueueListener(this); jobManager.removeOnEmptyQueueListener(this);
jobFinished(params, false); jobFinished(params, false);
Log.d(TAG, "jobFinished()");
} }
}); });
@ -84,7 +91,6 @@ public class JobSchedulerScheduler implements Scheduler {
@Override @Override
public boolean onStopJob(JobParameters params) { public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob()");
return false; return false;
} }
} }

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.jobmanager.impl;
import android.app.job.JobInfo;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.jobmanager.Constraint;
/**
* Job constraint for determining whether or not the device is actively charging.
*/
public class ChargingConstraint implements Constraint {
public static final String KEY = "ChargingConstraint";
private ChargingConstraint() {
}
@Override
public boolean isMet() {
return ChargingConstraintObserver.isCharging();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@RequiresApi(26)
@Override
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
jobInfoBuilder.setRequiresCharging(true);
}
public static final class Factory implements Constraint.Factory<ChargingConstraint> {
@Override
public ChargingConstraint create() {
return new ChargingConstraint();
}
}
}

View File

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.jobmanager.impl;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
/**
* Observes the charging state of the device and notifies the JobManager system when appropriate.
*/
public class ChargingConstraintObserver implements ConstraintObserver {
private static final String REASON = ChargingConstraintObserver.class.getSimpleName();
private static final int STATUS_BATTERY = 0;
private final Application application;
private static volatile boolean charging;
public ChargingConstraintObserver(@NonNull Application application) {
this.application = application;
}
@Override
public void register(@NonNull Notifier notifier) {
Intent intent = application.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean wasCharging = charging;
charging = isCharging(intent);
if (charging && !wasCharging) {
notifier.onConstraintMet(REASON);
}
}
}, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
charging = isCharging(intent);
}
public static boolean isCharging() {
return charging;
}
private static boolean isCharging(@Nullable Intent intent) {
if (intent == null) {
return false;
}
int status = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, STATUS_BATTERY);
return status != STATUS_BATTERY;
}
}

View File

@ -153,8 +153,8 @@ public final class AttachmentCompressionJob extends BaseJob {
if (MediaUtil.isJpeg(attachment)) { if (MediaUtil.isJpeg(attachment)) {
MediaStream stripped = getResizedMedia(context, attachment, constraints); MediaStream stripped = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, stripped, false); attachmentDatabase.updateAttachmentData(attachment, stripped, false);
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} }
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
} else if (constraints.canResize(attachment)) { } else if (constraints.canResize(attachment)) {
MediaStream resized = getResizedMedia(context, attachment, constraints); MediaStream resized = getResizedMedia(context, attachment, constraints);
attachmentDatabase.updateAttachmentData(attachment, resized, false); attachmentDatabase.updateAttachmentData(attachment, resized, false);
@ -249,7 +249,7 @@ public final class AttachmentCompressionJob extends BaseJob {
try { try {
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context, BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context,
new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), new DecryptableStreamUriLoader.DecryptableUri(attachment.getUri()),
constraints); constraints);
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()), return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()),

View File

@ -1,14 +1,11 @@
package org.thoughtcrime.securesms.jobs; package org.thoughtcrime.securesms.jobs;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.media.MediaDataSource;
import android.media.MediaMetadataRetriever;
import android.os.Build; import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@ -28,8 +25,6 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.service.GenericForegroundService; import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController; import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@ -40,6 +35,7 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -157,8 +153,8 @@ public final class AttachmentUploadJob extends BaseJob {
private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException { private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException {
try { try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!"); if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri()); InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder() SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
.withStream(is) .withStream(is)
.withContentType(attachment.getContentType()) .withContentType(attachment.getContentType())
@ -193,43 +189,34 @@ public final class AttachmentUploadJob extends BaseJob {
private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException { private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException {
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash(); if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
if (attachment.getDataUri() == null) return null; if (attachment.getUri() == null) return null;
return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getDataUri())); return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getUri()));
} }
private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException { private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException {
if (attachment.getThumbnailUri() != null) { if (attachment.getBlurHash() != null) {
return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getThumbnailUri())); return attachment.getBlurHash().getHash();
} }
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
if (Build.VERSION.SDK_INT < 23) { if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported..."); Log.w(TAG, "Video thumbnails not supported...");
return null; return null;
} }
try (MediaDataSource dataSource = DatabaseFactory.getAttachmentDatabase(context).mediaDataSourceFor(attachmentId)) { Bitmap bitmap = MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.getUri()), 1000);
if (dataSource == null) return null;
MediaMetadataRetriever retriever = new MediaMetadataRetriever(); if (bitmap != null) {
MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource); Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false);
bitmap.recycle();
Bitmap bitmap = retriever.getFrameAtTime(1000); Log.i(TAG, "Generated video thumbnail...");
String hash = BlurHashEncoder.encode(thumb);
thumb.recycle();
if (bitmap != null) { return hash;
Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false); } else {
bitmap.recycle(); return null;
Log.i(TAG, "Generated video thumbnail...");
String hash = BlurHashEncoder.encode(thumb);
thumb.recycle();
return hash;
} else {
return null;
}
} }
} }

View File

@ -10,6 +10,8 @@ import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobMigration; import org.thoughtcrime.securesms.jobmanager.JobMigration;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMi
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration; import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob; import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
@ -124,6 +127,7 @@ public final class JobManagerFactories {
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
// Migrations // Migrations
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory()); put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory()); put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
@ -158,6 +162,7 @@ public final class JobManagerFactories {
public static Map<String, Constraint.Factory> getConstraintFactories(@NonNull Application application) { public static Map<String, Constraint.Factory> getConstraintFactories(@NonNull Application application) {
return new HashMap<String, Constraint.Factory>() {{ return new HashMap<String, Constraint.Factory>() {{
put(ChargingConstraint.KEY, new ChargingConstraint.Factory());
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application)); put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application));
@ -168,6 +173,7 @@ public final class JobManagerFactories {
public static List<ConstraintObserver> getConstraintObservers(@NonNull Application application) { public static List<ConstraintObserver> getConstraintObservers(@NonNull Application application) {
return Arrays.asList(CellServiceConstraintObserver.getInstance(application), return Arrays.asList(CellServiceConstraintObserver.getInstance(application),
new ChargingConstraintObserver(application),
new NetworkConstraintObserver(application), new NetworkConstraintObserver(application),
new SqlCipherMigrationConstraintObserver(), new SqlCipherMigrationConstraintObserver(),
new WebsocketDrainedConstraintObserver()); new WebsocketDrainedConstraintObserver());

View File

@ -134,7 +134,7 @@ public class LeaveGroupJob extends BaseJob {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddresses(context, destinations); List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddresses(context, destinations);
List<SignalServiceAddress> memberAddresses = RecipientUtil.toSignalServiceAddresses(context, members); List<SignalServiceAddress> memberAddresses = RecipientUtil.toSignalServiceAddresses(context, members);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(Recipient::resolved).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList(); List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, Stream.of(destinations).map(Recipient::resolved).toList());
SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId.getDecodedId(), name, memberAddresses, null); SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId.getDecodedId(), name, memberAddresses, null);
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())

View File

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
@ -27,18 +28,30 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
public class LocalBackupJob extends BaseJob { public final class LocalBackupJob extends BaseJob {
public static final String KEY = "LocalBackupJob"; public static final String KEY = "LocalBackupJob";
private static final String TAG = LocalBackupJob.class.getSimpleName(); private static final String TAG = Log.tag(LocalBackupJob.class);
public LocalBackupJob() { public static final String TEMP_BACKUP_FILE_PREFIX = ".backup";
this(new Job.Parameters.Builder() public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp";
.setQueue("__LOCAL_BACKUP__")
.setMaxInstances(1) public LocalBackupJob(boolean forceNow) {
.setMaxAttempts(3) this(buildParameters(forceNow));
.build()); }
private static @NonNull Job.Parameters buildParameters(boolean forceNow) {
Job.Parameters.Builder builder = new Job.Parameters.Builder()
.setQueue("__LOCAL_BACKUP__")
.setMaxInstances(1)
.setMaxAttempts(3);
if (!forceNow) {
builder.addConstraint(ChargingConstraint.KEY);
}
return builder.build();
} }
private LocalBackupJob(@NonNull Job.Parameters parameters) { private LocalBackupJob(@NonNull Job.Parameters parameters) {
@ -76,6 +89,8 @@ public class LocalBackupJob extends BaseJob {
String fileName = String.format("signal-%s.backup", timestamp); String fileName = String.format("signal-%s.backup", timestamp);
File backupFile = new File(backupDirectory, fileName); File backupFile = new File(backupDirectory, fileName);
deleteOldTemporaryBackups(backupDirectory);
if (backupFile.exists()) { if (backupFile.exists()) {
throw new IOException("Backup file already exists?"); throw new IOException("Backup file already exists?");
} }
@ -84,7 +99,7 @@ public class LocalBackupJob extends BaseJob {
throw new IOException("Backup password is null"); throw new IOException("Backup password is null");
} }
File tempFile = File.createTempFile("backup", "tmp", StorageUtil.getBackupCacheDirectory(context)); File tempFile = File.createTempFile(TEMP_BACKUP_FILE_PREFIX, TEMP_BACKUP_FILE_SUFFIX, backupDirectory);
try { try {
FullBackupExporter.export(context, FullBackupExporter.export(context,
@ -111,6 +126,21 @@ public class LocalBackupJob extends BaseJob {
} }
} }
private static void deleteOldTemporaryBackups(@NonNull File backupDirectory) {
for (File file : backupDirectory.listFiles()) {
if (file.isFile()) {
String name = file.getName();
if (name.startsWith(TEMP_BACKUP_FILE_PREFIX) && name.endsWith(TEMP_BACKUP_FILE_SUFFIX)) {
if (file.delete()) {
Log.w(TAG, "Deleted old temporary backup file");
} else {
Log.w(TAG, "Could not delete old temporary backup file");
}
}
}
}
}
@Override @Override
public boolean onShouldRetry(@NonNull Exception e) { public boolean onShouldRetry(@NonNull Exception e) {
return false; return false;

View File

@ -13,6 +13,9 @@ import com.google.android.mms.pdu_alt.RetrieveConf;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.VCardUtil;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MessageDatabase;
@ -33,6 +36,7 @@ import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -189,6 +193,7 @@ public class MmsDownloadJob extends BaseJob {
Set<RecipientId> members = new HashSet<>(); Set<RecipientId> members = new HashSet<>();
String body = null; String body = null;
List<Attachment> attachments = new LinkedList<>(); List<Attachment> attachments = new LinkedList<>();
List<Contact> sharedContacts = new LinkedList<>();
RecipientId from = null; RecipientId from = null;
@ -223,14 +228,18 @@ public class MmsDownloadJob extends BaseJob {
PduPart part = media.getPart(i); PduPart part = media.getPart(i);
if (part.getData() != null) { if (part.getData() != null) {
Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory(); if (Util.toIsoString(part.getContentType()).toLowerCase().equals(MediaUtil.VCARD)){
String name = null; sharedContacts.addAll(VCardUtil.parseContacts(new String(part.getData())));
} else {
Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
String name = null;
if (part.getName() != null) name = Util.toIsoString(part.getName()); if (part.getName() != null) name = Util.toIsoString(part.getName());
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
AttachmentDatabase.TRANSFER_PROGRESS_DONE, AttachmentDatabase.TRANSFER_PROGRESS_DONE,
part.getData().length, name, false, false, false, null, null, null, null, null)); part.getData().length, name, false, false, false, null, null, null, null, null));
}
} }
} }
} }
@ -240,7 +249,7 @@ public class MmsDownloadJob extends BaseJob {
group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients)); group = Optional.of(DatabaseFactory.getGroupDatabase(context).getOrCreateMmsGroupForMembers(recipients));
} }
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false); IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, -1, attachments, subscriptionId, 0, false, false, false, Optional.of(sharedContacts));
Optional<InsertResult> insertResult = database.insertMessageInbox(message, contentLocation, threadId); Optional<InsertResult> insertResult = database.insertMessageInbox(message, contentLocation, threadId);
if (insertResult.isPresent()) { if (insertResult.isPresent()) {

Some files were not shown because too many files have changed in this diff Show More