Copione merged onto master
commit
1dbdf06ca4
|
@ -25,3 +25,4 @@ obj/
|
|||
jni/libspeex/.deps/
|
||||
*.sh
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
|
|
|
@ -80,8 +80,8 @@ protobuf {
|
|||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 708
|
||||
def canonicalVersionName = "4.71.5"
|
||||
def canonicalVersionCode = 709
|
||||
def canonicalVersionName = "4.72.0"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
|
@ -100,6 +100,15 @@ android {
|
|||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
staging {
|
||||
storeFile file("${project.rootDir}/dev.keystore")
|
||||
storePassword 'android'
|
||||
keyAlias 'staging'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
@ -195,6 +204,8 @@ android {
|
|||
}
|
||||
staging {
|
||||
initWith debug
|
||||
applicationIdSuffix ".staging"
|
||||
signingConfig signingConfigs.staging
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.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:ringrtc-android:2.5.1'
|
||||
implementation 'org.signal:ringrtc-android:2.7.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
|
|
@ -5,5 +5,12 @@
|
|||
|
||||
<application
|
||||
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>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<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:protectionLevel="signature" />
|
||||
|
||||
|
@ -113,7 +113,7 @@
|
|||
<meta-data android:name="google_analytics_adid_collection_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:excludeFromRecents="true"
|
||||
android:screenOrientation="portrait"
|
||||
|
@ -127,25 +127,25 @@
|
|||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:noHistory="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".InviteActivity"
|
||||
android:theme="@style/Signal.Light.NoActionBar.Invite"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
android:label="Configure MMS Settings"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DeviceProvisioningActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -155,7 +155,7 @@
|
|||
</activity>
|
||||
|
||||
<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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
|
@ -164,7 +164,7 @@
|
|||
android:taskAffinity=""
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
@ -195,7 +195,7 @@
|
|||
android:launchMode="singleTask"
|
||||
android:noHistory="true"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -242,7 +242,7 @@
|
|||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
|
@ -257,16 +257,16 @@
|
|||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
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"
|
||||
android:label="@string/AndroidManifest__message_details"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
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"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
|
@ -275,64 +275,64 @@
|
|||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
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"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
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"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseCreateActivity"
|
||||
android:label="@string/AndroidManifest__create_passphrase"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphrasePromptActivity"
|
||||
android:launchMode="singleTask"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PushContactSelectionActivity"
|
||||
android:label="@string/AndroidManifest__select_contacts"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
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"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
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"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ApplicationPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
|
||||
|
@ -343,45 +343,45 @@
|
|||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
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"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
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"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DeviceActivity"
|
||||
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"
|
||||
android:label="@string/AndroidManifest__log_submit"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MediaPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".AvatarPreviewActivity"
|
||||
android:label="@string/AndroidManifest__media_preview"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
|
@ -396,7 +396,7 @@
|
|||
|
||||
<activity android:name=".PlayServicesProblemActivity"
|
||||
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">
|
||||
<intent-filter>
|
||||
|
@ -420,7 +420,7 @@
|
|||
android:excludeFromRecents="true"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode">
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
@ -432,15 +432,15 @@
|
|||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
|
@ -449,16 +449,16 @@
|
|||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
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"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ClearProfileAvatarActivity"
|
||||
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:label="@string/AndroidManifest_remove_photo">
|
||||
|
||||
|
@ -474,39 +474,39 @@
|
|||
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="true"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity
|
||||
android:name=".maps.PlacePickerActivity"
|
||||
android:label="@string/PlacePickerActivity_title"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
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"
|
||||
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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
@ -521,14 +521,14 @@
|
|||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<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"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
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:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
|
@ -650,15 +650,15 @@
|
|||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="org.thoughtcrime.provider.securesms" />
|
||||
android:authorities="${applicationId}.part" />
|
||||
|
||||
<provider android:name=".providers.MmsBodyProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="org.thoughtcrime.provider.securesms.mms" />
|
||||
android:authorities="${applicationId}.mms" />
|
||||
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="org.thoughtcrime.securesms.fileprovider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
|
@ -667,23 +667,23 @@
|
|||
</provider>
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
||||
android:authorities="org.thoughtcrime.securesms.database.conversation"
|
||||
android:authorities="${applicationId}.database.conversation"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$ConversationList"
|
||||
android:authorities="org.thoughtcrime.securesms.database.conversationlist"
|
||||
android:authorities="${applicationId}.database.conversationlist"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
||||
android:authorities="org.thoughtcrime.securesms.database.attachment"
|
||||
android:authorities="${applicationId}.database.attachment"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||
android:authorities="org.thoughtcrime.securesms.database.sticker"
|
||||
android:authorities="${applicationId}.database.sticker"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
|
||||
android:authorities="${applicationId}.database.stickerpack"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
|
||||
public final class AppCapabilities {
|
||||
|
||||
|
@ -9,12 +8,13 @@ public final class AppCapabilities {
|
|||
}
|
||||
|
||||
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
|
||||
* asking if the user has set a Signal PIN or not.
|
||||
*/
|
||||
public static SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
|
||||
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,7 +137,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
|
@ -155,6 +154,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
Log.i(TAG, "App is now visible.");
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
ApplicationDependencies.getRecipientCache().warmUp();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -10,12 +9,7 @@ import android.graphics.drawable.Drawable;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.DisplayCutout;
|
||||
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 androidx.annotation.NonNull;
|
||||
|
@ -40,6 +34,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
|
||||
/**
|
||||
* Activity for displaying avatars full screen.
|
||||
|
@ -81,17 +76,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
|||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
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);
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
|
||||
|
@ -140,47 +125,13 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
|||
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) {
|
||||
window.getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
|
||||
boolean hide = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
for (View view : views) {
|
||||
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 );
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -188,36 +139,4 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
|||
onBackPressed();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.view.View;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
|
@ -22,7 +23,8 @@ import java.util.Locale;
|
|||
import java.util.Set;
|
||||
|
||||
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> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
|
|
|
@ -31,8 +31,6 @@ import android.view.MenuInflater;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
@ -69,6 +67,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.sharing.ShareActivity;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
|
||||
|
@ -119,6 +118,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
private boolean cameFromAllMedia;
|
||||
private boolean showThread;
|
||||
private MediaDatabase.Sorting sorting;
|
||||
private FullscreenHelper fullscreenHelper;
|
||||
|
||||
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.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.setDataAndType(attachment.getDataUri(), mediaRecord.getContentType());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
@ -147,10 +147,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
showSystemUI();
|
||||
fullscreenHelper = new FullscreenHelper(this);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
|
@ -273,9 +270,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
anchorMarginsToBottomInsets(detailsContainer);
|
||||
|
||||
anchorMarginsToTopInsets(toolbarLayout);
|
||||
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
|
||||
|
||||
showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
|
@ -546,7 +543,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
@Override
|
||||
public boolean singleTapOnMedia() {
|
||||
toggleUiVisibility();
|
||||
fullscreenHelper.toggleUiVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -556,32 +553,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
|
@ -801,7 +745,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
return new MediaItem(Recipient.live(recipientId).get(),
|
||||
Recipient.live(threadRecipientId).get(),
|
||||
attachment,
|
||||
Objects.requireNonNull(attachment.getDataUri()),
|
||||
Objects.requireNonNull(attachment.getUri()),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.isOutgoing());
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -65,16 +66,17 @@ public class NewConversationActivity extends ContactSelectionActivity
|
|||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
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);
|
||||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered. Doing a directory refresh.");
|
||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
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.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.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
|
|
@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
|
|||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
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.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
||||
|
|
|
@ -19,8 +19,6 @@ package org.thoughtcrime.securesms;
|
|||
|
||||
import android.Manifest;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
|
@ -41,9 +39,11 @@ import org.greenrobot.eventbus.EventBus;
|
|||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
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.WebRtcCallView;
|
||||
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.events.WebRtcViewModel;
|
||||
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.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
|
@ -85,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.webrtc_call_activity);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().hide();
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
|
@ -132,11 +134,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
super.onStop();
|
||||
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfiguration) {
|
||||
super.onConfigurationChanged(newConfiguration);
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
Intent intent = new Intent(this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -162,7 +166,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
}
|
||||
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (isSystemPipEnabledAndAvailable()) {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
|
@ -196,21 +200,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
}
|
||||
|
||||
private void initializeResources() {
|
||||
callScreen = ViewUtil.findById(this, R.id.callScreen);
|
||||
callScreen = findViewById(R.id.callScreen);
|
||||
callScreen.setControlsListener(new ControlsListener());
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
|
||||
viewModel.setIsInPipMode(isInPipMode());
|
||||
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
|
||||
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.getEvents().observe(this, this::handleViewModelEvent);
|
||||
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) {
|
||||
|
@ -375,19 +376,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
}
|
||||
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
private void handleOutgoingCall() {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||
}
|
||||
|
||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||
Log.i(TAG, "handleTerminate called: " + hangupType.name());
|
||||
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatusFromHangupType(hangupType);
|
||||
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
|
@ -398,62 +393,47 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleCallRinging(@NonNull WebRtcViewModel event) {
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
private void handleCallRinging() {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||
}
|
||||
|
||||
private void handleCallBusy(@NonNull WebRtcViewModel event) {
|
||||
private void handleCallBusy() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
private void handleCallConnected() {
|
||||
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);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleServerFailure(@NonNull WebRtcViewModel event) {
|
||||
private void handleServerFailure() {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
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
|
||||
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
|
||||
dialog.setTitle(R.string.RedPhone_number_not_registered);
|
||||
dialog.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
dialog.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice);
|
||||
dialog.setCancelable(true);
|
||||
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
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();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
|
||||
final IdentityKey theirKey = event.getIdentityKey();
|
||||
final Recipient recipient = event.getRecipient();
|
||||
final IdentityKey theirKey = event.getRemoteParticipants().get(0).getIdentityKey();
|
||||
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
|
||||
|
||||
if (theirKey == null) {
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
|
@ -493,32 +473,29 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
}
|
||||
|
||||
@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);
|
||||
|
||||
viewModel.setRecipient(event.getRecipient());
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(event); break;
|
||||
case CALL_RINGING: handleCallRinging(event); break;
|
||||
case CALL_CONNECTED: handleCallConnected(); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||
case CALL_RINGING: handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); 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_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
|
||||
case CALL_INCOMING: handleIncomingCall(event); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(); break;
|
||||
case CALL_BUSY: handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
callScreen.setLocalRenderer(event.getLocalRenderer());
|
||||
callScreen.setRemoteRenderer(event.getRemoteRenderer());
|
||||
|
||||
boolean enableVideo = event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
|
||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||
|
||||
|
@ -530,6 +507,24 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
|
||||
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
|
||||
public void onControlsFadeOut() {
|
||||
if (videoTooltip != null) {
|
||||
|
@ -594,8 +589,13 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onDownCaretPressed() {
|
||||
public void onShowParticipantsList() {
|
||||
CallParticipantsListDialog.show(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -106,10 +106,7 @@ public abstract class Attachment {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public abstract Uri getDataUri();
|
||||
|
||||
@Nullable
|
||||
public abstract Uri getThumbnailUri();
|
||||
public abstract Uri getUri();
|
||||
|
||||
public int getTransferState() {
|
||||
return transferState;
|
||||
|
|
|
@ -57,7 +57,7 @@ public class DatabaseAttachment extends Attachment {
|
|||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
if (hasData) {
|
||||
return PartAuthority.getAttachmentDataUri(attachmentId);
|
||||
} 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() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
|
|
@ -15,13 +15,7 @@ public class MmsNotificationAttachment extends Attachment {
|
|||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getDataUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getThumbnailUri() {
|
||||
public Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -42,17 +42,10 @@ public class PointerAttachment extends Attachment {
|
|||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getThumbnailUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
|
||||
List<Attachment> results = new LinkedList<>();
|
||||
|
||||
|
|
|
@ -20,12 +20,7 @@ public class TombstoneAttachment extends Attachment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getDataUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getThumbnailUri() {
|
||||
public @Nullable Uri getUri() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|||
public class UriAttachment extends Attachment {
|
||||
|
||||
private final @NonNull Uri dataUri;
|
||||
private final @Nullable Uri thumbnailUri;
|
||||
|
||||
public UriAttachment(@NonNull Uri uri,
|
||||
@NonNull String contentType,
|
||||
|
@ -29,11 +28,10 @@ public class UriAttachment extends Attachment {
|
|||
@Nullable AudioHash audioHash,
|
||||
@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,
|
||||
@Nullable Uri thumbnailUri,
|
||||
@NonNull String contentType,
|
||||
int transferState,
|
||||
long size,
|
||||
|
@ -51,22 +49,15 @@ public class UriAttachment extends Attachment {
|
|||
@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);
|
||||
this.dataUri = dataUri;
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
this.dataUri = dataUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Uri getDataUri() {
|
||||
public Uri getUri() {
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return thumbnailUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
|
||||
|
|
|
@ -138,13 +138,11 @@ public class FullBackupImporter extends FullBackupBase {
|
|||
inputStream.readAttachmentTo(output.second, attachment.getLength());
|
||||
|
||||
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
|
||||
contentValues.put(AttachmentDatabase.THUMBNAIL, (String)null);
|
||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
|
||||
} catch (BadMacException e) {
|
||||
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
|
||||
dataFile.delete();
|
||||
contentValues.put(AttachmentDatabase.DATA, (String) null);
|
||||
contentValues.put(AttachmentDatabase.THUMBNAIL, (String) null);
|
||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, (String) null);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ public class BorderlessImageView extends FrameLayout {
|
|||
}
|
||||
|
||||
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
boolean showControls = slide.asAttachment().getDataUri() == null;
|
||||
boolean showControls = slide.asAttachment().getUri() == null;
|
||||
|
||||
if (slide.hasSticker()) {
|
||||
image.setFit(new CenterInside());
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -242,14 +242,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
if (!viewOnceSlides.isEmpty()) {
|
||||
thumbnailView.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);
|
||||
attachmentContainerView.setVisibility(GONE);
|
||||
dismissView.setBackgroundResource(R.drawable.dismiss_background);
|
||||
if (imageVideoSlides.get(0).hasVideo()) {
|
||||
attachmentVideoOverlayView.setVisibility(VISIBLE);
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getUri()))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
|
|
|
@ -119,7 +119,7 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
|
|||
this.activeRecipients.clear();
|
||||
|
||||
presentContact(contact);
|
||||
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getDataUri() : null);
|
||||
presentAvatar(contact.getAvatarAttachment() != null ? contact.getAvatarAttachment().getUri() : null);
|
||||
presentActionButtons(ContactUtil.getRecipients(getContext(), contact));
|
||||
|
||||
for (LiveRecipient recipient : activeRecipients.values()) {
|
||||
|
|
|
@ -279,7 +279,7 @@ public class ThumbnailView extends FrameLayout {
|
|||
getTransferControls().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
|
||||
if (slide.getUri() != null && slide.hasPlayOverlay() &&
|
||||
(slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
{
|
||||
this.playOverlay.setVisibility(View.VISIBLE);
|
||||
|
@ -288,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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()))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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: " +
|
||||
slide.asAttachment().getFastPreflightId());
|
||||
|
||||
|
@ -327,7 +327,7 @@ public class ThumbnailView extends FrameLayout {
|
|||
blurhash.setImageDrawable(null);
|
||||
}
|
||||
|
||||
if (slide.getThumbnailUri() != null) {
|
||||
if (slide.getUri() != null) {
|
||||
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
|
||||
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
|
||||
thumbnailFuture.deferTo(result);
|
||||
|
@ -412,7 +412,7 @@ public class ThumbnailView extends FrameLayout {
|
|||
}
|
||||
|
||||
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)
|
||||
.transition(withCrossFade()), fit);
|
||||
|
||||
|
@ -469,10 +469,10 @@ public class ThumbnailView extends FrameLayout {
|
|||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (thumbnailClickListener != null &&
|
||||
slide != null &&
|
||||
slide.asAttachment().getDataUri() != null &&
|
||||
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
|
||||
if (thumbnailClickListener != null &&
|
||||
slide != null &&
|
||||
slide.asAttachment().getUri() != null &&
|
||||
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
|
||||
{
|
||||
thumbnailClickListener.onClick(view, slide);
|
||||
} else if (parentClickListener != null) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.view.VelocityTracker;
|
|||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -16,21 +17,26 @@ import androidx.core.view.GestureDetectorCompat;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
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 View child;
|
||||
private final int framePadding;
|
||||
private final int pipWidth;
|
||||
private final int pipHeight;
|
||||
private final ViewGroup parent;
|
||||
private final View child;
|
||||
private final int framePadding;
|
||||
private final Queue<Runnable> runAfterFling;
|
||||
|
||||
private int pipWidth;
|
||||
private int pipHeight;
|
||||
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
|
||||
private float lastTouchX;
|
||||
private float lastTouchY;
|
||||
|
@ -42,6 +48,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
private double projectionY;
|
||||
private VelocityTracker velocityTracker;
|
||||
private int maximumFlingVelocity;
|
||||
private boolean isLockedToBottomEnd;
|
||||
private Interpolator interpolator;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
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.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
|
||||
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
|
||||
this.runAfterFling = new LinkedList<>();
|
||||
this.interpolator = ADJUST_INTERPOLATOR;
|
||||
}
|
||||
|
||||
public void clearVerticalBoundaries() {
|
||||
|
@ -105,11 +115,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
|
||||
if (isAnimating) {
|
||||
fling();
|
||||
} else if (!isDragging) {
|
||||
onFling(null, null, 0, 0);
|
||||
}
|
||||
adjustPip();
|
||||
}
|
||||
|
||||
private boolean onGestureFinished(MotionEvent e) {
|
||||
|
@ -123,12 +129,46 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
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
|
||||
public boolean onDown(MotionEvent e) {
|
||||
activePointerId = e.getPointerId(0);
|
||||
lastTouchX = e.getX(activePointerId) + child.getX();
|
||||
lastTouchY = e.getY(activePointerId) + child.getY();
|
||||
isDragging = true;
|
||||
pipWidth = child.getMeasuredWidth();
|
||||
pipHeight = child.getMeasuredHeight();
|
||||
interpolator = FLING_INTERPOLATOR;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -167,6 +207,13 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
child.performClick();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void fling() {
|
||||
Point projection = new Point((int) projectionX, (int) projectionY);
|
||||
Point nearestCornerPosition = findNearestCornerPosition(projection);
|
||||
|
@ -178,17 +225,30 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
.translationX(getTranslationXForPoint(nearestCornerPosition))
|
||||
.translationY(getTranslationYForPoint(nearestCornerPosition))
|
||||
.setDuration(250)
|
||||
.setInterpolator(new ViscousFluidInterpolator())
|
||||
.setInterpolator(interpolator)
|
||||
.setListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
isAnimating = false;
|
||||
|
||||
Iterator<Runnable> afterFlingRunnables = runAfterFling.iterator();
|
||||
while (afterFlingRunnables.hasNext()) {
|
||||
Runnable runnable = afterFlingRunnables.next();
|
||||
|
||||
runnable.run();
|
||||
afterFlingRunnables.remove();
|
||||
}
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
private Point findNearestCornerPosition(Point projection) {
|
||||
if (isLockedToBottomEnd) {
|
||||
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
|
||||
: calculateBottomLeftCoordinates(parent);
|
||||
}
|
||||
|
||||
Point maxPoint = null;
|
||||
double maxDistance = Double.MAX_VALUE;
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
private boolean enableFixedSize;
|
||||
private int surfaceWidth;
|
||||
private int surfaceHeight;
|
||||
private boolean isInitialized;
|
||||
private BroadcastVideoSink attachedVideoSink;
|
||||
|
||||
public TextureViewRenderer(@NonNull Context context) {
|
||||
super(context);
|
||||
|
@ -49,8 +51,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
this.setSurfaceTextureListener(this);
|
||||
}
|
||||
|
||||
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents) {
|
||||
this.init(sharedContext, rendererEvents, EglBase.CONFIG_PLAIN, new GlRectDrawer());
|
||||
public void init(@NonNull EglBase eglBase) {
|
||||
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) {
|
||||
|
@ -63,6 +69,30 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
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() {
|
||||
eglRenderer.release();
|
||||
}
|
||||
|
@ -125,6 +155,9 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
|||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
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);
|
||||
|
||||
setMeasuredDimension(size.x, size.y);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
@ -13,67 +15,85 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.constraintlayout.widget.Guideline;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.AutoTransition;
|
||||
import androidx.transition.ChangeBounds;
|
||||
import androidx.transition.Transition;
|
||||
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.resource.bitmap.CenterCrop;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
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.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
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.ViewUtil;
|
||||
import org.webrtc.RendererCommon;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
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 FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||
private static final long TRANSITION_DURATION_MILLIS = 250;
|
||||
private static final int SMALL_ONGOING_CALL_BUTTON_MARGIN_DP = 8;
|
||||
private static final int LARGE_ONGOING_CALL_BUTTON_MARGIN_DP = 16;
|
||||
|
||||
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 AccessibleToggleButton videoToggle;
|
||||
private AccessibleToggleButton micToggle;
|
||||
private ViewGroup largeLocalRenderContainer;
|
||||
private ViewGroup localRenderPipFrame;
|
||||
private ViewGroup smallLocalRenderContainer;
|
||||
private ViewGroup remoteRenderContainer;
|
||||
private ViewGroup smallLocalRenderFrame;
|
||||
private TextureViewRenderer smallLocalRender;
|
||||
private View largeLocalRenderFrame;
|
||||
private TextureViewRenderer largeLocalRender;
|
||||
private View largeLocalRenderNoVideo;
|
||||
private ImageView largeLocalRenderNoVideoAvatar;
|
||||
private TextView recipientName;
|
||||
private TextView status;
|
||||
private ConstraintLayout parent;
|
||||
private AvatarImageView avatar;
|
||||
private ImageView avatarCard;
|
||||
private ConstraintLayout participantsParent;
|
||||
private ControlsListener controlsListener;
|
||||
private RecipientId recipientId;
|
||||
private CameraState.Direction cameraDirection;
|
||||
private ImageView answer;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView hangup;
|
||||
private View answerWithAudio;
|
||||
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> topViews = new HashSet<>();
|
||||
|
@ -82,7 +102,8 @@ public class WebRtcCallView extends FrameLayout {
|
|||
|
||||
private WebRtcControls controls = WebRtcControls.NONE;
|
||||
private final Runnable fadeOutRunnable = () -> {
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls(); };
|
||||
if (isAttachedToWindow() && controls.isFadeOutEnabled()) fadeOutControls();
|
||||
};
|
||||
|
||||
public WebRtcCallView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
|
@ -99,42 +120,61 @@ public class WebRtcCallView extends FrameLayout {
|
|||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
|
||||
videoToggle = findViewById(R.id.call_screen_video_toggle);
|
||||
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
|
||||
localRenderPipFrame = findViewById(R.id.call_screen_pip);
|
||||
largeLocalRenderContainer = findViewById(R.id.call_screen_large_local_renderer_holder);
|
||||
smallLocalRenderContainer = findViewById(R.id.call_screen_small_local_renderer_holder);
|
||||
remoteRenderContainer = findViewById(R.id.call_screen_remote_renderer_holder);
|
||||
recipientName = findViewById(R.id.call_screen_recipient_name);
|
||||
status = findViewById(R.id.call_screen_status);
|
||||
parent = findViewById(R.id.call_screen);
|
||||
avatar = findViewById(R.id.call_screen_recipient_avatar);
|
||||
avatarCard = findViewById(R.id.call_screen_recipient_avatar_call_card);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
answerWithAudioLabel = findViewById(R.id.call_screen_answer_with_audio_label);
|
||||
ongoingFooterGradient = findViewById(R.id.call_screen_ongoing_footer_gradient);
|
||||
audioToggle = findViewById(R.id.call_screen_speaker_toggle);
|
||||
videoToggle = findViewById(R.id.call_screen_video_toggle);
|
||||
micToggle = findViewById(R.id.call_screen_audio_mic_toggle);
|
||||
smallLocalRenderFrame = findViewById(R.id.call_screen_pip);
|
||||
smallLocalRender = findViewById(R.id.call_screen_small_local_renderer);
|
||||
largeLocalRenderFrame = findViewById(R.id.call_screen_large_local_renderer_frame);
|
||||
largeLocalRender = findViewById(R.id.call_screen_large_local_renderer);
|
||||
largeLocalRenderNoVideo = findViewById(R.id.call_screen_large_local_video_off);
|
||||
largeLocalRenderNoVideoAvatar = findViewById(R.id.call_screen_large_local_video_off_avatar);
|
||||
recipientName = findViewById(R.id.call_screen_recipient_name);
|
||||
status = findViewById(R.id.call_screen_status);
|
||||
parent = findViewById(R.id.call_screen);
|
||||
participantsParent = findViewById(R.id.call_screen_participants_parent);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
answerWithAudio = findViewById(R.id.call_screen_answer_with_audio);
|
||||
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 downCaret = findViewById(R.id.call_screen_down_arrow);
|
||||
View decline = findViewById(R.id.call_screen_decline_call);
|
||||
View answerLabel = findViewById(R.id.call_screen_answer_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);
|
||||
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(recipientName);
|
||||
|
||||
incomingCallViews.add(answer);
|
||||
incomingCallViews.add(answerLabel);
|
||||
incomingCallViews.add(decline);
|
||||
incomingCallViews.add(declineLabel);
|
||||
incomingCallViews.add(incomingFooterGradient);
|
||||
incomingCallViews.add(footerGradient);
|
||||
|
||||
adjustableMarginsSet.add(micToggle);
|
||||
adjustableMarginsSet.add(cameraDirectionToggle);
|
||||
|
@ -158,15 +198,18 @@ public class WebRtcCallView extends FrameLayout {
|
|||
hangup.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onEndCallPressed));
|
||||
decline.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDenyCallPressed));
|
||||
|
||||
downCaret.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onDownCaretPressed));
|
||||
|
||||
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
|
||||
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
||||
|
||||
setOnClickListener(v -> toggleControls());
|
||||
avatar.setOnClickListener(v -> toggleControls());
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
|
||||
|
||||
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);
|
||||
statusBarGuideline.setGuidelineBegin(statusBarHeight);
|
||||
|
@ -195,67 +238,99 @@ public class WebRtcCallView extends FrameLayout {
|
|||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
|
||||
public void setRemoteVideoEnabled(boolean isRemoteVideoEnabled) {
|
||||
if (isRemoteVideoEnabled) {
|
||||
remoteRenderContainer.setVisibility(View.VISIBLE);
|
||||
public void updateCallParticipants(@NonNull CallParticipantsState state) {
|
||||
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
|
||||
|
||||
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 {
|
||||
remoteRenderContainer.setVisibility(View.GONE);
|
||||
layoutParticipantsForSmallCount();
|
||||
}
|
||||
}
|
||||
|
||||
public void setLocalRenderer(@Nullable TextureViewRenderer surfaceViewRenderer) {
|
||||
if (localRenderer == surfaceViewRenderer) {
|
||||
return;
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
|
||||
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
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;
|
||||
|
||||
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) {
|
||||
switch (state) {
|
||||
case GONE:
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
setRenderer(largeLocalRenderContainer, null);
|
||||
setRenderer(smallLocalRenderContainer, null);
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
smallLocalRender.attachBroadcastVideoSink(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;
|
||||
case LARGE:
|
||||
localRenderPipFrame.setVisibility(View.GONE);
|
||||
largeLocalRenderContainer.setVisibility(View.VISIBLE);
|
||||
if (largeLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(largeLocalRenderContainer, localRenderer);
|
||||
}
|
||||
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
largeLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
|
||||
largeLocalRenderNoVideo.setVisibility(View.GONE);
|
||||
largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
|
||||
|
||||
smallLocalRender.attachBroadcastVideoSink(null);
|
||||
smallLocalRenderFrame.setVisibility(View.GONE);
|
||||
|
||||
videoToggle.setChecked(true, false);
|
||||
break;
|
||||
case SMALL:
|
||||
localRenderPipFrame.setVisibility(View.VISIBLE);
|
||||
largeLocalRenderContainer.setVisibility(View.GONE);
|
||||
case LARGE_NO_VIDEO:
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
|
||||
if (smallLocalRenderContainer.getChildCount() == 0) {
|
||||
setRenderer(smallLocalRenderContainer, localRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
|
||||
largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
|
||||
|
||||
public void setCameraDirection(@NonNull CameraState.Direction cameraDirection) {
|
||||
this.cameraDirection = cameraDirection;
|
||||
GlideApp.with(getContext().getApplicationContext())
|
||||
.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) {
|
||||
localRenderer.setMirror(cameraDirection == CameraState.Direction.FRONT);
|
||||
smallLocalRender.attachBroadcastVideoSink(null);
|
||||
smallLocalRenderFrame.setVisibility(View.GONE);
|
||||
|
||||
videoToggle.setChecked(false, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -265,17 +340,16 @@ public class WebRtcCallView extends FrameLayout {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void showCallCard(boolean showCallCard) {
|
||||
avatarCard.setVisibility(showCallCard ? VISIBLE : GONE);
|
||||
avatar.setVisibility(showCallCard ? GONE : VISIBLE);
|
||||
if (recipient.isGroup()) {
|
||||
recipientName.setText(R.string.WebRtcCallView__group_call);
|
||||
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
|
||||
toolbar.inflateMenu(R.menu.group_call);
|
||||
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
|
||||
}
|
||||
} else {
|
||||
recipientName.setText(recipient.getDisplayName(getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
visibleViewSet.clear();
|
||||
|
||||
if (webRtcControls.displayStartCallControls()) {
|
||||
visibleViewSet.add(footerGradient);
|
||||
visibleViewSet.add(startCallControls);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayTopViews()) {
|
||||
visibleViewSet.addAll(topViews);
|
||||
}
|
||||
|
@ -341,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
|
|||
|
||||
if (webRtcControls.displayEndCall()) {
|
||||
visibleViewSet.add(hangup);
|
||||
visibleViewSet.add(ongoingFooterGradient);
|
||||
visibleViewSet.add(footerGradient);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayMuteAudio()) {
|
||||
|
@ -358,6 +437,12 @@ public class WebRtcCallView extends FrameLayout {
|
|||
updateButtonStateForLargeButtons();
|
||||
}
|
||||
|
||||
if (webRtcControls.displayRemoteVideoRecycler()) {
|
||||
callParticipantsRecycler.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
callParticipantsRecycler.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (webRtcControls.isFadeOutEnabled()) {
|
||||
if (!controls.isFadeOutEnabled()) {
|
||||
scheduleFadeOut();
|
||||
|
@ -378,8 +463,39 @@ public class WebRtcCallView extends FrameLayout {
|
|||
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() {
|
||||
if (controls.isFadeOutEnabled() && status.getVisibility() == VISIBLE) {
|
||||
if (controls.isFadeOutEnabled() && toolbar.getVisibility() == VISIBLE) {
|
||||
fadeOutControls();
|
||||
} else {
|
||||
fadeInControls();
|
||||
|
@ -399,9 +515,44 @@ public class WebRtcCallView extends FrameLayout {
|
|||
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);
|
||||
|
||||
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);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
|
@ -412,6 +563,8 @@ public class WebRtcCallView extends FrameLayout {
|
|||
}
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
|
||||
layoutParticipants();
|
||||
}
|
||||
|
||||
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() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
|
||||
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);
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
return new ResourceContactPhoto(R.drawable.ic_profile_outline_120);
|
||||
}
|
||||
private boolean showParticipantsList() {
|
||||
controlsListener.onShowParticipantsList();
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface ControlsListener {
|
||||
void onStartCall(boolean isVideoCall);
|
||||
void onCancelStartCall();
|
||||
void onControlsFadeOut();
|
||||
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
|
||||
void onVideoChanged(boolean isVideoEnabled);
|
||||
|
@ -525,6 +644,7 @@ public class WebRtcCallView extends FrameLayout {
|
|||
void onDenyCallPressed();
|
||||
void onAcceptCallWithVoiceOnlyPressed();
|
||||
void onAcceptCallPressed();
|
||||
void onDownCaretPressed();
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,60 +10,38 @@ import androidx.lifecycle.MutableLiveData;
|
|||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Boolean> remoteVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<WebRtcLocalRenderState> localRenderState = new MutableLiveData<>(WebRtcLocalRenderState.GONE);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> localVideoEnabled = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<CameraState.Direction> cameraDirection = new MutableLiveData<>(CameraState.Direction.FRONT);
|
||||
private final LiveData<Boolean> shouldDisplayLocal = LiveDataUtil.combineLatest(isInPipMode, localVideoEnabled, (a, b) -> !a && b);
|
||||
private final LiveData<WebRtcLocalRenderState> realLocalRenderState = LiveDataUtil.combineLatest(shouldDisplayLocal, localRenderState, this::getRealLocalRenderState);
|
||||
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 final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
|
||||
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
|
||||
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> elapsed = new MutableLiveData<>(-1L);
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
|
||||
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
|
||||
|
||||
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();
|
||||
|
||||
public LiveData<Boolean> getRemoteVideoEnabled() {
|
||||
return Transformations.distinctUntilChanged(remoteVideoEnabled);
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getMicrophoneEnabled() {
|
||||
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() {
|
||||
return realWebRtcControls;
|
||||
}
|
||||
|
@ -81,7 +59,15 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -91,6 +77,15 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
@MainThread
|
||||
public void setIsInPipMode(boolean 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() {
|
||||
|
@ -99,27 +94,20 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
|
||||
@MainThread
|
||||
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
|
||||
remoteVideoEnabled.setValue(webRtcViewModel.isRemoteVideoEnabled());
|
||||
microphoneEnabled.setValue(webRtcViewModel.isMicrophoneEnabled());
|
||||
canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
|
||||
|
||||
if (isValidCameraDirectionForUi(webRtcViewModel.getLocalCameraState().getActiveDirection())) {
|
||||
cameraDirection.setValue(webRtcViewModel.getLocalCameraState().getActiveDirection());
|
||||
}
|
||||
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
|
||||
|
||||
localVideoEnabled.setValue(webRtcViewModel.getLocalCameraState().isEnabled());
|
||||
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
|
||||
|
||||
if (enableVideo) {
|
||||
showVideoForOutgoing = webRtcViewModel.getState() == WebRtcViewModel.State.CALL_OUTGOING;
|
||||
} else if (webRtcViewModel.getState() != WebRtcViewModel.State.CALL_OUTGOING) {
|
||||
showVideoForOutgoing = false;
|
||||
}
|
||||
//noinspection ConstantConditions
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
|
||||
|
||||
updateLocalRenderState(webRtcViewModel.getState());
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getLocalCameraState().isEnabled(),
|
||||
localParticipant.getCameraState().isEnabled(),
|
||||
webRtcViewModel.isRemoteVideoEnabled(),
|
||||
webRtcViewModel.isRemoteVideoOffer(),
|
||||
webRtcViewModel.getLocalCameraState().getCameraCount() > 1,
|
||||
localParticipant.isMoreThanOneCameraAvailable(),
|
||||
webRtcViewModel.isBluetoothAvailable(),
|
||||
repository.getAudioOutput());
|
||||
|
||||
|
@ -131,9 +119,9 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
callConnectedTime = -1;
|
||||
}
|
||||
|
||||
if (webRtcViewModel.getLocalCameraState().isEnabled()) {
|
||||
if (localParticipant.getCameraState().isEnabled()) {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
hasEnabledLocalVideo = true;
|
||||
hasEnabledLocalVideo = true;
|
||||
events.setValue(Event.DISMISS_VIDEO_TOOLTIP);
|
||||
}
|
||||
|
||||
|
@ -144,34 +132,36 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isValidCameraDirectionForUi(CameraState.Direction direction) {
|
||||
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,
|
||||
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
|
||||
boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
WebRtcAudioOutput audioOutput)
|
||||
@NonNull WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
|
||||
final WebRtcControls.CallState callState;
|
||||
|
||||
switch (state) {
|
||||
case CALL_PRE_JOIN:
|
||||
callState = WebRtcControls.CallState.PRE_JOIN;
|
||||
break;
|
||||
case CALL_INCOMING:
|
||||
callState = WebRtcControls.CallState.INCOMING;
|
||||
answerWithVideoAvailable = isRemoteVideoOffer;
|
||||
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:
|
||||
callState = WebRtcControls.CallState.ONGOING;
|
||||
}
|
||||
|
@ -180,25 +170,19 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
isRemoteVideoEnabled || isRemoteVideoOffer,
|
||||
isMoreThanOneCameraAvailable,
|
||||
isBluetoothAvailable,
|
||||
isInPipMode.getValue() == Boolean.TRUE,
|
||||
Boolean.TRUE.equals(isInPipMode.getValue()),
|
||||
callState,
|
||||
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) {
|
||||
if (isInPipMode) return WebRtcControls.PIP;
|
||||
else return controls;
|
||||
return isInPipMode ? WebRtcControls.PIP : controls;
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
cancelTimer();
|
||||
|
||||
ellapsedTimeHandler.post(ellapsedTimeRunnable);
|
||||
elapsedTimeHandler.post(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
private void handleTick() {
|
||||
|
@ -208,13 +192,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
|
||||
long newValue = (System.currentTimeMillis() - callConnectedTime) / 1000;
|
||||
|
||||
ellapsed.postValue(newValue);
|
||||
elapsed.postValue(newValue);
|
||||
|
||||
ellapsedTimeHandler.postDelayed(ellapsedTimeRunnable, 1000);
|
||||
elapsedTimeHandler.postDelayed(elapsedTimeRunnable, 1000);
|
||||
}
|
||||
|
||||
private void cancelTimer() {
|
||||
ellapsedTimeHandler.removeCallbacks(ellapsedTimeRunnable);
|
||||
elapsedTimeHandler.removeCallbacks(elapsedTimeRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -36,24 +36,32 @@ public final class WebRtcControls {
|
|||
this.audioOutput = audioOutput;
|
||||
}
|
||||
|
||||
boolean displayStartCallControls() {
|
||||
return isPreJoin();
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
return isOngoing();
|
||||
return isAtLeastOutgoing();
|
||||
}
|
||||
|
||||
boolean displayMuteAudio() {
|
||||
return isOngoing();
|
||||
return isPreJoin() || isAtLeastOutgoing();
|
||||
}
|
||||
|
||||
boolean displayVideoToggle() {
|
||||
return isOngoing();
|
||||
return isPreJoin() || isAtLeastOutgoing();
|
||||
}
|
||||
|
||||
boolean displayAudioToggle() {
|
||||
return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
|
||||
return (isPreJoin() || isAtLeastOutgoing()) && (!isLocalVideoEnabled || isBluetoothAvailable);
|
||||
}
|
||||
|
||||
boolean displayCameraToggle() {
|
||||
return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
|
||||
return (isPreJoin() || isAtLeastOutgoing()) && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
|
||||
}
|
||||
|
||||
boolean displayRemoteVideoRecycler() {
|
||||
return isOngoing();
|
||||
}
|
||||
|
||||
boolean displayAnswerWithAudio() {
|
||||
|
@ -73,25 +81,29 @@ public final class WebRtcControls {
|
|||
}
|
||||
|
||||
boolean isFadeOutEnabled() {
|
||||
return isOngoing() && isRemoteVideoEnabled;
|
||||
return isAtLeastOutgoing() && isRemoteVideoEnabled;
|
||||
}
|
||||
|
||||
boolean displaySmallOngoingCallButtons() {
|
||||
return isOngoing() && displayAudioToggle() && displayCameraToggle();
|
||||
return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle();
|
||||
}
|
||||
|
||||
boolean displayLargeOngoingCallButtons() {
|
||||
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
|
||||
return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle());
|
||||
}
|
||||
|
||||
boolean displayTopViews() {
|
||||
return !isInPipMode;
|
||||
}
|
||||
|
||||
WebRtcAudioOutput getAudioOutput() {
|
||||
@NonNull WebRtcAudioOutput getAudioOutput() {
|
||||
return audioOutput;
|
||||
}
|
||||
|
||||
private boolean isPreJoin() {
|
||||
return callState == CallState.PRE_JOIN;
|
||||
}
|
||||
|
||||
private boolean isOngoing() {
|
||||
return callState == CallState.ONGOING;
|
||||
}
|
||||
|
@ -100,9 +112,20 @@ public final class WebRtcControls {
|
|||
return callState == CallState.INCOMING;
|
||||
}
|
||||
|
||||
private boolean isAtLeastOutgoing() {
|
||||
return callState.isAtLeast(CallState.OUTGOING);
|
||||
}
|
||||
|
||||
public enum CallState {
|
||||
NONE,
|
||||
PRE_JOIN,
|
||||
INCOMING,
|
||||
ONGOING
|
||||
OUTGOING,
|
||||
ONGOING,
|
||||
ENDING;
|
||||
|
||||
boolean isAtLeast(@NonNull CallState other) {
|
||||
return compareTo(other) >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.webrtc;
|
|||
|
||||
public enum WebRtcLocalRenderState {
|
||||
GONE,
|
||||
SMALL,
|
||||
LARGE
|
||||
SMALL_RECTANGLE,
|
||||
SMALL_SQUARE,
|
||||
LARGE,
|
||||
LARGE_NO_VIDEO
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ import android.graphics.drawable.LayerDrawable;
|
|||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
@ -22,6 +24,8 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||
private final int smallResourceId;
|
||||
private final int callCardResourceId;
|
||||
|
||||
private ImageView.ScaleType scaleType = ImageView.ScaleType.CENTER;
|
||||
|
||||
public ResourceContactPhoto(@DrawableRes int resourceId) {
|
||||
this(resourceId, resourceId, resourceId);
|
||||
}
|
||||
|
@ -36,26 +40,31 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||
this.smallResourceId = smallResourceId;
|
||||
}
|
||||
|
||||
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
|
||||
this.scaleType = scaleType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
|
||||
return asDrawable(context, color, false);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
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);
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
|
||||
foreground.setScaleType(ImageView.ScaleType.CENTER);
|
||||
//noinspection ConstantConditions
|
||||
foreground.setScaleType(scaleType);
|
||||
|
||||
if (inverted) {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
|
@ -68,12 +77,12 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Drawable asCallCard(Context context) {
|
||||
public @Nullable Drawable asCallCard(@NonNull Context context) {
|
||||
return AppCompatResources.getDrawable(context, callCardResourceId);
|
||||
}
|
||||
|
||||
private static class ExpandingLayerDrawable extends LayerDrawable {
|
||||
public ExpandingLayerDrawable(Drawable[] layers) {
|
||||
public ExpandingLayerDrawable(@NonNull Drawable[] layers) {
|
||||
super(layers);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,9 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Uses CDS to map E164's to UUIDs.
|
||||
*/
|
||||
class ContactDiscoveryV2 {
|
||||
|
||||
private static final String TAG = Log.tag(ContactDiscoveryV2.class);
|
||||
|
|
|
@ -25,20 +25,21 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
|||
import org.thoughtcrime.securesms.contacts.ContactsDatabase;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.BulkOperationsHandle;
|
||||
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.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientDetails;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
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.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
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.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
|
@ -154,13 +158,7 @@ public class DirectoryHelper {
|
|||
return RegisteredState.NOT_REGISTERED;
|
||||
}
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
|
||||
} else {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(recipient.getE164().get());
|
||||
}
|
||||
DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, recipient.getE164().get());
|
||||
|
||||
stopwatch.split("e164-network");
|
||||
|
||||
|
@ -179,6 +177,13 @@ public class DirectoryHelper {
|
|||
} else {
|
||||
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 {
|
||||
recipientDatabase.markUnregistered(recipient.getId());
|
||||
}
|
||||
|
@ -218,13 +223,7 @@ public class DirectoryHelper {
|
|||
|
||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
} else {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
|
||||
}
|
||||
DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
|
@ -244,6 +243,17 @@ public class DirectoryHelper {
|
|||
|
||||
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);
|
||||
|
||||
stopwatch.split("update-registered");
|
||||
|
@ -275,16 +285,10 @@ public class DirectoryHelper {
|
|||
|
||||
private static boolean isUuidRegistered(@NonNull Context context, @NonNull Recipient recipient) throws IOException {
|
||||
try {
|
||||
ProfileUtil.retrieveProfile(context, recipient, SignalServiceProfile.RequestType.PROFILE).get(10, TimeUnit.SECONDS);
|
||||
ProfileUtil.retrieveProfileSync(context, recipient, SignalServiceProfile.RequestType.PROFILE);
|
||||
return true;
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof NotFoundException) {
|
||||
return false;
|
||||
} else {
|
||||
throw new IOException(e);
|
||||
}
|
||||
} catch (InterruptedException | TimeoutException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -420,6 +424,50 @@ public class DirectoryHelper {
|
|||
}).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 {
|
||||
private final Map<String, UUID> registeredNumbers;
|
||||
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 final boolean fresh;
|
||||
private final Account account;
|
||||
|
|
|
@ -14,7 +14,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
@ -648,7 +647,7 @@ public class Contact implements Parcelable {
|
|||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -215,7 +215,7 @@ class ContactFieldAdapter extends RecyclerView.Adapter<ContactFieldAdapter.Conta
|
|||
Field(@NonNull Avatar avatar) {
|
||||
this.value = "";
|
||||
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.selectable = avatar;
|
||||
this.label = "";
|
||||
|
|
|
@ -186,11 +186,11 @@ public final class ContactUtil {
|
|||
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 {
|
||||
ContentValues values = new ContentValues();
|
||||
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);
|
||||
valuesArray.add(values);
|
||||
|
|
|
@ -96,7 +96,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
|
|||
|
||||
presentContact(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()) {
|
||||
recipient.observe(this, r -> presentActionButtons(Collections.singletonList(r.getId())));
|
||||
|
|
|
@ -93,66 +93,7 @@ public class SharedContactRepository {
|
|||
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
|
||||
VCard vcard = Ezvcard.parse(stream).first();
|
||||
|
||||
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);
|
||||
contact = VCardUtil.getContactFromVcard(vcard);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to parse the vcard.", e);
|
||||
}
|
||||
|
@ -201,7 +142,7 @@ public class SharedContactRepository {
|
|||
|
||||
String number = ContactUtil.getNormalizedPhoneNumber(context, cursorNumber);
|
||||
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)) {
|
||||
numberMap.put(number, candidate);
|
||||
|
@ -224,7 +165,7 @@ public class SharedContactRepository {
|
|||
int cursorType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE));
|
||||
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 cursorCountry = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY));
|
||||
|
||||
postalAddresses.add(new PostalAddress(postalAddressTypeFromContactType(cursorType),
|
||||
postalAddresses.add(new PostalAddress(VCardUtil.postalAddressTypeFromContactType(cursorType),
|
||||
cursorLabel,
|
||||
cursorStreet,
|
||||
cursorPoBox,
|
||||
|
@ -304,70 +245,6 @@ public class SharedContactRepository {
|
|||
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> {
|
||||
void onComplete(@NonNull T value);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -676,11 +676,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
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())) {
|
||||
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())) {
|
||||
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 {
|
||||
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.isLoading()) {
|
||||
Log.d(TAG, "Loading link preview.");
|
||||
inputPanel.setLinkPreviewLoading();
|
||||
} else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
|
||||
Log.d(TAG, "No preview found.");
|
||||
inputPanel.setLinkPreviewNoPreview(previewState.getError());
|
||||
} else {
|
||||
Log.d(TAG, "Setting link preview: " + previewState.getLinkPreview().isPresent());
|
||||
inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
@ -84,6 +85,7 @@ public class ConversationAdapter
|
|||
private static final long FOOTER_ID = Long.MIN_VALUE + 1;
|
||||
|
||||
private final ItemClickListener clickListener;
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Locale locale;
|
||||
private final Recipient recipient;
|
||||
|
@ -99,12 +101,14 @@ public class ConversationAdapter
|
|||
private View headerView;
|
||||
private View footerView;
|
||||
|
||||
ConversationAdapter(@NonNull GlideRequests glideRequests,
|
||||
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
super(new DiffCallback());
|
||||
this.lifecycleOwner = lifecycleOwner;
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
|
@ -170,8 +174,6 @@ public class ConversationAdapter
|
|||
case MESSAGE_TYPE_OUTGOING_TEXT:
|
||||
case MESSAGE_TYPE_OUTGOING_MULTIMEDIA:
|
||||
case MESSAGE_TYPE_UPDATE:
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);
|
||||
BindableConversationItem bindable = (BindableConversationItem) itemView;
|
||||
|
||||
|
@ -190,7 +192,6 @@ public class ConversationAdapter
|
|||
|
||||
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);
|
||||
case MESSAGE_TYPE_PLACEHOLDER:
|
||||
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 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(nextMessage != null ? nextMessage.getMessageRecord() : null),
|
||||
glideRequests,
|
||||
|
|
|
@ -479,7 +479,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
private void initializeListAdapter() {
|
||||
if (this.recipient != null && this.threadId != -1) {
|
||||
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);
|
||||
setStickyHeaderDecoration(adapter);
|
||||
ConversationAdapter.initializePool(list.getRecycledViewPool());
|
||||
|
@ -810,7 +810,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
.toList();
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
|
||||
Uri uri = attachment.getUri();
|
||||
|
||||
if (uri != null) {
|
||||
mediaList.add(new Media(uri,
|
||||
|
@ -1424,7 +1424,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
|
||||
@Nullable ScrollRequestValidator scrollRequestValidator)
|
||||
{
|
||||
super(recyclerView, scrollRequestValidator);
|
||||
super(recyclerView, scrollRequestValidator, () -> {
|
||||
list.scrollToPosition(0);
|
||||
list.post(ConversationFragment.this::postMarkAsReadRequest);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -54,6 +54,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
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.DatabaseFactory;
|
||||
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.model.MediaMmsMessageRecord;
|
||||
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.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ConversationMessage conversationMessage,
|
||||
public void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@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);
|
||||
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) {
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
sharedContactStub.get().setSingularStyle();
|
||||
} else if (current.isOutgoing()) {
|
||||
sharedContactStub.get().setClusteredOutgoingStyle();
|
||||
} else {
|
||||
sharedContactStub.get().setClusteredIncomingStyle();
|
||||
if (TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))){
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
sharedContactStub.get().setSingularStyle();
|
||||
} else if (current.isOutgoing()) {
|
||||
sharedContactStub.get().setClusteredOutgoingStyle();
|
||||
} else {
|
||||
sharedContactStub.get().setClusteredIncomingStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1075,7 +1087,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
|
||||
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
|
||||
return stickerFooter;
|
||||
} else if (hasSharedContact(messageRecord)) {
|
||||
} else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
|
||||
return sharedContactStub.get().getFooter();
|
||||
} else if (hasOnlyThumbnail(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
|
||||
return mediaThumbnailStub.get().getFooter();
|
||||
|
@ -1442,7 +1454,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
Log.i(TAG, "Public URI: " + publicUri);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
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 {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Locale;
|
||||
|
@ -44,9 +44,7 @@ import java.util.Set;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class ConversationUpdateItem extends LinearLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationItem,
|
||||
Observer<SpannableString>
|
||||
implements BindableConversationItem
|
||||
{
|
||||
private static final String TAG = ConversationUpdateItem.class.getSimpleName();
|
||||
|
||||
|
@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
private Locale locale;
|
||||
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) {
|
||||
super(context);
|
||||
|
@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ConversationMessage conversationMessage,
|
||||
public void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
|
@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
bind(conversationMessage, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
unbind();
|
||||
super.onDetachedFromWindow();
|
||||
bind(lifecycleOwner, conversationMessage, locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
return conversationMessage;
|
||||
}
|
||||
|
||||
private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
|
||||
if (this.sender != null) {
|
||||
this.sender.removeForeverObserver(this);
|
||||
}
|
||||
|
||||
observeDisplayBody(null);
|
||||
setBodyText(null);
|
||||
|
||||
private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) {
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.sender = messageRecord.getIndividualRecipient().live();
|
||||
this.locale = locale;
|
||||
|
||||
this.sender.observeForever(this);
|
||||
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
|
||||
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<String> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription);
|
||||
LiveData<SpannableString> spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new);
|
||||
LiveData<SpannableString> spannableStringMessage = toSpannable(loading(liveUpdateMessage));
|
||||
|
||||
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 != null) {
|
||||
this.displayBody.removeObserver(this);
|
||||
this.displayBody.removeObserver(updateObserver);
|
||||
}
|
||||
|
||||
this.displayBody = displayBody;
|
||||
|
||||
if (this.displayBody != null) {
|
||||
this.displayBody.observeForever(this);
|
||||
this.displayBody.observe(lifecycleOwner, updateObserver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setBodyText(@Nullable CharSequence text) {
|
||||
if (text == null) {
|
||||
bodyClearDebouncer.publish(() -> body.setText(null));
|
||||
body.setVisibility(INVISIBLE);
|
||||
} else {
|
||||
bodyClearDebouncer.clear();
|
||||
body.setText(text);
|
||||
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 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);
|
||||
date.setVisibility(View.VISIBLE);
|
||||
|
@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout
|
|||
icon.setColorFilter(getIconTintFilter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
present(conversationMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(View.OnClickListener l) {
|
||||
super.setOnClickListener(new InternalClickListener(l));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbind() {
|
||||
if (sender != null) {
|
||||
sender.removeForeverObserver(this);
|
||||
}
|
||||
private final class SenderObserver implements Observer<Recipient> {
|
||||
|
||||
observeDisplayBody(null);
|
||||
@Override
|
||||
public void onChanged(Recipient recipient) {
|
||||
present(conversationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString update) {
|
||||
setBodyText(update);
|
||||
private final class UpdateObserver implements Observer<SpannableString> {
|
||||
|
||||
@Override
|
||||
public void onChanged(SpannableString update) {
|
||||
setBodyText(update);
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalClickListener implements View.OnClickListener {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,11 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MentionViewState implements MappingModel<MentionViewState> {
|
||||
public final class MentionViewState extends RecipientMappingModel<MentionViewState> {
|
||||
|
||||
private final Recipient recipient;
|
||||
|
||||
|
@ -19,23 +13,8 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
|
|||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@NonNull String getName(@NonNull Context context) {
|
||||
return recipient.getDisplayName(context);
|
||||
}
|
||||
|
||||
@NonNull Recipient getRecipient() {
|
||||
@Override
|
||||
public @NonNull Recipient getRecipient() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,20 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
|
|||
import androidx.annotation.NonNull;
|
||||
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.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder.EventListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MentionsPickerAdapter extends MappingAdapter {
|
||||
private final Runnable currentListChangedListener;
|
||||
|
||||
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener, @NonNull Runnable currentListChangedListener) {
|
||||
public MentionsPickerAdapter(@Nullable EventListener<MentionViewState> listener, @NonNull Runnable currentListChangedListener) {
|
||||
this.currentListChangedListener = currentListChangedListener;
|
||||
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
|
||||
registerFactory(MentionViewState.class, RecipientViewHolder.createFactory(R.layout.mentions_picker_recipient_list_item, listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,8 +12,6 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
|
||||
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.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
private final MutableLiveData<LiveRecipient> liveRecipient;
|
||||
private final MutableLiveData<Query> liveQuery;
|
||||
private final MutableLiveData<Boolean> isShowing;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
|
||||
@NonNull MegaphoneRepository megaphoneRepository)
|
||||
{
|
||||
this.megaphoneRepository = megaphoneRepository;
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
this.liveQuery = new MutableLiveData<>();
|
||||
this.selectedRecipient = new SingleLiveEvent<>();
|
||||
|
@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
|
||||
void onSelectionChange(@NonNull Recipient recipient) {
|
||||
selectedRecipient.setValue(recipient);
|
||||
megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
|
||||
}
|
||||
|
||||
void setIsShowing(boolean isShowing) {
|
||||
|
@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()),
|
||||
ApplicationDependencies.getMegaphoneRepository()));
|
||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,11 +123,13 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
|||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
protected void onItemSwiped(long threadId, int unreadCount) {
|
||||
new SnackbarAsyncTask<Long>(getView(),
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG, false)
|
||||
new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
|
||||
requireView(),
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG,
|
||||
false)
|
||||
{
|
||||
@Override
|
||||
protected void executeAction(@Nullable Long parameter) {
|
||||
|
|
|
@ -55,6 +55,7 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
@ -223,7 +224,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
list.setItemAnimator(new DeleteItemAnimator());
|
||||
list.addOnScrollListener(new ScrollListener());
|
||||
|
||||
snapToTopDataObserver = new SnapToTopDataObserver(list, null);
|
||||
snapToTopDataObserver = new SnapToTopDataObserver(list);
|
||||
|
||||
new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
|
||||
|
||||
|
@ -367,7 +368,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
@Override
|
||||
public void onContactClicked(@NonNull Recipient contact) {
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
|
||||
return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact.getId());
|
||||
}, threadId -> {
|
||||
hideKeyboard();
|
||||
getNavigator().goToConversation(contact.getId(),
|
||||
|
@ -423,6 +424,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
viewModel.onMegaphoneCompleted(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
|
||||
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
|
||||
}
|
||||
|
||||
private void onReminderAction(@IdRes int reminderActionId) {
|
||||
if (reminderActionId == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||
|
@ -679,7 +685,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
int count = selectedConversations.size();
|
||||
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
|
||||
|
||||
new SnackbarAsyncTask<Void>(getView(),
|
||||
new SnackbarAsyncTask<Void>(getViewLifecycleOwner().getLifecycle(),
|
||||
requireView(),
|
||||
snackBarTitle,
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
|
@ -1002,11 +1009,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
protected void onItemSwiped(long threadId, int unreadCount) {
|
||||
new SnackbarAsyncTask<Long>(getView(),
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG, false)
|
||||
new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
|
||||
requireView(),
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
|
||||
getString(R.string.ConversationListFragment_undo),
|
||||
getResources().getColor(R.color.amber_500),
|
||||
Snackbar.LENGTH_LONG,
|
||||
false)
|
||||
{
|
||||
@Override
|
||||
protected void executeAction(@Nullable Long parameter) {
|
||||
|
|
|
@ -453,11 +453,13 @@ public final class ConversationListItem extends RelativeLayout
|
|||
String time = ExpirationUtil.getExpirationDisplayValue(context, seconds);
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_message_time_updated_to_s, time));
|
||||
} else if (SmsDatabase.Types.isIdentityUpdate(thread.getType())) {
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_safety_number_changed));
|
||||
} else {
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context))));
|
||||
}
|
||||
return emphasisAdded(recipientToStringAsync(thread.getRecipient().getId(), r -> {
|
||||
if (r.isGroup()) {
|
||||
return context.getString(R.string.ThreadRecord_safety_number_changed);
|
||||
} else {
|
||||
return context.getString(R.string.ThreadRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context));
|
||||
}
|
||||
}));
|
||||
} else if (SmsDatabase.Types.isIdentityVerified(thread.getType())) {
|
||||
return emphasisAdded(context.getString(R.string.ThreadRecord_you_marked_verified));
|
||||
} else if (SmsDatabase.Types.isIdentityDefault(thread.getType())) {
|
||||
|
|
|
@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
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 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 {
|
||||
|
||||
|
@ -42,34 +49,64 @@ public class UnidentifiedAccessUtil {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
try {
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient);
|
||||
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return getAccessFor(context, recipient, true);
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
@WorkerThread
|
||||
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) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
|
||||
" | UUID certificate supported? " + recipient.isUuidSupported());
|
||||
@WorkerThread
|
||||
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients) {
|
||||
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) {
|
||||
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate),
|
||||
new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate)));
|
||||
try {
|
||||
access.add(Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
|
||||
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) {
|
||||
|
@ -95,21 +132,20 @@ public class UnidentifiedAccessUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
|
||||
CertificateType certificateType;
|
||||
private static @NonNull CertificateType getUnidentifiedAccessCertificateType(@NonNull Recipient recipient) {
|
||||
PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode();
|
||||
|
||||
switch (sendPhoneNumberTo) {
|
||||
case EVERYONE: certificateType = CertificateType.UUID_AND_E164; break;
|
||||
case CONTACTS: certificateType = recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY; break;
|
||||
case NOBODY : certificateType = CertificateType.UUID_ONLY; break;
|
||||
case EVERYONE: return CertificateType.UUID_AND_E164;
|
||||
case CONTACTS: return recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY;
|
||||
case NOBODY : return CertificateType.UUID_ONLY;
|
||||
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()
|
||||
.getUnidentifiedAccessCertificate(certificateType);
|
||||
.getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType(recipient));
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
|
|
|
@ -19,11 +19,8 @@ package org.thoughtcrime.securesms.database;
|
|||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaDataSource;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
|
@ -58,14 +55,10 @@ import org.thoughtcrime.securesms.mms.MmsException;
|
|||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
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.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
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 java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
@ -90,9 +82,6 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
public class AttachmentDatabase extends Database {
|
||||
|
||||
|
@ -111,8 +100,6 @@ public class AttachmentDatabase extends Database {
|
|||
private static final String TRANSFER_FILE = "transfer_file";
|
||||
public static final String SIZE = "data_size";
|
||||
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";
|
||||
static final String DIGEST = "digest";
|
||||
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 FAST_PREFLIGHT_ID = "fast_preflight_id";
|
||||
public static final String DATA_RANDOM = "data_random";
|
||||
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
|
||||
static final String WIDTH = "width";
|
||||
static final String HEIGHT = "height";
|
||||
static final String CAPTION = "caption";
|
||||
|
@ -149,11 +135,10 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
private static final String[] PROJECTION = new String[] {ROW_ID,
|
||||
MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION,
|
||||
CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL,
|
||||
TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL,
|
||||
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
|
||||
CDN_NUMBER, CONTENT_LOCATION, DATA,
|
||||
TRANSFER_STATE, SIZE, FILE_NAME, UNIQUE_ID, DIGEST,
|
||||
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,
|
||||
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
|
||||
UPLOAD_TIMESTAMP };
|
||||
|
@ -175,15 +160,12 @@ public class AttachmentDatabase extends Database {
|
|||
DATA + " TEXT, " +
|
||||
SIZE + " INTEGER, " +
|
||||
FILE_NAME + " TEXT, " +
|
||||
THUMBNAIL + " TEXT, " +
|
||||
THUMBNAIL_ASPECT_RATIO + " REAL, " +
|
||||
UNIQUE_ID + " INTEGER NOT NULL, " +
|
||||
DIGEST + " BLOB, " +
|
||||
FAST_PREFLIGHT_ID + " TEXT, " +
|
||||
VOICE_NOTE + " INTEGER DEFAULT 0, " +
|
||||
BORDERLESS + " INTEGER DEFAULT 0, " +
|
||||
DATA_RANDOM + " BLOB, " +
|
||||
THUMBNAIL_RANDOM + " BLOB, " +
|
||||
QUOTE + " INTEGER DEFAULT 0, " +
|
||||
WIDTH + " 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 + ");"
|
||||
};
|
||||
|
||||
private static final long STANDARD_THUMB_TIME = 1000;
|
||||
|
||||
private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
|
||||
|
||||
private final AttachmentSecret attachmentSecret;
|
||||
|
||||
public AttachmentDatabase(Context context, SQLCipherOpenHelper databaseHelper, AttachmentSecret attachmentSecret) {
|
||||
|
@ -228,29 +206,6 @@ public class AttachmentDatabase extends Database {
|
|||
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) {
|
||||
String selection = STICKER_PACK_ID + " = ?";
|
||||
String[] args = new String[] { stickerPackId };
|
||||
|
@ -365,12 +320,11 @@ public class AttachmentDatabase extends Database {
|
|||
Cursor cursor = null;
|
||||
|
||||
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);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
|
||||
cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
|
||||
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
|
||||
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
|
||||
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
|
||||
|
@ -418,12 +372,11 @@ public class AttachmentDatabase extends Database {
|
|||
Cursor cursor = null;
|
||||
|
||||
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);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
deleteAttachmentOnDisk(cursor.getString(cursor.getColumnIndex(DATA)),
|
||||
cursor.getString(cursor.getColumnIndex(THUMBNAIL)),
|
||||
cursor.getString(cursor.getColumnIndex(CONTENT_TYPE)),
|
||||
new AttachmentId(cursor.getLong(cursor.getColumnIndex(ROW_ID)),
|
||||
cursor.getLong(cursor.getColumnIndex(UNIQUE_ID))));
|
||||
|
@ -437,8 +390,6 @@ public class AttachmentDatabase extends Database {
|
|||
values.put(DATA, (String) null);
|
||||
values.put(DATA_RANDOM, (byte[]) 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(CAPTION, (String) null);
|
||||
values.put(SIZE, 0);
|
||||
|
@ -463,7 +414,7 @@ public class AttachmentDatabase extends Database {
|
|||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME,
|
||||
new String[]{DATA, THUMBNAIL, CONTENT_TYPE},
|
||||
new String[]{DATA, CONTENT_TYPE},
|
||||
PART_ID_WHERE,
|
||||
id.toStrings(),
|
||||
null,
|
||||
|
@ -475,11 +426,10 @@ public class AttachmentDatabase extends Database {
|
|||
return;
|
||||
}
|
||||
String data = cursor.getString(cursor.getColumnIndex(DATA));
|
||||
String thumbnail = cursor.getString(cursor.getColumnIndex(THUMBNAIL));
|
||||
String contentType = cursor.getString(cursor.getColumnIndex(CONTENT_TYPE));
|
||||
|
||||
database.delete(TABLE_NAME, PART_ID_WHERE, id.toStrings());
|
||||
deleteAttachmentOnDisk(data, thumbnail, contentType, id);
|
||||
deleteAttachmentOnDisk(data, contentType, id);
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
}
|
||||
|
@ -502,10 +452,9 @@ public class AttachmentDatabase extends Database {
|
|||
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()) {
|
||||
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,
|
||||
@Nullable String thumbnail,
|
||||
@Nullable String contentType,
|
||||
@NonNull AttachmentId attachmentId)
|
||||
{
|
||||
|
@ -561,8 +509,6 @@ public class AttachmentDatabase extends Database {
|
|||
values.putNull(DATA);
|
||||
values.putNull(DATA_RANDOM);
|
||||
values.putNull(DATA_HASH);
|
||||
values.putNull(THUMBNAIL);
|
||||
values.putNull(THUMBNAIL_RANDOM);
|
||||
deletedCount += database.update(TABLE_NAME, values, PART_ID_WHERE, weakReference.toStrings());
|
||||
}
|
||||
database.setTransactionSuccessful();
|
||||
|
@ -581,15 +527,7 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(thumbnail)) {
|
||||
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) {
|
||||
if (MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType)) {
|
||||
Glide.get(context).clearDiskCache();
|
||||
}
|
||||
}
|
||||
|
@ -623,22 +561,17 @@ public class AttachmentDatabase extends Database {
|
|||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
DataInfo oldInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
||||
DataInfo dataInfo = setAttachmentData(inputStream, false, attachmentId);
|
||||
DataInfo dataInfo = setAttachmentData(inputStream, attachmentId);
|
||||
File transferFile = getTransferFile(databaseHelper.getReadableDatabase(), attachmentId);
|
||||
|
||||
if (oldInfo != null) {
|
||||
updateAttachmentDataHash(database, oldInfo.hash, dataInfo);
|
||||
}
|
||||
|
||||
if (placeholder != null && placeholder.isQuote() && !placeholder.getContentType().startsWith("image")) {
|
||||
values.put(THUMBNAIL, dataInfo.file.getAbsolutePath());
|
||||
values.put(THUMBNAIL_RANDOM, dataInfo.random);
|
||||
} else {
|
||||
values.put(DATA, dataInfo.file.getAbsolutePath());
|
||||
values.put(SIZE, dataInfo.length);
|
||||
values.put(DATA_RANDOM, dataInfo.random);
|
||||
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);
|
||||
if (visualHashString != null) {
|
||||
|
@ -662,8 +595,6 @@ public class AttachmentDatabase extends Database {
|
|||
//noinspection ResultOfMethodCallIgnored
|
||||
transferFile.delete();
|
||||
}
|
||||
|
||||
thumbnailExecutor.submit(new ThumbnailFetchCallable(attachmentId, STANDARD_THUMB_TIME));
|
||||
}
|
||||
|
||||
private static @Nullable String getVisualHashStringOrNull(@Nullable Attachment attachment) {
|
||||
|
@ -846,7 +777,6 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
DataInfo dataInfo = setAttachmentData(destination,
|
||||
mediaStream.getStream(),
|
||||
false,
|
||||
databaseAttachment.getAttachmentId());
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
@ -1055,16 +985,8 @@ public class AttachmentDatabase extends Database {
|
|||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
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 {
|
||||
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);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
|
@ -1074,7 +996,7 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
return new DataInfo(new File(cursor.getString(cursor.getColumnIndexOrThrow(dataType))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(randomColumn)),
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(DATA_RANDOM)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(DATA_HASH)));
|
||||
} else {
|
||||
return null;
|
||||
|
@ -1087,26 +1009,24 @@ public class AttachmentDatabase extends Database {
|
|||
}
|
||||
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull Uri uri,
|
||||
boolean isThumbnail,
|
||||
@Nullable AttachmentId attachmentId)
|
||||
throws MmsException
|
||||
{
|
||||
try {
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
|
||||
return setAttachmentData(inputStream, isThumbnail, attachmentId);
|
||||
return setAttachmentData(inputStream, attachmentId);
|
||||
} catch (IOException e) {
|
||||
throw new MmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull InputStream in,
|
||||
boolean isThumbnail,
|
||||
@Nullable AttachmentId attachmentId)
|
||||
throws MmsException
|
||||
{
|
||||
try {
|
||||
File dataFile = newFile();
|
||||
return setAttachmentData(dataFile, in, isThumbnail, attachmentId);
|
||||
return setAttachmentData(dataFile, in, attachmentId);
|
||||
} catch (IOException e) {
|
||||
throw new MmsException(e);
|
||||
}
|
||||
|
@ -1119,7 +1039,6 @@ public class AttachmentDatabase extends Database {
|
|||
|
||||
private @NonNull DataInfo setAttachmentData(@NonNull File destination,
|
||||
@NonNull InputStream in,
|
||||
boolean isThumbnail,
|
||||
@Nullable AttachmentId attachmentId)
|
||||
throws MmsException
|
||||
{
|
||||
|
@ -1130,18 +1049,16 @@ public class AttachmentDatabase extends Database {
|
|||
long length = Util.copy(digestInputStream, out.second);
|
||||
String hash = Base64.encodeBytes(digestInputStream.getMessageDigest().digest());
|
||||
|
||||
if (!isThumbnail) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Optional<DataInfo> sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId);
|
||||
if (sharedDataInfo.isPresent()) {
|
||||
Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath());
|
||||
if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) {
|
||||
Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
|
||||
}
|
||||
return sharedDataInfo.get();
|
||||
} else {
|
||||
Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
Optional<DataInfo> sharedDataInfo = findDuplicateDataFileInfo(database, hash, attachmentId);
|
||||
if (sharedDataInfo.isPresent()) {
|
||||
Log.i(TAG, "[setAttachmentData] Duplicate data file found! " + sharedDataInfo.get().file.getAbsolutePath());
|
||||
if (!destination.equals(sharedDataInfo.get().file) && destination.delete()) {
|
||||
Log.i(TAG, "[setAttachmentData] Deleted original file. " + destination);
|
||||
}
|
||||
return sharedDataInfo.get();
|
||||
} else {
|
||||
Log.i(TAG, "[setAttachmentData] No matching attachment data found. " + destination.getAbsolutePath());
|
||||
}
|
||||
|
||||
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)),
|
||||
object.getLong(MMS_ID),
|
||||
!TextUtils.isEmpty(object.getString(DATA)),
|
||||
!TextUtils.isEmpty(object.getString(THUMBNAIL)),
|
||||
MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
|
||||
contentType,
|
||||
object.getInt(TRANSFER_STATE),
|
||||
object.getLong(SIZE),
|
||||
|
@ -1254,7 +1171,7 @@ public class AttachmentDatabase extends Database {
|
|||
cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(DATA)),
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow(THUMBNAIL)),
|
||||
MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType),
|
||||
contentType,
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)),
|
||||
|
@ -1296,10 +1213,9 @@ public class AttachmentDatabase extends Database {
|
|||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
DataInfo dataInfo = null;
|
||||
long uniqueId = System.currentTimeMillis();
|
||||
long thumbnailTimeUs;
|
||||
|
||||
if (attachment.getDataUri() != null) {
|
||||
dataInfo = setAttachmentData(attachment.getDataUri(), false, null);
|
||||
if (attachment.getUri() != null) {
|
||||
dataInfo = setAttachmentData(attachment.getUri(), null);
|
||||
Log.d(TAG, "Wrote part to file: " + dataInfo.file.getAbsolutePath());
|
||||
}
|
||||
|
||||
|
@ -1342,11 +1258,9 @@ public class AttachmentDatabase extends Database {
|
|||
if (attachment.getTransformProperties().isVideoEdited()) {
|
||||
contentValues.putNull(VISUAL_HASH);
|
||||
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = Math.max(STANDARD_THUMB_TIME, attachment.getTransformProperties().videoTrimStartTimeUs);
|
||||
} else {
|
||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
|
||||
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
|
||||
thumbnailTimeUs = STANDARD_THUMB_TIME;
|
||||
}
|
||||
|
||||
if (attachment.isSticker()) {
|
||||
|
@ -1370,38 +1284,6 @@ public class AttachmentDatabase extends Database {
|
|||
boolean notifyPacks = attachment.isSticker() && !hasStickerAttachments();
|
||||
long rowId = database.insert(TABLE_NAME, null, contentValues);
|
||||
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) {
|
||||
notifyStickerPackListeners();
|
||||
|
@ -1423,35 +1305,6 @@ public class AttachmentDatabase extends Database {
|
|||
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
|
||||
public void writeAudioHash(@NonNull AttachmentId attachmentId, @Nullable AudioWaveFormData audioWaveForm) {
|
||||
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());
|
||||
}
|
||||
|
||||
@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)
|
||||
public @Nullable MediaDataSource mediaDataSourceFor(@NonNull AttachmentId attachmentId) {
|
||||
|
|
|
@ -4,9 +4,12 @@ import android.content.ContentProvider;
|
|||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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
|
||||
* 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 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 {
|
||||
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) {
|
||||
return Uri.parse(CONTENT_URI_STRING + threadId);
|
||||
|
@ -34,15 +41,24 @@ public class DatabaseContentProviders {
|
|||
}
|
||||
|
||||
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 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 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 {
|
||||
|
|
|
@ -22,8 +22,6 @@ public class EarlyReceiptCache {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
if (receipts == null) {
|
||||
|
@ -43,10 +41,6 @@ public class EarlyReceiptCache {
|
|||
|
||||
public synchronized Map<RecipientId, Long> remove(long 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<>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", "
|
||||
|
|
|
@ -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> 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);
|
||||
|
|
|
@ -213,7 +213,6 @@ public class MmsDatabase extends MessageDatabase {
|
|||
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
|
||||
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
|
||||
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
|
||||
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
|
||||
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
|
||||
|
@ -390,7 +389,7 @@ public class MmsDatabase extends MessageDatabase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address) {
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
|
|
@ -411,7 +411,6 @@ public class MmsSmsDatabase extends Database {
|
|||
"'" + AttachmentDatabase.SIZE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " +
|
||||
"'" + AttachmentDatabase.FILE_NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " +
|
||||
"'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " +
|
||||
"'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " +
|
||||
"'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " +
|
||||
"'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " +
|
||||
|
|
|
@ -10,7 +10,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.gms.common.util.ArrayUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteConstraintException;
|
||||
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[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = Stream.of(
|
||||
new String[] { TABLE_NAME + "." + ID,
|
||||
TABLE_NAME + "." + STORAGE_PROTO },
|
||||
TYPED_RECIPIENT_PROJECTION,
|
||||
new String[] {
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
|
||||
});
|
||||
}).flatMap(Stream::of).toArray(String[]::new);
|
||||
|
||||
public static final String[] CREATE_INDEXS = new String[] {
|
||||
"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!");
|
||||
}
|
||||
|
||||
if (!FeatureFlags.cds()) {
|
||||
highTrust = true;
|
||||
}
|
||||
|
||||
RecipientId recipientNeedingRefresh = null;
|
||||
Pair<RecipientId, RecipientId> remapped = null;
|
||||
boolean transactionSuccessful = false;
|
||||
|
@ -1000,7 +995,7 @@ public class RecipientDatabase extends Database {
|
|||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
if (contact.isProfileSharingEnabled() && isInsert) {
|
||||
if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) {
|
||||
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;
|
||||
List<RecipientSettings> out = new ArrayList<>();
|
||||
|
||||
String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION,
|
||||
new String[]{GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY });
|
||||
String[] columns = Stream.of(RECIPIENT_FULL_PROJECTION,
|
||||
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)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
|
@ -1080,7 +1075,7 @@ public class RecipientDatabase extends Database {
|
|||
public @NonNull Map<RecipientId, StorageId> getContactStorageSyncIdsMap() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
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<>();
|
||||
|
||||
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) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
HashMap<RecipientId, String> uuidMap = new HashMap<>();
|
||||
|
|
|
@ -12,6 +12,7 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
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()});
|
||||
}
|
||||
|
||||
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 {
|
||||
private final RecipientId recipientId;
|
||||
private final int deviceId;
|
||||
|
|
|
@ -644,20 +644,20 @@ public class SmsDatabase extends MessageDatabase {
|
|||
|
||||
@Override
|
||||
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
|
||||
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
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address) {
|
||||
return insertCallLog(address, Types.MISSED_CALL_TYPE, true);
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp) {
|
||||
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);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
|
||||
|
@ -665,7 +665,7 @@ public class SmsDatabase extends MessageDatabase {
|
|||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
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(TYPE, type);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
|
|
@ -69,10 +69,8 @@ public final class ThreadBodyUtil {
|
|||
} else if (hasImage) {
|
||||
return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo);
|
||||
} 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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -872,22 +872,17 @@ public class ThreadDatabase extends Database {
|
|||
deleteAllThreads();
|
||||
}
|
||||
|
||||
public long getThreadIdIfExistsFor(Recipient recipient) {
|
||||
public long getThreadIdIfExistsFor(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = RECIPIENT_ID + " = ?";
|
||||
String[] recipientsArg = new String[] {recipient.getId().serialize()};
|
||||
Cursor cursor = null;
|
||||
String[] recipientsArg = new String[] {recipientId.serialize()};
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
else
|
||||
return -1L;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID }, where, recipientsArg, null, null, null, "1")) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return CursorUtil.requireLong(cursor, ID);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -950,6 +945,10 @@ public class ThreadDatabase extends Database {
|
|||
return Recipient.resolved(id);
|
||||
}
|
||||
|
||||
public boolean hasThread(@NonNull RecipientId recipientId) {
|
||||
return getThreadIdIfExistsFor(recipientId) > -1;
|
||||
}
|
||||
|
||||
public void setHasSent(long threadId, boolean hasSent) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
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();
|
||||
|
||||
if (thumbnail != null && !((MmsMessageRecord) record).isViewOnce()) {
|
||||
return thumbnail.getThumbnailUri();
|
||||
return thumbnail.getUri();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
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 STICKER_CONTENT_TYPE = 72;
|
||||
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 final Context context;
|
||||
|
@ -1023,6 +1026,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
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();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -270,7 +270,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
if (isPush() && getDateSent() < getDateReceived()) {
|
||||
if ((isPush() || isCallLog()) && getDateSent() < getDateReceived()) {
|
||||
return getDateSent();
|
||||
}
|
||||
return getDateReceived();
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -68,7 +69,7 @@ public final class StickerRecord {
|
|||
}
|
||||
|
||||
public @NonNull String getContentType() {
|
||||
return contentType == null ? MediaUtil.IMAGE_WEBP : contentType;
|
||||
return Util.isEmpty(contentType) ? MediaUtil.IMAGE_WEBP : contentType;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
|
|
|
@ -138,8 +138,7 @@ public class ApplicationDependencies {
|
|||
messageSender.update(
|
||||
IncomingMessageObserver.getPipe(),
|
||||
IncomingMessageObserver.getUnidentifiedPipe(),
|
||||
TextSecurePreferences.isMultiDevice(application),
|
||||
FeatureFlags.attachmentsV3());
|
||||
TextSecurePreferences.isMultiDevice(application));
|
||||
}
|
||||
|
||||
return messageSender;
|
||||
|
|
|
@ -95,7 +95,6 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
|||
new SignalProtocolStoreImpl(context),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
TextSecurePreferences.isMultiDevice(context),
|
||||
FeatureFlags.attachmentsV3(),
|
||||
Optional.fromNullable(IncomingMessageObserver.getPipe()),
|
||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,18 +1,20 @@
|
|||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
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.ringrtc.CameraState;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WebRtcViewModel {
|
||||
|
||||
public enum State {
|
||||
// Normal states
|
||||
CALL_PRE_JOIN,
|
||||
CALL_INCOMING,
|
||||
CALL_OUTGOING,
|
||||
CALL_CONNECTED,
|
||||
|
@ -33,70 +35,34 @@ public class WebRtcViewModel {
|
|||
CALL_ONGOING_ELSEWHERE
|
||||
}
|
||||
|
||||
|
||||
private final @NonNull State state;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @Nullable IdentityKey identityKey;
|
||||
|
||||
private final boolean remoteVideoEnabled;
|
||||
private final @NonNull State state;
|
||||
private final @NonNull Recipient recipient;
|
||||
|
||||
private final boolean isBluetoothAvailable;
|
||||
private final boolean isMicrophoneEnabled;
|
||||
private final boolean isRemoteVideoOffer;
|
||||
private final long callConnectedTime;
|
||||
|
||||
private final CameraState localCameraState;
|
||||
private final TextureViewRenderer localRenderer;
|
||||
private final TextureViewRenderer remoteRenderer;
|
||||
private final CallParticipant localParticipant;
|
||||
private final List<CallParticipant> remoteParticipants;
|
||||
|
||||
private final long callConnectedTime;
|
||||
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull TextureViewRenderer localRenderer,
|
||||
@NonNull TextureViewRenderer remoteRenderer,
|
||||
boolean remoteVideoEnabled,
|
||||
boolean isBluetoothAvailable,
|
||||
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)
|
||||
public WebRtcViewModel(@NonNull State state,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull CameraState localCameraState,
|
||||
@NonNull BroadcastVideoSink localSink,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isMicrophoneEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
long callConnectedTime,
|
||||
@NonNull List<CallParticipant> remoteParticipants)
|
||||
{
|
||||
this.state = state;
|
||||
this.recipient = recipient;
|
||||
this.localCameraState = localCameraState;
|
||||
this.localRenderer = localRenderer;
|
||||
this.remoteRenderer = remoteRenderer;
|
||||
this.identityKey = identityKey;
|
||||
this.remoteVideoEnabled = remoteVideoEnabled;
|
||||
this.isBluetoothAvailable = isBluetoothAvailable;
|
||||
this.isMicrophoneEnabled = isMicrophoneEnabled;
|
||||
this.isRemoteVideoOffer = isRemoteVideoOffer;
|
||||
this.callConnectedTime = callConnectedTime;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
|
||||
localParticipant = CallParticipant.createLocal(localCameraState, localSink, isMicrophoneEnabled);
|
||||
}
|
||||
|
||||
public @NonNull State getState() {
|
||||
|
@ -107,50 +73,28 @@ public class WebRtcViewModel {
|
|||
return recipient;
|
||||
}
|
||||
|
||||
public @NonNull CameraState getLocalCameraState() {
|
||||
return localCameraState;
|
||||
}
|
||||
|
||||
public @Nullable IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public boolean isRemoteVideoEnabled() {
|
||||
return remoteVideoEnabled;
|
||||
return Stream.of(remoteParticipants).anyMatch(CallParticipant::isVideoEnabled);
|
||||
}
|
||||
|
||||
public boolean isBluetoothAvailable() {
|
||||
return isBluetoothAvailable;
|
||||
}
|
||||
|
||||
public boolean isMicrophoneEnabled() {
|
||||
return isMicrophoneEnabled;
|
||||
}
|
||||
|
||||
public boolean isRemoteVideoOffer() {
|
||||
return isRemoteVideoOffer;
|
||||
}
|
||||
|
||||
public TextureViewRenderer getLocalRenderer() {
|
||||
return localRenderer;
|
||||
}
|
||||
|
||||
public TextureViewRenderer getRemoteRenderer() {
|
||||
return remoteRenderer;
|
||||
}
|
||||
|
||||
public long getCallConnectedTime() {
|
||||
return callConnectedTime;
|
||||
}
|
||||
|
||||
public @NonNull String toString() {
|
||||
return "[State: " + state +
|
||||
", recipient: " + recipient.getId().serialize() +
|
||||
", identity: " + identityKey +
|
||||
", remoteVideo: " + remoteVideoEnabled +
|
||||
", localVideo: " + localCameraState.isEnabled() +
|
||||
", isRemoteVideoOffer: " + isRemoteVideoOffer +
|
||||
", callConnectedTime: " + callConnectedTime +
|
||||
"]";
|
||||
public @NonNull CallParticipant getLocalParticipant() {
|
||||
return localParticipant;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getRemoteParticipants() {
|
||||
return remoteParticipants;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(masterKey, "HmacSHA256"));
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -152,9 +153,12 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
|||
|
||||
stopwatch.split("registered");
|
||||
|
||||
List<Recipient> recipientsAndSelf = new ArrayList<>(resolved);
|
||||
recipientsAndSelf.add(Recipient.self().resolve());
|
||||
|
||||
if (FeatureFlags.groupsV2create()) {
|
||||
try {
|
||||
GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(resolved);
|
||||
GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(recipientsAndSelf);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to refresh all recipient capabilities.", e);
|
||||
}
|
||||
|
@ -164,8 +168,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
|||
|
||||
resolved = Recipient.resolvedList(ids);
|
||||
|
||||
if (Stream.of(resolved).anyMatch(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) &&
|
||||
Stream.of(resolved).anyMatch(r -> !r.hasE164()))
|
||||
boolean gv2 = Stream.of(recipientsAndSelf).allMatch(r -> r.getGroupsV2Capability() == Recipient.Capability.SUPPORTED);
|
||||
if (!gv2 && Stream.of(resolved).anyMatch(r -> !r.hasE164()))
|
||||
{
|
||||
Log.w(TAG, "Invalid GV1 group...");
|
||||
ids = Collections.emptyList();
|
||||
|
|
|
@ -24,7 +24,9 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
|
@ -113,6 +115,13 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
});
|
||||
groupJoinButton.setVisibility(View.VISIBLE);
|
||||
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:
|
||||
groupJoinExplain.setText(details.joinRequiresAdminApproval() ? R.string.GroupJoinBottomSheetDialogFragment_admin_approval_needed
|
||||
: 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();
|
||||
|
||||
if (groupJoinStatus == FeatureFlags.GroupJoinStatus.LOCAL_CAN_JOIN) {
|
||||
if (!FeatureFlags.groupsV2() || Recipient.self().getGroupsV2Capability() == Recipient.Capability.NOT_SUPPORTED) {
|
||||
// TODO [Alan] GV2 additional copy could be presented in these cases
|
||||
return FeatureFlags.GroupJoinStatus.UPDATE_TO_JOIN;
|
||||
}
|
||||
|
||||
return groupJoinStatus;
|
||||
}
|
||||
switch (groupJoinStatus) {
|
||||
case COMING_SOON : return ExtendedGroupJoinStatus.COMING_SOON;
|
||||
case UPDATE_TO_JOIN: return ExtendedGroupJoinStatus.UPDATE_TO_JOIN;
|
||||
case LOCAL_CAN_JOIN: {
|
||||
if (Recipient.self().getGroupsV2Capability() != Recipient.Capability.SUPPORTED) {
|
||||
return ExtendedGroupJoinStatus.UPDATE_LINKED_DEVICE_TO_JOIN;
|
||||
}
|
||||
|
||||
return groupJoinStatus;
|
||||
return ExtendedGroupJoinStatus.LOCAL_CAN_JOIN;
|
||||
}
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelProviders;
|
|||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.InviteActivity;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
|
@ -78,7 +79,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private TextView pendingAndRequestingCount;
|
||||
private Toolbar toolbar;
|
||||
private TextView groupName;
|
||||
private LearnMoreTextView groupV1Indicator;
|
||||
private LearnMoreTextView groupInfoText;
|
||||
private TextView memberCountUnderAvatar;
|
||||
private TextView memberCountAboveList;
|
||||
private AvatarImageView avatar;
|
||||
|
@ -139,7 +140,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
avatar = view.findViewById(R.id.group_avatar);
|
||||
toolbar = view.findViewById(R.id.toolbar);
|
||||
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);
|
||||
memberCountAboveList = view.findViewById(R.id.member_count_2);
|
||||
groupMemberList = view.findViewById(R.id.group_members);
|
||||
|
@ -176,9 +177,6 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
groupLinkRow = view.findViewById(R.id.group_link_row);
|
||||
groupLinkButton = view.findViewById(R.id.group_link_button);
|
||||
|
||||
groupV1Indicator.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
|
||||
groupV1Indicator.setLearnMoreVisible(true);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@ -249,7 +247,6 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
|
||||
viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::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.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
|
||||
avatar.setRecipient(groupRecipient);
|
||||
|
@ -376,6 +373,26 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE);
|
||||
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) {
|
||||
|
|
|
@ -80,6 +80,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final LiveData<Boolean> showLegacyIndicator;
|
||||
private final LiveData<String> mentionSetting;
|
||||
private final LiveData<Boolean> groupLinkOn;
|
||||
private final LiveData<GroupInfoMessage> groupInfoMessage;
|
||||
|
||||
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||
this.context = context;
|
||||
|
@ -123,6 +124,16 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
|
||||
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
|
||||
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
|
||||
|
@ -152,10 +163,6 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return fullMemberCountSummary;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getShowLegacyIndicator() {
|
||||
return showLegacyIndicator;
|
||||
}
|
||||
|
||||
LiveData<Recipient> getGroupRecipient() {
|
||||
return groupRecipient;
|
||||
}
|
||||
|
@ -228,6 +235,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return groupLinkOn;
|
||||
}
|
||||
|
||||
LiveData<GroupInfoMessage> getGroupInfoMessage() {
|
||||
return groupInfoMessage;
|
||||
}
|
||||
|
||||
void handleExpirationSelection() {
|
||||
manageGroupRepository.getRecipient(groupRecipient ->
|
||||
ExpirationDialog.show(context,
|
||||
|
@ -397,6 +408,12 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
enum GroupInfoMessage {
|
||||
NONE,
|
||||
LEGACY_GROUP_LEARN_MORE,
|
||||
MMS_WARNING
|
||||
}
|
||||
|
||||
private enum CollapseState {
|
||||
OPEN,
|
||||
COLLAPSED
|
||||
|
|
|
@ -11,7 +11,6 @@ import android.content.SharedPreferences;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
|
@ -36,7 +35,15 @@ public class JobSchedulerScheduler implements Scheduler {
|
|||
@RequiresApi(26)
|
||||
@Override
|
||||
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)
|
||||
.setPersisted(true);
|
||||
|
||||
|
@ -44,12 +51,15 @@ public class JobSchedulerScheduler implements Scheduler {
|
|||
constraint.applyToJobInfo(jobInfoBuilder);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Scheduling a run in " + delay + " ms.");
|
||||
JobScheduler jobScheduler = application.getSystemService(JobScheduler.class);
|
||||
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);
|
||||
int returnedId = prefs.getInt(PREF_NEXT_ID, 0);
|
||||
int nextId = returnedId + 1 > MAX_ID ? 0 : returnedId + 1;
|
||||
|
@ -64,8 +74,6 @@ public class JobSchedulerScheduler implements Scheduler {
|
|||
|
||||
@Override
|
||||
public boolean onStartJob(JobParameters params) {
|
||||
Log.d(TAG, "onStartJob()");
|
||||
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
|
||||
jobManager.addOnEmptyQueueListener(new JobManager.EmptyQueueListener() {
|
||||
|
@ -73,7 +81,6 @@ public class JobSchedulerScheduler implements Scheduler {
|
|||
public void onQueueEmpty() {
|
||||
jobManager.removeOnEmptyQueueListener(this);
|
||||
jobFinished(params, false);
|
||||
Log.d(TAG, "jobFinished()");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -84,7 +91,6 @@ public class JobSchedulerScheduler implements Scheduler {
|
|||
|
||||
@Override
|
||||
public boolean onStopJob(JobParameters params) {
|
||||
Log.d(TAG, "onStopJob()");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -153,8 +153,8 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
if (MediaUtil.isJpeg(attachment)) {
|
||||
MediaStream stripped = getResizedMedia(context, attachment, constraints);
|
||||
attachmentDatabase.updateAttachmentData(attachment, stripped, false);
|
||||
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
|
||||
}
|
||||
attachmentDatabase.markAttachmentAsTransformed(attachmentId);
|
||||
} else if (constraints.canResize(attachment)) {
|
||||
MediaStream resized = getResizedMedia(context, attachment, constraints);
|
||||
attachmentDatabase.updateAttachmentData(attachment, resized, false);
|
||||
|
@ -249,7 +249,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
|
||||
try {
|
||||
BitmapUtil.ScaleResult scaleResult = BitmapUtil.createScaledBytes(context,
|
||||
new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()),
|
||||
new DecryptableStreamUriLoader.DecryptableUri(attachment.getUri()),
|
||||
constraints);
|
||||
|
||||
return new MediaStream(new ByteArrayInputStream(scaleResult.getBitmap()),
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaDataSource;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
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.service.GenericForegroundService;
|
||||
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.whispersystems.libsignal.util.guava.Optional;
|
||||
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.InputStream;
|
||||
import java.util.Objects;
|
||||
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 {
|
||||
try {
|
||||
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
|
||||
if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
|
||||
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(is)
|
||||
.withContentType(attachment.getContentType())
|
||||
|
@ -193,43 +189,34 @@ public final class AttachmentUploadJob extends BaseJob {
|
|||
|
||||
private @Nullable String getImageBlurHash(@NonNull Attachment attachment) throws IOException {
|
||||
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 {
|
||||
if (attachment.getThumbnailUri() != null) {
|
||||
return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getThumbnailUri()));
|
||||
if (attachment.getBlurHash() != null) {
|
||||
return attachment.getBlurHash().getHash();
|
||||
}
|
||||
|
||||
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
|
||||
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Log.w(TAG, "Video thumbnails not supported...");
|
||||
return null;
|
||||
}
|
||||
|
||||
try (MediaDataSource dataSource = DatabaseFactory.getAttachmentDatabase(context).mediaDataSourceFor(attachmentId)) {
|
||||
if (dataSource == null) return null;
|
||||
Bitmap bitmap = MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.getUri()), 1000);
|
||||
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
MediaMetadataRetrieverUtil.setDataSource(retriever, dataSource);
|
||||
if (bitmap != null) {
|
||||
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) {
|
||||
Bitmap thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false);
|
||||
bitmap.recycle();
|
||||
|
||||
Log.i(TAG, "Generated video thumbnail...");
|
||||
String hash = BlurHashEncoder.encode(thumb);
|
||||
thumb.recycle();
|
||||
|
||||
return hash;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return hash;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
|||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigration;
|
||||
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.NetworkConstraintObserver;
|
||||
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.RetrieveProfileJobMigration;
|
||||
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
|
||||
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
|
||||
|
@ -124,6 +127,7 @@ public final class JobManagerFactories {
|
|||
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
|
||||
|
||||
// Migrations
|
||||
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.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) {
|
||||
return new HashMap<String, Constraint.Factory>() {{
|
||||
put(ChargingConstraint.KEY, new ChargingConstraint.Factory());
|
||||
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
|
||||
put(NetworkOrCellServiceConstraint.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) {
|
||||
return Arrays.asList(CellServiceConstraintObserver.getInstance(application),
|
||||
new ChargingConstraintObserver(application),
|
||||
new NetworkConstraintObserver(application),
|
||||
new SqlCipherMigrationConstraintObserver(),
|
||||
new WebsocketDrainedConstraintObserver());
|
||||
|
|
|
@ -134,7 +134,7 @@ public class LeaveGroupJob extends BaseJob {
|
|||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddresses(context, destinations);
|
||||
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);
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChargingConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
@ -27,18 +28,30 @@ import java.text.SimpleDateFormat;
|
|||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocalBackupJob extends BaseJob {
|
||||
public final class LocalBackupJob extends BaseJob {
|
||||
|
||||
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() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("__LOCAL_BACKUP__")
|
||||
.setMaxInstances(1)
|
||||
.setMaxAttempts(3)
|
||||
.build());
|
||||
public static final String TEMP_BACKUP_FILE_PREFIX = ".backup";
|
||||
public static final String TEMP_BACKUP_FILE_SUFFIX = ".tmp";
|
||||
|
||||
public LocalBackupJob(boolean forceNow) {
|
||||
this(buildParameters(forceNow));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -76,6 +89,8 @@ public class LocalBackupJob extends BaseJob {
|
|||
String fileName = String.format("signal-%s.backup", timestamp);
|
||||
File backupFile = new File(backupDirectory, fileName);
|
||||
|
||||
deleteOldTemporaryBackups(backupDirectory);
|
||||
|
||||
if (backupFile.exists()) {
|
||||
throw new IOException("Backup file already exists?");
|
||||
}
|
||||
|
@ -84,7 +99,7 @@ public class LocalBackupJob extends BaseJob {
|
|||
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 {
|
||||
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
|
||||
public boolean onShouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
|
|
|
@ -13,6 +13,9 @@ import com.google.android.mms.pdu_alt.RetrieveConf;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
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.DatabaseFactory;
|
||||
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.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -189,6 +193,7 @@ public class MmsDownloadJob extends BaseJob {
|
|||
Set<RecipientId> members = new HashSet<>();
|
||||
String body = null;
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
List<Contact> sharedContacts = new LinkedList<>();
|
||||
|
||||
RecipientId from = null;
|
||||
|
||||
|
@ -223,14 +228,18 @@ public class MmsDownloadJob extends BaseJob {
|
|||
PduPart part = media.getPart(i);
|
||||
|
||||
if (part.getData() != null) {
|
||||
Uri uri = BlobProvider.getInstance().forData(part.getData()).createForSingleUseInMemory();
|
||||
String name = null;
|
||||
if (Util.toIsoString(part.getContentType()).toLowerCase().equals(MediaUtil.VCARD)){
|
||||
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()),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
part.getData().length, name, false, false, false, null, null, null, null, null));
|
||||
attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()),
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue