Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da6c83ce4 | ||
|
|
184b7db43c | ||
|
|
e442e34c1b | ||
|
|
011efb0ce7 | ||
|
|
63d00f87d8 | ||
|
|
0323858145 | ||
|
|
a70e8ec7a7 | ||
|
|
b306a3ef41 | ||
|
|
ccd3467a61 | ||
|
|
40338afe7a | ||
|
|
ff97f6af56 | ||
|
|
6e7858e00f | ||
|
|
95468c85a8 | ||
|
|
f59e10d82c | ||
|
|
930370783e | ||
|
|
75062ada8a | ||
|
|
23618923d8 | ||
|
|
f1d3a2f322 | ||
|
|
3b7fbbaf6e | ||
|
|
725d793b20 | ||
|
|
5c3baca055 | ||
|
|
6e5abc92a0 | ||
|
|
8df6e95781 | ||
|
|
2290a6c0df | ||
|
|
907e8d93a3 | ||
|
|
918497fb94 | ||
|
|
3ccd6304c7 | ||
|
|
51d47adf57 | ||
|
|
f1e5206f56 | ||
|
|
f410635e2c | ||
|
|
302d57bf19 | ||
|
|
4c301a49b4 | ||
|
|
4ecfee292e | ||
|
|
2a193ef455 | ||
|
|
96e241ef9c | ||
|
|
e294a895e8 | ||
|
|
003b9b1551 | ||
|
|
a4e4af502e | ||
|
|
06aada20c1 | ||
|
|
2dace38d43 | ||
|
|
554aa1ddf0 | ||
|
|
3b2a5f1ce3 | ||
|
|
3fc4b098e8 | ||
|
|
a7d672f6b4 | ||
|
|
7e347f5cce | ||
|
|
4eaa6ebb47 | ||
|
|
e85ef6881d | ||
|
|
b1f6786392 | ||
|
|
696fffb603 | ||
|
|
3bb366ee04 | ||
|
|
6a59974f89 | ||
|
|
8e39267c42 | ||
|
|
b937534ce5 | ||
|
|
f5b46f7356 | ||
|
|
cd58c09be3 | ||
|
|
e8f0038c36 | ||
|
|
0b77b33902 | ||
|
|
c3b5323010 | ||
|
|
81eaae4070 | ||
|
|
65461ce86f | ||
|
|
536e3139a2 | ||
|
|
e9c7b120a0 | ||
|
|
d6a230a235 | ||
|
|
6bf300ada8 | ||
|
|
d307db8a95 | ||
|
|
c4c32d80b2 | ||
|
|
f4c1e34402 |
2
.github/workflows/android.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
java-version: 1.8
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}
|
||||
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT}
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
@@ -11,13 +11,15 @@ buildscript {
|
||||
jcenter {
|
||||
content {
|
||||
includeVersion 'org.jetbrains.trove4j', 'trove4j', '20160824'
|
||||
includeGroupByRegex "com\\.archinamon.*"
|
||||
}
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
|
||||
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
|
||||
classpath 'com.archinamon:android-gradle-aspectj:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +27,22 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'com.archinamon.aspectj-ext'
|
||||
apply from: 'translations.gradle'
|
||||
apply from: 'witness-verifications.gradle'
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Internal")) {
|
||||
aspectj {
|
||||
includeJar 'sqlcipher'
|
||||
includeAspectsFromJar 'Signal-Android'
|
||||
java = JavaVersion.VERSION_1_8
|
||||
}
|
||||
} else if (getGradle().getStartParameter().getTaskRequests().toString().contains("Test")) {
|
||||
aspectj {
|
||||
compileTests = false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
|
||||
@@ -80,8 +95,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 736
|
||||
def canonicalVersionName = "4.76.3"
|
||||
def canonicalVersionCode = 743
|
||||
def canonicalVersionName = "4.78.2"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -94,8 +109,8 @@ def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties')
|
||||
|
||||
android {
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.2'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
|
||||
dexOptions {
|
||||
@@ -118,7 +133,7 @@ android {
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -144,6 +159,7 @@ android {
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "int", "TRACE_EVENT_MAX", "2000"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
@@ -243,6 +259,7 @@ android {
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
|
||||
}
|
||||
|
||||
prod {
|
||||
@@ -300,27 +317,28 @@ android {
|
||||
dependencies {
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
implementation('androidx.appcompat:appcompat:1.1.0-beta01') {
|
||||
implementation ('androidx.appcompat:appcompat:1.2.0') {
|
||||
force = true
|
||||
}
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta01"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta01"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
|
||||
implementation "androidx.camera:camera-core:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
|
||||
implementation "androidx.camera:camera-view:1.0.0-alpha18"
|
||||
implementation "androidx.concurrent:concurrent-futures:1.0.0"
|
||||
implementation "androidx.autofill:autofill:1.0.0"
|
||||
implementation "androidx.paging:paging-common:2.1.2"
|
||||
@@ -346,10 +364,11 @@ dependencies {
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation 'org.signal:zkgroup-android:0.7.0'
|
||||
|
||||
implementation 'org.whispersystems:signal-client-android:0.1.3'
|
||||
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.7.3'
|
||||
implementation 'org.signal:ringrtc-android:2.8.0'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
@@ -424,7 +443,6 @@ dependencyVerification {
|
||||
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
|
||||
}
|
||||
|
||||
|
||||
def assembleWebsiteDescriptor = { variant, file ->
|
||||
if (file.exists()) {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.tracing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
/**
|
||||
* Uses AspectJ to augment relevant methods to be traced with the {@link TracerImpl}.
|
||||
*/
|
||||
@Aspect
|
||||
public class TraceAspect {
|
||||
|
||||
@Pointcut("within(@org.thoughtcrime.securesms.tracing.Trace *)")
|
||||
public void withinAnnotatedClass() {}
|
||||
|
||||
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
|
||||
public void methodInsideAnnotatedType() {}
|
||||
|
||||
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
|
||||
public void constructorInsideAnnotatedType() {}
|
||||
|
||||
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace * *(..)) || methodInsideAnnotatedType()")
|
||||
public void annotatedMethod() {}
|
||||
|
||||
@Pointcut("execution(@org.thoughtcrime.securesms.tracing.Trace *.new(..)) || constructorInsideAnnotatedType()")
|
||||
public void annotatedConstructor() {}
|
||||
|
||||
@Pointcut("execution(* *(..)) && within(net.sqlcipher.database.*)")
|
||||
public void sqlcipher() {}
|
||||
|
||||
@Pointcut("execution(* net.sqlcipher.database.SQLiteDatabase.rawQuery(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.query(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.insert(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.insertOrThrow(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.insertWithOnConflict(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.delete(..)) || " +
|
||||
"execution(* net.sqlcipher.database.SQLiteDatabase.update(..))")
|
||||
public void sqlcipherQuery() {}
|
||||
|
||||
@Around("annotatedMethod() || annotatedConstructor() || (sqlcipher() && !sqlcipherQuery())")
|
||||
public @NonNull Object profile(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String methodName = joinPoint.getSignature().toShortString();
|
||||
|
||||
Tracer.getInstance().start(methodName);
|
||||
Object result = joinPoint.proceed();
|
||||
Tracer.getInstance().end(methodName);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Around("sqlcipherQuery()")
|
||||
public @NonNull Object profileQuery(@NonNull ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String table;
|
||||
String query;
|
||||
|
||||
if (joinPoint.getSignature().getName().equals("query")) {
|
||||
if (joinPoint.getArgs().length == 9) {
|
||||
table = (String) joinPoint.getArgs()[1];
|
||||
query = (String) joinPoint.getArgs()[3];
|
||||
} else if (joinPoint.getArgs().length == 7 || joinPoint.getArgs().length == 8) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = (String) joinPoint.getArgs()[2];
|
||||
} else {
|
||||
table = "N/A";
|
||||
query = "N/A";
|
||||
}
|
||||
} else if (joinPoint.getSignature().getName().equals("rawQuery")) {
|
||||
table = "";
|
||||
query = (String) joinPoint.getArgs()[0];
|
||||
} else if (joinPoint.getSignature().getName().equals("insert")) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = "";
|
||||
} else if (joinPoint.getSignature().getName().equals("insertOrThrow")) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = "";
|
||||
} else if (joinPoint.getSignature().getName().equals("insertWithOnConflict")) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = "";
|
||||
} else if (joinPoint.getSignature().getName().equals("delete")) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = (String) joinPoint.getArgs()[1];
|
||||
} else if (joinPoint.getSignature().getName().equals("update")) {
|
||||
table = (String) joinPoint.getArgs()[0];
|
||||
query = (String) joinPoint.getArgs()[2];
|
||||
} else {
|
||||
table = "N/A";
|
||||
query = "N/A";
|
||||
}
|
||||
|
||||
query = query == null ? "null" : query;
|
||||
query = "[" + table + "] " + query;
|
||||
|
||||
String methodName = joinPoint.getSignature().toShortString();
|
||||
|
||||
Tracer.getInstance().start(methodName, "query", query);
|
||||
Object result = joinPoint.proceed();
|
||||
Tracer.getInstance().end(methodName);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package org.thoughtcrime.securesms.tracing;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.trace.TraceProtos;
|
||||
import org.thoughtcrime.securesms.trace.TraceProtos.Trace;
|
||||
import org.thoughtcrime.securesms.trace.TraceProtos.TracePacket;
|
||||
import org.thoughtcrime.securesms.trace.TraceProtos.TrackDescriptor;
|
||||
import org.thoughtcrime.securesms.trace.TraceProtos.TrackEvent;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A class to create Perfetto-compatible traces. Currently keeps the entire trace in memory to
|
||||
* avoid weirdness with synchronizing to disk.
|
||||
*
|
||||
* Some general info on how the Perfetto format works:
|
||||
* - The file format is just a Trace proto (see Trace.proto)
|
||||
* - The Trace proto is just a series of TracePackets
|
||||
* - TracePackets can describe:
|
||||
* - Threads
|
||||
* - Start of a method
|
||||
* - End of a method
|
||||
* - (And a bunch of other stuff that's not relevant to use at this point)
|
||||
*
|
||||
* We keep a circular buffer of TracePackets for method calls, and we keep a separate list of
|
||||
* TracePackets for threads so we don't lose any of those.
|
||||
*
|
||||
* Serializing is just a matter of throwing all the TracePackets we have into a proto.
|
||||
*
|
||||
* Note: This class aims to be largely-thread-safe, but prioritizes speed and memory efficiency
|
||||
* above all else. These methods are going to be called very quickly from every thread imaginable,
|
||||
* and we want to create as little overhead as possible. The idea being that it's ok if we don't,
|
||||
* for example, keep a perfect circular buffer size if it allows us to reduce overhead. The only
|
||||
* cost of screwing up would be dropping a trace packet or something, which, while sad, won't affect
|
||||
* how the app functions.
|
||||
*/
|
||||
public final class TracerImpl implements Tracer {
|
||||
|
||||
private static final int TRUSTED_SEQUENCE_ID = 1;
|
||||
private static final byte[] SYNCHRONIZATION_MARKER = UuidUtil.toByteArray(UUID.fromString("82477a76-b28d-42ba-81dc-33326d57a079"));
|
||||
|
||||
private final Clock clock;
|
||||
private final Map<Long, TracePacket> threadPackets;
|
||||
private final Queue<TracePacket> eventPackets;
|
||||
private final AtomicInteger eventCount;
|
||||
|
||||
TracerImpl() {
|
||||
this.clock = SystemClock::elapsedRealtimeNanos;
|
||||
this.threadPackets = new ConcurrentHashMap<>();
|
||||
this.eventPackets = new ConcurrentLinkedQueue<>();
|
||||
this.eventCount = new AtomicInteger(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(@NonNull String methodName) {
|
||||
long time = clock.getTimeNanos();
|
||||
Thread currentThread = Thread.currentThread();
|
||||
|
||||
if (!threadPackets.containsKey(currentThread.getId())) {
|
||||
threadPackets.put(currentThread.getId(), forThread(currentThread));
|
||||
}
|
||||
|
||||
addPacket(forMethodStart(methodName, time, currentThread.getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(@NonNull String methodName, @NonNull String key, @NonNull String value) {
|
||||
long time = clock.getTimeNanos();
|
||||
Thread currentThread = Thread.currentThread();
|
||||
|
||||
if (!threadPackets.containsKey(currentThread.getId())) {
|
||||
threadPackets.put(currentThread.getId(), forThread(currentThread));
|
||||
}
|
||||
|
||||
addPacket(forMethodStart(methodName, time, currentThread.getId(), key, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end(@NonNull String methodName) {
|
||||
addPacket(forMethodEnd(methodName, clock.getTimeNanos(), Thread.currentThread().getId()));
|
||||
}
|
||||
|
||||
public @NonNull byte[] serialize() {
|
||||
Trace.Builder trace = Trace.newBuilder();
|
||||
|
||||
for (TracePacket thread : threadPackets.values()) {
|
||||
trace.addPacket(thread);
|
||||
}
|
||||
|
||||
for (TracePacket event : eventPackets) {
|
||||
trace.addPacket(event);
|
||||
}
|
||||
|
||||
trace.addPacket(forSynchronization(clock.getTimeNanos()));
|
||||
|
||||
return trace.build().toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to add a packet to our list while keeping the size of our circular buffer in-check.
|
||||
* The tracking of the event count is not perfectly thread-safe, but doing it in a thread-safe
|
||||
* way would likely involve adding a lock, which we really don't want to do, since it'll add
|
||||
* unnecessary overhead.
|
||||
*
|
||||
* Note that we keep track of the event count separately because
|
||||
* {@link ConcurrentLinkedQueue#size()} is NOT a constant-time operation.
|
||||
*/
|
||||
private void addPacket(@NonNull TracePacket packet) {
|
||||
eventPackets.add(packet);
|
||||
|
||||
int size = eventCount.incrementAndGet();
|
||||
|
||||
for (int i = size; i > BuildConfig.TRACE_EVENT_MAX; i--) {
|
||||
eventPackets.poll();
|
||||
eventCount.decrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
private static TracePacket forThread(@NonNull Thread thread) {
|
||||
return TracePacket.newBuilder()
|
||||
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||
.setTrackDescriptor(TrackDescriptor.newBuilder()
|
||||
.setUuid(thread.getId())
|
||||
.setName(thread.getName()))
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId) {
|
||||
return TracePacket.newBuilder()
|
||||
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||
.setTimestamp(time)
|
||||
.setTrackEvent(TrackEvent.newBuilder()
|
||||
.setTrackUuid(threadId)
|
||||
.setName(name)
|
||||
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static TracePacket forMethodStart(@NonNull String name, long time, long threadId, @NonNull String key, @NonNull String value) {
|
||||
return TracePacket.newBuilder()
|
||||
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||
.setTimestamp(time)
|
||||
.setTrackEvent(TrackEvent.newBuilder()
|
||||
.setTrackUuid(threadId)
|
||||
.setName(name)
|
||||
.setType(TrackEvent.Type.TYPE_SLICE_BEGIN)
|
||||
.addDebugAnnotations(TraceProtos.DebugAnnotation.newBuilder()
|
||||
.setName(key)
|
||||
.setStringValue(value)
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static TracePacket forMethodEnd(@NonNull String name, long time, long threadId) {
|
||||
return TracePacket.newBuilder()
|
||||
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||
.setTimestamp(time)
|
||||
.setTrackEvent(TrackEvent.newBuilder()
|
||||
.setTrackUuid(threadId)
|
||||
.setName(name)
|
||||
.setType(TrackEvent.Type.TYPE_SLICE_END))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static TracePacket forSynchronization(long time) {
|
||||
return TracePacket.newBuilder()
|
||||
.setTrustedPacketSequenceId(TRUSTED_SEQUENCE_ID)
|
||||
.setTimestamp(time)
|
||||
.setSynchronizationMarker(ByteString.copyFrom(SYNCHRONIZATION_MARKER))
|
||||
.build();
|
||||
}
|
||||
|
||||
private interface Clock {
|
||||
long getTimeNanos();
|
||||
}
|
||||
}
|
||||
151
app/src/internal/proto/Trace.proto
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.thoughtcrime.securesms.trace";
|
||||
option java_outer_classname = "TraceProtos";
|
||||
|
||||
/*
|
||||
* Minimal interface needed to work with Perfetto.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/master:external/perfetto/protos/perfetto/trace/trace.proto
|
||||
*/
|
||||
message Trace {
|
||||
repeated TracePacket packet = 1;
|
||||
}
|
||||
|
||||
message TracePacket {
|
||||
optional uint64 timestamp = 8;
|
||||
optional uint32 timestamp_clock_id = 58;
|
||||
|
||||
oneof data {
|
||||
TrackEvent track_event = 11;
|
||||
TrackDescriptor track_descriptor = 60;
|
||||
bytes synchronization_marker = 36;
|
||||
}
|
||||
|
||||
oneof optional_trusted_packet_sequence_id {
|
||||
uint32 trusted_packet_sequence_id = 10;
|
||||
}
|
||||
}
|
||||
|
||||
message TrackEvent {
|
||||
repeated uint64 category_iids = 3;
|
||||
repeated string categories = 22;
|
||||
|
||||
repeated DebugAnnotation debug_annotations = 4;
|
||||
|
||||
oneof name_field {
|
||||
uint64 name_iid = 10;
|
||||
string name = 23;
|
||||
}
|
||||
|
||||
enum Type {
|
||||
TYPE_UNSPECIFIED = 0;
|
||||
TYPE_SLICE_BEGIN = 1;
|
||||
TYPE_SLICE_END = 2;
|
||||
TYPE_INSTANT = 3;
|
||||
TYPE_COUNTER = 4;
|
||||
}
|
||||
|
||||
optional Type type = 9;
|
||||
optional uint64 track_uuid = 11;
|
||||
optional int64 counter_value = 30;
|
||||
|
||||
oneof timestamp {
|
||||
int64 timestamp_delta_us = 1;
|
||||
int64 timestamp_absolute_us = 16;
|
||||
}
|
||||
|
||||
oneof thread_time {
|
||||
int64 thread_time_delta_us = 2;
|
||||
int64 thread_time_absolute_us = 17;
|
||||
}
|
||||
}
|
||||
|
||||
message TrackDescriptor {
|
||||
optional uint64 uuid = 1;
|
||||
optional uint64 parent_uuid = 5;
|
||||
optional string name = 2;
|
||||
optional ThreadDescriptor thread = 4;
|
||||
optional CounterDescriptor counter = 8;
|
||||
}
|
||||
|
||||
|
||||
message ThreadDescriptor {
|
||||
optional int32 pid = 1;
|
||||
optional int32 tid = 2;
|
||||
|
||||
optional string thread_name = 5;
|
||||
}
|
||||
|
||||
message CounterDescriptor {
|
||||
enum BuiltinCounterType {
|
||||
COUNTER_UNSPECIFIED = 0;
|
||||
COUNTER_THREAD_TIME_NS = 1;
|
||||
COUNTER_THREAD_INSTRUCTION_COUNT = 2;
|
||||
}
|
||||
|
||||
enum Unit {
|
||||
UNIT_UNSPECIFIED = 0;
|
||||
UNIT_TIME_NS = 1;
|
||||
UNIT_COUNT = 2;
|
||||
UNIT_SIZE_BYTES = 3;
|
||||
}
|
||||
optional BuiltinCounterType type = 1;
|
||||
repeated string categories = 2;
|
||||
optional Unit unit = 3;
|
||||
optional int64 unit_multiplier = 4;
|
||||
optional bool is_incremental = 5;
|
||||
}
|
||||
|
||||
message DebugAnnotation {
|
||||
message NestedValue {
|
||||
enum NestedType {
|
||||
UNSPECIFIED = 0;
|
||||
DICT = 1;
|
||||
ARRAY = 2;
|
||||
}
|
||||
|
||||
optional NestedType nested_type = 1;
|
||||
repeated string dict_keys = 2;
|
||||
repeated NestedValue dict_values = 3;
|
||||
repeated NestedValue array_values = 4;
|
||||
optional int64 int_value = 5;
|
||||
optional double double_value = 6;
|
||||
optional bool bool_value = 7;
|
||||
optional string string_value = 8;
|
||||
}
|
||||
|
||||
oneof name_field {
|
||||
uint64 name_iid = 1;
|
||||
string name = 10;
|
||||
}
|
||||
|
||||
oneof value {
|
||||
bool bool_value = 2;
|
||||
uint64 uint_value = 3;
|
||||
int64 int_value = 4;
|
||||
double double_value = 5;
|
||||
string string_value = 6;
|
||||
uint64 pointer_value = 7;
|
||||
NestedValue nested_value = 8;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle,androidx.camera.view" />
|
||||
|
||||
<permission android:name="${applicationId}.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
@@ -35,6 +35,7 @@
|
||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
@@ -443,7 +444,7 @@
|
||||
android:theme="@style/TextSecure.FullScreenMedia"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".BlockedContactsActivity"
|
||||
<activity android:name=".blocked.BlockedUsersActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 344 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 395 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 622 KiB After Width: | Height: | Size: 664 KiB |
|
Before Width: | Height: | Size: 599 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 559 KiB After Width: | Height: | Size: 552 KiB |
|
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 631 KiB |
|
Before Width: | Height: | Size: 647 KiB After Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 652 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 685 KiB |
BIN
app/src/main/assets/emoji/People_7.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 391 KiB |
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
package androidx.camera.view;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -45,43 +45,44 @@ import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.RestrictTo.Scope;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
|
||||
import androidx.camera.core.FocusMeteringAction;
|
||||
import androidx.camera.core.FocusMeteringResult;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.camera.core.Logger;
|
||||
import androidx.camera.core.MeteringPoint;
|
||||
import androidx.camera.core.MeteringPointFactory;
|
||||
import androidx.camera.core.VideoCapture;
|
||||
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
|
||||
import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
import androidx.camera.core.impl.utils.futures.Futures;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A {@link View} that displays a preview of the camera with methods {@link
|
||||
* #takePicture(Executor, OnImageCapturedCallback)},
|
||||
* {@link #startRecording(FileDescriptor, Executor, VideoCapture.OnVideoSavedCallback)} and {@link #stopRecording()}.
|
||||
* {@link #takePicture(ImageCapture.OutputFileOptions, Executor, OnImageSavedCallback)},
|
||||
* {@link #startRecording(File , Executor , OnVideoSavedCallback callback)}
|
||||
* and {@link #stopRecording()}.
|
||||
*
|
||||
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
|
||||
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
|
||||
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
@SuppressLint("RestrictedApi")
|
||||
// End Signal Custom Code Block
|
||||
public final class CameraXView extends FrameLayout {
|
||||
static final String TAG = CameraXView.class.getSimpleName();
|
||||
static final boolean DEBUG = false;
|
||||
public final class SignalCameraView extends FrameLayout {
|
||||
static final String TAG = SignalCameraView.class.getSimpleName();
|
||||
|
||||
static final int INDEFINITE_VIDEO_DURATION = -1;
|
||||
static final int INDEFINITE_VIDEO_SIZE = -1;
|
||||
@@ -107,7 +108,7 @@ public final class CameraXView extends FrameLayout {
|
||||
// For pinch-to-zoom
|
||||
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
|
||||
private boolean mIsPinchToZoomEnabled = true;
|
||||
CameraXModule mCameraModule;
|
||||
SignalCameraXModule mCameraModule;
|
||||
private final DisplayManager.DisplayListener mDisplayListener =
|
||||
new DisplayListener() {
|
||||
@Override
|
||||
@@ -124,26 +125,25 @@ public final class CameraXView extends FrameLayout {
|
||||
}
|
||||
};
|
||||
private PreviewView mPreviewView;
|
||||
private ScaleType mScaleType = ScaleType.CENTER_CROP;
|
||||
// For accessibility event
|
||||
private MotionEvent mUpEvent;
|
||||
|
||||
public CameraXView(@NonNull Context context) {
|
||||
public SignalCameraView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs);
|
||||
}
|
||||
@@ -172,23 +172,23 @@ public final class CameraXView extends FrameLayout {
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
|
||||
mCameraModule = new CameraXModule(this);
|
||||
mCameraModule = new SignalCameraXModule(this);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraXView);
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
|
||||
setScaleType(
|
||||
ScaleType.fromId(
|
||||
a.getInteger(R.styleable.CameraXView_scaleType,
|
||||
PreviewView.ScaleType.fromId(
|
||||
a.getInteger(R.styleable.CameraView_scaleType,
|
||||
getScaleType().getId())));
|
||||
setPinchToZoomEnabled(
|
||||
a.getBoolean(
|
||||
R.styleable.CameraXView_pinchToZoomEnabled, isPinchToZoomEnabled()));
|
||||
R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
|
||||
setCaptureMode(
|
||||
CaptureMode.fromId(
|
||||
a.getInteger(R.styleable.CameraXView_captureMode,
|
||||
a.getInteger(R.styleable.CameraView_captureMode,
|
||||
getCaptureMode().getId())));
|
||||
|
||||
int lensFacing = a.getInt(R.styleable.CameraXView_lensFacing, LENS_FACING_BACK);
|
||||
int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
|
||||
switch (lensFacing) {
|
||||
case LENS_FACING_NONE:
|
||||
setCameraLensFacing(null);
|
||||
@@ -203,7 +203,7 @@ public final class CameraXView extends FrameLayout {
|
||||
// Unhandled event.
|
||||
}
|
||||
|
||||
int flashMode = a.getInt(R.styleable.CameraXView_flash, 0);
|
||||
int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
|
||||
switch (flashMode) {
|
||||
case FLASH_MODE_AUTO:
|
||||
setFlash(ImageCapture.FLASH_MODE_AUTO);
|
||||
@@ -265,7 +265,7 @@ public final class CameraXView extends FrameLayout {
|
||||
if (savedState instanceof Bundle) {
|
||||
Bundle state = (Bundle) savedState;
|
||||
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
|
||||
setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
||||
setScaleType(PreviewView.ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
|
||||
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
|
||||
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
|
||||
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
|
||||
@@ -298,6 +298,21 @@ public final class CameraXView extends FrameLayout {
|
||||
dpyMgr.unregisterDisplayListener(mDisplayListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LiveData} of the underlying {@link PreviewView}'s
|
||||
* {@link PreviewView.StreamState}.
|
||||
*
|
||||
* @return A {@link LiveData} containing the {@link PreviewView.StreamState}. Apps can either
|
||||
* get current value by {@link LiveData#getValue()} or register a observer by
|
||||
* {@link LiveData#observe}.
|
||||
* @see PreviewView#getPreviewStreamState()
|
||||
*/
|
||||
@NonNull
|
||||
public LiveData<PreviewView.StreamState> getPreviewStreamState() {
|
||||
return mPreviewView.getPreviewStreamState();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
PreviewView getPreviewView() {
|
||||
return mPreviewView;
|
||||
}
|
||||
@@ -347,11 +362,11 @@ public final class CameraXView extends FrameLayout {
|
||||
/**
|
||||
* Returns the scale type used to scale the preview.
|
||||
*
|
||||
* @return The current {@link ScaleType}.
|
||||
* @return The current {@link PreviewView.ScaleType}.
|
||||
*/
|
||||
@NonNull
|
||||
public ScaleType getScaleType() {
|
||||
return mScaleType;
|
||||
public PreviewView.ScaleType getScaleType() {
|
||||
return mPreviewView.getScaleType();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,13 +374,10 @@ public final class CameraXView extends FrameLayout {
|
||||
*
|
||||
* <p>This controls how the view finder should be scaled and positioned within the view.
|
||||
*
|
||||
* @param scaleType The desired {@link ScaleType}.
|
||||
* @param scaleType The desired {@link PreviewView.ScaleType}.
|
||||
*/
|
||||
public void setScaleType(@NonNull ScaleType scaleType) {
|
||||
if (scaleType != mScaleType) {
|
||||
mScaleType = scaleType;
|
||||
requestLayout();
|
||||
}
|
||||
public void setScaleType(@NonNull PreviewView.ScaleType scaleType) {
|
||||
mPreviewView.setScaleType(scaleType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,8 +413,10 @@ public final class CameraXView extends FrameLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is
|
||||
* called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
||||
* Sets the maximum video duration before
|
||||
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called
|
||||
* automatically.
|
||||
* Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
|
||||
*/
|
||||
private void setMaxVideoDuration(long duration) {
|
||||
mCameraModule.setMaxVideoDuration(duration);
|
||||
@@ -417,7 +431,8 @@ public final class CameraXView extends FrameLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)}
|
||||
* Sets the maximum video size in bytes before
|
||||
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)}
|
||||
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
|
||||
*/
|
||||
private void setMaxVideoSize(long size) {
|
||||
@@ -435,28 +450,38 @@ public final class CameraXView extends FrameLayout {
|
||||
mCameraModule.takePicture(executor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a picture and calls
|
||||
* {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done.
|
||||
*
|
||||
* <p> The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
|
||||
* {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
|
||||
* front camera, it will be set to true; for back camera, it will be set to false.
|
||||
*
|
||||
* @param outputFileOptions Options to store the newly captured image.
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback which will receive success or failure.
|
||||
*/
|
||||
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
|
||||
@NonNull Executor executor,
|
||||
@NonNull OnImageSavedCallback callback) {
|
||||
mCameraModule.takePicture(outputFileOptions, executor, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a video and calls the OnVideoSavedCallback when done.
|
||||
*
|
||||
* @param file The destination.
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback which will receive success or failure.
|
||||
* @param outputFileOptions Options to store the newly captured video.
|
||||
* @param executor The executor in which the callback methods will be run.
|
||||
* @param callback Callback which will receive success or failure.
|
||||
*/
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
// End Signal Custom Code Block
|
||||
public void startRecording(// Begin Signal Custom Code Block
|
||||
@NonNull FileDescriptor file,
|
||||
// End Signal Custom Code Block
|
||||
public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
|
||||
@NonNull Executor executor,
|
||||
@NonNull VideoCapture.OnVideoSavedCallback callback) {
|
||||
mCameraModule.startRecording(file, executor, callback);
|
||||
@NonNull OnVideoSavedCallback callback) {
|
||||
mCameraModule.startRecording(outputFileOptions, executor, callback);
|
||||
}
|
||||
|
||||
/** Stops an in progress video. */
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
// End Signal Custom Code Block
|
||||
public void stopRecording() {
|
||||
mCameraModule.stopRecording();
|
||||
}
|
||||
@@ -554,7 +579,8 @@ public final class CameraXView extends FrameLayout {
|
||||
mDownEventTimestamp = System.currentTimeMillis();
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (delta() < ViewConfiguration.getLongPressTimeout()) {
|
||||
if (delta() < ViewConfiguration.getLongPressTimeout()
|
||||
&& mCameraModule.isBoundToLifecycle()) {
|
||||
mUpEvent = event;
|
||||
performClick();
|
||||
}
|
||||
@@ -578,19 +604,14 @@ public final class CameraXView extends FrameLayout {
|
||||
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
|
||||
mUpEvent = null;
|
||||
|
||||
CameraSelector cameraSelector =
|
||||
new CameraSelector.Builder().requireLensFacing(
|
||||
mCameraModule.getLensFacing()).build();
|
||||
|
||||
DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
|
||||
getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
|
||||
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
||||
float aePointWidth = afPointWidth * 1.5f;
|
||||
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
|
||||
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
|
||||
|
||||
Camera camera = mCameraModule.getCamera();
|
||||
if (camera != null) {
|
||||
MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
|
||||
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
|
||||
float aePointWidth = afPointWidth * 1.5f;
|
||||
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
|
||||
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
|
||||
|
||||
ListenableFuture<FocusMeteringResult> future =
|
||||
camera.getCameraControl().startFocusAndMetering(
|
||||
new FocusMeteringAction.Builder(afPoint,
|
||||
@@ -609,7 +630,7 @@ public final class CameraXView extends FrameLayout {
|
||||
}, CameraXExecutors.directExecutor());
|
||||
|
||||
} else {
|
||||
Log.d(TAG, "cannot access camera");
|
||||
Logger.d(TAG, "cannot access camera");
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -711,45 +732,11 @@ public final class CameraXView extends FrameLayout {
|
||||
return mCameraModule.isTorchOn();
|
||||
}
|
||||
|
||||
/** Options for scaling the bounds of the view finder to the bounds of this view. */
|
||||
public enum ScaleType {
|
||||
/**
|
||||
* Scale the view finder, maintaining the source aspect ratio, so the view finder fills the
|
||||
* entire view. This will cause the view finder to crop the source image if the camera
|
||||
* aspect ratio does not match the view aspect ratio.
|
||||
*/
|
||||
CENTER_CROP(0),
|
||||
/**
|
||||
* Scale the view finder, maintaining the source aspect ratio, so the view finder is
|
||||
* entirely contained within the view.
|
||||
*/
|
||||
CENTER_INSIDE(1);
|
||||
|
||||
private final int mId;
|
||||
|
||||
int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
ScaleType(int id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
static ScaleType fromId(int id) {
|
||||
for (ScaleType st : values()) {
|
||||
if (st.mId == id) {
|
||||
return st;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The capture mode used by CameraView.
|
||||
*
|
||||
* <p>This enum can be used to determine which capture mode will be enabled for {@link
|
||||
* CameraXView}.
|
||||
* SignalCameraView}.
|
||||
*/
|
||||
public enum CaptureMode {
|
||||
/** A mode where image capture is enabled. */
|
||||
@@ -832,4 +819,4 @@ public final class CameraXView extends FrameLayout {
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
package androidx.camera.view;
|
||||
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -27,17 +29,21 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.camera.core.AspectRatio;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
|
||||
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
|
||||
import androidx.camera.core.Logger;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.core.TorchState;
|
||||
import androidx.camera.core.UseCase;
|
||||
import androidx.camera.core.VideoCapture;
|
||||
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
|
||||
import androidx.camera.core.impl.CameraInternal;
|
||||
import androidx.camera.core.impl.LensFacingConverter;
|
||||
import androidx.camera.core.impl.VideoCaptureConfig;
|
||||
import androidx.camera.core.impl.utils.CameraOrientationUtil;
|
||||
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
|
||||
import androidx.camera.core.impl.utils.futures.FutureCallback;
|
||||
@@ -51,11 +57,10 @@ import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -65,13 +70,10 @@ import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
|
||||
|
||||
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(21)
|
||||
// End Signal Custom Code Block
|
||||
final class CameraXModule {
|
||||
@SuppressLint("RestrictedApi")
|
||||
final class SignalCameraXModule {
|
||||
public static final String TAG = "CameraXModule";
|
||||
|
||||
private static final float UNITY_ZOOM_SCALE = 1f;
|
||||
@@ -82,13 +84,13 @@ final class CameraXModule {
|
||||
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
|
||||
|
||||
private final Preview.Builder mPreviewBuilder;
|
||||
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
||||
private final VideoCapture.Builder mVideoCaptureBuilder;
|
||||
private final ImageCapture.Builder mImageCaptureBuilder;
|
||||
private final CameraXView mCameraXView;
|
||||
private final SignalCameraView mCameraView;
|
||||
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
||||
private CameraXView.CaptureMode mCaptureMode = CameraXView.CaptureMode.IMAGE;
|
||||
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
||||
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
||||
private SignalCameraView.CaptureMode mCaptureMode = SignalCameraView.CaptureMode.IMAGE;
|
||||
private long mMaxVideoDuration = SignalCameraView.INDEFINITE_VIDEO_DURATION;
|
||||
private long mMaxVideoSize = SignalCameraView.INDEFINITE_VIDEO_SIZE;
|
||||
@ImageCapture.FlashMode
|
||||
private int mFlash = FLASH_MODE_OFF;
|
||||
@Nullable
|
||||
@@ -110,7 +112,6 @@ final class CameraXModule {
|
||||
public void onDestroy(LifecycleOwner owner) {
|
||||
if (owner == mCurrentLifecycle) {
|
||||
clearCurrentLifecycle();
|
||||
mPreview.setSurfaceProvider(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -123,8 +124,8 @@ final class CameraXModule {
|
||||
@Nullable
|
||||
ProcessCameraProvider mCameraProvider;
|
||||
|
||||
CameraXModule(CameraXView view) {
|
||||
mCameraXView = view;
|
||||
SignalCameraXModule(SignalCameraView view) {
|
||||
mCameraView = view;
|
||||
|
||||
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
|
||||
new FutureCallback<ProcessCameraProvider>() {
|
||||
@@ -149,14 +150,12 @@ final class CameraXModule {
|
||||
|
||||
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
mVideoCaptureConfigBuilder =
|
||||
new VideoCaptureConfig.Builder().setTargetName("VideoCapture")
|
||||
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
||||
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
||||
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
||||
// End Signal Custom Code Block
|
||||
mVideoCaptureBuilder = new VideoCapture.Builder().setTargetName("VideoCapture")
|
||||
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
|
||||
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
|
||||
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
mNewLifecycle = lifecycleOwner;
|
||||
@@ -173,12 +172,15 @@ final class CameraXModule {
|
||||
}
|
||||
|
||||
clearCurrentLifecycle();
|
||||
if (mNewLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
|
||||
// Lifecycle is already in a destroyed state. Since it may have been a valid
|
||||
// lifecycle when bound, but became destroyed while waiting for layout, treat this as
|
||||
// a no-op now that we have cleared the previous lifecycle.
|
||||
mNewLifecycle = null;
|
||||
return;
|
||||
}
|
||||
mCurrentLifecycle = mNewLifecycle;
|
||||
mNewLifecycle = null;
|
||||
if (mCurrentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
|
||||
mCurrentLifecycle = null;
|
||||
throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
|
||||
}
|
||||
|
||||
if (mCameraProvider == null) {
|
||||
// try again once the camera provider is no longer null
|
||||
@@ -188,18 +190,18 @@ final class CameraXModule {
|
||||
Set<Integer> available = getAvailableCameraLensFacing();
|
||||
|
||||
if (available.isEmpty()) {
|
||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||
Logger.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||
mCameraLensFacing = null;
|
||||
}
|
||||
|
||||
// Ensure the current camera exists, or default to another camera
|
||||
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
||||
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||
Logger.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||
|
||||
// Default to the first available camera direction
|
||||
mCameraLensFacing = available.iterator().next();
|
||||
|
||||
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||
Logger.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||
}
|
||||
|
||||
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
||||
@@ -216,14 +218,12 @@ final class CameraXModule {
|
||||
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
||||
|| getDisplayRotationDegrees() == 180;
|
||||
|
||||
Rational targetAspectRatio;
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||
// End Signal Custom Code Block
|
||||
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_4_3);
|
||||
Rational targetAspectRatio;
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
@@ -232,7 +232,6 @@ final class CameraXModule {
|
||||
// Begin Signal Custom Code Block
|
||||
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
|
||||
// End Signal Custom Code Block
|
||||
// mImageCaptureBuilder.setTargetAspectRatio(AspectRatio.RATIO_16_9);
|
||||
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
|
||||
}
|
||||
|
||||
@@ -245,15 +244,14 @@ final class CameraXModule {
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
Size size = VideoUtil.getVideoRecordingSize();
|
||||
mVideoCaptureConfigBuilder.setTargetResolution(size);
|
||||
mVideoCaptureConfigBuilder.setMaxResolution(size);
|
||||
mVideoCaptureBuilder.setTargetResolution(size);
|
||||
mVideoCaptureBuilder.setMaxResolution(size);
|
||||
// End Signal Custom Code Block
|
||||
|
||||
mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
|
||||
mVideoCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
// Begin Signal Custom Code Block
|
||||
if (MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.getUseCaseConfig());
|
||||
mVideoCapture = mVideoCaptureBuilder.build();
|
||||
}
|
||||
// End Signal Custom Code Block
|
||||
|
||||
@@ -262,15 +260,15 @@ final class CameraXModule {
|
||||
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
|
||||
|
||||
mPreview = mPreviewBuilder.build();
|
||||
mPreview.setSurfaceProvider(mCameraXView.getPreviewView().getPreviewSurfaceProvider());
|
||||
mPreview.setSurfaceProvider(mCameraView.getPreviewView().getSurfaceProvider());
|
||||
|
||||
CameraSelector cameraSelector =
|
||||
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||
mImageCapture,
|
||||
mPreview);
|
||||
} else if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||
} else if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
|
||||
mVideoCapture,
|
||||
mPreview);
|
||||
@@ -301,7 +299,7 @@ final class CameraXModule {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.VIDEO) {
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
@@ -312,17 +310,32 @@ final class CameraXModule {
|
||||
mImageCapture.takePicture(executor, callback);
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
public void startRecording(FileDescriptor file,
|
||||
// End Signal Custom Code Block
|
||||
Executor executor,
|
||||
final VideoCapture.OnVideoSavedCallback callback) {
|
||||
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
|
||||
@NonNull Executor executor, OnImageSavedCallback callback) {
|
||||
if (mImageCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
|
||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
if (callback == null) {
|
||||
throw new IllegalArgumentException("OnImageSavedCallback should not be empty");
|
||||
}
|
||||
|
||||
outputFileOptions.getMetadata().setReversedHorizontal(mCameraLensFacing != null
|
||||
&& mCameraLensFacing == CameraSelector.LENS_FACING_FRONT);
|
||||
mImageCapture.takePicture(outputFileOptions, executor, callback);
|
||||
}
|
||||
|
||||
public void startRecording(VideoCapture.OutputFileOptions outputFileOptions,
|
||||
Executor executor, final OnVideoSavedCallback callback) {
|
||||
if (mVideoCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CameraXView.CaptureMode.IMAGE) {
|
||||
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
|
||||
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
||||
}
|
||||
|
||||
@@ -332,15 +345,14 @@ final class CameraXModule {
|
||||
|
||||
mVideoIsRecording.set(true);
|
||||
mVideoCapture.startRecording(
|
||||
file,
|
||||
outputFileOptions,
|
||||
executor,
|
||||
new VideoCapture.OnVideoSavedCallback() {
|
||||
@Override
|
||||
// Begin Signal Custom Code Block
|
||||
public void onVideoSaved(@NonNull FileDescriptor savedFile) {
|
||||
// End Signal Custom Code Block
|
||||
public void onVideoSaved(
|
||||
@NonNull VideoCapture.OutputFileResults outputFileResults) {
|
||||
mVideoIsRecording.set(false);
|
||||
callback.onVideoSaved(savedFile);
|
||||
callback.onVideoSaved(outputFileResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -349,15 +361,12 @@ final class CameraXModule {
|
||||
@NonNull String message,
|
||||
@Nullable Throwable cause) {
|
||||
mVideoIsRecording.set(false);
|
||||
Log.e(TAG, message, cause);
|
||||
Logger.e(TAG, message, cause);
|
||||
callback.onError(videoCaptureError, message, cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Begin Signal Custom Code Block
|
||||
@RequiresApi(26)
|
||||
// End Signal Custom Code Block
|
||||
public void stopRecording() {
|
||||
if (mVideoCapture == null) {
|
||||
return;
|
||||
@@ -388,14 +397,15 @@ final class CameraXModule {
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
|
||||
String cameraId;
|
||||
try {
|
||||
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to query lens facing.", e);
|
||||
if (mCameraProvider == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return mCameraProvider.hasCamera(
|
||||
new CameraSelector.Builder().requireLensFacing(lensFacing).build());
|
||||
} catch (CameraInfoUnavailableException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cameraId != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -454,7 +464,7 @@ final class CameraXModule {
|
||||
}
|
||||
}, CameraXExecutors.directExecutor());
|
||||
} else {
|
||||
Log.e(TAG, "Failed to set zoom ratio");
|
||||
Logger.e(TAG, "Failed to set zoom ratio");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +496,10 @@ final class CameraXModule {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBoundToLifecycle() {
|
||||
return mCamera != null;
|
||||
}
|
||||
|
||||
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||
int rotationDegrees = 0;
|
||||
if (mCamera != null) {
|
||||
@@ -520,6 +534,11 @@ final class CameraXModule {
|
||||
if (!toUnbind.isEmpty()) {
|
||||
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
|
||||
}
|
||||
|
||||
// Remove surface provider once unbound.
|
||||
if (mPreview != null) {
|
||||
mPreview.setSurfaceProvider(null);
|
||||
}
|
||||
}
|
||||
mCamera = null;
|
||||
mCurrentLifecycle = null;
|
||||
@@ -532,7 +551,7 @@ final class CameraXModule {
|
||||
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||
}
|
||||
|
||||
if (mVideoCapture != null && MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
if (mVideoCapture != null) {
|
||||
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||
}
|
||||
}
|
||||
@@ -567,7 +586,7 @@ final class CameraXModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
CameraInternal camera = mImageCapture.getBoundCamera();
|
||||
CameraInternal camera = mImageCapture.getCamera();
|
||||
|
||||
if (camera == null) {
|
||||
return false;
|
||||
@@ -614,15 +633,15 @@ final class CameraXModule {
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mCameraXView.getContext();
|
||||
return mCameraView.getContext();
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mCameraXView.getWidth();
|
||||
return mCameraView.getWidth();
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mCameraXView.getHeight();
|
||||
return mCameraView.getHeight();
|
||||
}
|
||||
|
||||
public int getDisplayRotationDegrees() {
|
||||
@@ -630,15 +649,15 @@ final class CameraXModule {
|
||||
}
|
||||
|
||||
protected int getDisplaySurfaceRotation() {
|
||||
return mCameraXView.getDisplaySurfaceRotation();
|
||||
return mCameraView.getDisplaySurfaceRotation();
|
||||
}
|
||||
|
||||
private int getMeasuredWidth() {
|
||||
return mCameraXView.getMeasuredWidth();
|
||||
return mCameraView.getMeasuredWidth();
|
||||
}
|
||||
|
||||
private int getMeasuredHeight() {
|
||||
return mCameraXView.getMeasuredHeight();
|
||||
return mCameraView.getMeasuredHeight();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -647,11 +666,11 @@ final class CameraXModule {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public CameraXView.CaptureMode getCaptureMode() {
|
||||
public SignalCameraView.CaptureMode getCaptureMode() {
|
||||
return mCaptureMode;
|
||||
}
|
||||
|
||||
public void setCaptureMode(@NonNull CameraXView.CaptureMode captureMode) {
|
||||
public void setCaptureMode(@NonNull SignalCameraView.CaptureMode captureMode) {
|
||||
this.mCaptureMode = captureMode;
|
||||
rebindToLifecycle();
|
||||
}
|
||||
@@ -34,6 +34,11 @@ public final class AppInitialization {
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
|
||||
@@ -35,8 +35,6 @@ 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;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -70,6 +68,8 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -92,15 +92,14 @@ import java.util.concurrent.TimeUnit;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
@Trace
|
||||
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private ViewOnceMessageManager viewOnceMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
private TypingStatusSender typingStatusSender;
|
||||
private PersistentLogger persistentLogger;
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private ViewOnceMessageManager viewOnceMessageManager;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
@@ -122,8 +121,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializeMessageRetrieval();
|
||||
initializeExpiringMessageManager();
|
||||
initializeRevealableMessageManager();
|
||||
initializeTypingStatusRepository();
|
||||
initializeTypingStatusSender();
|
||||
initializeGcmCheck();
|
||||
initializeSignedPreKeyCheck();
|
||||
initializePeriodicTasks();
|
||||
@@ -146,6 +143,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
@@ -181,14 +181,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
return viewOnceMessageManager;
|
||||
}
|
||||
|
||||
public TypingStatusRepository getTypingStatusRepository() {
|
||||
return typingStatusRepository;
|
||||
}
|
||||
|
||||
public TypingStatusSender getTypingStatusSender() {
|
||||
return typingStatusSender;
|
||||
}
|
||||
|
||||
public boolean isAppVisible() {
|
||||
return isAppVisible;
|
||||
}
|
||||
@@ -288,14 +280,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
this.viewOnceMessageManager = new ViewOnceMessageManager(this);
|
||||
}
|
||||
|
||||
private void initializeTypingStatusRepository() {
|
||||
this.typingStatusRepository = new TypingStatusRepository();
|
||||
}
|
||||
|
||||
private void initializeTypingStatusSender() {
|
||||
this.typingStatusSender = new TypingStatusSender(this);
|
||||
}
|
||||
|
||||
private void initializePeriodicTasks() {
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
@@ -424,7 +408,8 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
DynamicLanguageContextWrapper.updateContext(base);
|
||||
super.attachBaseContext(base);
|
||||
}
|
||||
|
||||
private static class ProviderInitializationException extends RuntimeException {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.PorterDuff;
|
||||
@@ -27,9 +26,9 @@ import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
@@ -83,9 +82,13 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
|
||||
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private boolean wasConfigurationUpdated = false;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
@@ -103,9 +106,17 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
initFragment(android.R.id.content, new BackupsPreferenceFragment());
|
||||
} else if (icicle == null) {
|
||||
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
|
||||
} else {
|
||||
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
@@ -127,20 +138,28 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
if (fragmentManager.getBackStackEntryCount() > 0) {
|
||||
fragmentManager.popBackStack();
|
||||
} else {
|
||||
// TODO [greyson] Navigation
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
if (wasConfigurationUpdated) {
|
||||
setResult(MainActivity.RESULT_CONFIG_CHANGED);
|
||||
} else {
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
onSupportNavigateUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(TextSecurePreferences.THEME_PREF)) {
|
||||
DynamicTheme.setDefaultDayNightMode(this);
|
||||
recreate();
|
||||
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
|
||||
wasConfigurationUpdated = true;
|
||||
recreate();
|
||||
|
||||
Intent intent = new Intent(this, KeyCachingService.class);
|
||||
@@ -195,7 +214,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
if (Build.VERSION.SDK_INT >= 21) return;
|
||||
|
||||
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
|
||||
preference.getIcon().setColorFilter(ThemeUtil.getThemedColor(requireContext(), R.attr.icon_tint), PorterDuff.Mode.SRC_IN);
|
||||
preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,21 +3,24 @@ package org.thoughtcrime.securesms;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
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;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationUtil;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageActivityHelper;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -40,7 +43,6 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
initializeScreenshotSecurity();
|
||||
DynamicLanguageActivityHelper.recreateIfNotInCorrectLanguage(this, TextSecurePreferences.getLanguage(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,16 +77,23 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
ActivityCompat.startActivity(this, intent, bundle);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
protected void setStatusBarColor(int color) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color);
|
||||
}
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
super.attachBaseContext(newBase);
|
||||
|
||||
Configuration configuration = new Configuration(newBase.getResources().getConfiguration());
|
||||
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
|
||||
: AppCompatDelegate.getDefaultNightMode();
|
||||
|
||||
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
|
||||
|
||||
applyOverrideConfiguration(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context newBase) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(newBase, TextSecurePreferences.getLanguage(newBase)));
|
||||
public void applyOverrideConfiguration(@NonNull Configuration overrideConfiguration) {
|
||||
DynamicLanguageContextWrapper.prepareOverrideConfiguration(this, overrideConfiguration);
|
||||
super.applyOverrideConfiguration(overrideConfiguration);
|
||||
}
|
||||
|
||||
private void logEvent(@NonNull String event) {
|
||||
@@ -94,4 +103,13 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
public final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
|
||||
private static int mapNightModeToConfigurationUiMode(@NonNull Context context, @AppCompatDelegate.NightMode int appCompatNightMode) {
|
||||
if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_YES) {
|
||||
return Configuration.UI_MODE_NIGHT_YES;
|
||||
} else if (appCompatNightMode == AppCompatDelegate.MODE_NIGHT_NO) {
|
||||
return Configuration.UI_MODE_NIGHT_NO;
|
||||
}
|
||||
return ConfigurationUtil.getNightModeConfiguration(context.getApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.cursoradapter.widget.CursorAdapter;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class BlockedContactsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle, boolean ready) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.BlockedContactsActivity_blocked_contacts);
|
||||
initFragment(android.R.id.content, new BlockedContactsFragment());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class BlockedContactsFragment
|
||||
extends ListFragment
|
||||
implements LoaderManager.LoaderCallbacks<Cursor>, ListView.OnItemClickListener
|
||||
{
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
return inflater.inflate(R.layout.blocked_contacts_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
getListView().setOnItemClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new BlockedContactsLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (getListAdapter() != null) {
|
||||
((CursorAdapter) getListAdapter()).changeCursor(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
if (getListAdapter() != null) {
|
||||
((CursorAdapter) getListAdapter()).changeCursor(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
|
||||
BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
|
||||
RecipientUtil.unblock(requireContext(), recipient);
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
});
|
||||
}
|
||||
|
||||
private static class BlockedContactAdapter extends CursorAdapter {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
|
||||
BlockedContactAdapter(@NonNull Context context, @NonNull GlideRequests glideRequests, @Nullable Cursor c) {
|
||||
super(context, c);
|
||||
this.glideRequests = glideRequests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return LayoutInflater.from(context)
|
||||
.inflate(R.layout.blocked_contact_list_item, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID)));
|
||||
LiveRecipient recipient = Recipient.live(recipientId);
|
||||
|
||||
((BlockedContactListItem) view).set(glideRequests, recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public final class GroupMembersDialog {
|
||||
public void display() {
|
||||
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||
.setTitle(R.string.ConversationActivity_group_members)
|
||||
.setIconAttribute(R.attr.group_members_dialog_icon)
|
||||
.setIcon(R.drawable.ic_group_24)
|
||||
.setCancelable(true)
|
||||
.setView(R.layout.dialog_group_members)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
|
||||
@@ -164,10 +164,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
private void setPrimaryColorsToolbarNormal() {
|
||||
primaryToolbar.setBackgroundColor(0);
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(null);
|
||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.title_text_color_primary));
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
getWindow().setStatusBarColor(ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||
WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
|
||||
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
|
||||
WindowUtil.setLightStatusBarFromTheme(this);
|
||||
}
|
||||
@@ -177,11 +177,11 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
|
||||
private void setPrimaryColorsToolbarForSms() {
|
||||
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
|
||||
primaryToolbar.getNavigationIcon().setColorFilter(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
|
||||
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
|
||||
WindowUtil.clearLightStatusBar(getWindow());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
@Trace
|
||||
public class MainActivity extends PassphraseRequiredActivity {
|
||||
|
||||
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final MainNavigator navigator = new MainNavigator(this);
|
||||
|
||||
@@ -50,6 +56,14 @@ public class MainActivity extends PassphraseRequiredActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MainNavigator getNavigator() {
|
||||
return navigator;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
public class MainNavigator {
|
||||
|
||||
public static final int REQUEST_CONFIG_CHANGES = 901;
|
||||
|
||||
private final MainActivity activity;
|
||||
|
||||
public MainNavigator(@NonNull MainActivity activity) {
|
||||
@@ -65,10 +67,9 @@ public class MainNavigator {
|
||||
|
||||
public void goToAppSettings() {
|
||||
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
|
||||
activity.startActivity(intent);
|
||||
activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
|
||||
}
|
||||
|
||||
|
||||
public void goToArchiveList() {
|
||||
getFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
|
||||
@@ -39,6 +39,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -142,6 +143,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
@@ -442,7 +449,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setIcon(R.drawable.ic_warning);
|
||||
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
|
||||
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);
|
||||
builder.setCancelable(true);
|
||||
|
||||
@@ -100,7 +100,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA));
|
||||
intent.setDataAndType(getIntent().getData(), getIntent().getType());
|
||||
|
||||
|
||||
@@ -66,12 +66,6 @@ public class PassphraseCreateActivity extends PassphraseActivity {
|
||||
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
|
||||
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
|
||||
|
||||
TextSecurePreferences.setLastExperienceVersionCode(PassphraseCreateActivity.this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setPasswordDisabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setReadReceiptsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(PassphraseCreateActivity.this, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(PassphraseCreateActivity.this, false);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class SmsSendtoActivity extends Activity {
|
||||
nextIntent = new Intent(this, ConversationActivity.class);
|
||||
nextIntent.putExtra(ConversationActivity.TEXT_EXTRA, destination.getBody());
|
||||
nextIntent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
nextIntent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId().serialize());
|
||||
}
|
||||
return nextIntent;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.fingerprint.Fingerprint;
|
||||
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
|
||||
@@ -228,9 +229,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
||||
}
|
||||
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
@@ -207,6 +207,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
private void initializeResources() {
|
||||
callScreen = findViewById(R.id.callScreen);
|
||||
callScreen.setControlsListener(new ControlsListener());
|
||||
callScreen.setEventListener(new EventListener());
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
@@ -381,8 +382,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
private void handleOutgoingCall() {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
} else {
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
|
||||
@@ -408,8 +413,11 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected() {
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
|
||||
if (event.getGroupState().isNotIdleOrConnected()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRecipientUnavailable() {
|
||||
@@ -428,7 +436,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.RedPhone_number_not_registered)
|
||||
.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.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))
|
||||
@@ -486,7 +494,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
|
||||
switch (event.getState()) {
|
||||
case CALL_CONNECTED: handleCallConnected(); break;
|
||||
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
|
||||
case CALL_CONNECTED: handleCallConnected(event); break;
|
||||
case NETWORK_FAILURE: handleServerFailure(); break;
|
||||
case CALL_RINGING: handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
@@ -496,7 +505,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
|
||||
case NO_SUCH_USER: handleNoSuchUser(event); break;
|
||||
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(); break;
|
||||
case CALL_OUTGOING: handleOutgoingCall(event); break;
|
||||
case CALL_BUSY: handleCallBusy(); break;
|
||||
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
|
||||
}
|
||||
@@ -511,6 +520,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
}
|
||||
}
|
||||
|
||||
private final class ControlsListener implements WebRtcCallView.ControlsListener {
|
||||
|
||||
@Override
|
||||
@@ -605,4 +620,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
|
||||
}
|
||||
}
|
||||
|
||||
private class EventListener implements WebRtcCallView.EventListener {
|
||||
@Override
|
||||
public void onPotentialLayoutChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_GROUP_UPDATE_RENDERED_RESOLUTIONS);
|
||||
startService(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
import com.annimon.stream.function.Predicate;
|
||||
import com.google.android.collect.Sets;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
@@ -38,6 +37,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.kdf.HKDFv3;
|
||||
@@ -69,7 +69,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = FullBackupExporter.class.getSimpleName();
|
||||
|
||||
private static final Set<String> BLACKLISTED_TABLES = Sets.newHashSet(
|
||||
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
|
||||
SignedPreKeyDatabase.TABLE_NAME,
|
||||
OneTimePreKeyDatabase.TABLE_NAME,
|
||||
SessionDatabase.TABLE_NAME,
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ViewSwitcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
|
||||
|
||||
private static final String CONTACT_SELECTION_FRAGMENT = "Contact.Selection.Fragment";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private BlockedUsersViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.blocked_users_activity);
|
||||
|
||||
BlockedUsersRepository repository = new BlockedUsersRepository(this);
|
||||
BlockedUsersViewModel.Factory factory = new BlockedUsersViewModel.Factory(repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class);
|
||||
|
||||
ViewSwitcher viewSwitcher = findViewById(R.id.toolbar_switcher);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
ContactFilterToolbar contactFilterToolbar = findViewById(R.id.filter_toolbar);
|
||||
View container = findViewById(R.id.fragment_container);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
contactFilterToolbar.setNavigationOnClickListener(unused -> onBackPressed());
|
||||
contactFilterToolbar.setOnFilterChangedListener(query -> {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
|
||||
if (fragment != null) {
|
||||
((ContactSelectionListFragment) fragment).setQueryFilter(query);
|
||||
}
|
||||
});
|
||||
contactFilterToolbar.setHint(R.string.BlockedUsersActivity__add_blocked_user);
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(() -> {
|
||||
viewSwitcher.setDisplayedChild(getSupportFragmentManager().getBackStackEntryCount());
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
contactFilterToolbar.focusAndShowKeyboard();
|
||||
}
|
||||
});
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.fragment_container, new BlockedUsersFragment())
|
||||
.commit();
|
||||
|
||||
viewModel.getEvents().observe(this, event -> handleEvent(container, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
|
||||
|
||||
AlertDialog confirmationDialog = new AlertDialog.Builder(BlockedUsersActivity.this)
|
||||
.setTitle(R.string.BlockedUsersActivity__block_user)
|
||||
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
|
||||
if (recipientId.isPresent()) {
|
||||
viewModel.block(recipientId.get());
|
||||
} else {
|
||||
viewModel.createAndBlock(number);
|
||||
}
|
||||
dialog.dismiss();
|
||||
onBackPressed();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
confirmationDialog.setOnShowListener(dialog -> {
|
||||
confirmationDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.RED);
|
||||
});
|
||||
|
||||
confirmationDialog.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleAddUserToBlockedList() {
|
||||
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
|
||||
Intent intent = getIntent();
|
||||
|
||||
fragment.setOnContactSelectedListener(this);
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
|
||||
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE,
|
||||
ContactsCursorLoader.DisplayMode.FLAG_PUSH |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_SMS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_INACTIVE_GROUPS |
|
||||
ContactsCursorLoader.DisplayMode.FLAG_BLOCK);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment, CONTACT_SELECTION_FRAGMENT)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void handleEvent(@NonNull View view, @NonNull BlockedUsersViewModel.Event event) {
|
||||
final String displayName;
|
||||
|
||||
if (event.getRecipient() == null) {
|
||||
displayName = event.getNumber();
|
||||
} else {
|
||||
displayName = event.getRecipient().getDisplayName(this);
|
||||
}
|
||||
|
||||
final @StringRes int messageResId;
|
||||
switch (event.getEventType()) {
|
||||
case BLOCK_SUCCEEDED:
|
||||
messageResId = R.string.BlockedUsersActivity__s_has_been_blocked;
|
||||
break;
|
||||
case BLOCK_FAILED:
|
||||
messageResId = R.string.BlockedUsersActivity__failed_to_block_s;
|
||||
break;
|
||||
case UNBLOCK_SUCCEEDED:
|
||||
messageResId = R.string.BlockedUsersActivity__s_has_been_unblocked;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported event type " + event);
|
||||
}
|
||||
|
||||
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class BlockedUsersAdapter extends ListAdapter<Recipient, BlockedUsersAdapter.ViewHolder> {
|
||||
|
||||
private final RecipientClickedListener recipientClickedListener;
|
||||
|
||||
BlockedUsersAdapter(@NonNull RecipientClickedListener recipientClickedListener) {
|
||||
super(new RecipientDiffCallback());
|
||||
|
||||
this.recipientClickedListener = recipientClickedListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.blocked_users_adapter_item, parent, false),
|
||||
position -> recipientClickedListener.onRecipientClicked(Objects.requireNonNull(getItem(position))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(Objects.requireNonNull(getItem(position)));
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView displayName;
|
||||
private final TextView numberOrUsername;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, Consumer<Integer> clickConsumer) {
|
||||
super(itemView);
|
||||
|
||||
this.avatar = itemView.findViewById(R.id.avatar);
|
||||
this.displayName = itemView.findViewById(R.id.display_name);
|
||||
this.numberOrUsername = itemView.findViewById(R.id.number_or_username);
|
||||
|
||||
itemView.setOnClickListener(unused -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
clickConsumer.accept(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void bind(@NonNull Recipient recipient) {
|
||||
avatar.setAvatar(recipient);
|
||||
displayName.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
|
||||
if (recipient.hasAUserSetDisplayName(itemView.getContext())) {
|
||||
String identifier = recipient.getE164().or(recipient.getUsername()).orNull();
|
||||
|
||||
if (identifier != null) {
|
||||
numberOrUsername.setText(identifier);
|
||||
numberOrUsername.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
numberOrUsername.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
numberOrUsername.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecipientDiffCallback extends DiffUtil.ItemCallback<Recipient> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Recipient oldItem, @NonNull Recipient newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Recipient oldItem, @NonNull Recipient newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
interface RecipientClickedListener {
|
||||
void onRecipientClicked(@NonNull Recipient recipient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
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.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public class BlockedUsersFragment extends Fragment {
|
||||
|
||||
private BlockedUsersViewModel viewModel;
|
||||
private Listener listener;
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (context instanceof Listener) {
|
||||
listener = (Listener) context;
|
||||
} else {
|
||||
throw new ClassCastException("Expected context to implement Listener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
|
||||
listener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.blocked_users_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
View addUser = view.findViewById(R.id.add_blocked_user_touch_target);
|
||||
RecyclerView recycler = view.findViewById(R.id.blocked_users_recycler);
|
||||
View empty = view.findViewById(R.id.no_blocked_users);
|
||||
BlockedUsersAdapter adapter = new BlockedUsersAdapter(this::handleRecipientClicked);
|
||||
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
addUser.setOnClickListener(unused -> {
|
||||
if (listener != null) {
|
||||
listener.handleAddUserToBlockedList();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(BlockedUsersViewModel.class);
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> {
|
||||
if (list.isEmpty()) {
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
empty.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
adapter.submitList(list);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleRecipientClicked(@NonNull Recipient recipient) {
|
||||
AlertDialog confirmationDialog = new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.BlockedUsersActivity__unblock_user)
|
||||
.setMessage(getString(R.string.BlockedUsersActivity__do_you_want_to_unblock_s, recipient.getDisplayName(requireContext())))
|
||||
.setPositiveButton(R.string.BlockedUsersActivity__unblock, (dialog, which) -> {
|
||||
viewModel.unblock(recipient.getId());
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(true)
|
||||
.create();
|
||||
|
||||
confirmationDialog.setOnShowListener(dialog -> {
|
||||
confirmationDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(Color.RED);
|
||||
});
|
||||
|
||||
confirmationDialog.show();
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void handleAddUserToBlockedList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class BlockedUsersRepository {
|
||||
|
||||
private static final String TAG = Log.tag(BlockedUsersRepository.class);
|
||||
|
||||
private final Context context;
|
||||
|
||||
BlockedUsersRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void getBlocked(@NonNull Consumer<List<Recipient>> blockedUsers) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
||||
try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
|
||||
int count = reader.getCount();
|
||||
if (count == 0) {
|
||||
blockedUsers.accept(Collections.emptyList());
|
||||
} else {
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
while (reader.getNext() != null) {
|
||||
recipients.add(reader.getCurrent());
|
||||
}
|
||||
blockedUsers.accept(recipients);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void block(@NonNull RecipientId recipientId, @NonNull Runnable success, @NonNull Runnable failure) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
RecipientUtil.block(context, Recipient.resolved(recipientId));
|
||||
success.run();
|
||||
} catch (IOException | GroupChangeFailedException | GroupChangeBusyException e) {
|
||||
Log.w(TAG, "block: failed to block recipient: ", e);
|
||||
failure.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void createAndBlock(@NonNull String number, @NonNull Runnable success) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientUtil.blockNonGroup(context, Recipient.external(context, number));
|
||||
success.run();
|
||||
});
|
||||
}
|
||||
|
||||
void unblock(@NonNull RecipientId recipientId, @NonNull Runnable success) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientUtil.unblock(context, Recipient.resolved(recipientId));
|
||||
success.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BlockedUsersViewModel extends ViewModel {
|
||||
|
||||
private final BlockedUsersRepository repository;
|
||||
private final MutableLiveData<List<Recipient>> recipients;
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
|
||||
|
||||
private BlockedUsersViewModel(@NonNull BlockedUsersRepository repository) {
|
||||
this.repository = repository;
|
||||
this.recipients = new MutableLiveData<>();
|
||||
|
||||
loadRecipients();
|
||||
}
|
||||
|
||||
public LiveData<List<Recipient>> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
void block(@NonNull RecipientId recipientId) {
|
||||
repository.block(recipientId,
|
||||
() -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.BLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
},
|
||||
() -> events.postValue(new Event(EventType.BLOCK_FAILED, Recipient.resolved(recipientId))));
|
||||
}
|
||||
|
||||
void createAndBlock(@NonNull String number) {
|
||||
repository.createAndBlock(number, () -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.BLOCK_SUCCEEDED, number));
|
||||
});
|
||||
}
|
||||
|
||||
void unblock(@NonNull RecipientId recipientId) {
|
||||
repository.unblock(recipientId, () -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.UNBLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
});
|
||||
}
|
||||
|
||||
private void loadRecipients() {
|
||||
repository.getBlocked(recipients::postValue);
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
BLOCK_SUCCEEDED,
|
||||
BLOCK_FAILED,
|
||||
UNBLOCK_SUCCEEDED
|
||||
}
|
||||
|
||||
public static final class Event {
|
||||
|
||||
private final EventType eventType;
|
||||
private final Recipient recipient;
|
||||
private final String number;
|
||||
|
||||
private Event(@NonNull EventType eventType, @NonNull Recipient recipient) {
|
||||
this.eventType = eventType;
|
||||
this.recipient = recipient;
|
||||
this.number = null;
|
||||
}
|
||||
|
||||
private Event(@NonNull EventType eventType, @NonNull String number) {
|
||||
this.eventType = eventType;
|
||||
this.recipient = null;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public @Nullable Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @Nullable String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
public @NonNull EventType getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final BlockedUsersRepository repository;
|
||||
|
||||
public Factory(@NonNull BlockedUsersRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new BlockedUsersViewModel(repository)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,10 +371,11 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void showPlayButton() {
|
||||
if (!smallView || seekBar.getProgress() == 0) {
|
||||
if (!smallView) {
|
||||
circleProgress.setVisibility(GONE);
|
||||
} else if (seekBar.getProgress() == 0) {
|
||||
circleProgress.setInstantProgress(1);
|
||||
}
|
||||
circleProgress.setVisibility(GONE);
|
||||
playPauseButton.setVisibility(VISIBLE);
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.view.inputmethod.InputConnection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
@@ -257,7 +258,7 @@ public class ComposeText extends EmojiEditText {
|
||||
setImeOptions(getImeOptions() | 16777216);
|
||||
}
|
||||
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
|
||||
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.conversation_mention_background_color));
|
||||
|
||||
addTextChangedListener(new MentionDeleter());
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
|
||||
|
||||
public final class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
@@ -125,6 +126,10 @@ public final class ContactFilterToolbar extends DarkOverflowToolbar {
|
||||
attributes.recycle();
|
||||
}
|
||||
|
||||
public void focusAndShowKeyboard() {
|
||||
ViewUtil.focusAndShowKeyboard(searchText);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
searchText.setText("");
|
||||
notifyListener();
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
@@ -57,7 +59,7 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
this.cornerMask = new CornerMask(this);
|
||||
this.outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
|
||||
|
||||
@@ -30,6 +30,6 @@ public class DarkSearchView extends androidx.appcompat.widget.SearchView {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
EditText searchText = findViewById(androidx.appcompat.R.id.search_src_text);
|
||||
searchText.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_subtitle_color));
|
||||
searchText.setTextColor(ContextCompat.getColor(context, R.color.signal_text_toolbar_subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class ImageDivet extends AppCompatImageView {
|
||||
private static final float CORNER_OFFSET = 12F;
|
||||
private static final String[] POSITIONS = new String[] {"bottom_right"};
|
||||
|
||||
private Drawable drawable;
|
||||
|
||||
private int drawableIntrinsicWidth;
|
||||
private int drawableIntrinsicHeight;
|
||||
private int position;
|
||||
private float density;
|
||||
|
||||
public ImageDivet(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
public ImageDivet(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize(attrs);
|
||||
}
|
||||
|
||||
public ImageDivet(Context context) {
|
||||
super(context);
|
||||
initialize(null);
|
||||
}
|
||||
|
||||
private void initialize(AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
position = attrs.getAttributeListValue(null, "position", POSITIONS, -1);
|
||||
}
|
||||
|
||||
density = getContext().getResources().getDisplayMetrics().density;
|
||||
setDrawable();
|
||||
}
|
||||
|
||||
private void setDrawable() {
|
||||
int attributes[] = new int[] {R.attr.lower_right_divet};
|
||||
|
||||
TypedArray drawables = getContext().obtainStyledAttributes(attributes);
|
||||
|
||||
switch (position) {
|
||||
case 0:
|
||||
drawable = drawables.getDrawable(0);
|
||||
break;
|
||||
}
|
||||
|
||||
drawableIntrinsicWidth = drawable.getIntrinsicWidth();
|
||||
drawableIntrinsicHeight = drawable.getIntrinsicHeight();
|
||||
|
||||
drawables.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
c.save();
|
||||
computeBounds(c);
|
||||
drawable.draw(c);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
setDrawable();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public float getCloseOffset() {
|
||||
return CORNER_OFFSET * density;
|
||||
}
|
||||
|
||||
public float getFarOffset() {
|
||||
return getCloseOffset() + drawableIntrinsicHeight;
|
||||
}
|
||||
|
||||
private void computeBounds(Canvas c) {
|
||||
final int right = getWidth();
|
||||
final int bottom = getHeight();
|
||||
|
||||
switch (position) {
|
||||
case 0:
|
||||
drawable.setBounds(
|
||||
right - drawableIntrinsicWidth,
|
||||
bottom - drawableIntrinsicHeight,
|
||||
right,
|
||||
bottom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
@@ -79,7 +80,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.Canvas;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
@@ -37,7 +38,7 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
|
||||
int radius = 0;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -76,8 +78,8 @@ public class TooltipPopup extends PopupWindow {
|
||||
View bubble = getContentView().findViewById(R.id.tooltip_bubble);
|
||||
|
||||
if (backgroundTint == 0) {
|
||||
bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(ThemeUtil.getThemedColor(anchor.getContext(), R.attr.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
bubble.getBackground().setColorFilter(ContextCompat.getColor(anchor.getContext(), R.color.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(ContextCompat.getColor(anchor.getContext(), R.color.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
bubble.getBackground().setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -54,6 +54,10 @@ public class TypingStatusSender {
|
||||
onTypingStopped(threadId, false);
|
||||
}
|
||||
|
||||
public synchronized void onTypingStoppedWithNotify(long threadId) {
|
||||
onTypingStopped(threadId, true);
|
||||
}
|
||||
|
||||
private synchronized void onTypingStopped(long threadId, boolean notify) {
|
||||
TimerPair pair = Util.getOrDefault(selfTypingTimers, threadId, new TimerPair());
|
||||
selfTypingTimers.put(threadId, pair);
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
@@ -38,7 +40,7 @@ public class AsciiEmojiView extends View {
|
||||
float targetFontSize = 0.75f * getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
|
||||
paint.setTextSize(targetFontSize);
|
||||
paint.setColor(ResUtil.getColor(getContext(), R.attr.emoji_text_color));
|
||||
paint.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_primary));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
int xPos = (getWidth() / 2);
|
||||
|
||||
@@ -81,9 +81,9 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
|
||||
@Override
|
||||
public int getProviderIconView(boolean selected) {
|
||||
if (selected) {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark_selected : R.layout.emoji_keyboard_icon_light_selected;
|
||||
return R.layout.emoji_keyboard_icon_selected;
|
||||
} else {
|
||||
return ThemeUtil.isDarkTheme(context) ? R.layout.emoji_keyboard_icon_dark : R.layout.emoji_keyboard_icon_light;
|
||||
return R.layout.emoji_keyboard_icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.AppCompatImageButton;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -44,9 +46,9 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
this.emojiToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_emoji_toggle);
|
||||
this.stickerToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_sticker_toggle);
|
||||
this.imeToggle = ResUtil.getDrawable(getContext(), R.attr.conversation_keyboard_toggle);
|
||||
this.emojiToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_emoji_smiley_24);
|
||||
this.stickerToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_sticker_24);
|
||||
this.imeToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_keyboard_24);
|
||||
this.mediaToggle = emojiToggle;
|
||||
|
||||
setToMedia();
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.components.emoji;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -28,7 +30,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
|
||||
this.listener = listener;
|
||||
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
|
||||
|
||||
setBackgroundDrawable(ThemeUtil.getThemedDrawable(context, R.attr.emoji_variation_selector_background));
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.emoji_variation_selector_background));
|
||||
setOutsideTouchable(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
|
||||
@@ -31,7 +31,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
|
||||
this.resendListener = resendListener;
|
||||
|
||||
setTitle(R.string.UntrustedSendDialog_send_message);
|
||||
setIconAttribute(R.attr.dialog_alert_icon);
|
||||
setIcon(R.drawable.ic_warning);
|
||||
setMessage(message);
|
||||
setPositiveButton(R.string.UntrustedSendDialog_send, this);
|
||||
setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
@@ -30,7 +30,7 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
|
||||
this.resendListener = resendListener;
|
||||
|
||||
setTitle(R.string.UnverifiedSendDialog_send_message);
|
||||
setIconAttribute(R.attr.dialog_alert_icon);
|
||||
setIcon(R.drawable.ic_warning);
|
||||
setMessage(message);
|
||||
setPositiveButton(R.string.UnverifiedSendDialog_send, this);
|
||||
setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows a reminder to upgrade a group to GV2.
|
||||
*/
|
||||
public class GroupsV1MigrationInitiationReminder extends Reminder {
|
||||
|
||||
public GroupsV1MigrationInitiationReminder(@NonNull Context context) {
|
||||
super(null, context.getString(R.string.GroupsV1MigrationInitiationReminder_to_access_new_features_like_mentions));
|
||||
addAction(new Action(context.getString(R.string.GroupsV1MigrationInitiationReminder_update_group), R.id.reminder_action_gv1_initiation_update_group));
|
||||
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationInitiationReminder_not_now), R.id.reminder_action_gv1_initiation_not_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows a reminder to add anyone that might have been missed in GV1->GV2 migration.
|
||||
*/
|
||||
public class GroupsV1MigrationSuggestionsReminder extends Reminder {
|
||||
public GroupsV1MigrationSuggestionsReminder(@NonNull Context context, @NonNull List<RecipientId> suggestions) {
|
||||
super(null, context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, suggestions.size(), suggestions.size()));
|
||||
addAction(new Action(context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, suggestions.size()), R.id.reminder_action_gv1_suggestion_add_members));
|
||||
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_not_now), R.id.reminder_action_gv1_suggestion_not_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -140,56 +140,58 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
||||
}
|
||||
|
||||
private void applyDescriptionsToQueue(@NonNull List<MediaDescriptionCompat> descriptions) {
|
||||
for (MediaDescriptionCompat description : descriptions) {
|
||||
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
|
||||
MediaDescriptionCompat next = createNextClone(description);
|
||||
int currentIndex = player.getCurrentWindowIndex();
|
||||
synchronized (queueDataAdapter) {
|
||||
for (MediaDescriptionCompat description : descriptions) {
|
||||
int holderIndex = queueDataAdapter.indexOf(description.getMediaUri());
|
||||
MediaDescriptionCompat next = createNextClone(description);
|
||||
int currentIndex = player.getCurrentWindowIndex();
|
||||
|
||||
if (holderIndex != -1) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
|
||||
if (!queueDataAdapter.isEmpty()) {
|
||||
if (holderIndex != -1) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
}
|
||||
|
||||
queueDataAdapter.add(holderIndex, createNextClone(description));
|
||||
queueDataAdapter.add(holderIndex, description);
|
||||
|
||||
if (currentIndex != holderIndex) {
|
||||
dataSource.removeMediaSource(holderIndex);
|
||||
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
|
||||
if (currentIndex != holderIndex + 1) {
|
||||
if (dataSource.getSize() > 1) {
|
||||
dataSource.removeMediaSource(holderIndex + 1);
|
||||
if (!queueDataAdapter.isEmpty()) {
|
||||
queueDataAdapter.remove(holderIndex);
|
||||
}
|
||||
|
||||
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
|
||||
queueDataAdapter.add(holderIndex, createNextClone(description));
|
||||
queueDataAdapter.add(holderIndex, description);
|
||||
|
||||
if (currentIndex != holderIndex) {
|
||||
dataSource.removeMediaSource(holderIndex);
|
||||
dataSource.addMediaSource(holderIndex, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
|
||||
if (currentIndex != holderIndex + 1) {
|
||||
if (dataSource.getSize() > 1) {
|
||||
dataSource.removeMediaSource(holderIndex + 1);
|
||||
}
|
||||
|
||||
dataSource.addMediaSource(holderIndex + 1, mediaSourceFactory.createMediaSource(next));
|
||||
}
|
||||
} else {
|
||||
int insertLocation = queueDataAdapter.indexAfter(description);
|
||||
|
||||
queueDataAdapter.add(insertLocation, next);
|
||||
queueDataAdapter.add(insertLocation, description);
|
||||
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
} else {
|
||||
int insertLocation = queueDataAdapter.indexAfter(description);
|
||||
|
||||
queueDataAdapter.add(insertLocation, next);
|
||||
queueDataAdapter.add(insertLocation, description);
|
||||
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(next));
|
||||
dataSource.addMediaSource(insertLocation, mediaSourceFactory.createMediaSource(description));
|
||||
}
|
||||
}
|
||||
|
||||
int lastIndex = queueDataAdapter.size() - 1;
|
||||
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
|
||||
int lastIndex = queueDataAdapter.size() - 1;
|
||||
MediaDescriptionCompat last = queueDataAdapter.getMediaDescription(lastIndex);
|
||||
|
||||
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
|
||||
queueDataAdapter.remove(lastIndex);
|
||||
dataSource.removeMediaSource(lastIndex);
|
||||
if (Objects.equals(last.getMediaUri(), NEXT_URI)) {
|
||||
queueDataAdapter.remove(lastIndex);
|
||||
dataSource.removeMediaSource(lastIndex);
|
||||
|
||||
if (queueDataAdapter.size() > 1) {
|
||||
MediaDescriptionCompat end = createEndClone(last);
|
||||
if (queueDataAdapter.size() > 1) {
|
||||
MediaDescriptionCompat end = createEndClone(last);
|
||||
|
||||
queueDataAdapter.add(lastIndex, end);
|
||||
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
|
||||
queueDataAdapter.add(lastIndex, end);
|
||||
dataSource.addMediaSource(lastIndex, mediaSourceFactory.createMediaSource(end));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,14 +144,15 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
||||
switch (playbackState) {
|
||||
case Player.STATE_BUFFERING:
|
||||
case Player.STATE_READY:
|
||||
voiceNoteProximityManager.onPlayerReady();
|
||||
voiceNoteNotificationManager.showNotification(player);
|
||||
|
||||
if (!playWhenReady) {
|
||||
stopForeground(false);
|
||||
becomingNoisyReceiver.unregister();
|
||||
voiceNoteProximityManager.onPlayerEnded();
|
||||
} else {
|
||||
becomingNoisyReceiver.register();
|
||||
voiceNoteProximityManager.onPlayerReady();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -20,31 +20,31 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
|
||||
private final List<MediaDescriptionCompat> descriptions = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(int position) {
|
||||
public synchronized MediaDescriptionCompat getMediaDescription(int position) {
|
||||
return descriptions.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int position, MediaDescriptionCompat description) {
|
||||
public synchronized void add(int position, MediaDescriptionCompat description) {
|
||||
descriptions.add(position, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(int position) {
|
||||
public synchronized void remove(int position) {
|
||||
descriptions.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(int from, int to) {
|
||||
public synchronized void move(int from, int to) {
|
||||
MediaDescriptionCompat description = descriptions.remove(from);
|
||||
descriptions.add(to, description);
|
||||
}
|
||||
|
||||
int size() {
|
||||
synchronized int size() {
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
int indexOf(@NonNull Uri uri) {
|
||||
synchronized int indexOf(@NonNull Uri uri) {
|
||||
for (int i = 0; i < descriptions.size(); i++) {
|
||||
if (Objects.equals(uri, descriptions.get(i).getMediaUri())) {
|
||||
return i;
|
||||
@@ -54,7 +54,7 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
|
||||
return -1;
|
||||
}
|
||||
|
||||
int indexAfter(@NonNull MediaDescriptionCompat target) {
|
||||
synchronized int indexAfter(@NonNull MediaDescriptionCompat target) {
|
||||
if (isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -71,11 +71,11 @@ final class VoiceNoteQueueDataAdapter implements TimelineQueueEditor.QueueDataAd
|
||||
return descriptions.size();
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
synchronized boolean isEmpty() {
|
||||
return descriptions.isEmpty();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
synchronized void clear() {
|
||||
descriptions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,91 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.VideoFrame;
|
||||
import org.webrtc.VideoSink;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
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;
|
||||
private final WeakHashMap<Object, Point> requestingSizes;
|
||||
|
||||
public BroadcastVideoSink(@Nullable EglBase eglBase) {
|
||||
this.eglBase = eglBase;
|
||||
this.sinks = new WeakHashMap<>();
|
||||
this.eglBase = eglBase;
|
||||
this.sinks = new WeakHashMap<>();
|
||||
this.requestingSizes = new WeakHashMap<>();
|
||||
}
|
||||
|
||||
public @Nullable EglBase getEglBase() {
|
||||
return eglBase;
|
||||
}
|
||||
|
||||
public void addSink(@NonNull VideoSink sink) {
|
||||
public synchronized void addSink(@NonNull VideoSink sink) {
|
||||
sinks.put(sink, true);
|
||||
}
|
||||
|
||||
public void removeSink(@NonNull VideoSink sink) {
|
||||
public synchronized void removeSink(@NonNull VideoSink sink) {
|
||||
sinks.remove(sink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrame(@NonNull VideoFrame videoFrame) {
|
||||
public synchronized void onFrame(@NonNull VideoFrame videoFrame) {
|
||||
for (VideoSink sink : sinks.keySet()) {
|
||||
sink.onFrame(videoFrame);
|
||||
}
|
||||
}
|
||||
|
||||
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.put(object, size);
|
||||
}
|
||||
}
|
||||
|
||||
void removeRequestingSize(@NonNull Object object) {
|
||||
synchronized (requestingSizes) {
|
||||
requestingSizes.remove(object);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull RequestedSize getMaxRequestingSize() {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
synchronized (requestingSizes) {
|
||||
for (Point size : requestingSizes.values()) {
|
||||
if (width < size.x) {
|
||||
width = size.x;
|
||||
height = size.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new RequestedSize(width, height);
|
||||
}
|
||||
|
||||
public static class RequestedSize {
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
private RequestedSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -21,6 +22,7 @@ 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 org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -32,6 +34,9 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
|
||||
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
|
||||
|
||||
private static final int SMALL_AVATAR = ViewUtil.dpToPx(96);
|
||||
private static final int LARGE_AVATAR = ViewUtil.dpToPx(112);
|
||||
|
||||
private RecipientId recipientId;
|
||||
private AvatarImageView avatar;
|
||||
private TextureViewRenderer renderer;
|
||||
@@ -59,6 +64,7 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
renderer = findViewById(R.id.call_participant_renderer);
|
||||
|
||||
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
|
||||
useLargeAvatar();
|
||||
}
|
||||
|
||||
void setCallParticipant(@NonNull CallParticipant participant) {
|
||||
@@ -89,6 +95,23 @@ public class CallParticipantView extends ConstraintLayout {
|
||||
pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
void useLargeAvatar() {
|
||||
changeAvatarParams(LARGE_AVATAR);
|
||||
}
|
||||
|
||||
void useSmallAvatar() {
|
||||
changeAvatarParams(SMALL_AVATAR);
|
||||
}
|
||||
|
||||
private void changeAvatarParams(int dimension) {
|
||||
ViewGroup.LayoutParams params = avatar.getLayoutParams();
|
||||
if (params.height != dimension) {
|
||||
params.height = dimension;
|
||||
params.width = dimension;
|
||||
avatar.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPipAvatar(@NonNull Recipient recipient) {
|
||||
ContactPhoto contactPhoto = recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.getFallbackContactPhoto(FALLBACK_PHOTO_PROVIDER);
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.cardview.widget.CardView;
|
||||
|
||||
import com.google.android.flexbox.AlignItems;
|
||||
import com.google.android.flexbox.FlexboxLayout;
|
||||
@@ -14,6 +15,7 @@ import com.google.android.flexbox.FlexboxLayout;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -24,6 +26,9 @@ import java.util.List;
|
||||
*/
|
||||
public class CallParticipantsLayout extends FlexboxLayout {
|
||||
|
||||
private static final int MULTIPLE_PARTICIPANT_SPACING = ViewUtil.dpToPx(3);
|
||||
private static final int CORNER_RADIUS = ViewUtil.dpToPx(10);
|
||||
|
||||
private List<CallParticipant> callParticipants = Collections.emptyList();
|
||||
private boolean shouldRenderInPip;
|
||||
|
||||
@@ -46,17 +51,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
}
|
||||
|
||||
private void updateLayout() {
|
||||
int previousChildCount = getChildCount();
|
||||
|
||||
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
|
||||
updateChildrenCount(1);
|
||||
update(0, callParticipants.get(0));
|
||||
update(0, 1, callParticipants.get(0));
|
||||
} else {
|
||||
int count = callParticipants.size();
|
||||
updateChildrenCount(count);
|
||||
|
||||
for (int i = 0; i < callParticipants.size(); i++) {
|
||||
update(i, callParticipants.get(i));
|
||||
for (int i = 0; i < count; i++) {
|
||||
update(i, count, callParticipants.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (previousChildCount != getChildCount()) {
|
||||
updateMarginsForLayout();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMarginsForLayout() {
|
||||
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
|
||||
if (callParticipants.size() > 1 && !shouldRenderInPip) {
|
||||
layoutParams.setMargins(MULTIPLE_PARTICIPANT_SPACING, ViewUtil.getStatusBarHeight(this), MULTIPLE_PARTICIPANT_SPACING, 0);
|
||||
} else {
|
||||
layoutParams.setMargins(0, 0, 0, 0);
|
||||
}
|
||||
setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
private void updateChildrenCount(int count) {
|
||||
@@ -72,15 +93,33 @@ public class CallParticipantsLayout extends FlexboxLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void update(int index, @NonNull CallParticipant participant) {
|
||||
CallParticipantView callParticipantView = (CallParticipantView) getChildAt(index);
|
||||
private void update(int index, int count, @NonNull CallParticipant participant) {
|
||||
View view = getChildAt(index);
|
||||
CardView cardView = view.findViewById(R.id.group_call_participant_card_wrapper);
|
||||
CallParticipantView callParticipantView = view.findViewById(R.id.group_call_participant);
|
||||
|
||||
callParticipantView.setCallParticipant(participant);
|
||||
callParticipantView.setRenderInPip(shouldRenderInPip);
|
||||
setChildLayoutParams(callParticipantView, index, getChildCount());
|
||||
|
||||
if (count > 1) {
|
||||
view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING);
|
||||
cardView.setRadius(CORNER_RADIUS);
|
||||
} else {
|
||||
view.setPadding(0, 0, 0, 0);
|
||||
cardView.setRadius(0);
|
||||
}
|
||||
|
||||
if (count > 2) {
|
||||
callParticipantView.useSmallAvatar();
|
||||
} else {
|
||||
callParticipantView.useLargeAvatar();
|
||||
}
|
||||
|
||||
setChildLayoutParams(view, index, getChildCount());
|
||||
}
|
||||
|
||||
private void addCallParticipantView() {
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.call_participant_item, this, false);
|
||||
View view = LayoutInflater.from(getContext()).inflate(R.layout.group_call_participant_item, this, false);
|
||||
FlexboxLayout.LayoutParams params = (FlexboxLayout.LayoutParams) view.getLayoutParams();
|
||||
|
||||
params.setAlignSelf(AlignItems.STRETCH);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
@@ -21,24 +24,27 @@ 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);
|
||||
WebRtcViewModel.GroupCallState.IDLE,
|
||||
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;
|
||||
private final WebRtcViewModel.State callState;
|
||||
private final WebRtcViewModel.GroupCallState groupCallState;
|
||||
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 WebRtcViewModel.GroupCallState groupCallState,
|
||||
@NonNull List<CallParticipant> remoteParticipants,
|
||||
@NonNull CallParticipant localParticipant,
|
||||
@Nullable CallParticipant focusedParticipant,
|
||||
@@ -48,6 +54,7 @@ public final class CallParticipantsState {
|
||||
boolean isViewingFocusedParticipant)
|
||||
{
|
||||
this.callState = callState;
|
||||
this.groupCallState = groupCallState;
|
||||
this.remoteParticipants = remoteParticipants;
|
||||
this.localParticipant = localParticipant;
|
||||
this.localRenderState = localRenderState;
|
||||
@@ -61,6 +68,10 @@ public final class CallParticipantsState {
|
||||
return callState;
|
||||
}
|
||||
|
||||
public @NonNull WebRtcViewModel.GroupCallState getGroupCallState() {
|
||||
return groupCallState;
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getGridParticipants() {
|
||||
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
|
||||
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
|
||||
@@ -87,6 +98,30 @@ public final class CallParticipantsState {
|
||||
return listParticipants;
|
||||
}
|
||||
|
||||
public @NonNull String getRemoteParticipantsDescription(@NonNull Context context) {
|
||||
switch (remoteParticipants.size()) {
|
||||
case 0:
|
||||
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
|
||||
case 1:
|
||||
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
|
||||
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getRecipient().getShortDisplayName(context));
|
||||
} else {
|
||||
return remoteParticipants.get(0).getRecipient().getDisplayName(context);
|
||||
}
|
||||
case 2:
|
||||
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
|
||||
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
|
||||
remoteParticipants.get(1).getRecipient().getShortDisplayName(context));
|
||||
default:
|
||||
int others = remoteParticipants.size() - 2;
|
||||
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
|
||||
others,
|
||||
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
|
||||
remoteParticipants.get(1).getRecipient().getShortDisplayName(context),
|
||||
others);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
|
||||
return remoteParticipants;
|
||||
}
|
||||
@@ -132,6 +167,7 @@ public final class CallParticipantsState {
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
return new CallParticipantsState(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
webRtcViewModel.getRemoteParticipants(),
|
||||
webRtcViewModel.getLocalParticipant(),
|
||||
focused,
|
||||
@@ -152,6 +188,7 @@ public final class CallParticipantsState {
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
focused,
|
||||
@@ -172,6 +209,7 @@ public final class CallParticipantsState {
|
||||
selectedPage == SelectedPage.FOCUSED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
focused,
|
||||
@@ -193,8 +231,8 @@ public final class CallParticipantsState {
|
||||
|
||||
if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 3) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_SQUARE;
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
} else {
|
||||
localRenderState = WebRtcLocalRenderState.SMALL_RECTANGLE;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@ import android.view.TextureView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.EglRenderer;
|
||||
import org.webrtc.GlRectDrawer;
|
||||
@@ -38,6 +42,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
private int surfaceHeight;
|
||||
private boolean isInitialized;
|
||||
private BroadcastVideoSink attachedVideoSink;
|
||||
private Lifecycle lifecycle;
|
||||
|
||||
public TextureViewRenderer(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -59,7 +64,7 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
this.init(eglBase.getEglBaseContext(), null, EglBase.CONFIG_PLAIN, new GlRectDrawer());
|
||||
}
|
||||
|
||||
public void init(@NonNull EglBase.Context sharedContext, @NonNull RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
|
||||
public void init(@NonNull EglBase.Context sharedContext, @Nullable RendererCommon.RendererEvents rendererEvents, @NonNull int[] configAttributes, @NonNull RendererCommon.GlDrawer drawer) {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
|
||||
this.rendererEvents = rendererEvents;
|
||||
@@ -67,6 +72,16 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
this.rotatedFrameHeight = 0;
|
||||
|
||||
this.eglRenderer.init(sharedContext, this, configAttributes, drawer);
|
||||
|
||||
this.lifecycle = ViewUtil.getActivityLifecycle(this);
|
||||
if (lifecycle != null) {
|
||||
lifecycle.addObserver(new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
release();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void attachBroadcastVideoSink(@Nullable BroadcastVideoSink videoSink) {
|
||||
@@ -76,10 +91,12 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.removeSink(this);
|
||||
attachedVideoSink.removeRequestingSize(this);
|
||||
}
|
||||
|
||||
if (videoSink != null) {
|
||||
videoSink.addSink(this);
|
||||
videoSink.putRequestingSize(this, new Point(getWidth(), getHeight()));
|
||||
} else {
|
||||
clearImage();
|
||||
}
|
||||
@@ -90,11 +107,17 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
release();
|
||||
if (lifecycle == null || lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
eglRenderer.release();
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.removeSink(this);
|
||||
attachedVideoSink.removeRequestingSize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addFrameListener(@NonNull EglRenderer.FrameListener listener, float scale, @NonNull RendererCommon.GlDrawer drawerParam) {
|
||||
@@ -163,6 +186,10 @@ public class TextureViewRenderer extends TextureView implements TextureView.Surf
|
||||
setMeasuredDimension(size.x, size.y);
|
||||
|
||||
Log.d(TAG, "onMeasure(). New size: " + size.x + "x" + size.y);
|
||||
|
||||
if (attachedVideoSink != null) {
|
||||
attachedVideoSink.putRequestingSize(this, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
@@ -30,12 +31,14 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -88,8 +91,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
private ViewPager2 callParticipantsPager;
|
||||
private RecyclerView callParticipantsRecycler;
|
||||
private Toolbar toolbar;
|
||||
private MaterialButton startCall;
|
||||
private int pagerBottomMarginDp;
|
||||
private boolean controlsVisible = true;
|
||||
private EventListener eventListener;
|
||||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
@@ -142,13 +147,13 @@ public class WebRtcCallView extends FrameLayout {
|
||||
callParticipantsPager = findViewById(R.id.call_screen_participants_pager);
|
||||
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
|
||||
toolbar = findViewById(R.id.call_screen_toolbar);
|
||||
startCall = findViewById(R.id.call_screen_start_call_start_call);
|
||||
|
||||
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);
|
||||
|
||||
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
|
||||
@@ -163,6 +168,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
runIfNonNull(controlsListener, listener -> listener.onPageChanged(position == 0 ? CallParticipantsState.SelectedPage.GRID : CallParticipantsState.SelectedPage.FOCUSED));
|
||||
runIfNonNull(eventListener, EventListener::onPotentialLayoutChange);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,6 +239,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
this.controlsListener = controlsListener;
|
||||
}
|
||||
|
||||
public void setEventListener(@Nullable EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setMicEnabled(boolean isMicEnabled) {
|
||||
micToggle.setChecked(isMicEnabled, false);
|
||||
}
|
||||
@@ -248,6 +258,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isConnected()) {
|
||||
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
|
||||
}
|
||||
|
||||
pagerAdapter.submitList(pages);
|
||||
recyclerAdapter.submitList(state.getListParticipants());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
|
||||
@@ -257,6 +271,10 @@ public class WebRtcCallView extends FrameLayout {
|
||||
} else {
|
||||
layoutParticipantsForSmallCount();
|
||||
}
|
||||
|
||||
if (eventListener != null) {
|
||||
eventListener.onPotentialLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
|
||||
@@ -283,17 +301,17 @@ public class WebRtcCallView extends FrameLayout {
|
||||
case SMALL_RECTANGLE:
|
||||
smallLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
animatePipToRectangle();
|
||||
animatePipToLargeRectangle();
|
||||
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
|
||||
videoToggle.setChecked(true, false);
|
||||
break;
|
||||
case SMALL_SQUARE:
|
||||
case SMALLER_RECTANGLE:
|
||||
smallLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
smallLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
animatePipToSquare();
|
||||
animatePipToSmallRectangle();
|
||||
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
@@ -341,7 +359,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
recipientId = recipient.getId();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
recipientName.setText(R.string.WebRtcCallView__group_call);
|
||||
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
|
||||
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
|
||||
toolbar.inflateMenu(R.menu.group_call);
|
||||
toolbar.setOnMenuItemClickListener(unused -> showParticipantsList());
|
||||
@@ -375,6 +393,27 @@ public class WebRtcCallView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatusFromGroupCallState(@NonNull WebRtcViewModel.GroupCallState groupCallState) {
|
||||
switch (groupCallState) {
|
||||
case DISCONNECTED:
|
||||
status.setText(R.string.WebRtcCallView__disconnected);
|
||||
break;
|
||||
case CONNECTING:
|
||||
status.setText(R.string.WebRtcCallView__connecting);
|
||||
break;
|
||||
case RECONNECTING:
|
||||
status.setText(R.string.WebRtcCallView__reconnecting);
|
||||
break;
|
||||
case CONNECTED_AND_JOINING:
|
||||
status.setText(R.string.WebRtcCallView__joining);
|
||||
break;
|
||||
case CONNECTED_AND_JOINED:
|
||||
case CONNECTED:
|
||||
status.setText("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setWebRtcControls(@NonNull WebRtcControls webRtcControls) {
|
||||
Set<View> lastVisibleSet = new HashSet<>(visibleViewSet);
|
||||
|
||||
@@ -383,6 +422,14 @@ public class WebRtcCallView extends FrameLayout {
|
||||
if (webRtcControls.displayStartCallControls()) {
|
||||
visibleViewSet.add(footerGradient);
|
||||
visibleViewSet.add(startCallControls);
|
||||
|
||||
startCall.setText(webRtcControls.getStartCallButtonText());
|
||||
}
|
||||
|
||||
MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list);
|
||||
if (item != null) {
|
||||
item.setVisible(webRtcControls.displayGroupMembersButton());
|
||||
item.setEnabled(webRtcControls.displayGroupMembersButton());
|
||||
}
|
||||
|
||||
if (webRtcControls.displayTopViews()) {
|
||||
@@ -462,7 +509,7 @@ public class WebRtcCallView extends FrameLayout {
|
||||
return videoToggle;
|
||||
}
|
||||
|
||||
private void animatePipToRectangle() {
|
||||
private void animatePipToLargeRectangle() {
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
@@ -476,11 +523,11 @@ public class WebRtcCallView extends FrameLayout {
|
||||
smallLocalRenderFrame.startAnimation(animation);
|
||||
}
|
||||
|
||||
private void animatePipToSquare() {
|
||||
private void animatePipToSmallRectangle() {
|
||||
pictureInPictureGestureHelper.lockToBottomEnd();
|
||||
|
||||
pictureInPictureGestureHelper.performAfterFling(() -> {
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(72), ViewUtil.dpToPx(72));
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(40), ViewUtil.dpToPx(72));
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
@Override
|
||||
@@ -606,9 +653,9 @@ public class WebRtcCallView extends FrameLayout {
|
||||
getHandler().removeCallbacks(fadeOutRunnable);
|
||||
}
|
||||
|
||||
private static void runIfNonNull(@Nullable ControlsListener controlsListener, @NonNull Consumer<ControlsListener> controlsListenerConsumer) {
|
||||
if (controlsListener != null) {
|
||||
controlsListenerConsumer.accept(controlsListener);
|
||||
private static <T> void runIfNonNull(@Nullable T listener, @NonNull Consumer<T> listenerConsumer) {
|
||||
if (listener != null) {
|
||||
listenerConsumer.accept(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,4 +695,8 @@ public class WebRtcCallView extends FrameLayout {
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onPotentialLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
public class WebRtcCallViewModel extends ViewModel {
|
||||
@@ -104,11 +105,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
|
||||
|
||||
updateWebRtcControls(webRtcViewModel.getState(),
|
||||
webRtcViewModel.getGroupState(),
|
||||
localParticipant.getCameraState().isEnabled(),
|
||||
webRtcViewModel.isRemoteVideoEnabled(),
|
||||
webRtcViewModel.isRemoteVideoOffer(),
|
||||
localParticipant.isMoreThanOneCameraAvailable(),
|
||||
webRtcViewModel.isBluetoothAvailable(),
|
||||
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
|
||||
repository.getAudioOutput());
|
||||
|
||||
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
|
||||
@@ -133,11 +136,13 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
private void updateWebRtcControls(@NonNull WebRtcViewModel.State state,
|
||||
@NonNull WebRtcViewModel.GroupCallState groupState,
|
||||
boolean isLocalVideoEnabled,
|
||||
boolean isRemoteVideoEnabled,
|
||||
boolean isRemoteVideoOffer,
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean hasAtLeastOneRemote,
|
||||
@NonNull WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
final WebRtcControls.CallState callState;
|
||||
@@ -166,12 +171,34 @@ public class WebRtcCallViewModel extends ViewModel {
|
||||
callState = WebRtcControls.CallState.ONGOING;
|
||||
}
|
||||
|
||||
final WebRtcControls.GroupCallState groupCallState;
|
||||
|
||||
switch (groupState) {
|
||||
case DISCONNECTED:
|
||||
groupCallState = WebRtcControls.GroupCallState.DISCONNECTED;
|
||||
break;
|
||||
case CONNECTING:
|
||||
case RECONNECTING:
|
||||
groupCallState = WebRtcControls.GroupCallState.CONNECTING;
|
||||
break;
|
||||
case CONNECTED:
|
||||
case CONNECTED_AND_JOINING:
|
||||
case CONNECTED_AND_JOINED:
|
||||
groupCallState = WebRtcControls.GroupCallState.CONNECTED;
|
||||
break;
|
||||
default:
|
||||
groupCallState = WebRtcControls.GroupCallState.NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
webRtcControls.setValue(new WebRtcControls(isLocalVideoEnabled,
|
||||
isRemoteVideoEnabled || isRemoteVideoOffer,
|
||||
isMoreThanOneCameraAvailable,
|
||||
isBluetoothAvailable,
|
||||
Boolean.TRUE.equals(isInPipMode.getValue()),
|
||||
hasAtLeastOneRemote,
|
||||
callState,
|
||||
groupCallState,
|
||||
audioOutput));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class WebRtcControls {
|
||||
|
||||
public static final WebRtcControls NONE = new WebRtcControls();
|
||||
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, CallState.NONE, WebRtcAudioOutput.HANDSET);
|
||||
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
|
||||
|
||||
private final boolean isRemoteVideoEnabled;
|
||||
private final boolean isLocalVideoEnabled;
|
||||
private final boolean isMoreThanOneCameraAvailable;
|
||||
private final boolean isBluetoothAvailable;
|
||||
private final boolean isInPipMode;
|
||||
private final boolean hasAtLeastOneRemote;
|
||||
private final CallState callState;
|
||||
private final GroupCallState groupCallState;
|
||||
private final WebRtcAudioOutput audioOutput;
|
||||
|
||||
private WebRtcControls() {
|
||||
this(false, false, false, false, false, CallState.NONE, WebRtcAudioOutput.HANDSET);
|
||||
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
|
||||
}
|
||||
|
||||
WebRtcControls(boolean isLocalVideoEnabled,
|
||||
@@ -24,7 +29,9 @@ public final class WebRtcControls {
|
||||
boolean isMoreThanOneCameraAvailable,
|
||||
boolean isBluetoothAvailable,
|
||||
boolean isInPipMode,
|
||||
boolean hasAtLeastOneRemote,
|
||||
@NonNull CallState callState,
|
||||
@NonNull GroupCallState groupCallState,
|
||||
@NonNull WebRtcAudioOutput audioOutput)
|
||||
{
|
||||
this.isLocalVideoEnabled = isLocalVideoEnabled;
|
||||
@@ -32,7 +39,9 @@ public final class WebRtcControls {
|
||||
this.isBluetoothAvailable = isBluetoothAvailable;
|
||||
this.isMoreThanOneCameraAvailable = isMoreThanOneCameraAvailable;
|
||||
this.isInPipMode = isInPipMode;
|
||||
this.hasAtLeastOneRemote = hasAtLeastOneRemote;
|
||||
this.callState = callState;
|
||||
this.groupCallState = groupCallState;
|
||||
this.audioOutput = audioOutput;
|
||||
}
|
||||
|
||||
@@ -40,6 +49,17 @@ public final class WebRtcControls {
|
||||
return isPreJoin();
|
||||
}
|
||||
|
||||
@StringRes int getStartCallButtonText() {
|
||||
if (isGroupCall() && hasAtLeastOneRemote) {
|
||||
return R.string.WebRtcCallView__join_call;
|
||||
}
|
||||
return R.string.WebRtcCallView__start_call;
|
||||
}
|
||||
|
||||
boolean displayGroupMembersButton() {
|
||||
return groupCallState == GroupCallState.CONNECTED;
|
||||
}
|
||||
|
||||
boolean displayEndCall() {
|
||||
return isAtLeastOutgoing();
|
||||
}
|
||||
@@ -116,6 +136,10 @@ public final class WebRtcControls {
|
||||
return callState.isAtLeast(CallState.OUTGOING);
|
||||
}
|
||||
|
||||
private boolean isGroupCall() {
|
||||
return groupCallState != GroupCallState.NONE;
|
||||
}
|
||||
|
||||
public enum CallState {
|
||||
NONE,
|
||||
PRE_JOIN,
|
||||
@@ -124,8 +148,16 @@ public final class WebRtcControls {
|
||||
ONGOING,
|
||||
ENDING;
|
||||
|
||||
boolean isAtLeast(@NonNull CallState other) {
|
||||
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull CallState other) {
|
||||
return compareTo(other) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupCallState {
|
||||
NONE,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.components.webrtc;
|
||||
public enum WebRtcLocalRenderState {
|
||||
GONE,
|
||||
SMALL_RECTANGLE,
|
||||
SMALL_SQUARE,
|
||||
SMALLER_RECTANGLE,
|
||||
LARGE,
|
||||
LARGE_NO_VIDEO
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
@@ -79,9 +80,14 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
|
||||
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
|
||||
List<MappingModel<?>> items = new ArrayList<>();
|
||||
|
||||
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + 1));
|
||||
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
|
||||
|
||||
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
|
||||
|
||||
if (includeSelf) {
|
||||
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
|
||||
}
|
||||
|
||||
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
|
||||
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
|
||||
items.add(new CallParticipantViewState(callParticipant));
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ public class ContactAccessor {
|
||||
GroupRecord record;
|
||||
|
||||
try {
|
||||
reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(constraint, true);
|
||||
reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(constraint, true, false);
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
numberList.add(record.getId().toString());
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -62,14 +63,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
private static final int VIEW_TYPE_CONTACT = 0;
|
||||
private static final int VIEW_TYPE_DIVIDER = 1;
|
||||
|
||||
private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
|
||||
R.attr.contact_selection_lay_user};
|
||||
|
||||
public static final int PAYLOAD_SELECTION_CHANGE = 1;
|
||||
|
||||
private final boolean multiSelect;
|
||||
private final LayoutInflater layoutInflater;
|
||||
private final TypedArray drawables;
|
||||
private final ItemClickListener clickListener;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Set<RecipientId> currentContacts;
|
||||
@@ -181,7 +178,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
super(context, cursor);
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
this.glideRequests = glideRequests;
|
||||
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
this.multiSelect = multiSelect;
|
||||
this.clickListener = clickListener;
|
||||
this.currentContacts = currentContacts;
|
||||
@@ -219,8 +215,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
|
||||
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
|
||||
numberType, label).toString();
|
||||
|
||||
int color = (contactType == ContactRepository.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) :
|
||||
drawables.getColor(1, 0xff000000);
|
||||
int color = (contactType == ContactRepository.PUSH_TYPE) ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
|
||||
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
|
||||
|
||||
boolean currentContact = currentContacts.contains(id);
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
|
||||
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
|
||||
public static final int FLAG_SELF = 1 << 4;
|
||||
public static final int FLAG_BLOCK = 1 << 5;
|
||||
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
|
||||
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
|
||||
}
|
||||
|
||||
@@ -266,7 +268,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext());
|
||||
|
||||
MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX);
|
||||
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly)) {
|
||||
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode))) {
|
||||
ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations);
|
||||
ThreadRecord threadRecord;
|
||||
while ((threadRecord = reader.getNext()) != null) {
|
||||
@@ -305,7 +307,7 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
|
||||
private Cursor getGroupsCursor() {
|
||||
MatrixCursor groupContacts = new MatrixCursor(CONTACT_PROJECTION);
|
||||
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(filter, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS))) {
|
||||
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(getContext()).getGroupsFilteredByTitle(filter, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode))) {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
groupContacts.addRow(new Object[] { groupRecord.getRecipientId().serialize(),
|
||||
@@ -342,8 +344,13 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
}
|
||||
|
||||
private String getUnknownContactTitle() {
|
||||
return getContext().getString(newConversation(mode) ? R.string.contact_selection_list__unknown_contact
|
||||
: R.string.contact_selection_list__unknown_contact_add_to_group);
|
||||
if (blockUser(mode)) {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact_block);
|
||||
} else if (newConversation(mode)) {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact);
|
||||
} else {
|
||||
return getContext().getString(R.string.contact_selection_list__unknown_contact_add_to_group);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull Cursor filterNonPushContacts(@NonNull Cursor cursor) {
|
||||
@@ -382,6 +389,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
return flagSet(mode, DisplayMode.FLAG_SELF);
|
||||
}
|
||||
|
||||
private static boolean blockUser(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_BLOCK);
|
||||
}
|
||||
|
||||
private static boolean newConversation(int mode) {
|
||||
return groupsEnabled(mode);
|
||||
}
|
||||
@@ -402,6 +413,10 @@ public class ContactsCursorLoader extends CursorLoader {
|
||||
return mode == DisplayMode.FLAG_ACTIVE_GROUPS;
|
||||
}
|
||||
|
||||
private static boolean hideGroupsV1(int mode) {
|
||||
return flagSet(mode, DisplayMode.FLAG_HIDE_GROUPS_V1);
|
||||
}
|
||||
|
||||
private static boolean flagSet(int mode, int flag) {
|
||||
return (mode & flag) > 0;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class FallbackPhoto20dp implements FallbackContactPhoto {
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int color) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable20dp);
|
||||
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
|
||||
Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(2);
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class FallbackPhoto80dp implements FallbackContactPhoto {
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
|
||||
Drawable gradient = ThemeUtil.getThemedDrawable(context, R.attr.resource_placeholder_gradient);
|
||||
Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(24);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.text.TextUtils;
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -53,8 +54,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
||||
.endConfig()
|
||||
.buildRound(character, inverted ? Color.WHITE : color);
|
||||
|
||||
Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark
|
||||
: R.drawable.avatar_gradient_light);
|
||||
Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient);
|
||||
return new LayerDrawable(new Drawable[] { base, gradient });
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
@@ -70,8 +72,7 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
Drawable gradient = context.getResources().getDrawable(ThemeUtil.isDarkTheme(context) ? R.drawable.avatar_gradient_dark
|
||||
: R.drawable.avatar_gradient_light);
|
||||
Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient);
|
||||
|
||||
return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.push.IasTrustStore;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
|
||||
@@ -69,7 +70,7 @@ class ContactDiscoveryV2 {
|
||||
FuzzyPhoneNumberHelper.OutputResultV2 outputResult = FuzzyPhoneNumberHelper.generateOutputV2(results, inputResult);
|
||||
|
||||
return new DirectoryResult(outputResult.getNumbers(), outputResult.getRewrites(), ignoredNumbers);
|
||||
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException e) {
|
||||
} catch (SignatureException | UnauthenticatedQuoteException | UnauthenticatedResponseException | Quote.InvalidQuoteFormatException |InvalidKeyException e) {
|
||||
Log.w(TAG, "Attestation error.", e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
@@ -72,6 +73,7 @@ import java.util.concurrent.TimeoutException;
|
||||
/**
|
||||
* Manages all the stuff around determining if a user is registered or not.
|
||||
*/
|
||||
@Trace
|
||||
public class DirectoryHelper {
|
||||
|
||||
private static final String TAG = Log.tag(DirectoryHelper.class);
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
@@ -34,6 +35,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -118,15 +120,7 @@ public class SharedContactDetailsActivity extends PassphraseRequiredActivity {
|
||||
getSupportActionBar().setTitle("");
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
int[] attrs = {R.attr.shared_contact_details_titlebar};
|
||||
TypedArray array = obtainStyledAttributes(attrs);
|
||||
int color = array.getResourceId(0, android.R.color.black);
|
||||
|
||||
array.recycle();
|
||||
|
||||
getWindow().setStatusBarColor(getResources().getColor(color));
|
||||
}
|
||||
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.shared_contact_details_titlebar));
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
|
||||
@@ -24,8 +24,8 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
@@ -85,7 +85,6 @@ import com.bumptech.glide.request.transition.Transition;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||
import org.thoughtcrime.securesms.GroupMembersDialog;
|
||||
@@ -110,6 +109,7 @@ import org.thoughtcrime.securesms.components.InputPanel;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
@@ -117,6 +117,8 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationInitiationReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
@@ -167,6 +169,8 @@ import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
@@ -209,10 +213,8 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
@@ -234,11 +236,14 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
@@ -253,9 +258,9 @@ import org.thoughtcrime.securesms.util.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
@@ -290,6 +295,7 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
@Trace
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationActivity extends PassphraseRequiredActivity
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
@@ -349,7 +355,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private InputAwareLayout container;
|
||||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<GroupShareProfileView> groupShareProfileView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
@@ -410,12 +415,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
long threadId)
|
||||
{
|
||||
Intent intent = new Intent(context, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId.serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientId getRecipientId(@NonNull Intent intent) {
|
||||
return RecipientId.from(Objects.requireNonNull(intent.getStringExtra(RECIPIENT_EXTRA)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
@@ -424,7 +434,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state, boolean ready) {
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
|
||||
RecipientId recipientId = getRecipientId(getIntent());
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "[onCreate] Missing recipientId!");
|
||||
@@ -434,14 +444,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
reportShortcutLaunch(recipientId);
|
||||
setContentView(R.layout.conversation_activity);
|
||||
|
||||
TypedArray typedArray = obtainStyledAttributes(new int[] {R.attr.conversation_background});
|
||||
int color = typedArray.getColor(0, Color.WHITE);
|
||||
typedArray.recycle();
|
||||
|
||||
getWindow().getDecorView().setBackgroundColor(color);
|
||||
getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
|
||||
|
||||
fragment = initFragment(R.id.fragment_content, new ConversationFragment(), dynamicLanguage.getCurrentLocale());
|
||||
|
||||
@@ -457,6 +463,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
initializeMentionsViewModel();
|
||||
initializeEnabledCheck();
|
||||
initializePendingRequestsBanner();
|
||||
initializeGroupV1MigrationsBanners();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -505,7 +512,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
silentlySetComposeText("");
|
||||
}
|
||||
|
||||
RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
|
||||
RecipientId recipientId = getRecipientId(intent);
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "[onNewIntent] Missing recipientId!");
|
||||
@@ -515,6 +522,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
reportShortcutLaunch(recipientId);
|
||||
setIntent(intent);
|
||||
initializeResources();
|
||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@@ -559,6 +567,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId);
|
||||
|
||||
ConversationUtil.pushShortcutForRecipient(getApplicationContext(), recipientSnapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -743,6 +753,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
|
||||
}
|
||||
|
||||
private void reportShortcutLaunch(@NonNull RecipientId recipientId) {
|
||||
if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(this);
|
||||
if (shortcutManager != null) {
|
||||
shortcutManager.reportShortcutUsed(ConversationUtil.getShortcutId(recipientId));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleImageFromDeviceCameraApp() {
|
||||
if (attachmentManager.getCaptureUri() == null) {
|
||||
Log.w(TAG, "No image available.");
|
||||
@@ -819,6 +840,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
|
||||
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
|
||||
} else if (isGroupConversation()) {
|
||||
if (isActiveV2Group && FeatureFlags.groupCalling()) {
|
||||
inflater.inflate(R.menu.conversation_callable_groupv2, menu);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.conversation_group_options, menu);
|
||||
|
||||
if (!isPushGroupConversation()) {
|
||||
@@ -1164,7 +1189,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void handleResetSecureSession() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.ConversationActivity_reset_secure_session_question);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setIcon(R.drawable.ic_warning);
|
||||
builder.setCancelable(true);
|
||||
builder.setMessage(R.string.ConversationActivity_this_may_help_if_youre_having_encryption_problems);
|
||||
builder.setPositiveButton(R.string.ConversationActivity_reset, (dialog, which) -> {
|
||||
@@ -1547,6 +1572,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.observe(this, actionablePendingGroupRequests -> updateReminders());
|
||||
}
|
||||
|
||||
private void initializeGroupV1MigrationsBanners() {
|
||||
groupViewModel.getGroupV1MigrationSuggestions()
|
||||
.observe(this, s -> updateReminders());
|
||||
groupViewModel.getShowGroupsV1MigrationBanner()
|
||||
.observe(this, b -> updateReminders());
|
||||
}
|
||||
|
||||
|
||||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
@@ -1702,6 +1735,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
protected void updateReminders() {
|
||||
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
|
||||
Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
|
||||
List<RecipientId> gv1MigrationSuggestions = groupViewModel.getGroupV1MigrationSuggestions().getValue();
|
||||
Boolean gv1MigrationBanner = groupViewModel.getShowGroupsV1MigrationBanner().getValue();
|
||||
|
||||
if (UnauthorizedReminder.isEligible(this)) {
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
@@ -1726,25 +1761,41 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(this, getRecipient().getGroupId().get().requireV2()));
|
||||
}
|
||||
});
|
||||
} else if (gv1MigrationBanner == Boolean.TRUE && recipient.get().isPushV1Group()) {
|
||||
reminderView.get().showReminder(new GroupsV1MigrationInitiationReminder(this));
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
if (actionId == R.id.reminder_action_gv1_initiation_update_group) {
|
||||
GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(getSupportFragmentManager(), recipient.getId());
|
||||
} else if (actionId == R.id.reminder_action_gv1_initiation_not_now) {
|
||||
groupViewModel.onMigrationInitiationReminderBannerDismissed(recipient.getId());
|
||||
}
|
||||
});
|
||||
} else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
|
||||
reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(this, gv1MigrationSuggestions));
|
||||
reminderView.get().setOnActionClickListener(actionId -> {
|
||||
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
|
||||
GroupsV1MigrationSuggestionsDialog.show(this, recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
|
||||
} else if (actionId == R.id.reminder_action_gv1_suggestion_not_now) {
|
||||
groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId());
|
||||
}
|
||||
});
|
||||
reminderView.get().setOnDismissListener(() -> {
|
||||
});
|
||||
} else if (reminderView.resolved()) {
|
||||
reminderView.get().hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReminderAction(@IdRes int reminderActionId) {
|
||||
switch (reminderActionId) {
|
||||
case R.id.reminder_action_invite:
|
||||
handleInviteLink();
|
||||
reminderView.get().requestDismiss();
|
||||
break;
|
||||
case R.id.reminder_action_view_insights:
|
||||
InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
|
||||
break;
|
||||
case R.id.reminder_action_update_now:
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
|
||||
if (reminderActionId == R.id.reminder_action_invite) {
|
||||
handleInviteLink();
|
||||
reminderView.get().requestDismiss();
|
||||
} else if (reminderActionId == R.id.reminder_action_view_insights) {
|
||||
InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
|
||||
} else if (reminderActionId == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1837,7 +1888,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
container = ViewUtil.findById(this, R.id.layout_container);
|
||||
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
|
||||
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
|
||||
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
|
||||
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
|
||||
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
|
||||
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
|
||||
@@ -1926,7 +1976,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
recipient.removeObservers(this);
|
||||
}
|
||||
|
||||
recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
recipient = Recipient.live(getRecipientId(getIntent()));
|
||||
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
|
||||
distributionType = getIntent().getIntExtra(DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
glideRequests = GlideApp.with(this);
|
||||
@@ -1934,7 +1984,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
recipient.observe(this, this::onRecipientChanged);
|
||||
}
|
||||
|
||||
|
||||
private void initializeLinkPreviewObserver() {
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);
|
||||
|
||||
@@ -2255,7 +2304,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setIconAttribute(R.attr.conversation_attach_contact_info);
|
||||
builder.setIcon(R.drawable.ic_account_box);
|
||||
builder.setTitle(R.string.ConversationActivity_select_contact_info);
|
||||
|
||||
builder.setItems(numberItems, (dialog, which) -> composeText.append(numbers[which]));
|
||||
@@ -2340,7 +2389,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
ActionBar supportActionBar = getSupportActionBar();
|
||||
if (supportActionBar == null) throw new AssertionError();
|
||||
supportActionBar.setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
setStatusBarColor(color.toStatusBarColor(this));
|
||||
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||
}
|
||||
|
||||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
@@ -2549,11 +2598,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
boolean initiating = threadId == -1;
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
|
||||
|
||||
ApplicationContext.getInstance(this).getTypingStatusSender().onTypingStopped(threadId);
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
inputPanel.clearQuote();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
@@ -2625,7 +2673,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (isSecureText && !forceSms) {
|
||||
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate);
|
||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
|
||||
} else {
|
||||
outgoingMessage = outgoingMessageCandidate;
|
||||
}
|
||||
@@ -2671,7 +2719,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (isSecureText && !forceSms) {
|
||||
message = new OutgoingEncryptedMessage(recipient.get(), messageBody, expiresIn);
|
||||
ApplicationContext.getInstance(context).getTypingStatusSender().onTypingStopped(threadId);
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
|
||||
} else {
|
||||
message = new OutgoingTextMessage(recipient.get(), messageBody, expiresIn, subscriptionId);
|
||||
}
|
||||
@@ -2967,7 +3015,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
Permissions.with(ConversationActivity.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_solid_24)
|
||||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
composeText.clearFocus();
|
||||
@@ -3059,10 +3107,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
private boolean enabled = true;
|
||||
|
||||
private String previousText = "";
|
||||
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
if (enabled && threadId > 0 && isSecureText && !isSmsForced() && !recipient.get().isBlocked()) {
|
||||
ApplicationContext.getInstance(ConversationActivity.this).getTypingStatusSender().onTypingStarted(threadId);
|
||||
TypingStatusSender typingStatusSender = ApplicationDependencies.getTypingStatusSender();
|
||||
|
||||
if (text.length() == 0) {
|
||||
typingStatusSender.onTypingStoppedWithNotify(threadId);
|
||||
} else if (text.length() < previousText.length() && previousText.contains(text)) {
|
||||
typingStatusSender.onTypingStopped(threadId);
|
||||
} else {
|
||||
typingStatusSender.onTypingStarted(threadId);
|
||||
}
|
||||
|
||||
previousText = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3077,6 +3137,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRequestBottomView.setDeleteOnClickListener(v -> onMessageRequestDeleteClicked(viewModel));
|
||||
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
|
||||
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
|
||||
messageRequestBottomView.setGroupV1MigrationContinueListener(v -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(getSupportFragmentManager(), recipient.getId()));
|
||||
|
||||
viewModel.getRequestReviewDisplayState().observe(this, this::presentRequestReviewBanner);
|
||||
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
|
||||
@@ -3115,8 +3176,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
reviewBanner.get().setBannerMessage(message);
|
||||
|
||||
Drawable drawable = Objects.requireNonNull(ThemeUtil.getThemedDrawable(this, R.attr.menu_info_icon)).mutate();
|
||||
DrawableCompat.setTint(drawable, ThemeUtil.getThemedColor(this, R.attr.icon_tint));
|
||||
Drawable drawable = ContextUtil.requireDrawable(this, R.drawable.ic_info_white_24).mutate();
|
||||
DrawableCompat.setTint(drawable, ContextCompat.getColor(this, R.color.signal_icon_tint_primary));
|
||||
|
||||
reviewBanner.get().setBannerIcon(drawable);
|
||||
reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
|
||||
@@ -3380,22 +3441,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
switch (displayState) {
|
||||
case DISPLAY_MESSAGE_REQUEST:
|
||||
messageRequestBottomView.setVisibility(View.VISIBLE);
|
||||
if (groupShareProfileView.resolved()) {
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
case DISPLAY_PRE_MESSAGE_REQUEST:
|
||||
if (recipient.get().isGroup()) {
|
||||
groupShareProfileView.get().setRecipient(recipient.get());
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
}
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
break;
|
||||
case DISPLAY_NONE:
|
||||
messageRequestBottomView.setVisibility(View.GONE);
|
||||
if (groupShareProfileView.resolved()) {
|
||||
groupShareProfileView.get().setVisibility(View.GONE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -3499,7 +3547,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ConversationActivity.this);
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setIcon(R.drawable.ic_warning);
|
||||
builder.setTitle("No longer verified");
|
||||
builder.setItems(unverifiedNames, (dialog, which) -> {
|
||||
startActivity(VerifyIdentityActivity.newIntent(ConversationActivity.this, unverifiedIdentities.get(which), false));
|
||||
|
||||
@@ -10,7 +10,6 @@ final class ConversationData {
|
||||
private final int lastScrolledPosition;
|
||||
private final boolean hasSent;
|
||||
private final boolean isMessageRequestAccepted;
|
||||
private final boolean hasPreMessageRequestMessages;
|
||||
private final int jumpToPosition;
|
||||
private final int threadSize;
|
||||
|
||||
@@ -20,7 +19,6 @@ final class ConversationData {
|
||||
int lastScrolledPosition,
|
||||
boolean hasSent,
|
||||
boolean isMessageRequestAccepted,
|
||||
boolean hasPreMessageRequestMessages,
|
||||
int jumpToPosition,
|
||||
int threadSize)
|
||||
{
|
||||
@@ -30,7 +28,6 @@ final class ConversationData {
|
||||
this.lastScrolledPosition = lastScrolledPosition;
|
||||
this.hasSent = hasSent;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
this.hasPreMessageRequestMessages = hasPreMessageRequestMessages;
|
||||
this.jumpToPosition = jumpToPosition;
|
||||
this.threadSize = threadSize;
|
||||
}
|
||||
@@ -59,10 +56,6 @@ final class ConversationData {
|
||||
return isMessageRequestAccepted;
|
||||
}
|
||||
|
||||
boolean hasPreMessageRequestMessages() {
|
||||
return hasPreMessageRequestMessages;
|
||||
}
|
||||
|
||||
boolean shouldJumpToMessage() {
|
||||
return jumpToPosition >= 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
@@ -32,6 +33,7 @@ import java.util.concurrent.Executor;
|
||||
/**
|
||||
* Core data source for loading an individual conversation.
|
||||
*/
|
||||
@Trace
|
||||
class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationDataSource.class);
|
||||
|
||||
@@ -52,6 +52,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -60,7 +61,6 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
@@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationScrollToView;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
@@ -82,14 +83,13 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -105,7 +105,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.UnknownSenderView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
@@ -119,17 +118,21 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
@@ -144,6 +147,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Trace
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationFragment extends LoggingFragment {
|
||||
private static final String TAG = ConversationFragment.class.getSimpleName();
|
||||
@@ -167,7 +171,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||
private ViewSwitcher topLoadMoreView;
|
||||
private ViewSwitcher bottomLoadMoreView;
|
||||
private ConversationTypingView typingView;
|
||||
private UnknownSenderView unknownSenderView;
|
||||
private View composeDivider;
|
||||
private ConversationScrollToView scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToMentionButton;
|
||||
@@ -348,7 +351,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(threadId).removeObservers(this);
|
||||
ApplicationDependencies.getTypingStatusRepository().getTypists(threadId).removeObservers(this);
|
||||
}
|
||||
|
||||
public void onNewIntent() {
|
||||
@@ -477,9 +480,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
|
||||
this.recipient = Recipient.live(ConversationActivity.getRecipientId(requireActivity().getIntent()));
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
this.unknownSenderView = new UnknownSenderView(getActivity(), recipient.get(), threadId, () -> clearHeaderIfNotTyping(getListAdapter()));
|
||||
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||
|
||||
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
|
||||
@@ -495,7 +497,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
list.addOnScrollListener(conversationScrollListener);
|
||||
|
||||
if (oldThreadId != threadId) {
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(oldThreadId).removeObservers(this);
|
||||
ApplicationDependencies.getTypingStatusRepository().getTypists(oldThreadId).removeObservers(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,8 +531,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(threadId).removeObservers(this);
|
||||
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypists(threadId).observe(this, typingState -> {
|
||||
LiveData<TypingStatusRepository.TypingState> typists = ApplicationDependencies.getTypingStatusRepository().getTypists(threadId);
|
||||
|
||||
typists.removeObservers(this);
|
||||
typists.observe(this, typingState -> {
|
||||
List<Recipient> recipients;
|
||||
boolean replacedByIncomingMessage;
|
||||
|
||||
@@ -945,15 +949,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
setLastSeen(conversation.getLastSeen());
|
||||
|
||||
if (!conversation.hasPreMessageRequestMessages()) {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
} else {
|
||||
if (!conversation.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isGroup() && recipient.get().getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) {
|
||||
adapter.setHeaderView(unknownSenderView);
|
||||
} else {
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
}
|
||||
}
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
|
||||
listener.onCursorChanged();
|
||||
|
||||
@@ -1421,7 +1417,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
@Override
|
||||
public void onGroupMigrationLearnMoreClicked(@NonNull List<RecipientId> pendingRecipients) {
|
||||
GroupsV1MigrationBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
|
||||
GroupsV1MigrationInfoBottomSheetDialogFragment.showForLearnMore(requireFragmentManager(), pendingRecipients);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1502,8 +1498,8 @@ public class ConversationFragment extends LoggingFragment {
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(Sets.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(SetUtil.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_copy: handleCopyMessage(SetUtil.newHashSet(conversationMessage)); return true;
|
||||
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
|
||||
case R.id.action_forward: handleForwardMessage(conversationMessage); return true;
|
||||
@@ -1527,7 +1523,11 @@ public class ConversationFragment extends LoggingFragment {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
statusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
WindowUtil.setStatusBarColor(window, getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.setLightStatusBar(getActivity().getWindow());
|
||||
}
|
||||
|
||||
setCorrectMenuVisibility(menu);
|
||||
@@ -1547,9 +1547,10 @@ public class ConversationFragment extends LoggingFragment {
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(statusBarColor);
|
||||
WindowUtil.setStatusBarColor(requireActivity().getWindow(), statusBarColor);
|
||||
}
|
||||
|
||||
WindowUtil.clearLightStatusBar(getActivity().getWindow());
|
||||
actionMode = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
@@ -21,25 +23,34 @@ import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private static final long GV1_MIGRATION_REMINDER_INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
|
||||
private final LiveData<Boolean> gv1MigrationReminder;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
@@ -58,6 +69,8 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
|
||||
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
|
||||
this.gv1MigrationReminder = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationReminder));
|
||||
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
|
||||
duplicates,
|
||||
(record, dups) -> dups.isEmpty()
|
||||
@@ -70,6 +83,22 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
liveRecipient.setValue(recipient);
|
||||
}
|
||||
|
||||
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
if (groupId.isV2()) {
|
||||
DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).clearFormerV1Members(groupId.requireV2());
|
||||
liveRecipient.postValue(liveRecipient.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onMigrationInitiationReminderBannerDismissed(@NonNull RecipientId recipientId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markGroupsV1MigrationReminderSeen(recipientId, System.currentTimeMillis());
|
||||
liveRecipient.postValue(liveRecipient.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of pending group join requests that can be actioned by this client.
|
||||
*/
|
||||
@@ -89,6 +118,14 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
return reviewState;
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<RecipientId>> getGroupV1MigrationSuggestions() {
|
||||
return gv1MigrationSuggestions;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowGroupsV1MigrationBanner() {
|
||||
return gv1MigrationReminder;
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
@@ -127,6 +164,39 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
return record.memberLevel(Recipient.self());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static List<RecipientId> mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Set<RecipientId> difference = SetUtil.difference(record.getFormerV1Members(), record.getMembers());
|
||||
|
||||
return Stream.of(Recipient.resolvedList(difference))
|
||||
.filter(GroupsV1MigrationUtil::isAutoMigratable)
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static boolean mapToGroupV1MigrationReminder(@Nullable GroupRecord record) {
|
||||
if (record == null || !record.isV1Group() || !record.isActive() || !FeatureFlags.groupsV1ManualMigration()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canAutoMigrate = Stream.of(Recipient.resolvedList(record.getMembers()))
|
||||
.allMatch(GroupsV1MigrationUtil::isAutoMigratable);
|
||||
|
||||
if (canAutoMigrate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
long lastReminderTime = DatabaseFactory.getRecipientDatabase(context).getGroupsV1MigrationReminderLastSeen(record.getRecipientId());
|
||||
|
||||
return System.currentTimeMillis() - lastReminderTime > GV1_MIGRATION_REMINDER_INTERVAL;
|
||||
}
|
||||
|
||||
public static void onCancelJoinRequest(@NonNull Recipient recipient,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
|
||||
@@ -115,6 +115,7 @@ import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
@@ -394,11 +395,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
private void initializeAttributes() {
|
||||
final int[] attributes = new int[] {R.attr.conversation_item_bubble_background};
|
||||
final TypedArray attrs = context.obtainStyledAttributes(attributes);
|
||||
|
||||
defaultBubbleColor = attrs.getColor(0, Color.WHITE);
|
||||
attrs.recycle();
|
||||
defaultBubbleColor = ContextCompat.getColor(context, R.color.signal_background_secondary);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -421,24 +418,24 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
private void setBubbleState(MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
|
||||
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
|
||||
bodyBubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(context, R.attr.conversation_item_reveal_viewed_background_color), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_sent_icon_color));
|
||||
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
|
||||
} else {
|
||||
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
|
||||
footer.setIconColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_secondary_color));
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
}
|
||||
|
||||
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_sent_text_secondary_color));
|
||||
outliner.setColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
|
||||
pulseOutliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_mention_pulse_color));
|
||||
pulseOutliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent));
|
||||
pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4));
|
||||
|
||||
outliners.clear();
|
||||
@@ -619,7 +616,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
String deletedMessage = context.getString(messageRecord.isOutgoing() ? R.string.ConversationItem_you_deleted_this_message : R.string.ConversationItem_this_message_was_deleted);
|
||||
SpannableString italics = new SpannableString(deletedMessage);
|
||||
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
italics.setSpan(new ForegroundColorSpan(ThemeUtil.getThemedColor(context, R.attr.conversation_item_delete_for_everyone_text_color)),
|
||||
italics.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.signal_text_primary)),
|
||||
0,
|
||||
deletedMessage.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@@ -646,7 +643,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_40));
|
||||
}
|
||||
|
||||
bodyText.setText(styledText);
|
||||
bodyText.setText(StringUtil.trim(styledText));
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@@ -1135,7 +1132,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) {
|
||||
if (groupSender != null && groupSenderProfileName != null) {
|
||||
int stickerAuthorColor = ThemeUtil.getThemedColor(context, R.attr.conversation_sticker_author_color);
|
||||
int stickerAuthorColor = ContextCompat.getColor(context, R.color.signal_text_primary);
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
groupSenderProfileName.setTextColor(stickerAuthorColor);
|
||||
@@ -1143,8 +1140,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
groupSenderProfileName.setTextColor(stickerAuthorColor);
|
||||
} else {
|
||||
groupSender.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
groupSenderProfileName.setTextColor(ThemeUtil.getThemedColor(context, R.attr.conversation_item_received_text_primary_color));
|
||||
groupSender.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
|
||||
groupSenderProfileName.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class ConversationPopupActivity extends ConversationActivity {
|
||||
public void onSuccess(Long result) {
|
||||
ActivityOptionsCompat transition = ActivityOptionsCompat.makeScaleUpAnimation(getWindow().getDecorView(), 0, 0, getWindow().getAttributes().width, getWindow().getAttributes().height);
|
||||
Intent intent = new Intent(ConversationPopupActivity.this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId());
|
||||
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, getRecipient().getId().serialize());
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
||||
|
||||
startActivity(intent, transition.toBundle());
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -38,6 +39,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@@ -199,10 +201,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
activity.getWindow().setStatusBarColor(ThemeUtil.getThemedColor(getContext(), R.attr.reactions_overlay_toolbar_background_color));
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), ContextCompat.getColor(getContext(), R.color.action_mode_status_bar));
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.setLightStatusBar(activity.getWindow());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,9 +242,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 23 && activity != null) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
|
||||
WindowUtil.clearLightStatusBar(activity.getWindow());
|
||||
activity = null;
|
||||
}
|
||||
|
||||
@@ -405,7 +407,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
} else if (isAtCustomIndex) {
|
||||
view.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.reactions_overlay_custom_emoji_icon));
|
||||
view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
|
||||
view.setTag(null);
|
||||
} else {
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
|
||||
@@ -44,8 +44,7 @@ class ConversationRepository {
|
||||
long lastScrolled = metadata.getLastScrolled();
|
||||
int lastScrolledPosition = 0;
|
||||
|
||||
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
||||
boolean hasPreMessageRequestMessages = RecipientUtil.isPreMessageRequestThread(context, threadId);
|
||||
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
||||
|
||||
if (lastSeen > 0) {
|
||||
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
|
||||
@@ -59,6 +58,6 @@ class ConversationRepository {
|
||||
lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled);
|
||||
}
|
||||
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, hasPreMessageRequestMessages, jumpToPosition, threadSize);
|
||||
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, jumpToPosition, threadSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.text.TextUtils;
|
||||
@@ -24,17 +23,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = ConversationTitleView.class.getSimpleName();
|
||||
|
||||
private View content;
|
||||
private AvatarImageView avatar;
|
||||
private TextView title;
|
||||
private TextView subtitle;
|
||||
@@ -56,7 +48,6 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
this.content = findViewById(R.id.content);
|
||||
this.title = findViewById(R.id.title);
|
||||
this.subtitle = findViewById(R.id.subtitle);
|
||||
this.verified = findViewById(R.id.verified_indicator);
|
||||
@@ -84,8 +75,8 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
public void setTitle(@NonNull GlideRequests glideRequests, @Nullable Recipient recipient) {
|
||||
this.subtitleContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
if (recipient == null) setComposeTitle();
|
||||
else setRecipientTitle(recipient);
|
||||
if (recipient == null) setComposeTitle();
|
||||
else setRecipientTitle(recipient);
|
||||
|
||||
int startDrawable = 0;
|
||||
int endDrawable = 0;
|
||||
@@ -122,31 +113,18 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
updateSubtitleVisibility();
|
||||
}
|
||||
|
||||
private void setRecipientTitle(Recipient recipient) {
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isSelf()) setSelfTitle();
|
||||
else setIndividualRecipientTitle(recipient);
|
||||
private void setRecipientTitle(@NonNull Recipient recipient) {
|
||||
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
|
||||
else if (recipient.isSelf()) setSelfTitle();
|
||||
else setIndividualRecipientTitle(recipient);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void setNonContactRecipientTitle(Recipient recipient) {
|
||||
this.title.setText(Util.getFirstNonEmpty(recipient.getE164().orNull(), recipient.getUuid().transform(UUID::toString).orNull()));
|
||||
|
||||
if (recipient.getProfileName().isEmpty()) {
|
||||
this.subtitle.setText(null);
|
||||
} else {
|
||||
this.subtitle.setText("~" + recipient.getProfileName().toString());
|
||||
}
|
||||
|
||||
updateSubtitleVisibility();
|
||||
}
|
||||
|
||||
private void setGroupRecipientTitle(Recipient recipient) {
|
||||
private void setGroupRecipientTitle(@NonNull Recipient recipient) {
|
||||
this.title.setText(recipient.getDisplayName(getContext()));
|
||||
this.subtitle.setText(Stream.of(recipient.getParticipants())
|
||||
.sorted((a, b) -> Boolean.compare(a.isSelf(), b.isSelf()))
|
||||
.map(r -> r.isSelf() ? getResources().getString(R.string.ConversationTitleView_you)
|
||||
: r.getDisplayName(getContext()))
|
||||
: r.getDisplayName(getContext()))
|
||||
.collect(Collectors.joining(", ")));
|
||||
|
||||
updateSubtitleVisibility();
|
||||
@@ -157,10 +135,11 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
this.subtitleContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setIndividualRecipientTitle(Recipient recipient) {
|
||||
private void setIndividualRecipientTitle(@NonNull Recipient recipient) {
|
||||
final String displayName = recipient.getDisplayNameOrUsername(getContext());
|
||||
this.title.setText(displayName);
|
||||
this.subtitle.setText(null);
|
||||
updateSubtitleVisibility();
|
||||
updateVerifiedSubtitleVisibility();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -109,7 +110,7 @@ public final class ConversationUpdateItem extends LinearLayout
|
||||
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
|
||||
|
||||
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
|
||||
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription);
|
||||
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color));
|
||||
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
|
||||
|
||||
present(conversationMessage, nextMessageRecord);
|
||||
|
||||
@@ -37,11 +37,13 @@ import com.google.android.material.snackbar.Snackbar;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@Trace
|
||||
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
|
||||
{
|
||||
private RecyclerView list;
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.tracing.Trace;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.paging.Invalidator;
|
||||
@@ -30,6 +31,7 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Trace
|
||||
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
|
||||
|
||||
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
|
||||
|
||||