mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-20 17:57:29 +00:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50edb5d1f4 | ||
|
|
c6ccfd7e75 | ||
|
|
3796ce69e4 | ||
|
|
9835e31b46 | ||
|
|
a35040c909 | ||
|
|
a4c94638ca | ||
|
|
e70a8ae6a0 | ||
|
|
100359e38d | ||
|
|
cd995aca56 | ||
|
|
3a4bae88ca | ||
|
|
e60eae27fb | ||
|
|
cd6c01e230 | ||
|
|
0af264429f | ||
|
|
a6d3862350 | ||
|
|
3fca4850dd | ||
|
|
ba7e41d9a6 | ||
|
|
fe33ce3413 | ||
|
|
4e25e8aaa2 | ||
|
|
91be826c7d | ||
|
|
fdfe0cddb8 | ||
|
|
e8ef62116f | ||
|
|
caf8bb39d8 | ||
|
|
222ba6ee53 | ||
|
|
8dcda73072 | ||
|
|
810365d334 | ||
|
|
4b31510589 | ||
|
|
dfce9a34b8 | ||
|
|
dc9370c32b | ||
|
|
8dbc721c08 | ||
|
|
6448b84430 | ||
|
|
93d6ce40c3 | ||
|
|
ce5be2c1be | ||
|
|
20fe837022 | ||
|
|
e3ce18fa3e | ||
|
|
864a1d5e93 | ||
|
|
9cf7eec247 | ||
|
|
d9c15621f6 | ||
|
|
fea14218a9 | ||
|
|
dbbded5250 | ||
|
|
d65cfc7981 | ||
|
|
dc9124f291 | ||
|
|
4cd433b6bc | ||
|
|
f9a9ee6b0c | ||
|
|
1741f7ed58 | ||
|
|
d459c751be | ||
|
|
34ef8b52f6 | ||
|
|
5ae96905bb | ||
|
|
b1fdbc0151 | ||
|
|
a5ad27b5f2 | ||
|
|
efcd5052a2 | ||
|
|
f2b10c0ba8 | ||
|
|
f182be2d79 | ||
|
|
41b10630bb | ||
|
|
45915bed90 | ||
|
|
a2c2ab428a | ||
|
|
a05f74d302 | ||
|
|
74e94f3a97 | ||
|
|
15ee8c6cac | ||
|
|
18957b1f41 | ||
|
|
29930cac41 | ||
|
|
e3338dc3ff | ||
|
|
97b7b4a501 | ||
|
|
b471a72856 | ||
|
|
fed7d911a3 | ||
|
|
ca442970a3 | ||
|
|
9dbb77c10a | ||
|
|
1116502bc0 | ||
|
|
edaf17bdd4 | ||
|
|
c61d731358 | ||
|
|
a8415a3484 | ||
|
|
cd2467085e | ||
|
|
64efb3d2a4 | ||
|
|
e05f137bd8 | ||
|
|
0c73ddc08b | ||
|
|
19cc43c442 | ||
|
|
7108fc81a9 | ||
|
|
5943b9d7d6 | ||
|
|
0271e4c918 | ||
|
|
9dc33eff3a | ||
|
|
5aef1c8a68 | ||
|
|
c608a05270 | ||
|
|
e2cfd247c3 | ||
|
|
97eb9154b2 | ||
|
|
d7ff635445 | ||
|
|
aff57fb54e | ||
|
|
e89285a219 | ||
|
|
706f43caa8 | ||
|
|
dc4faf57cb | ||
|
|
7baf8052a2 | ||
|
|
d3c59585fd | ||
|
|
859bb8dc79 | ||
|
|
58cd2e07ba | ||
|
|
a5a6fb590a | ||
|
|
3619993e68 | ||
|
|
88e12c78fa | ||
|
|
5c285b4ac6 | ||
|
|
c6b729c470 | ||
|
|
890014759e | ||
|
|
68c1c43381 | ||
|
|
d0dfcaaad5 | ||
|
|
3cffaddc0a | ||
|
|
bf4cac0c82 | ||
|
|
f680749a00 | ||
|
|
13a67980d9 | ||
|
|
f110d595d2 | ||
|
|
9c8857352b | ||
|
|
c09a1fdba8 | ||
|
|
cdc7033a51 | ||
|
|
fa30c759d7 | ||
|
|
d040be2df0 | ||
|
|
935c831a7f | ||
|
|
867e95eef1 | ||
|
|
2ee04bd1b6 | ||
|
|
75d567e555 | ||
|
|
d8a489971c | ||
|
|
19ce5b5c76 | ||
|
|
7c70ea4d3e | ||
|
|
2784285d47 | ||
|
|
c946a7a1d5 | ||
|
|
3e60b49b8b | ||
|
|
4e7331bbb8 | ||
|
|
b8c7e86223 | ||
|
|
3b925f8674 | ||
|
|
f1f6d41c73 | ||
|
|
29ef1cb1be | ||
|
|
4296085d65 | ||
|
|
c797b09228 | ||
|
|
a870ef0030 | ||
|
|
43ed9e7310 | ||
|
|
bcd27355f9 | ||
|
|
6a14dc69c0 | ||
|
|
ed9acd25f9 | ||
|
|
7b24e66ed3 | ||
|
|
abd3d4b546 | ||
|
|
4040c4240a | ||
|
|
1ee747f3ef | ||
|
|
f88874bec8 | ||
|
|
ed440a2150 | ||
|
|
2fd46b196b | ||
|
|
12dfcaf7e7 | ||
|
|
f4a199f621 | ||
|
|
bb708e0aa3 | ||
|
|
d625740ca4 | ||
|
|
250402e9b9 | ||
|
|
1d2ffe56fb | ||
|
|
d16c0d2887 | ||
|
|
b3555f2f94 | ||
|
|
83a638fc6d | ||
|
|
f1534a710f | ||
|
|
a16845340b | ||
|
|
ffa4725f8e | ||
|
|
7792c66c64 | ||
|
|
1a3985d709 | ||
|
|
4714895c59 | ||
|
|
1e37951701 | ||
|
|
e8be1ad752 | ||
|
|
e316a70b6c | ||
|
|
40a8d21c15 | ||
|
|
28d5ca7ed9 | ||
|
|
110b18545f | ||
|
|
a478605da4 | ||
|
|
f5f1589813 | ||
|
|
0c332b6adb | ||
|
|
ba712ce357 | ||
|
|
2d2395accf | ||
|
|
8634289b7a | ||
|
|
45043fb9a8 | ||
|
|
0449795725 | ||
|
|
a96093f1b7 | ||
|
|
bd4f7691e9 | ||
|
|
e12acbae70 | ||
|
|
48dc4eac10 | ||
|
|
a869c92eee | ||
|
|
4fefd14538 | ||
|
|
c09dbfa47c | ||
|
|
d3c9f66de6 | ||
|
|
01d7694108 | ||
|
|
1425b651d4 | ||
|
|
b1befbeefc | ||
|
|
3a9a84a0b1 | ||
|
|
368284cccc | ||
|
|
ef777f4db9 | ||
|
|
a8e4e8e882 | ||
|
|
cf93760d00 | ||
|
|
dd8b9ff8fb | ||
|
|
bfed03b7b5 | ||
|
|
860f06ec9e | ||
|
|
b58376920f | ||
|
|
4ace075ddf | ||
|
|
dda98a474d | ||
|
|
f1c0df7d87 | ||
|
|
c78e098cb4 | ||
|
|
a3438c4f8d | ||
|
|
92ecf2d5de | ||
|
|
f18b653725 | ||
|
|
5128438cfb | ||
|
|
f29f25822b | ||
|
|
ecfe218840 | ||
|
|
dd33d2b5d0 | ||
|
|
12a8d4e10b | ||
|
|
c5c2fb31b1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ obj/
|
||||
jni/libspeex/.deps/
|
||||
*.sh
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
|
||||
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 695
|
||||
def canonicalVersionName = "4.69.5"
|
||||
def canonicalVersionCode = 714
|
||||
def canonicalVersionName = "4.72.5"
|
||||
|
||||
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
|
||||
@@ -122,8 +131,9 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
@@ -163,6 +173,10 @@ android {
|
||||
exclude 'META-INF/proguard/androidx-annotations.pro'
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled true
|
||||
@@ -190,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\""
|
||||
@@ -199,6 +215,7 @@ android {
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\""
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
|
||||
}
|
||||
@@ -304,7 +321,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.5.0'
|
||||
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>
|
||||
|
||||
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -29,13 +31,23 @@ import java.util.Map;
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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"
|
||||
@@ -136,7 +136,7 @@
|
||||
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"
|
||||
@@ -223,7 +223,8 @@
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<intent-filter android:autoVerify="true"
|
||||
tools:targetApi="23">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -268,6 +269,10 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
@@ -518,7 +523,12 @@
|
||||
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<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=".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"/>
|
||||
@@ -635,18 +645,20 @@
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<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">
|
||||
|
||||
@@ -655,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">
|
||||
|
||||
58
app/src/main/java/org/signal/glide/Log.java
Normal file
58
app/src/main/java/org/signal/glide/Log.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class Log {
|
||||
|
||||
private Log() {}
|
||||
|
||||
public static void v(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().v(tag, message);
|
||||
}
|
||||
|
||||
public static void d(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().d(tag, message);
|
||||
}
|
||||
|
||||
public static void i(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().i(tag, message);
|
||||
}
|
||||
|
||||
public static void w(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().w(tag, message);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
void v(@NonNull String tag, @NonNull String message);
|
||||
void d(@NonNull String tag, @NonNull String message);
|
||||
void i(@NonNull String tag, @NonNull String message);
|
||||
void w(@NonNull String tag, @NonNull String message);
|
||||
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
|
||||
|
||||
Provider EMPTY = new Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class SignalGlideCodecs {
|
||||
|
||||
private static Log.Provider logProvider = Log.Provider.EMPTY;
|
||||
|
||||
private SignalGlideCodecs() {}
|
||||
|
||||
public static void setLogProvider(@NonNull Log.Provider provider) {
|
||||
logProvider = provider;
|
||||
}
|
||||
|
||||
public static @NonNull Log.Provider getLogProvider() {
|
||||
return logProvider;
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.common.FrameAnimationDrawable;
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||
import org.signal.glide.common.loader.FileLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||
|
||||
/**
|
||||
* @Description: APNGDrawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
|
||||
public APNGDrawable(Loader provider) {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
public APNGDrawable(APNGDecoder decoder) {
|
||||
super(decoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
|
||||
return new APNGDecoder(streamLoader, listener);
|
||||
}
|
||||
|
||||
|
||||
public static APNGDrawable fromAsset(Context context, String assetPath) {
|
||||
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
|
||||
return new APNGDrawable(assetStreamLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromFile(String filePath) {
|
||||
FileLoader fileLoader = new FileLoader(filePath);
|
||||
return new APNGDrawable(fileLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromResource(Context context, int resId) {
|
||||
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
|
||||
return new APNGDrawable(resourceStreamLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class ACTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("acTL");
|
||||
int num_frames;
|
||||
int num_plays;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader apngReader) throws IOException {
|
||||
num_frames = apngReader.readInt();
|
||||
num_plays = apngReader.readInt();
|
||||
}
|
||||
}
|
||||
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
|
||||
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||
|
||||
private APNGWriter apngWriter;
|
||||
private int mLoopCount;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
|
||||
private class SnapShot {
|
||||
byte dispose_op;
|
||||
Rect dstRect = new Rect();
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
|
||||
private SnapShot snapShot = new SnapShot();
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
|
||||
super(loader, renderListener);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGWriter getWriter() {
|
||||
if (apngWriter == null) {
|
||||
apngWriter = new APNGWriter();
|
||||
}
|
||||
return apngWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGReader getReader(Reader reader) {
|
||||
return new APNGReader(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLoopCount() {
|
||||
return mLoopCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release() {
|
||||
snapShot.byteBuffer = null;
|
||||
apngWriter = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Rect read(APNGReader reader) throws IOException {
|
||||
List<Chunk> chunks = APNGParser.parse(reader);
|
||||
List<Chunk> otherChunks = new ArrayList<>();
|
||||
|
||||
boolean actl = false;
|
||||
APNGFrame lastFrame = null;
|
||||
byte[] ihdrData = new byte[0];
|
||||
int canvasWidth = 0, canvasHeight = 0;
|
||||
for (Chunk chunk : chunks) {
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
mLoopCount = ((ACTLChunk) chunk).num_plays;
|
||||
actl = true;
|
||||
} else if (chunk instanceof FCTLChunk) {
|
||||
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
|
||||
frame.prefixChunks = otherChunks;
|
||||
frame.ihdrData = ihdrData;
|
||||
frames.add(frame);
|
||||
lastFrame = frame;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
} else if (chunk instanceof IDATChunk) {
|
||||
if (!actl) {
|
||||
//如果为非APNG图片,则只解码PNG
|
||||
Frame frame = new StillFrame(reader);
|
||||
frame.frameWidth = canvasWidth;
|
||||
frame.frameHeight = canvasHeight;
|
||||
frames.add(frame);
|
||||
mLoopCount = 1;
|
||||
break;
|
||||
}
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
|
||||
} else if (chunk instanceof IHDRChunk) {
|
||||
canvasWidth = ((IHDRChunk) chunk).width;
|
||||
canvasHeight = ((IHDRChunk) chunk).height;
|
||||
ihdrData = ((IHDRChunk) chunk).data;
|
||||
} else if (!(chunk instanceof IENDChunk)) {
|
||||
otherChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderFrame(Frame frame) {
|
||||
if (frame == null || fullRect == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
|
||||
Canvas canvas = cachedCanvas.get(bitmap);
|
||||
if (canvas == null) {
|
||||
canvas = new Canvas(bitmap);
|
||||
cachedCanvas.put(bitmap, canvas);
|
||||
}
|
||||
if (frame instanceof APNGFrame) {
|
||||
// 从缓存中恢复当前帧
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
// 开始绘制前,处理快照中的设定
|
||||
if (this.frameIndex == 0) {
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.clipRect(snapShot.dstRect);
|
||||
switch (snapShot.dispose_op) {
|
||||
// 从快照中恢复上一帧之前的显示内容
|
||||
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
|
||||
break;
|
||||
// 清空上一帧所画区域
|
||||
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
break;
|
||||
// 什么都不做
|
||||
case FCTLChunk.APNG_DISPOSE_OP_NON:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// 然后根据dispose设定传递到快照信息中
|
||||
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
|
||||
canvas.save();
|
||||
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
|
||||
canvas.clipRect(
|
||||
frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
}
|
||||
|
||||
|
||||
snapShot.dstRect.set(frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.restore();
|
||||
}
|
||||
//开始真正绘制当前帧的内容
|
||||
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
|
||||
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
|
||||
recycleBitmap(inBitmap);
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(frameBuffer);
|
||||
recycleBitmap(bitmap);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "Failed to render!", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
|
||||
public final byte blend_op;
|
||||
public final byte dispose_op;
|
||||
byte[] ihdrData;
|
||||
List<Chunk> imageChunks = new ArrayList<>();
|
||||
List<Chunk> prefixChunks = new ArrayList<>();
|
||||
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
|
||||
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
|
||||
|
||||
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
|
||||
|
||||
private CRC32 getCRC32() {
|
||||
CRC32 crc32 = sCRC32.get();
|
||||
if (crc32 == null) {
|
||||
crc32 = new CRC32();
|
||||
sCRC32.set(crc32);
|
||||
}
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
|
||||
super(reader);
|
||||
blend_op = fctlChunk.blend_op;
|
||||
dispose_op = fctlChunk.dispose_op;
|
||||
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
|
||||
frameWidth = fctlChunk.width;
|
||||
frameHeight = fctlChunk.height;
|
||||
frameX = fctlChunk.x_offset;
|
||||
frameY = fctlChunk.y_offset;
|
||||
}
|
||||
|
||||
private int encode(APNGWriter apngWriter) throws IOException {
|
||||
int fileSize = 8 + 13 + 12;
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
fileSize += chunk.length + 12;
|
||||
}
|
||||
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
fileSize += chunk.length + 12;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
fileSize += chunk.length + 8;
|
||||
}
|
||||
}
|
||||
fileSize += sPNGEndChunk.length;
|
||||
apngWriter.reset(fileSize);
|
||||
apngWriter.putBytes(sPNGSignatures);
|
||||
//IHDR Chunk
|
||||
apngWriter.writeInt(13);
|
||||
int start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IHDRChunk.ID);
|
||||
apngWriter.writeInt(frameWidth);
|
||||
apngWriter.writeInt(frameHeight);
|
||||
apngWriter.putBytes(ihdrData);
|
||||
CRC32 crc32 = getCRC32();
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, 17);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
if (chunk instanceof IENDChunk) {
|
||||
continue;
|
||||
}
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
}
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
apngWriter.writeInt(chunk.length - 4);
|
||||
start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IDATChunk.ID);
|
||||
|
||||
reader.reset();
|
||||
// skip to fdat data position
|
||||
reader.skip(chunk.offset + 4 + 4 + 4);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
|
||||
|
||||
apngWriter.skip(chunk.length - 4);
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, chunk.length);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
}
|
||||
}
|
||||
//endChunk
|
||||
apngWriter.putBytes(sPNGEndChunk);
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
try {
|
||||
int length = encode(writer);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
byte[] bytes = writer.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
|
||||
assert bitmap != null;
|
||||
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
|
||||
return bitmap;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGParser {
|
||||
static class FormatException extends IOException {
|
||||
FormatException() {
|
||||
super("APNG Format error");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(String filePath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(filePath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, String assetPath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, int resId) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getResources().openRawResource(resId);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Reader in) {
|
||||
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
|
||||
try {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
while (reader.available() > 0) {
|
||||
Chunk chunk = parseChunk(reader);
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Chunk> parse(APNGReader reader) throws IOException {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
List<Chunk> chunks = new ArrayList<>();
|
||||
while (reader.available() > 0) {
|
||||
chunks.add(parseChunk(reader));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static Chunk parseChunk(APNGReader reader) throws IOException {
|
||||
int offset = reader.position();
|
||||
int size = reader.readInt();
|
||||
int fourCC = reader.readFourCC();
|
||||
Chunk chunk;
|
||||
if (fourCC == ACTLChunk.ID) {
|
||||
chunk = new ACTLChunk();
|
||||
} else if (fourCC == FCTLChunk.ID) {
|
||||
chunk = new FCTLChunk();
|
||||
} else if (fourCC == FDATChunk.ID) {
|
||||
chunk = new FDATChunk();
|
||||
} else if (fourCC == IDATChunk.ID) {
|
||||
chunk = new IDATChunk();
|
||||
} else if (fourCC == IENDChunk.ID) {
|
||||
chunk = new IENDChunk();
|
||||
} else if (fourCC == IHDRChunk.ID) {
|
||||
chunk = new IHDRChunk();
|
||||
} else {
|
||||
chunk = new Chunk();
|
||||
}
|
||||
chunk.offset = offset;
|
||||
chunk.fourcc = fourCC;
|
||||
chunk.length = size;
|
||||
chunk.parse(reader);
|
||||
chunk.crc = reader.readInt();
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
|
||||
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
|
||||
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
|
||||
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
|
||||
* @Link https://www.w3.org/TR/PNG
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class Chunk {
|
||||
int length;
|
||||
int fourcc;
|
||||
int crc;
|
||||
int offset;
|
||||
|
||||
static int fourCCToInt(String fourCC) {
|
||||
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
|
||||
return 0xbadeffff;
|
||||
}
|
||||
return (fourCC.charAt(0) & 0xff)
|
||||
| (fourCC.charAt(1) & 0xff) << 8
|
||||
| (fourCC.charAt(2) & 0xff) << 16
|
||||
| (fourCC.charAt(3) & 0xff) << 24
|
||||
;
|
||||
}
|
||||
|
||||
void parse(APNGReader reader) throws IOException {
|
||||
int available = reader.available();
|
||||
innerParse(reader);
|
||||
int offset = available - reader.available();
|
||||
if (offset > length) {
|
||||
throw new IOException("Out of chunk area");
|
||||
} else if (offset < length) {
|
||||
reader.skip(length - offset);
|
||||
}
|
||||
}
|
||||
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
|
||||
*/
|
||||
class FCTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fcTL");
|
||||
int sequence_number;
|
||||
/**
|
||||
* x_offset >= 0
|
||||
* y_offset >= 0
|
||||
* width > 0
|
||||
* height > 0
|
||||
* x_offset + width <= 'IHDR' width
|
||||
* y_offset + height <= 'IHDR' height
|
||||
*/
|
||||
/**
|
||||
* Width of the following frame.
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* Height of the following frame.
|
||||
*/
|
||||
int height;
|
||||
/**
|
||||
* X position at which to render the following frame.
|
||||
*/
|
||||
int x_offset;
|
||||
/**
|
||||
* Y position at which to render the following frame.
|
||||
*/
|
||||
int y_offset;
|
||||
|
||||
/**
|
||||
* The delay_num and delay_den parameters together specify a fraction indicating the time to
|
||||
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
|
||||
* were 100 (that is, delay_num then specifies 1/100ths of a second).
|
||||
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
|
||||
* possible, though viewers may impose a reasonable lower bound.
|
||||
* <p>
|
||||
* Frame timings should be independent of the time required for decoding and display of each frame,
|
||||
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Frame delay fraction numerator.
|
||||
*/
|
||||
short delay_num;
|
||||
|
||||
/**
|
||||
* Frame delay fraction denominator.
|
||||
*/
|
||||
short delay_den;
|
||||
|
||||
/**
|
||||
* Type of frame area disposal to be done after rendering this frame.
|
||||
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
|
||||
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
|
||||
*/
|
||||
byte dispose_op;
|
||||
|
||||
/**
|
||||
* Type of frame area rendering for this frame.
|
||||
*/
|
||||
byte blend_op;
|
||||
|
||||
/**
|
||||
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_NON = 0;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
|
||||
|
||||
/**
|
||||
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
|
||||
* or whether it should completely replace its region in the output buffer.
|
||||
*/
|
||||
/**
|
||||
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_SOURCE = 0;
|
||||
|
||||
/**
|
||||
* The frame should be composited onto the output buffer based on its alpha,
|
||||
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
|
||||
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_OVER = 1;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
x_offset = reader.readInt();
|
||||
y_offset = reader.readInt();
|
||||
delay_num = reader.readShort();
|
||||
delay_den = reader.readShort();
|
||||
dispose_op = reader.peek();
|
||||
blend_op = reader.peek();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class FDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fdAT");
|
||||
int sequence_number;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IDAT");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IENDChunk extends Chunk {
|
||||
static final int ID = Chunk.fourCCToInt("IEND");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
|
||||
* <p>
|
||||
* Width 4 bytes
|
||||
* Height 4 bytes
|
||||
* Bit depth 1 byte
|
||||
* Colour type 1 byte
|
||||
* Compression method 1 byte
|
||||
* Filter method 1 byte
|
||||
* Interlace method 1 byte
|
||||
*
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IHDRChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IHDR");
|
||||
/**
|
||||
* 图像宽度,以像素为单位
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* 图像高度,以像素为单位
|
||||
*/
|
||||
int height;
|
||||
|
||||
byte[] data = new byte[5];
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
reader.read(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class StillFrame extends Frame<APNGReader, APNGWriter> {
|
||||
|
||||
public StillFrame(APNGReader reader) {
|
||||
super(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
reader.reset();
|
||||
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
|
||||
assert bitmap != null;
|
||||
paint.setXfermode(null);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.common.io.FilterReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNGReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGReader extends FilterReader {
|
||||
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
|
||||
|
||||
|
||||
protected static byte[] ensureBytes() {
|
||||
byte[] bytes = __intBytes.get();
|
||||
if (bytes == null) {
|
||||
bytes = new byte[4];
|
||||
__intBytes.set(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public APNGReader(Reader in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[3] & 0xFF |
|
||||
(buf[2] & 0xFF) << 8 |
|
||||
(buf[1] & 0xFF) << 16 |
|
||||
(buf[0] & 0xFF) << 24;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 2);
|
||||
return (short) (buf[1] & 0xFF |
|
||||
(buf[0] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return read FourCC and match chars
|
||||
*/
|
||||
public boolean matchFourCC(String chars) throws IOException {
|
||||
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
int fourCC = readFourCC();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int readFourCC() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferWriter;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: APNGWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGWriter extends ByteBufferWriter {
|
||||
public APNGWriter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void writeFourCC(int val) {
|
||||
putByte((byte) (val & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
}
|
||||
|
||||
public void writeInt(int val) {
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) (val & 0xff));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
super.reset(size);
|
||||
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.DrawFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PaintFlagsDrawFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: Frame animation drawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||
private final Paint paint = new Paint();
|
||||
private final Decoder frameSeqDecoder;
|
||||
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
private Matrix matrix = new Matrix();
|
||||
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
|
||||
private Bitmap bitmap;
|
||||
private static final int MSG_ANIMATION_START = 1;
|
||||
private static final int MSG_ANIMATION_END = 2;
|
||||
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ANIMATION_START:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
case MSG_ANIMATION_END:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private Runnable invalidateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
invalidateSelf();
|
||||
}
|
||||
};
|
||||
private boolean autoPlay = true;
|
||||
|
||||
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = frameSeqDecoder;
|
||||
}
|
||||
|
||||
public FrameAnimationDrawable(Loader provider) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
|
||||
}
|
||||
|
||||
public void setAutoPlay(boolean autoPlay) {
|
||||
this.autoPlay = autoPlay;
|
||||
}
|
||||
|
||||
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
|
||||
|
||||
/**
|
||||
* @param loopLimit <=0为无限播放,>0为实际播放次数
|
||||
*/
|
||||
public void setLoopLimit(int loopLimit) {
|
||||
frameSeqDecoder.setLoopLimit(loopLimit);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
frameSeqDecoder.reset();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
frameSeqDecoder.pause();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
frameSeqDecoder.resume();
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return frameSeqDecoder.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.start();
|
||||
} else {
|
||||
this.frameSeqDecoder.addRenderListener(this);
|
||||
if (!this.frameSeqDecoder.isRunning()) {
|
||||
this.frameSeqDecoder.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.stop();
|
||||
} else {
|
||||
this.frameSeqDecoder.removeRenderListener(this);
|
||||
this.frameSeqDecoder.stopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return frameSeqDecoder.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
canvas.setDrawFilter(drawFilter);
|
||||
canvas.drawBitmap(bitmap, matrix, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
|
||||
matrix.setScale(
|
||||
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
|
||||
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
|
||||
|
||||
if (sampleSizeChanged)
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(ByteBuffer byteBuffer) {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (this.bitmap == null || this.bitmap.isRecycled()) {
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
byteBuffer.rewind();
|
||||
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
|
||||
Log.e(TAG, "onRender:Buffer not large enough for pixels");
|
||||
return;
|
||||
}
|
||||
this.bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||
uiHandler.post(invalidateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
if (this.autoPlay) {
|
||||
if (visible) {
|
||||
if (!isRunning()) {
|
||||
start();
|
||||
}
|
||||
} else if (isRunning()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
return super.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().width();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().height();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
this.animationCallbacks.add(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
return this.animationCallbacks.remove(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimationCallbacks() {
|
||||
this.animationCallbacks.clear();
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
|
||||
/**
|
||||
* @Description: One frame in an animation
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public abstract class Frame<R extends Reader, W extends Writer> {
|
||||
protected final R reader;
|
||||
public int frameWidth;
|
||||
public int frameHeight;
|
||||
public int frameX;
|
||||
public int frameY;
|
||||
public int frameDuration;
|
||||
|
||||
public Frame(R reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* @Description: Abstract Frame Animation Decoder
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||
private final int taskId;
|
||||
|
||||
private final Loader mLoader;
|
||||
private final Handler workerHandler;
|
||||
protected List<Frame> frames = new ArrayList<>();
|
||||
protected int frameIndex = -1;
|
||||
private int playCount;
|
||||
private Integer loopLimit = null;
|
||||
private Set<RenderListener> renderListeners = new HashSet<>();
|
||||
private AtomicBoolean paused = new AtomicBoolean(true);
|
||||
private static final Rect RECT_EMPTY = new Rect();
|
||||
private Runnable renderTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (paused.get()) {
|
||||
return;
|
||||
}
|
||||
if (canStep()) {
|
||||
long start = System.currentTimeMillis();
|
||||
long delay = step();
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
workerHandler.postDelayed(this, Math.max(0, delay - cost));
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onRender(frameBuffer);
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
protected int sampleSize = 1;
|
||||
|
||||
private Set<Bitmap> cacheBitmaps = new HashSet<>();
|
||||
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
|
||||
protected ByteBuffer frameBuffer;
|
||||
protected volatile Rect fullRect;
|
||||
private W mWriter = getWriter();
|
||||
private R mReader = null;
|
||||
|
||||
/**
|
||||
* If played all the needed
|
||||
*/
|
||||
private boolean finished = false;
|
||||
|
||||
private enum State {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
INITIALIZING,
|
||||
FINISHING,
|
||||
}
|
||||
|
||||
private volatile State mState = State.IDLE;
|
||||
|
||||
public Loader getLoader() {
|
||||
return mLoader;
|
||||
}
|
||||
|
||||
protected abstract W getWriter();
|
||||
|
||||
protected abstract R getReader(Reader reader);
|
||||
|
||||
protected Bitmap obtainBitmap(int width, int height) {
|
||||
Bitmap ret = null;
|
||||
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int reuseSize = width * height * 4;
|
||||
ret = iterator.next();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
|
||||
iterator.remove();
|
||||
if (ret.getWidth() != width || ret.getHeight() != height) {
|
||||
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
ret.eraseColor(0);
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
if (ret != null && ret.getByteCount() >= reuseSize) {
|
||||
if (ret.getWidth() == width && ret.getHeight() == height) {
|
||||
iterator.remove();
|
||||
ret.eraseColor(0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
||||
ret = Bitmap.createBitmap(width, height, config);
|
||||
} catch (OutOfMemoryError e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected void recycleBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
|
||||
cacheBitmaps.add(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码器的渲染回调
|
||||
*/
|
||||
public interface RenderListener {
|
||||
/**
|
||||
* 播放开始
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 帧播放
|
||||
*/
|
||||
void onRender(ByteBuffer byteBuffer);
|
||||
|
||||
/**
|
||||
* 播放结束
|
||||
*/
|
||||
void onEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
|
||||
this.mLoader = loader;
|
||||
if (renderListener != null) {
|
||||
this.renderListeners.add(renderListener);
|
||||
}
|
||||
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
|
||||
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
|
||||
}
|
||||
|
||||
|
||||
public void addRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.add(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.remove(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopIfNeeded() {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (renderListeners.size() == 0) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Rect getBounds() {
|
||||
if (fullRect == null) {
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, "In finishing,do not interrupt");
|
||||
}
|
||||
final Thread thread = Thread.currentThread();
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (fullRect == null) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fullRect = RECT_EMPTY;
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
}
|
||||
});
|
||||
LockSupport.park(thread);
|
||||
}
|
||||
return fullRect;
|
||||
}
|
||||
|
||||
private void initCanvasBounds(Rect rect) {
|
||||
fullRect = rect;
|
||||
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||
if (mWriter == null) {
|
||||
mWriter = getWriter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int getFrameCount() {
|
||||
return this.frames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Loop Count defined in file
|
||||
*/
|
||||
protected abstract int getLoopCount();
|
||||
|
||||
public void start() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.RUNNING || mState == State.INITIALIZING) {
|
||||
Log.i(TAG, debugInfo() + " Already started");
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.INITIALIZING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStart();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStart() {
|
||||
paused.compareAndSet(true, false);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
try {
|
||||
if (frames.size() == 0) {
|
||||
try {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
|
||||
mState = State.RUNNING;
|
||||
}
|
||||
if (getNumPlays() == 0 || !finished) {
|
||||
this.frameIndex = -1;
|
||||
renderTask.run();
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onStart();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, debugInfo() + " No need to started");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStop() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
frames.clear();
|
||||
for (Bitmap bitmap : cacheBitmaps) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
cacheBitmaps.clear();
|
||||
if (frameBuffer != null) {
|
||||
frameBuffer = null;
|
||||
}
|
||||
cachedCanvas.clear();
|
||||
try {
|
||||
if (mReader != null) {
|
||||
mReader.close();
|
||||
mReader = null;
|
||||
}
|
||||
if (mWriter != null) {
|
||||
mWriter.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
release();
|
||||
mState = State.IDLE;
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING || mState == State.IDLE) {
|
||||
Log.i(TAG, debugInfo() + "No need to stop");
|
||||
return;
|
||||
}
|
||||
if (mState == State.INITIALIZING) {
|
||||
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.FINISHING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStop();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String debugInfo() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected abstract void release();
|
||||
|
||||
public boolean isRunning() {
|
||||
return mState == State.RUNNING || mState == State.INITIALIZING;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused.get();
|
||||
}
|
||||
|
||||
public void setLoopLimit(int limit) {
|
||||
this.loopLimit = limit;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.playCount = 0;
|
||||
this.frameIndex = -1;
|
||||
this.finished = false;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
paused.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.compareAndSet(true, false);
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(renderTask);
|
||||
}
|
||||
|
||||
|
||||
public int getSampleSize() {
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
public boolean setDesiredSize(int width, int height) {
|
||||
boolean sampleSizeChanged = false;
|
||||
int sample = getDesiredSample(width, height);
|
||||
if (sample != this.sampleSize) {
|
||||
this.sampleSize = sample;
|
||||
sampleSizeChanged = true;
|
||||
final boolean tempRunning = isRunning();
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
try {
|
||||
initCanvasBounds(read(getReader(mLoader.obtain())));
|
||||
if (tempRunning) {
|
||||
innerStart();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return sampleSizeChanged;
|
||||
}
|
||||
|
||||
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
|
||||
if (desiredWidth == 0 || desiredHeight == 0) {
|
||||
return 1;
|
||||
}
|
||||
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
|
||||
int sample = 1;
|
||||
while ((sample * 2) <= radio) {
|
||||
sample *= 2;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
protected abstract Rect read(R reader) throws IOException;
|
||||
|
||||
private int getNumPlays() {
|
||||
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
|
||||
}
|
||||
|
||||
private boolean canStep() {
|
||||
if (!isRunning()) {
|
||||
return false;
|
||||
}
|
||||
if (frames.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (getNumPlays() <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.playCount < getNumPlays() - 1) {
|
||||
return true;
|
||||
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private long step() {
|
||||
this.frameIndex++;
|
||||
if (this.frameIndex >= this.getFrameCount()) {
|
||||
this.frameIndex = 0;
|
||||
this.playCount++;
|
||||
}
|
||||
Frame frame = getFrame(this.frameIndex);
|
||||
if (frame == null) {
|
||||
return 0;
|
||||
}
|
||||
renderFrame(frame);
|
||||
return frame.frameDuration;
|
||||
}
|
||||
|
||||
protected abstract void renderFrame(Frame frame);
|
||||
|
||||
private Frame getFrame(int index) {
|
||||
if (index < 0 || index >= frames.size()) {
|
||||
return null;
|
||||
}
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Indexed frame
|
||||
*
|
||||
* @param index <0 means reverse from last index
|
||||
*/
|
||||
public Bitmap getFrameBitmap(int index) throws IOException {
|
||||
if (mState != State.IDLE) {
|
||||
Log.e(TAG, debugInfo() + ",stop first");
|
||||
return null;
|
||||
}
|
||||
mState = State.RUNNING;
|
||||
paused.compareAndSet(true, false);
|
||||
if (frames.size() == 0) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
if (index < 0) {
|
||||
index += this.frames.size();
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
frameIndex = -1;
|
||||
while (frameIndex < index) {
|
||||
if (canStep()) {
|
||||
step();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
frameBuffer.rewind();
|
||||
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
innerStop();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.executor;
|
||||
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Description: com.github.penfeizhou.animation.executor
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-11-21
|
||||
*/
|
||||
public class FrameDecoderExecutor {
|
||||
private static int sPoolNumber = 4;
|
||||
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private FrameDecoderExecutor() {
|
||||
}
|
||||
|
||||
static class Inner {
|
||||
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
|
||||
}
|
||||
|
||||
public void setPoolSize(int size) {
|
||||
sPoolNumber = size;
|
||||
}
|
||||
|
||||
public static FrameDecoderExecutor getInstance() {
|
||||
return Inner.sInstance;
|
||||
}
|
||||
|
||||
public Looper getLooper(int taskId) {
|
||||
int idx = taskId % sPoolNumber;
|
||||
if (idx >= mHandlerThreadGroup.size()) {
|
||||
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
|
||||
handlerThread.start();
|
||||
|
||||
mHandlerThreadGroup.add(handlerThread);
|
||||
Looper looper = handlerThread.getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
if (mHandlerThreadGroup.get(idx) != null) {
|
||||
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int generateTaskId() {
|
||||
return counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public class ByteBufferReader implements Reader {
|
||||
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferReader(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
byteBuffer.position((int) (byteBuffer.position() + total));
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return byteBuffer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
byteBuffer.get(buffer, start, byteCount);
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return byteBuffer.limit() - byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(byteBuffer.array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public class ByteBufferWriter implements Writer {
|
||||
|
||||
protected ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferWriter() {
|
||||
reset(10 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putByte(byte b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBytes(byte[] b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip(int length) {
|
||||
byteBuffer.position(length + position());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
if (byteBuffer == null || size > byteBuffer.capacity()) {
|
||||
byteBuffer = ByteBuffer.allocate(size);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
byteBuffer.clear();
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: FileReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FileReader extends FilterReader {
|
||||
private final File mFile;
|
||||
|
||||
public FileReader(File file) throws IOException {
|
||||
super(new StreamReader(new FileInputStream(file)));
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.close();
|
||||
reader = new StreamReader(new FileInputStream(mFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: FilterReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FilterReader implements Reader {
|
||||
protected Reader reader;
|
||||
|
||||
public FilterReader(Reader in) {
|
||||
this.reader = in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
return reader.skip(total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return reader.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return reader.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
return reader.read(buffer, start, byteCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return reader.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
reset();
|
||||
return reader.toInputStream();
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public interface Reader {
|
||||
long skip(long total) throws IOException;
|
||||
|
||||
byte peek() throws IOException;
|
||||
|
||||
void reset() throws IOException;
|
||||
|
||||
int position();
|
||||
|
||||
int read(byte[] buffer, int start, int byteCount) throws IOException;
|
||||
|
||||
int available() throws IOException;
|
||||
|
||||
/**
|
||||
* close io
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
InputStream toInputStream() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public class StreamReader extends FilterInputStream implements Reader {
|
||||
private int position;
|
||||
|
||||
public StreamReader(InputStream in) {
|
||||
super(in);
|
||||
try {
|
||||
in.reset();
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
byte ret = (byte) read();
|
||||
position++;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int ret = super.read(b, off, len);
|
||||
position += Math.max(0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
super.reset();
|
||||
position = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long ret = super.skip(n);
|
||||
position += ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public interface Writer {
|
||||
void reset(int size);
|
||||
|
||||
void putByte(byte b);
|
||||
|
||||
void putBytes(byte[] b);
|
||||
|
||||
int position();
|
||||
|
||||
void skip(int length);
|
||||
|
||||
byte[] toByteArray();
|
||||
|
||||
void close() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从Asset中读取流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class AssetStreamLoader extends StreamLoader {
|
||||
|
||||
private final Context mContext;
|
||||
private final String mAssetName;
|
||||
|
||||
public AssetStreamLoader(Context context, String assetName) {
|
||||
mContext = context.getApplicationContext();
|
||||
mAssetName = assetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getAssets().open(mAssetName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferLoader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-15
|
||||
*/
|
||||
public abstract class ByteBufferLoader implements Loader {
|
||||
public abstract ByteBuffer getByteBuffer();
|
||||
|
||||
@Override
|
||||
public Reader obtain() throws IOException {
|
||||
return new ByteBufferReader(getByteBuffer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.FileReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: 从文件加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class FileLoader implements Loader {
|
||||
|
||||
private final File mFile;
|
||||
private Reader mReader;
|
||||
|
||||
public FileLoader(String path) {
|
||||
mFile = new File(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Reader obtain() throws IOException {
|
||||
return new FileReader(mFile);
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Loader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public interface Loader {
|
||||
Reader obtain() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从资源加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class ResourceStreamLoader extends StreamLoader {
|
||||
private final Context mContext;
|
||||
private final int mResId;
|
||||
|
||||
|
||||
public ResourceStreamLoader(Context context, int resId) {
|
||||
mContext = context.getApplicationContext();
|
||||
mResId = resId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getResources().openRawResource(mResId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public abstract class StreamLoader implements Loader {
|
||||
protected abstract InputStream getInputStream() throws IOException;
|
||||
|
||||
|
||||
public final synchronized Reader obtain() throws IOException {
|
||||
return new StreamReader(getInputStream());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
@@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -68,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
@@ -127,12 +131,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeGlideCodecs();
|
||||
|
||||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
RefreshPreKeysJob.scheduleIfNecessary();
|
||||
StorageSyncHelper.scheduleRoutineSync();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
RegistrationUtil.maybeMarkRegistrationComplete(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
|
||||
@@ -150,10 +154,12 @@ 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();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
checkBuildExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
@@ -378,6 +391,35 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) {
|
||||
Log.v(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) {
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
Log.e(tag, message, throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
|
||||
@@ -17,12 +17,15 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -30,6 +33,7 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
@@ -39,10 +43,14 @@ import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
@@ -60,6 +68,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
|
||||
|
||||
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
|
||||
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
|
||||
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
|
||||
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
|
||||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||
@@ -69,6 +78,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
@@ -134,6 +144,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
public void pushFragment(@NonNull Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(android.R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
@Override
|
||||
@@ -142,6 +160,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setOnPreferenceClickListener(new UsernameClickListener());
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
@@ -159,7 +179,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
|
||||
tintIcons();
|
||||
}
|
||||
@@ -174,6 +196,24 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
|
||||
pref.setVisible(shouldDisplayUsernameReminder());
|
||||
pref.setOnLongClickListener(v -> {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
|
||||
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
SignalStore.misc().hideUsernameReminder();
|
||||
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -188,6 +228,11 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private void setCategorySummaries() {
|
||||
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
|
||||
|
||||
if (FeatureFlags.usernames()) {
|
||||
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
|
||||
.setVisible(shouldDisplayUsernameReminder());
|
||||
}
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
@@ -207,6 +252,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldDisplayUsernameReminder() {
|
||||
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
|
||||
@@ -247,6 +296,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -255,14 +307,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
|
||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||
fragmentTransaction.addToBackStack(null);
|
||||
fragmentTransaction.commit();
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -276,6 +321,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -19,6 +20,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base class for all activities. The vast majority of activities shouldn't extend this directly.
|
||||
* Instead, they should extend {@link PassphraseRequiredActivity} so they're protected by
|
||||
@@ -72,9 +75,9 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.LOLLIPOP)
|
||||
@TargetApi(21)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
}
|
||||
@@ -87,4 +90,8 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
private void logEvent(@NonNull String event) {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
|
||||
public final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -113,7 +113,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
@@ -107,7 +106,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||
public static final String SELECTION_LIMIT = "selection_limit";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
@@ -209,7 +208,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
@@ -475,7 +474,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
|
||||
markContactSelected(selected);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
@@ -493,7 +492,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
});
|
||||
} else {
|
||||
if (onContactSelectedListener != null) {
|
||||
if (onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
|
||||
markContactSelected(selectedContact);
|
||||
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
|
||||
}
|
||||
@@ -513,7 +512,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
}
|
||||
|
||||
private boolean selectionLimitReached() {
|
||||
return getChipCount() >= selectionLimit;
|
||||
return getChipCount() + currentSelection.size() >= selectionLimit;
|
||||
}
|
||||
|
||||
private void markContactSelected(@NonNull SelectedContact selectedContact) {
|
||||
@@ -632,7 +631,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
public interface OnContactSelectedListener {
|
||||
/** @return True if the contact is allowed to be selected, otherwise false. */
|
||||
boolean onContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
|
||||
void onContactDeselected(Optional<RecipientId> recipientId, String number);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
|
||||
@@ -103,7 +102,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
|
||||
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
@@ -121,14 +120,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
}
|
||||
|
||||
private void sendSmsInvites() {
|
||||
@@ -138,12 +137,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
.toArray(new SelectedContact[0]));
|
||||
}
|
||||
|
||||
private void updateSmsButtonText() {
|
||||
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
|
||||
private void updateSmsButtonText(int count) {
|
||||
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
|
||||
selectedContacts.size(),
|
||||
selectedContacts.size()));
|
||||
smsSendButton.setEnabled(!selectedContacts.isEmpty());
|
||||
count,
|
||||
count));
|
||||
smsSendButton.setEnabled(count > 0);
|
||||
}
|
||||
|
||||
@Override public void onBackPressed() {
|
||||
@@ -157,7 +155,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private void cancelSmsSelection() {
|
||||
setPrimaryColorsToolbarNormal();
|
||||
contactsFragment.reset();
|
||||
updateSmsButtonText();
|
||||
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
|
||||
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -261,6 +258,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
albumRail.setAdapter(albumRailAdapter);
|
||||
|
||||
@@ -273,9 +271,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() {
|
||||
@@ -431,13 +429,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
inflater.inflate(R.menu.media_preview, menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (!isMediaInDb()) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
menu.findItem(R.id.delete).setVisible(false);
|
||||
@@ -447,6 +449,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -541,7 +544,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
public boolean singleTapOnMedia() {
|
||||
toggleUiVisibility();
|
||||
fullscreenHelper.toggleUiVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -551,32 +554,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
|
||||
@@ -692,33 +669,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")
|
||||
@@ -796,7 +746,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;
|
||||
@@ -60,21 +61,22 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
if (recipientId.isPresent()) {
|
||||
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);
|
||||
@@ -138,11 +140,11 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.clear();
|
||||
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
|
||||
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,12 +132,13 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.log_submit, menu);
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,17 +19,12 @@ 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;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -37,7 +32,6 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -45,10 +39,12 @@ 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.crypto.storage.TextSecureIdentityKeyStore;
|
||||
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;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
@@ -56,24 +52,19 @@ 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.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity {
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
|
||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -95,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.webrtc_call_activity);
|
||||
//noinspection ConstantConditions
|
||||
getSupportActionBar().hide();
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
|
||||
@@ -134,6 +126,11 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
if (!isInPipMode()) {
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,11 +139,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
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
|
||||
@@ -172,7 +171,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private boolean enterPipModeIfPossible() {
|
||||
if (isSystemPipEnabledAndAvailable()) {
|
||||
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
|
||||
PictureInPictureParams params = new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(9, 16))
|
||||
.build();
|
||||
@@ -206,21 +205,18 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -385,19 +381,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
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);
|
||||
@@ -408,99 +398,70 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
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(BUSY_SIGNAL_DELAY_FINISH);
|
||||
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);
|
||||
}
|
||||
|
||||
String name = recipient.getDisplayName(this);
|
||||
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||
}
|
||||
|
||||
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()));
|
||||
|
||||
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
||||
untrustedIdentityExplanation.setText(spannableString);
|
||||
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(untrustedIdentityExplanation)
|
||||
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
|
||||
synchronized (SESSION_LOCK) {
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
||||
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
|
||||
}
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
|
||||
d.dismiss();
|
||||
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||
|
||||
startService(intent);
|
||||
})
|
||||
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
|
||||
d.dismiss();
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
})
|
||||
.show();
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
@@ -517,32 +478,29 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
@@ -554,6 +512,24 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
|
||||
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) {
|
||||
@@ -618,8 +594,13 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@@ -156,7 +154,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File stickerDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File stickerDirectory = context.getDir(StickerDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
|
||||
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -46,7 +46,8 @@ import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
||||
public class ComposeText extends EmojiEditText {
|
||||
|
||||
private CharSequence combinedHint;
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private MentionValidatorWatcher mentionValidatorWatcher;
|
||||
|
||||
@@ -84,8 +85,14 @@ public class ComposeText extends EmojiEditText {
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
if (!TextUtils.isEmpty(combinedHint)) {
|
||||
setHint(combinedHint);
|
||||
if (!TextUtils.isEmpty(hint)) {
|
||||
if (!TextUtils.isEmpty(subHint)) {
|
||||
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(subHint)));
|
||||
} else {
|
||||
setHint(ellipsizeToWidth(hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,18 +150,24 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
|
||||
if (subHint != null) {
|
||||
Spannable subHintSpannable = new SpannableString(subHint);
|
||||
subHintSpannable.setSpan(new RelativeSizeSpan(0.5f), 0, subHintSpannable.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
this.hint = hint;
|
||||
|
||||
combinedHint = new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(subHintSpannable));
|
||||
if (subHint != null) {
|
||||
this.subHint = new SpannableString(subHint);
|
||||
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
} else {
|
||||
combinedHint = ellipsizeToWidth(hint);
|
||||
this.subHint = null;
|
||||
}
|
||||
|
||||
super.setHint(combinedHint);
|
||||
if (this.subHint != null) {
|
||||
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(this.subHint)));
|
||||
} else {
|
||||
super.setHint(ellipsizeToWidth(this.hint));
|
||||
}
|
||||
|
||||
super.setHint(hint);
|
||||
}
|
||||
|
||||
public void appendInvite(String invite) {
|
||||
|
||||
@@ -119,6 +119,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
thumbnail.setMinimumThumbnailWidth(width);
|
||||
}
|
||||
|
||||
public void setBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -21,11 +21,16 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
/**
|
||||
* The view shown in the compose box that represents the state of the link preview.
|
||||
* The view shown in the compose box or conversation that represents the state of the link preview.
|
||||
*/
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
@@ -35,6 +40,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
private ViewGroup container;
|
||||
private OutlinedThumbnailView thumbnail;
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private TextView site;
|
||||
private View divider;
|
||||
private View closeButton;
|
||||
@@ -63,6 +69,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container = findViewById(R.id.linkpreview_container);
|
||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
||||
title = findViewById(R.id.linkpreview_title);
|
||||
description = findViewById(R.id.linkpreview_description);
|
||||
site = findViewById(R.id.linkpreview_site);
|
||||
divider = findViewById(R.id.linkpreview_divider);
|
||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
||||
@@ -85,6 +92,8 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
divider.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
title.setMaxLines(2);
|
||||
description.setMaxLines(2);
|
||||
|
||||
closeButton.setOnClickListener(v -> {
|
||||
if (closeClickedListener != null) {
|
||||
@@ -108,6 +117,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
public void setLoading() {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
description.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
noPreview.setVisibility(INVISIBLE);
|
||||
@@ -123,17 +133,43 @@ public class LinkPreviewView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
title.setVisibility(VISIBLE);
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
if (!Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
description.setVisibility(GONE);
|
||||
}
|
||||
|
||||
String domain = null;
|
||||
|
||||
if (!Util.isEmpty(linkPreview.getUrl())) {
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
domain = url.topPrivateDomain();
|
||||
}
|
||||
}
|
||||
|
||||
if (domain != null && linkPreview.getDate() > 0) {
|
||||
site.setText(getContext().getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.getDate())));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (domain != null) {
|
||||
site.setText(domain);
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (linkPreview.getDate() > 0) {
|
||||
site.setText(formatDate(linkPreview.getDate()));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else {
|
||||
site.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
@@ -165,6 +201,11 @@ public class LinkPreviewView extends FrameLayout {
|
||||
: R.string.LinkPreviewView_no_link_preview_available;
|
||||
}
|
||||
|
||||
private static String formatDate(long date) {
|
||||
DateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
|
||||
return dateFormat.format(date);
|
||||
}
|
||||
|
||||
public interface CloseClickedListener {
|
||||
void onCloseClicked();
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -141,6 +141,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
captionIcon.setScaleY(captionIconScale);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
bounds[MIN_WIDTH] = width;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
@@ -274,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);
|
||||
@@ -283,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());
|
||||
@@ -296,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());
|
||||
|
||||
@@ -322,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);
|
||||
@@ -407,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);
|
||||
|
||||
@@ -464,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,93 @@
|
||||
package org.thoughtcrime.securesms.components.qr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SquareImageView;
|
||||
import org.thoughtcrime.securesms.qr.QrCode;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Generates a bitmap asynchronously for the supplied {@link BitMatrix} data and displays it.
|
||||
*/
|
||||
public class QrView extends SquareImageView {
|
||||
|
||||
private static final @ColorInt int DEFAULT_FOREGROUND_COLOR = Color.BLACK;
|
||||
private static final @ColorInt int DEFAULT_BACKGROUND_COLOR = Color.TRANSPARENT;
|
||||
|
||||
private @Nullable Bitmap qrBitmap;
|
||||
private @ColorInt int foregroundColor;
|
||||
private @ColorInt int backgroundColor;
|
||||
|
||||
public QrView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public QrView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
public QrView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QrView, 0, 0);
|
||||
foregroundColor = typedArray.getColor(R.styleable.QrView_qr_foreground_color, DEFAULT_FOREGROUND_COLOR);
|
||||
backgroundColor = typedArray.getColor(R.styleable.QrView_qr_background_color, DEFAULT_BACKGROUND_COLOR);
|
||||
typedArray.recycle();
|
||||
} else {
|
||||
foregroundColor = DEFAULT_FOREGROUND_COLOR;
|
||||
backgroundColor = DEFAULT_BACKGROUND_COLOR;
|
||||
}
|
||||
|
||||
if (isInEditMode()) {
|
||||
setQrText("https://signal.org");
|
||||
}
|
||||
}
|
||||
|
||||
public void setQrText(@Nullable String text) {
|
||||
setQrBitmap(QrCode.create(text, foregroundColor, backgroundColor));
|
||||
}
|
||||
|
||||
private void setQrBitmap(@Nullable Bitmap qrBitmap) {
|
||||
if (this.qrBitmap == qrBitmap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.qrBitmap != null) {
|
||||
this.qrBitmap.recycle();
|
||||
}
|
||||
|
||||
this.qrBitmap = qrBitmap;
|
||||
|
||||
setImageBitmap(this.qrBitmap);
|
||||
}
|
||||
|
||||
public @Nullable Bitmap getQrBitmap() {
|
||||
return qrBitmap;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,25 @@ package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Showed when a build has fully expired (either via the compile-time constant, or remote
|
||||
* deprecation).
|
||||
*/
|
||||
public class ExpiredBuildReminder extends Reminder {
|
||||
|
||||
public ExpiredBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_expired_build),
|
||||
context.getString(R.string.reminder_header_expired_build_details));
|
||||
super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,8 +28,17 @@ public class ExpiredBuildReminder extends Reminder {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 0;
|
||||
@Override
|
||||
public List<Action> getActions() {
|
||||
return super.getActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Importance getImportance() {
|
||||
return Importance.TERMINAL;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return SignalStore.misc().isClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,24 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Reminder that is shown when a build is getting close to expiry (either because of the
|
||||
* compile-time constant, or remote deprecation).
|
||||
*/
|
||||
public class OutdatedBuildReminder extends Reminder {
|
||||
|
||||
public OutdatedBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_outdated_build),
|
||||
getPluralsText(context));
|
||||
super(null, getPluralsText(context));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
private static CharSequence getPluralsText(final Context context) {
|
||||
int days = Util.getDaysTillBuildExpiry() - 1;
|
||||
if (days == 0) {
|
||||
return context.getString(R.string.reminder_header_outdated_build_details_today);
|
||||
}
|
||||
return context.getResources().getQuantityString(R.plurals.reminder_header_outdated_build_details, days, days);
|
||||
int days = getDaysUntilExpiry() - 1;
|
||||
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,7 +32,10 @@ public class OutdatedBuildReminder extends Reminder {
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 10;
|
||||
return getDaysUntilExpiry() <= 10;
|
||||
}
|
||||
|
||||
private static int getDaysUntilExpiry() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public abstract class Reminder {
|
||||
return Importance.NORMAL;
|
||||
}
|
||||
|
||||
public void addAction(@NonNull Action action) {
|
||||
protected void addAction(@NonNull Action action) {
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public abstract class Reminder {
|
||||
}
|
||||
|
||||
public enum Importance {
|
||||
NORMAL, ERROR
|
||||
NORMAL, ERROR, TERMINAL
|
||||
}
|
||||
|
||||
public final class Action {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -19,7 +17,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -48,7 +45,6 @@ public final class ReminderView extends FrameLayout {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.HONEYCOMB)
|
||||
public ReminderView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
@@ -56,14 +52,14 @@ public final class ReminderView extends FrameLayout {
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true);
|
||||
progressBar = ViewUtil.findById(this, R.id.reminder_progress);
|
||||
progressText = ViewUtil.findById(this, R.id.reminder_progress_text);
|
||||
container = ViewUtil.findById(this, R.id.container);
|
||||
closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
title = ViewUtil.findById(this, R.id.reminder_title);
|
||||
text = ViewUtil.findById(this, R.id.reminder_text);
|
||||
space = ViewUtil.findById(this, R.id.reminder_space);
|
||||
actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions);
|
||||
progressBar = findViewById(R.id.reminder_progress);
|
||||
progressText = findViewById(R.id.reminder_progress_text);
|
||||
container = findViewById(R.id.container);
|
||||
closeButton = findViewById(R.id.cancel);
|
||||
title = findViewById(R.id.reminder_title);
|
||||
text = findViewById(R.id.reminder_text);
|
||||
space = findViewById(R.id.reminder_space);
|
||||
actionsRecycler = findViewById(R.id.reminder_actions);
|
||||
}
|
||||
|
||||
public void showReminder(final Reminder reminder) {
|
||||
@@ -76,9 +72,26 @@ public final class ReminderView extends FrameLayout {
|
||||
title.setVisibility(GONE);
|
||||
space.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (!reminder.isDismissable()) {
|
||||
space.setVisibility(GONE);
|
||||
}
|
||||
|
||||
text.setText(reminder.getText());
|
||||
container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error
|
||||
: R.drawable.reminder_background_normal);
|
||||
|
||||
switch (reminder.getImportance()) {
|
||||
case NORMAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_normal);
|
||||
break;
|
||||
case ERROR:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_error);
|
||||
break;
|
||||
case TERMINAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_terminal);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
setOnClickListener(reminder.getOkListener());
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
/**
|
||||
* Reusable adapter for generic settings list.
|
||||
*/
|
||||
public class BaseSettingsAdapter extends MappingAdapter {
|
||||
public void configureSingleSelect(@NonNull SingleSelectSetting.SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
registerFactory(SingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item));
|
||||
}
|
||||
|
||||
public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) {
|
||||
registerFactory(CustomizableSingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A simple settings screen that takes its configuration via {@link Configuration}.
|
||||
*/
|
||||
public class BaseSettingsFragment extends Fragment {
|
||||
|
||||
private static final String CONFIGURATION_ARGUMENT = "current_selection";
|
||||
|
||||
private RecyclerView recycler;
|
||||
|
||||
public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) {
|
||||
BaseSettingsFragment fragment = new BaseSettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration);
|
||||
fragment.setArguments(arguments);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.base_settings_fragment, container, false);
|
||||
|
||||
recycler = view.findViewById(R.id.base_settings_list);
|
||||
recycler.setItemAnimator(null);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
BaseSettingsAdapter adapter = new BaseSettingsAdapter();
|
||||
|
||||
recycler.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT));
|
||||
configuration.configure(requireActivity(), adapter);
|
||||
configuration.setArguments(getArguments());
|
||||
configuration.configureAdapter(adapter);
|
||||
|
||||
adapter.submitList(configuration.getSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration for a settings screen. Utilizes serializable to hide
|
||||
* reflection of instantiating from a fragment argument.
|
||||
*/
|
||||
public static abstract class Configuration implements Serializable {
|
||||
protected transient FragmentActivity activity;
|
||||
protected transient BaseSettingsAdapter adapter;
|
||||
|
||||
public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) {
|
||||
this.activity = activity;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any runtime information from the fragment's arguments.
|
||||
*/
|
||||
public void setArguments(@Nullable Bundle arguments) {}
|
||||
|
||||
protected void updateSettingsList() {
|
||||
adapter.submitList(getSettings());
|
||||
}
|
||||
|
||||
public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter);
|
||||
|
||||
public abstract @NonNull MappingModelList getSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Adds ability to customize a value for a single select (radio) setting.
|
||||
*/
|
||||
public class CustomizableSingleSelectSetting {
|
||||
|
||||
public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener {
|
||||
void onCustomizeClicked(@NonNull Item item);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
private final TextView summaryText;
|
||||
private final View customize;
|
||||
private final RadioButton radio;
|
||||
private final SingleSelectSetting.ViewHolder delegate;
|
||||
private final Group customizeGroup;
|
||||
private final CustomizableSingleSelectionListener selectionListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) {
|
||||
super(itemView);
|
||||
this.selectionListener = selectionListener;
|
||||
|
||||
radio = findViewById(R.id.customizable_single_select_radio);
|
||||
summaryText = findViewById(R.id.customizable_single_select_summary);
|
||||
customize = findViewById(R.id.customizable_single_select_customize);
|
||||
customizeGroup = findViewById(R.id.customizable_single_select_customize_group);
|
||||
|
||||
delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener) {
|
||||
@Override
|
||||
protected void setChecked(boolean checked) {
|
||||
radio.setChecked(checked);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
delegate.bind(model.singleSelectItem);
|
||||
customizeGroup.setVisibility(radio.isChecked() ? View.VISIBLE : View.GONE);
|
||||
customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model));
|
||||
if (model.getCustomValue() != null) {
|
||||
summaryText.setText(model.getSummaryText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private SingleSelectSetting.Item singleSelectItem;
|
||||
private Object customValue;
|
||||
private String summaryText;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) {
|
||||
this.customValue = customValue;
|
||||
this.summaryText = summaryText;
|
||||
|
||||
singleSelectItem = new SingleSelectSetting.Item(item, text, isSelected);
|
||||
}
|
||||
|
||||
public @Nullable Object getCustomValue() {
|
||||
return customValue;
|
||||
}
|
||||
|
||||
public @Nullable String getSummaryText() {
|
||||
return summaryText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areItemsTheSame(newItem.singleSelectItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue) && Objects.equals(summaryText, newItem.summaryText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckedTextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Single select (radio) setting option
|
||||
*/
|
||||
public class SingleSelectSetting {
|
||||
|
||||
public interface SingleSelectSelectionChangedListener {
|
||||
void onSelectionChanged(@NonNull Object selection);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
|
||||
protected final CheckedTextView text;
|
||||
protected final SingleSelectSelectionChangedListener selectionChangedListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
super(itemView);
|
||||
this.selectionChangedListener = selectionChangedListener;
|
||||
this.text = findViewById(R.id.single_select_item_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
text.setText(model.text);
|
||||
setChecked(model.isSelected);
|
||||
itemView.setOnClickListener(v -> selectionChangedListener.onSelectionChanged(model.item));
|
||||
}
|
||||
|
||||
protected void setChecked(boolean checked) {
|
||||
text.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private final String text;
|
||||
private final Object item;
|
||||
private final boolean isSelected;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected) {
|
||||
this.item = item;
|
||||
this.text = text != null ? text : item.toString();
|
||||
this.isSelected = isSelected;
|
||||
}
|
||||
|
||||
public @NonNull String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public @NonNull Object getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return item.equals(newItem.item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return Objects.equals(text, newItem.text) && isSelected == newItem.isSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,21 +103,19 @@ 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() {
|
||||
setVerticalBoundaries(0, parent.getMeasuredHeight());
|
||||
setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop());
|
||||
}
|
||||
|
||||
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
|
||||
extraPaddingTop = topBoundary;
|
||||
extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary;
|
||||
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,66 +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<>();
|
||||
@@ -81,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);
|
||||
@@ -98,41 +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);
|
||||
View topGradient = findViewById(R.id.call_screen_header_gradient);
|
||||
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);
|
||||
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);
|
||||
@@ -156,21 +198,21 @@ 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));
|
||||
|
||||
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
MarginLayoutParams params = (MarginLayoutParams) parent.getLayoutParams();
|
||||
ColorMatrix greyScaleMatrix = new ColorMatrix();
|
||||
greyScaleMatrix.setSaturation(0);
|
||||
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
|
||||
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
|
||||
|
||||
params.topMargin = statusBarHeight;
|
||||
parent.setLayoutParams(params);
|
||||
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
statusBarGuideline.setGuidelineBegin(statusBarHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,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) {
|
||||
@@ -303,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);
|
||||
}
|
||||
@@ -342,7 +420,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
if (webRtcControls.displayEndCall()) {
|
||||
visibleViewSet.add(hangup);
|
||||
visibleViewSet.add(ongoingFooterGradient);
|
||||
visibleViewSet.add(footerGradient);
|
||||
}
|
||||
|
||||
if (webRtcControls.displayMuteAudio()) {
|
||||
@@ -359,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();
|
||||
@@ -371,7 +455,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
if (!visibleViewSet.equals(lastVisibleSet) || !controls.isFadeOutEnabled()) {
|
||||
fadeInNewUiState(lastVisibleSet, webRtcControls.displaySmallOngoingCallButtons());
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop()));
|
||||
post(() -> pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,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();
|
||||
@@ -395,14 +510,49 @@ public class WebRtcCallView extends FrameLayout {
|
||||
|
||||
private void fadeInControls() {
|
||||
fadeControls(ConstraintSet.VISIBLE);
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(status.getBottom(), videoToggle.getTop());
|
||||
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
|
||||
|
||||
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();
|
||||
@@ -413,6 +563,8 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
|
||||
constraintSet.applyTo(parent);
|
||||
|
||||
layoutParticipants();
|
||||
}
|
||||
|
||||
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {
|
||||
@@ -459,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);
|
||||
@@ -509,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);
|
||||
@@ -526,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,13 +25,15 @@ 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;
|
||||
@@ -49,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;
|
||||
@@ -92,61 +97,35 @@ public class DirectoryHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("full");
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Set<String> databaseNumbers = sanitizeNumbers(recipientDatabase.getAllPhoneNumbers());
|
||||
Set<String> systemNumbers = sanitizeNumbers(ContactAccessor.getInstance().getAllContactsWithNumbers(context));
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
} else {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
|
||||
}
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
}
|
||||
|
||||
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
|
||||
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
|
||||
Set<RecipientId> activeIds = uuidMap.keySet();
|
||||
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
|
||||
.filterNot(activeNumbers::contains)
|
||||
.filterNot(n -> result.getNumberRewrites().containsKey(n))
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
|
||||
|
||||
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
||||
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
|
||||
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
|
||||
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
|
||||
|
||||
newlyActiveIds.removeAll(existingSignalIds);
|
||||
newlyActiveIds.retainAll(existingSystemIds);
|
||||
|
||||
notifyNewUsers(context, newlyActiveIds);
|
||||
} else {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
||||
}
|
||||
refreshNumbers(context, databaseNumbers, systemNumbers, notifyOfNewUsers);
|
||||
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
stopwatch.split("disk");
|
||||
stopwatch.stop(TAG);
|
||||
@WorkerThread
|
||||
public static void refreshDirectoryFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean notifyOfNewUsers) throws IOException {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
if (recipient.hasUuid() && !recipient.hasE164()) {
|
||||
if (isUuidRegistered(context, recipient)) {
|
||||
recipientDatabase.markRegistered(recipient.getId(), recipient.requireUuid());
|
||||
} else {
|
||||
recipientDatabase.markUnregistered(recipient.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> numbers = Stream.of(recipients)
|
||||
.filter(Recipient::hasE164)
|
||||
.map(Recipient::requireE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
refreshNumbers(context, numbers, numbers, notifyOfNewUsers);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -179,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");
|
||||
|
||||
@@ -204,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());
|
||||
}
|
||||
@@ -231,18 +211,84 @@ public class DirectoryHelper {
|
||||
return newRegisteredState;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static void refreshNumbers(@NonNull Context context, @NonNull Set<String> databaseNumbers, @NonNull Set<String> systemNumbers, boolean notifyOfNewUsers) throws IOException {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
Set<String> allNumbers = SetUtil.union(databaseNumbers, systemNumbers);
|
||||
|
||||
if (allNumbers.isEmpty()) {
|
||||
Log.w(TAG, "No numbers to refresh!");
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
||||
|
||||
DirectoryResult result = ContactDiscoveryV2.getDirectoryResult(context, databaseNumbers, systemNumbers);
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
}
|
||||
|
||||
Map<RecipientId, String> uuidMap = recipientDatabase.bulkProcessCdsResult(result.getRegisteredNumbers());
|
||||
Set<String> activeNumbers = result.getRegisteredNumbers().keySet();
|
||||
Set<RecipientId> activeIds = uuidMap.keySet();
|
||||
Set<RecipientId> inactiveIds = Stream.of(allNumbers)
|
||||
.filterNot(activeNumbers::contains)
|
||||
.filterNot(n -> result.getNumberRewrites().containsKey(n))
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
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");
|
||||
|
||||
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
|
||||
|
||||
stopwatch.split("contacts-db");
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
|
||||
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
|
||||
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
|
||||
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
|
||||
|
||||
newlyActiveIds.removeAll(existingSignalIds);
|
||||
newlyActiveIds.retainAll(existingSystemIds);
|
||||
|
||||
notifyNewUsers(context, newlyActiveIds);
|
||||
} else {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
||||
}
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,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;
|
||||
@@ -399,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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user