mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-04 17:28:53 +01:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6b729c470 | ||
|
|
890014759e | ||
|
|
68c1c43381 | ||
|
|
d0dfcaaad5 | ||
|
|
3cffaddc0a | ||
|
|
bf4cac0c82 | ||
|
|
f680749a00 | ||
|
|
13a67980d9 | ||
|
|
f110d595d2 | ||
|
|
9c8857352b | ||
|
|
c09a1fdba8 | ||
|
|
cdc7033a51 | ||
|
|
fa30c759d7 | ||
|
|
d040be2df0 | ||
|
|
935c831a7f | ||
|
|
867e95eef1 | ||
|
|
2ee04bd1b6 | ||
|
|
75d567e555 | ||
|
|
d8a489971c | ||
|
|
19ce5b5c76 | ||
|
|
7c70ea4d3e | ||
|
|
2784285d47 | ||
|
|
c946a7a1d5 | ||
|
|
3e60b49b8b | ||
|
|
4e7331bbb8 | ||
|
|
b8c7e86223 | ||
|
|
3b925f8674 | ||
|
|
f1f6d41c73 | ||
|
|
29ef1cb1be | ||
|
|
4296085d65 | ||
|
|
c797b09228 | ||
|
|
a870ef0030 | ||
|
|
43ed9e7310 | ||
|
|
bcd27355f9 | ||
|
|
6a14dc69c0 | ||
|
|
ed9acd25f9 | ||
|
|
7b24e66ed3 | ||
|
|
abd3d4b546 | ||
|
|
4040c4240a | ||
|
|
1ee747f3ef | ||
|
|
f88874bec8 | ||
|
|
ed440a2150 | ||
|
|
2fd46b196b | ||
|
|
12dfcaf7e7 | ||
|
|
f4a199f621 | ||
|
|
bb708e0aa3 | ||
|
|
d625740ca4 | ||
|
|
250402e9b9 | ||
|
|
1d2ffe56fb | ||
|
|
d16c0d2887 | ||
|
|
b3555f2f94 | ||
|
|
83a638fc6d | ||
|
|
f1534a710f | ||
|
|
a16845340b |
@@ -80,8 +80,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 701
|
||||
def canonicalVersionName = "4.70.4"
|
||||
def canonicalVersionCode = 706
|
||||
def canonicalVersionName = "4.71.3"
|
||||
|
||||
def postFixSize = 10
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -122,7 +122,7 @@ android {
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
|
||||
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
|
||||
@@ -310,7 +310,7 @@ dependencies {
|
||||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.5.0'
|
||||
implementation 'org.signal:ringrtc-android:2.5.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.16"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
||||
@@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -29,13 +31,23 @@ import java.util.Map;
|
||||
*/
|
||||
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
|
||||
|
||||
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
|
||||
|
||||
public FlipperSqlCipherAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
|
||||
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
|
||||
} catch (Exception e) {
|
||||
Log.i(TAG, "Unable to use reflection to access raw database.", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -523,6 +523,11 @@
|
||||
<activity android:name=".groups.ui.chooseadmin.ChooseNewAdminActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"/>
|
||||
|
||||
<activity android:name=".megaphone.ClientDeprecatedActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
@@ -640,6 +645,8 @@
|
||||
|
||||
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
|
||||
|
||||
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
|
||||
|
||||
<provider android:name=".providers.PartProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
|
||||
58
app/src/main/java/org/signal/glide/Log.java
Normal file
58
app/src/main/java/org/signal/glide/Log.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class Log {
|
||||
|
||||
private Log() {}
|
||||
|
||||
public static void v(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().v(tag, message);
|
||||
}
|
||||
|
||||
public static void d(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().d(tag, message);
|
||||
}
|
||||
|
||||
public static void i(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().i(tag, message);
|
||||
}
|
||||
|
||||
public static void w(@NonNull String tag, @NonNull String message) {
|
||||
SignalGlideCodecs.getLogProvider().w(tag, message);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
SignalGlideCodecs.getLogProvider().e(tag, message, throwable);
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
void v(@NonNull String tag, @NonNull String message);
|
||||
void d(@NonNull String tag, @NonNull String message);
|
||||
void i(@NonNull String tag, @NonNull String message);
|
||||
void w(@NonNull String tag, @NonNull String message);
|
||||
void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable);
|
||||
|
||||
Provider EMPTY = new Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) { }
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @NonNull Throwable throwable) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.signal.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class SignalGlideCodecs {
|
||||
|
||||
private static Log.Provider logProvider = Log.Provider.EMPTY;
|
||||
|
||||
private SignalGlideCodecs() {}
|
||||
|
||||
public static void setLogProvider(@NonNull Log.Provider provider) {
|
||||
logProvider = provider;
|
||||
}
|
||||
|
||||
public static @NonNull Log.Provider getLogProvider() {
|
||||
return logProvider;
|
||||
}
|
||||
}
|
||||
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.common.FrameAnimationDrawable;
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.AssetStreamLoader;
|
||||
import org.signal.glide.common.loader.FileLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.common.loader.ResourceStreamLoader;
|
||||
|
||||
/**
|
||||
* @Description: APNGDrawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public class APNGDrawable extends FrameAnimationDrawable<APNGDecoder> {
|
||||
public APNGDrawable(Loader provider) {
|
||||
super(provider);
|
||||
}
|
||||
|
||||
public APNGDrawable(APNGDecoder decoder) {
|
||||
super(decoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGDecoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener) {
|
||||
return new APNGDecoder(streamLoader, listener);
|
||||
}
|
||||
|
||||
|
||||
public static APNGDrawable fromAsset(Context context, String assetPath) {
|
||||
AssetStreamLoader assetStreamLoader = new AssetStreamLoader(context, assetPath);
|
||||
return new APNGDrawable(assetStreamLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromFile(String filePath) {
|
||||
FileLoader fileLoader = new FileLoader(filePath);
|
||||
return new APNGDrawable(fileLoader);
|
||||
}
|
||||
|
||||
public static APNGDrawable fromResource(Context context, int resId) {
|
||||
ResourceStreamLoader resourceStreamLoader = new ResourceStreamLoader(context, resId);
|
||||
return new APNGDrawable(resourceStreamLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27acTL.27:_The_Animation_Control_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class ACTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("acTL");
|
||||
int num_frames;
|
||||
int num_plays;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader apngReader) throws IOException {
|
||||
num_frames = apngReader.readInt();
|
||||
num_plays = apngReader.readInt();
|
||||
}
|
||||
}
|
||||
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
|
||||
private static final String TAG = APNGDecoder.class.getSimpleName();
|
||||
|
||||
private APNGWriter apngWriter;
|
||||
private int mLoopCount;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
|
||||
private class SnapShot {
|
||||
byte dispose_op;
|
||||
Rect dstRect = new Rect();
|
||||
ByteBuffer byteBuffer;
|
||||
}
|
||||
|
||||
private SnapShot snapShot = new SnapShot();
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public APNGDecoder(Loader loader, FrameSeqDecoder.RenderListener renderListener) {
|
||||
super(loader, renderListener);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGWriter getWriter() {
|
||||
if (apngWriter == null) {
|
||||
apngWriter = new APNGWriter();
|
||||
}
|
||||
return apngWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected APNGReader getReader(Reader reader) {
|
||||
return new APNGReader(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLoopCount() {
|
||||
return mLoopCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release() {
|
||||
snapShot.byteBuffer = null;
|
||||
apngWriter = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Rect read(APNGReader reader) throws IOException {
|
||||
List<Chunk> chunks = APNGParser.parse(reader);
|
||||
List<Chunk> otherChunks = new ArrayList<>();
|
||||
|
||||
boolean actl = false;
|
||||
APNGFrame lastFrame = null;
|
||||
byte[] ihdrData = new byte[0];
|
||||
int canvasWidth = 0, canvasHeight = 0;
|
||||
for (Chunk chunk : chunks) {
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
mLoopCount = ((ACTLChunk) chunk).num_plays;
|
||||
actl = true;
|
||||
} else if (chunk instanceof FCTLChunk) {
|
||||
APNGFrame frame = new APNGFrame(reader, (FCTLChunk) chunk);
|
||||
frame.prefixChunks = otherChunks;
|
||||
frame.ihdrData = ihdrData;
|
||||
frames.add(frame);
|
||||
lastFrame = frame;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
} else if (chunk instanceof IDATChunk) {
|
||||
if (!actl) {
|
||||
//如果为非APNG图片,则只解码PNG
|
||||
Frame frame = new StillFrame(reader);
|
||||
frame.frameWidth = canvasWidth;
|
||||
frame.frameHeight = canvasHeight;
|
||||
frames.add(frame);
|
||||
mLoopCount = 1;
|
||||
break;
|
||||
}
|
||||
if (lastFrame != null) {
|
||||
lastFrame.imageChunks.add(chunk);
|
||||
}
|
||||
|
||||
} else if (chunk instanceof IHDRChunk) {
|
||||
canvasWidth = ((IHDRChunk) chunk).width;
|
||||
canvasHeight = ((IHDRChunk) chunk).height;
|
||||
ihdrData = ((IHDRChunk) chunk).data;
|
||||
} else if (!(chunk instanceof IENDChunk)) {
|
||||
otherChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderFrame(Frame frame) {
|
||||
if (frame == null || fullRect == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Bitmap bitmap = obtainBitmap(fullRect.width() / sampleSize, fullRect.height() / sampleSize);
|
||||
Canvas canvas = cachedCanvas.get(bitmap);
|
||||
if (canvas == null) {
|
||||
canvas = new Canvas(bitmap);
|
||||
cachedCanvas.put(bitmap, canvas);
|
||||
}
|
||||
if (frame instanceof APNGFrame) {
|
||||
// 从缓存中恢复当前帧
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
// 开始绘制前,处理快照中的设定
|
||||
if (this.frameIndex == 0) {
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.clipRect(snapShot.dstRect);
|
||||
switch (snapShot.dispose_op) {
|
||||
// 从快照中恢复上一帧之前的显示内容
|
||||
case FCTLChunk.APNG_DISPOSE_OP_PREVIOUS:
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsFromBuffer(snapShot.byteBuffer);
|
||||
break;
|
||||
// 清空上一帧所画区域
|
||||
case FCTLChunk.APNG_DISPOSE_OP_BACKGROUND:
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
break;
|
||||
// 什么都不做
|
||||
case FCTLChunk.APNG_DISPOSE_OP_NON:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// 然后根据dispose设定传递到快照信息中
|
||||
if (((APNGFrame) frame).dispose_op == FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
if (snapShot.dispose_op != FCTLChunk.APNG_DISPOSE_OP_PREVIOUS) {
|
||||
snapShot.byteBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(snapShot.byteBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
snapShot.dispose_op = ((APNGFrame) frame).dispose_op;
|
||||
canvas.save();
|
||||
if (((APNGFrame) frame).blend_op == FCTLChunk.APNG_BLEND_OP_SOURCE) {
|
||||
canvas.clipRect(
|
||||
frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
}
|
||||
|
||||
|
||||
snapShot.dstRect.set(frame.frameX / sampleSize,
|
||||
frame.frameY / sampleSize,
|
||||
(frame.frameX + frame.frameWidth) / sampleSize,
|
||||
(frame.frameY + frame.frameHeight) / sampleSize);
|
||||
canvas.restore();
|
||||
}
|
||||
//开始真正绘制当前帧的内容
|
||||
Bitmap inBitmap = obtainBitmap(frame.frameWidth, frame.frameHeight);
|
||||
recycleBitmap(frame.draw(canvas, paint, sampleSize, inBitmap, getWriter()));
|
||||
recycleBitmap(inBitmap);
|
||||
frameBuffer.rewind();
|
||||
bitmap.copyPixelsToBuffer(frameBuffer);
|
||||
recycleBitmap(bitmap);
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG, "Failed to render!", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGFrame extends Frame<APNGReader, APNGWriter> {
|
||||
public final byte blend_op;
|
||||
public final byte dispose_op;
|
||||
byte[] ihdrData;
|
||||
List<Chunk> imageChunks = new ArrayList<>();
|
||||
List<Chunk> prefixChunks = new ArrayList<>();
|
||||
private static final byte[] sPNGSignatures = {(byte) 137, 80, 78, 71, 13, 10, 26, 10};
|
||||
private static final byte[] sPNGEndChunk = {0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, (byte) 0xAE, 0x42, 0x60, (byte) 0x82};
|
||||
|
||||
private static ThreadLocal<CRC32> sCRC32 = new ThreadLocal<>();
|
||||
|
||||
private CRC32 getCRC32() {
|
||||
CRC32 crc32 = sCRC32.get();
|
||||
if (crc32 == null) {
|
||||
crc32 = new CRC32();
|
||||
sCRC32.set(crc32);
|
||||
}
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public APNGFrame(APNGReader reader, FCTLChunk fctlChunk) {
|
||||
super(reader);
|
||||
blend_op = fctlChunk.blend_op;
|
||||
dispose_op = fctlChunk.dispose_op;
|
||||
frameDuration = fctlChunk.delay_num * 1000 / (fctlChunk.delay_den == 0 ? 100 : fctlChunk.delay_den);
|
||||
frameWidth = fctlChunk.width;
|
||||
frameHeight = fctlChunk.height;
|
||||
frameX = fctlChunk.x_offset;
|
||||
frameY = fctlChunk.y_offset;
|
||||
}
|
||||
|
||||
private int encode(APNGWriter apngWriter) throws IOException {
|
||||
int fileSize = 8 + 13 + 12;
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
fileSize += chunk.length + 12;
|
||||
}
|
||||
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
fileSize += chunk.length + 12;
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
fileSize += chunk.length + 8;
|
||||
}
|
||||
}
|
||||
fileSize += sPNGEndChunk.length;
|
||||
apngWriter.reset(fileSize);
|
||||
apngWriter.putBytes(sPNGSignatures);
|
||||
//IHDR Chunk
|
||||
apngWriter.writeInt(13);
|
||||
int start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IHDRChunk.ID);
|
||||
apngWriter.writeInt(frameWidth);
|
||||
apngWriter.writeInt(frameHeight);
|
||||
apngWriter.putBytes(ihdrData);
|
||||
CRC32 crc32 = getCRC32();
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, 17);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
|
||||
//prefixChunks
|
||||
for (Chunk chunk : prefixChunks) {
|
||||
if (chunk instanceof IENDChunk) {
|
||||
continue;
|
||||
}
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
}
|
||||
//imageChunks
|
||||
for (Chunk chunk : imageChunks) {
|
||||
if (chunk instanceof IDATChunk) {
|
||||
reader.reset();
|
||||
reader.skip(chunk.offset);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length + 12);
|
||||
apngWriter.skip(chunk.length + 12);
|
||||
} else if (chunk instanceof FDATChunk) {
|
||||
apngWriter.writeInt(chunk.length - 4);
|
||||
start = apngWriter.position();
|
||||
apngWriter.writeFourCC(IDATChunk.ID);
|
||||
|
||||
reader.reset();
|
||||
// skip to fdat data position
|
||||
reader.skip(chunk.offset + 4 + 4 + 4);
|
||||
reader.read(apngWriter.toByteArray(), apngWriter.position(), chunk.length - 4);
|
||||
|
||||
apngWriter.skip(chunk.length - 4);
|
||||
crc32.reset();
|
||||
crc32.update(apngWriter.toByteArray(), start, chunk.length);
|
||||
apngWriter.writeInt((int) crc32.getValue());
|
||||
}
|
||||
}
|
||||
//endChunk
|
||||
apngWriter.putBytes(sPNGEndChunk);
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
try {
|
||||
int length = encode(writer);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
byte[] bytes = writer.toByteArray();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, length, options);
|
||||
assert bitmap != null;
|
||||
canvas.drawBitmap(bitmap, (float) frameX / sampleSize, (float) frameY / sampleSize, paint);
|
||||
return bitmap;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
143
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @link {https://www.w3.org/TR/PNG/#5PNG-file-signature}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGParser {
|
||||
static class FormatException extends IOException {
|
||||
FormatException() {
|
||||
super("APNG Format error");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(String filePath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(filePath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, String assetPath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Context context, int resId) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getResources().openRawResource(resId);
|
||||
return isAPNG(new StreamReader(inputStream));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isAPNG(Reader in) {
|
||||
APNGReader reader = (in instanceof APNGReader) ? (APNGReader) in : new APNGReader(in);
|
||||
try {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
while (reader.available() > 0) {
|
||||
Chunk chunk = parseChunk(reader);
|
||||
if (chunk instanceof ACTLChunk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<Chunk> parse(APNGReader reader) throws IOException {
|
||||
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
List<Chunk> chunks = new ArrayList<>();
|
||||
while (reader.available() > 0) {
|
||||
chunks.add(parseChunk(reader));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private static Chunk parseChunk(APNGReader reader) throws IOException {
|
||||
int offset = reader.position();
|
||||
int size = reader.readInt();
|
||||
int fourCC = reader.readFourCC();
|
||||
Chunk chunk;
|
||||
if (fourCC == ACTLChunk.ID) {
|
||||
chunk = new ACTLChunk();
|
||||
} else if (fourCC == FCTLChunk.ID) {
|
||||
chunk = new FCTLChunk();
|
||||
} else if (fourCC == FDATChunk.ID) {
|
||||
chunk = new FDATChunk();
|
||||
} else if (fourCC == IDATChunk.ID) {
|
||||
chunk = new IDATChunk();
|
||||
} else if (fourCC == IENDChunk.ID) {
|
||||
chunk = new IENDChunk();
|
||||
} else if (fourCC == IHDRChunk.ID) {
|
||||
chunk = new IHDRChunk();
|
||||
} else {
|
||||
chunk = new Chunk();
|
||||
}
|
||||
chunk.offset = offset;
|
||||
chunk.fourcc = fourCC;
|
||||
chunk.length = size;
|
||||
chunk.parse(reader);
|
||||
chunk.crc = reader.readInt();
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(231-1)字节
|
||||
* Chunk Type Code (数据块类型码) 4字节 数据块类型码由ASCII字母(A-Z和a-z)组成
|
||||
* Chunk Data (数据块数据) 可变长度 存储按照Chunk Type Code指定的数据
|
||||
* CRC (循环冗余检测) 4字节 存储用来检测是否有错误的循环冗余码
|
||||
* @Link https://www.w3.org/TR/PNG
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class Chunk {
|
||||
int length;
|
||||
int fourcc;
|
||||
int crc;
|
||||
int offset;
|
||||
|
||||
static int fourCCToInt(String fourCC) {
|
||||
if (TextUtils.isEmpty(fourCC) || fourCC.length() != 4) {
|
||||
return 0xbadeffff;
|
||||
}
|
||||
return (fourCC.charAt(0) & 0xff)
|
||||
| (fourCC.charAt(1) & 0xff) << 8
|
||||
| (fourCC.charAt(2) & 0xff) << 16
|
||||
| (fourCC.charAt(3) & 0xff) << 24
|
||||
;
|
||||
}
|
||||
|
||||
void parse(APNGReader reader) throws IOException {
|
||||
int available = reader.available();
|
||||
innerParse(reader);
|
||||
int offset = available - reader.available();
|
||||
if (offset > length) {
|
||||
throw new IOException("Out of chunk area");
|
||||
} else if (offset < length) {
|
||||
reader.skip(length - offset);
|
||||
}
|
||||
}
|
||||
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
}
|
||||
}
|
||||
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
* @see {link=https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fcTL.27:_The_Frame_Control_Chunk}
|
||||
*/
|
||||
class FCTLChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fcTL");
|
||||
int sequence_number;
|
||||
/**
|
||||
* x_offset >= 0
|
||||
* y_offset >= 0
|
||||
* width > 0
|
||||
* height > 0
|
||||
* x_offset + width <= 'IHDR' width
|
||||
* y_offset + height <= 'IHDR' height
|
||||
*/
|
||||
/**
|
||||
* Width of the following frame.
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* Height of the following frame.
|
||||
*/
|
||||
int height;
|
||||
/**
|
||||
* X position at which to render the following frame.
|
||||
*/
|
||||
int x_offset;
|
||||
/**
|
||||
* Y position at which to render the following frame.
|
||||
*/
|
||||
int y_offset;
|
||||
|
||||
/**
|
||||
* The delay_num and delay_den parameters together specify a fraction indicating the time to
|
||||
* display the current frame, in seconds. If the denominator is 0, it is to be treated as if it
|
||||
* were 100 (that is, delay_num then specifies 1/100ths of a second).
|
||||
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as
|
||||
* possible, though viewers may impose a reasonable lower bound.
|
||||
* <p>
|
||||
* Frame timings should be independent of the time required for decoding and display of each frame,
|
||||
* so that animations will run at the same rate regardless of the performance of the decoder implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Frame delay fraction numerator.
|
||||
*/
|
||||
short delay_num;
|
||||
|
||||
/**
|
||||
* Frame delay fraction denominator.
|
||||
*/
|
||||
short delay_den;
|
||||
|
||||
/**
|
||||
* Type of frame area disposal to be done after rendering this frame.
|
||||
* dispose_op specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
|
||||
* If the first 'fcTL' chunk uses a dispose_op of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
|
||||
*/
|
||||
byte dispose_op;
|
||||
|
||||
/**
|
||||
* Type of frame area rendering for this frame.
|
||||
*/
|
||||
byte blend_op;
|
||||
|
||||
/**
|
||||
* No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_NON = 0;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_BACKGROUND = 1;
|
||||
|
||||
/**
|
||||
* The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||
*/
|
||||
static final int APNG_DISPOSE_OP_PREVIOUS = 2;
|
||||
|
||||
/**
|
||||
* blend_op<code> specifies whether the frame is to be alpha blended into the current output buffer content,
|
||||
* or whether it should completely replace its region in the output buffer.
|
||||
*/
|
||||
/**
|
||||
* All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_SOURCE = 0;
|
||||
|
||||
/**
|
||||
* The frame should be composited onto the output buffer based on its alpha,
|
||||
* using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions
|
||||
* to the PNG Specification, Version 1.2.0. Note that the second variation of the sample code is applicable.
|
||||
*/
|
||||
static final int APNG_BLEND_OP_OVER = 1;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
x_offset = reader.readInt();
|
||||
y_offset = reader.readInt();
|
||||
delay_num = reader.readShort();
|
||||
delay_den = reader.readShort();
|
||||
dispose_op = reader.peek();
|
||||
blend_op = reader.peek();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#.27fdAT.27:_The_Frame_Data_Chunk
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class FDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("fdAT");
|
||||
int sequence_number;
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
sequence_number = reader.readInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IDATChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IDAT");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
/**
|
||||
* @Description: 作用描述
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IENDChunk extends Chunk {
|
||||
static final int ID = Chunk.fourCCToInt("IEND");
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* The IHDR chunk shall be the first chunk in the PNG datastream. It contains:
|
||||
* <p>
|
||||
* Width 4 bytes
|
||||
* Height 4 bytes
|
||||
* Bit depth 1 byte
|
||||
* Colour type 1 byte
|
||||
* Compression method 1 byte
|
||||
* Filter method 1 byte
|
||||
* Interlace method 1 byte
|
||||
*
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
class IHDRChunk extends Chunk {
|
||||
static final int ID = fourCCToInt("IHDR");
|
||||
/**
|
||||
* 图像宽度,以像素为单位
|
||||
*/
|
||||
int width;
|
||||
/**
|
||||
* 图像高度,以像素为单位
|
||||
*/
|
||||
int height;
|
||||
|
||||
byte[] data = new byte[5];
|
||||
|
||||
@Override
|
||||
void innerParse(APNGReader reader) throws IOException {
|
||||
width = reader.readInt();
|
||||
height = reader.readInt();
|
||||
reader.read(data, 0, data.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.apng.io.APNGReader;
|
||||
import org.signal.glide.apng.io.APNGWriter;
|
||||
import org.signal.glide.common.decode.Frame;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class StillFrame extends Frame<APNGReader, APNGWriter> {
|
||||
|
||||
public StillFrame(APNGReader reader) {
|
||||
super(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, APNGWriter writer) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inMutable = true;
|
||||
options.inBitmap = reusedBitmap;
|
||||
Bitmap bitmap = null;
|
||||
try {
|
||||
reader.reset();
|
||||
bitmap = BitmapFactory.decodeStream(reader.toInputStream(), null, options);
|
||||
assert bitmap != null;
|
||||
paint.setXfermode(null);
|
||||
canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.signal.glide.common.io.FilterReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNGReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGReader extends FilterReader {
|
||||
private static ThreadLocal<byte[]> __intBytes = new ThreadLocal<>();
|
||||
|
||||
|
||||
protected static byte[] ensureBytes() {
|
||||
byte[] bytes = __intBytes.get();
|
||||
if (bytes == null) {
|
||||
bytes = new byte[4];
|
||||
__intBytes.set(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public APNGReader(Reader in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[3] & 0xFF |
|
||||
(buf[2] & 0xFF) << 8 |
|
||||
(buf[1] & 0xFF) << 16 |
|
||||
(buf[0] & 0xFF) << 24;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 2);
|
||||
return (short) (buf[1] & 0xFF |
|
||||
(buf[0] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return read FourCC and match chars
|
||||
*/
|
||||
public boolean matchFourCC(String chars) throws IOException {
|
||||
if (TextUtils.isEmpty(chars) || chars.length() != 4) {
|
||||
return false;
|
||||
}
|
||||
int fourCC = readFourCC();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (((fourCC >> (i * 8)) & 0xff) != chars.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int readFourCC() throws IOException {
|
||||
byte[] buf = ensureBytes();
|
||||
read(buf, 0, 4);
|
||||
return buf[0] & 0xff | (buf[1] & 0xff) << 8 | (buf[2] & 0xff) << 16 | (buf[3] & 0xff) << 24;
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.apng.io;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferWriter;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: APNGWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public class APNGWriter extends ByteBufferWriter {
|
||||
public APNGWriter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void writeFourCC(int val) {
|
||||
putByte((byte) (val & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
}
|
||||
|
||||
public void writeInt(int val) {
|
||||
putByte((byte) ((val >> 24) & 0xff));
|
||||
putByte((byte) ((val >> 16) & 0xff));
|
||||
putByte((byte) ((val >> 8) & 0xff));
|
||||
putByte((byte) (val & 0xff));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
super.reset(size);
|
||||
this.byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.DrawFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PaintFlagsDrawFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.decode.FrameSeqDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: Frame animation drawable
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameAnimationDrawable<Decoder extends FrameSeqDecoder> extends Drawable implements Animatable2Compat, FrameSeqDecoder.RenderListener {
|
||||
private static final String TAG = FrameAnimationDrawable.class.getSimpleName();
|
||||
private final Paint paint = new Paint();
|
||||
private final Decoder frameSeqDecoder;
|
||||
private DrawFilter drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
private Matrix matrix = new Matrix();
|
||||
private Set<AnimationCallback> animationCallbacks = new HashSet<>();
|
||||
private Bitmap bitmap;
|
||||
private static final int MSG_ANIMATION_START = 1;
|
||||
private static final int MSG_ANIMATION_END = 2;
|
||||
private Handler uiHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_ANIMATION_START:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationStart(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
case MSG_ANIMATION_END:
|
||||
for (AnimationCallback animationCallback : animationCallbacks) {
|
||||
animationCallback.onAnimationEnd(FrameAnimationDrawable.this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private Runnable invalidateRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
invalidateSelf();
|
||||
}
|
||||
};
|
||||
private boolean autoPlay = true;
|
||||
|
||||
public FrameAnimationDrawable(Decoder frameSeqDecoder) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = frameSeqDecoder;
|
||||
}
|
||||
|
||||
public FrameAnimationDrawable(Loader provider) {
|
||||
paint.setAntiAlias(true);
|
||||
this.frameSeqDecoder = createFrameSeqDecoder(provider, this);
|
||||
}
|
||||
|
||||
public void setAutoPlay(boolean autoPlay) {
|
||||
this.autoPlay = autoPlay;
|
||||
}
|
||||
|
||||
protected abstract Decoder createFrameSeqDecoder(Loader streamLoader, FrameSeqDecoder.RenderListener listener);
|
||||
|
||||
/**
|
||||
* @param loopLimit <=0为无限播放,>0为实际播放次数
|
||||
*/
|
||||
public void setLoopLimit(int loopLimit) {
|
||||
frameSeqDecoder.setLoopLimit(loopLimit);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
frameSeqDecoder.reset();
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
frameSeqDecoder.pause();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
frameSeqDecoder.resume();
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return frameSeqDecoder.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.start();
|
||||
} else {
|
||||
this.frameSeqDecoder.addRenderListener(this);
|
||||
if (!this.frameSeqDecoder.isRunning()) {
|
||||
this.frameSeqDecoder.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (autoPlay) {
|
||||
frameSeqDecoder.stop();
|
||||
} else {
|
||||
this.frameSeqDecoder.removeRenderListener(this);
|
||||
this.frameSeqDecoder.stopIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return frameSeqDecoder.isRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
canvas.setDrawFilter(drawFilter);
|
||||
canvas.drawBitmap(bitmap, matrix, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
boolean sampleSizeChanged = frameSeqDecoder.setDesiredSize(getBounds().width(), getBounds().height());
|
||||
matrix.setScale(
|
||||
1.0f * getBounds().width() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().width(),
|
||||
1.0f * getBounds().height() * frameSeqDecoder.getSampleSize() / frameSeqDecoder.getBounds().height());
|
||||
|
||||
if (sampleSizeChanged)
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_START).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(ByteBuffer byteBuffer) {
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
if (this.bitmap == null || this.bitmap.isRecycled()) {
|
||||
this.bitmap = Bitmap.createBitmap(
|
||||
frameSeqDecoder.getBounds().width() / frameSeqDecoder.getSampleSize(),
|
||||
frameSeqDecoder.getBounds().height() / frameSeqDecoder.getSampleSize(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
byteBuffer.rewind();
|
||||
if (byteBuffer.remaining() < this.bitmap.getByteCount()) {
|
||||
Log.e(TAG, "onRender:Buffer not large enough for pixels");
|
||||
return;
|
||||
}
|
||||
this.bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||
uiHandler.post(invalidateRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnd() {
|
||||
Message.obtain(uiHandler, MSG_ANIMATION_END).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
if (this.autoPlay) {
|
||||
if (visible) {
|
||||
if (!isRunning()) {
|
||||
start();
|
||||
}
|
||||
} else if (isRunning()) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
return super.setVisible(visible, restart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().width();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
try {
|
||||
return frameSeqDecoder.getBounds().height();
|
||||
} catch (Exception exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
this.animationCallbacks.add(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unregisterAnimationCallback(@NonNull AnimationCallback animationCallback) {
|
||||
return this.animationCallbacks.remove(animationCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAnimationCallbacks() {
|
||||
this.animationCallbacks.clear();
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
|
||||
/**
|
||||
* @Description: One frame in an animation
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-13
|
||||
*/
|
||||
public abstract class Frame<R extends Reader, W extends Writer> {
|
||||
protected final R reader;
|
||||
public int frameWidth;
|
||||
public int frameHeight;
|
||||
public int frameX;
|
||||
public int frameY;
|
||||
public int frameDuration;
|
||||
|
||||
public Frame(R reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public abstract Bitmap draw(Canvas canvas, Paint paint, int sampleSize, Bitmap reusedBitmap, W writer);
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.decode;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* @Description: Abstract Frame Animation Decoder
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/27
|
||||
*/
|
||||
public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||
private final int taskId;
|
||||
|
||||
private final Loader mLoader;
|
||||
private final Handler workerHandler;
|
||||
protected List<Frame> frames = new ArrayList<>();
|
||||
protected int frameIndex = -1;
|
||||
private int playCount;
|
||||
private Integer loopLimit = null;
|
||||
private Set<RenderListener> renderListeners = new HashSet<>();
|
||||
private AtomicBoolean paused = new AtomicBoolean(true);
|
||||
private static final Rect RECT_EMPTY = new Rect();
|
||||
private Runnable renderTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (paused.get()) {
|
||||
return;
|
||||
}
|
||||
if (canStep()) {
|
||||
long start = System.currentTimeMillis();
|
||||
long delay = step();
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
workerHandler.postDelayed(this, Math.max(0, delay - cost));
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onRender(frameBuffer);
|
||||
}
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
protected int sampleSize = 1;
|
||||
|
||||
private Set<Bitmap> cacheBitmaps = new HashSet<>();
|
||||
protected Map<Bitmap, Canvas> cachedCanvas = new WeakHashMap<>();
|
||||
protected ByteBuffer frameBuffer;
|
||||
protected volatile Rect fullRect;
|
||||
private W mWriter = getWriter();
|
||||
private R mReader = null;
|
||||
|
||||
/**
|
||||
* If played all the needed
|
||||
*/
|
||||
private boolean finished = false;
|
||||
|
||||
private enum State {
|
||||
IDLE,
|
||||
RUNNING,
|
||||
INITIALIZING,
|
||||
FINISHING,
|
||||
}
|
||||
|
||||
private volatile State mState = State.IDLE;
|
||||
|
||||
public Loader getLoader() {
|
||||
return mLoader;
|
||||
}
|
||||
|
||||
protected abstract W getWriter();
|
||||
|
||||
protected abstract R getReader(Reader reader);
|
||||
|
||||
protected Bitmap obtainBitmap(int width, int height) {
|
||||
Bitmap ret = null;
|
||||
Iterator<Bitmap> iterator = cacheBitmaps.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
int reuseSize = width * height * 4;
|
||||
ret = iterator.next();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (ret != null && ret.getAllocationByteCount() >= reuseSize) {
|
||||
iterator.remove();
|
||||
if (ret.getWidth() != width || ret.getHeight() != height) {
|
||||
ret.reconfigure(width, height, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
ret.eraseColor(0);
|
||||
return ret;
|
||||
}
|
||||
} else {
|
||||
if (ret != null && ret.getByteCount() >= reuseSize) {
|
||||
if (ret.getWidth() == width && ret.getHeight() == height) {
|
||||
iterator.remove();
|
||||
ret.eraseColor(0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Bitmap.Config config = Bitmap.Config.ARGB_8888;
|
||||
ret = Bitmap.createBitmap(width, height, config);
|
||||
} catch (OutOfMemoryError e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected void recycleBitmap(Bitmap bitmap) {
|
||||
if (bitmap != null && !cacheBitmaps.contains(bitmap)) {
|
||||
cacheBitmaps.add(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码器的渲染回调
|
||||
*/
|
||||
public interface RenderListener {
|
||||
/**
|
||||
* 播放开始
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* 帧播放
|
||||
*/
|
||||
void onRender(ByteBuffer byteBuffer);
|
||||
|
||||
/**
|
||||
* 播放结束
|
||||
*/
|
||||
void onEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param loader webp的reader
|
||||
* @param renderListener 渲染的回调
|
||||
*/
|
||||
public FrameSeqDecoder(Loader loader, @Nullable RenderListener renderListener) {
|
||||
this.mLoader = loader;
|
||||
if (renderListener != null) {
|
||||
this.renderListeners.add(renderListener);
|
||||
}
|
||||
this.taskId = FrameDecoderExecutor.getInstance().generateTaskId();
|
||||
this.workerHandler = new Handler(FrameDecoderExecutor.getInstance().getLooper(taskId));
|
||||
}
|
||||
|
||||
|
||||
public void addRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.add(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeRenderListener(final RenderListener renderListener) {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
renderListeners.remove(renderListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopIfNeeded() {
|
||||
this.workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (renderListeners.size() == 0) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Rect getBounds() {
|
||||
if (fullRect == null) {
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, "In finishing,do not interrupt");
|
||||
}
|
||||
final Thread thread = Thread.currentThread();
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (fullRect == null) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fullRect = RECT_EMPTY;
|
||||
} finally {
|
||||
LockSupport.unpark(thread);
|
||||
}
|
||||
}
|
||||
});
|
||||
LockSupport.park(thread);
|
||||
}
|
||||
return fullRect;
|
||||
}
|
||||
|
||||
private void initCanvasBounds(Rect rect) {
|
||||
fullRect = rect;
|
||||
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||
if (mWriter == null) {
|
||||
mWriter = getWriter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int getFrameCount() {
|
||||
return this.frames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Loop Count defined in file
|
||||
*/
|
||||
protected abstract int getLoopCount();
|
||||
|
||||
public void start() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.RUNNING || mState == State.INITIALIZING) {
|
||||
Log.i(TAG, debugInfo() + " Already started");
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING) {
|
||||
Log.e(TAG, debugInfo() + " Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.INITIALIZING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStart();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStart() {
|
||||
paused.compareAndSet(true, false);
|
||||
|
||||
final long start = System.currentTimeMillis();
|
||||
try {
|
||||
if (frames.size() == 0) {
|
||||
try {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Log.i(TAG, debugInfo() + " Set state to RUNNING,cost " + (System.currentTimeMillis() - start));
|
||||
mState = State.RUNNING;
|
||||
}
|
||||
if (getNumPlays() == 0 || !finished) {
|
||||
this.frameIndex = -1;
|
||||
renderTask.run();
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onStart();
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, debugInfo() + " No need to started");
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void innerStop() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
frames.clear();
|
||||
for (Bitmap bitmap : cacheBitmaps) {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
cacheBitmaps.clear();
|
||||
if (frameBuffer != null) {
|
||||
frameBuffer = null;
|
||||
}
|
||||
cachedCanvas.clear();
|
||||
try {
|
||||
if (mReader != null) {
|
||||
mReader.close();
|
||||
mReader = null;
|
||||
}
|
||||
if (mWriter != null) {
|
||||
mWriter.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
release();
|
||||
mState = State.IDLE;
|
||||
for (RenderListener renderListener : renderListeners) {
|
||||
renderListener.onEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (fullRect == RECT_EMPTY) {
|
||||
return;
|
||||
}
|
||||
if (mState == State.FINISHING || mState == State.IDLE) {
|
||||
Log.i(TAG, debugInfo() + "No need to stop");
|
||||
return;
|
||||
}
|
||||
if (mState == State.INITIALIZING) {
|
||||
Log.e(TAG, debugInfo() + "Processing,wait for finish at " + mState);
|
||||
}
|
||||
mState = State.FINISHING;
|
||||
if (Looper.myLooper() == workerHandler.getLooper()) {
|
||||
innerStop();
|
||||
} else {
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private String debugInfo() {
|
||||
return "";
|
||||
}
|
||||
|
||||
protected abstract void release();
|
||||
|
||||
public boolean isRunning() {
|
||||
return mState == State.RUNNING || mState == State.INITIALIZING;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return paused.get();
|
||||
}
|
||||
|
||||
public void setLoopLimit(int limit) {
|
||||
this.loopLimit = limit;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.playCount = 0;
|
||||
this.frameIndex = -1;
|
||||
this.finished = false;
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
paused.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
paused.compareAndSet(true, false);
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(renderTask);
|
||||
}
|
||||
|
||||
|
||||
public int getSampleSize() {
|
||||
return sampleSize;
|
||||
}
|
||||
|
||||
public boolean setDesiredSize(int width, int height) {
|
||||
boolean sampleSizeChanged = false;
|
||||
int sample = getDesiredSample(width, height);
|
||||
if (sample != this.sampleSize) {
|
||||
this.sampleSize = sample;
|
||||
sampleSizeChanged = true;
|
||||
final boolean tempRunning = isRunning();
|
||||
workerHandler.removeCallbacks(renderTask);
|
||||
workerHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
innerStop();
|
||||
try {
|
||||
initCanvasBounds(read(getReader(mLoader.obtain())));
|
||||
if (tempRunning) {
|
||||
innerStart();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return sampleSizeChanged;
|
||||
}
|
||||
|
||||
protected int getDesiredSample(int desiredWidth, int desiredHeight) {
|
||||
if (desiredWidth == 0 || desiredHeight == 0) {
|
||||
return 1;
|
||||
}
|
||||
int radio = Math.min(getBounds().width() / desiredWidth, getBounds().height() / desiredHeight);
|
||||
int sample = 1;
|
||||
while ((sample * 2) <= radio) {
|
||||
sample *= 2;
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
|
||||
protected abstract Rect read(R reader) throws IOException;
|
||||
|
||||
private int getNumPlays() {
|
||||
return this.loopLimit != null ? this.loopLimit : this.getLoopCount();
|
||||
}
|
||||
|
||||
private boolean canStep() {
|
||||
if (!isRunning()) {
|
||||
return false;
|
||||
}
|
||||
if (frames.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
if (getNumPlays() <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.playCount < getNumPlays() - 1) {
|
||||
return true;
|
||||
} else if (this.playCount == getNumPlays() - 1 && this.frameIndex < this.getFrameCount() - 1) {
|
||||
return true;
|
||||
}
|
||||
finished = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private long step() {
|
||||
this.frameIndex++;
|
||||
if (this.frameIndex >= this.getFrameCount()) {
|
||||
this.frameIndex = 0;
|
||||
this.playCount++;
|
||||
}
|
||||
Frame frame = getFrame(this.frameIndex);
|
||||
if (frame == null) {
|
||||
return 0;
|
||||
}
|
||||
renderFrame(frame);
|
||||
return frame.frameDuration;
|
||||
}
|
||||
|
||||
protected abstract void renderFrame(Frame frame);
|
||||
|
||||
private Frame getFrame(int index) {
|
||||
if (index < 0 || index >= frames.size()) {
|
||||
return null;
|
||||
}
|
||||
return frames.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Indexed frame
|
||||
*
|
||||
* @param index <0 means reverse from last index
|
||||
*/
|
||||
public Bitmap getFrameBitmap(int index) throws IOException {
|
||||
if (mState != State.IDLE) {
|
||||
Log.e(TAG, debugInfo() + ",stop first");
|
||||
return null;
|
||||
}
|
||||
mState = State.RUNNING;
|
||||
paused.compareAndSet(true, false);
|
||||
if (frames.size() == 0) {
|
||||
if (mReader == null) {
|
||||
mReader = getReader(mLoader.obtain());
|
||||
} else {
|
||||
mReader.reset();
|
||||
}
|
||||
initCanvasBounds(read(mReader));
|
||||
}
|
||||
if (index < 0) {
|
||||
index += this.frames.size();
|
||||
}
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
frameIndex = -1;
|
||||
while (frameIndex < index) {
|
||||
if (canStep()) {
|
||||
step();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
frameBuffer.rewind();
|
||||
Bitmap bitmap = Bitmap.createBitmap(getBounds().width() / getSampleSize(), getBounds().height() / getSampleSize(), Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(frameBuffer);
|
||||
innerStop();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.executor;
|
||||
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @Description: com.github.penfeizhou.animation.executor
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-11-21
|
||||
*/
|
||||
public class FrameDecoderExecutor {
|
||||
private static int sPoolNumber = 4;
|
||||
private ArrayList<HandlerThread> mHandlerThreadGroup = new ArrayList<>();
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private FrameDecoderExecutor() {
|
||||
}
|
||||
|
||||
static class Inner {
|
||||
static final FrameDecoderExecutor sInstance = new FrameDecoderExecutor();
|
||||
}
|
||||
|
||||
public void setPoolSize(int size) {
|
||||
sPoolNumber = size;
|
||||
}
|
||||
|
||||
public static FrameDecoderExecutor getInstance() {
|
||||
return Inner.sInstance;
|
||||
}
|
||||
|
||||
public Looper getLooper(int taskId) {
|
||||
int idx = taskId % sPoolNumber;
|
||||
if (idx >= mHandlerThreadGroup.size()) {
|
||||
HandlerThread handlerThread = new HandlerThread("FrameDecoderExecutor-" + idx);
|
||||
handlerThread.start();
|
||||
|
||||
mHandlerThreadGroup.add(handlerThread);
|
||||
Looper looper = handlerThread.getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
if (mHandlerThreadGroup.get(idx) != null) {
|
||||
Looper looper = mHandlerThreadGroup.get(idx).getLooper();
|
||||
if (looper != null) {
|
||||
return looper;
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
} else {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int generateTaskId() {
|
||||
return counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public class ByteBufferReader implements Reader {
|
||||
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferReader(ByteBuffer byteBuffer) {
|
||||
this.byteBuffer = byteBuffer;
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
byteBuffer.position((int) (byteBuffer.position() + total));
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return byteBuffer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
byteBuffer.position(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
byteBuffer.get(buffer, start, byteCount);
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return byteBuffer.limit() - byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return new ByteArrayInputStream(byteBuffer.array());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferWriter
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public class ByteBufferWriter implements Writer {
|
||||
|
||||
protected ByteBuffer byteBuffer;
|
||||
|
||||
public ByteBufferWriter() {
|
||||
reset(10 * 1024);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putByte(byte b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putBytes(byte[] b) {
|
||||
byteBuffer.put(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return byteBuffer.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skip(int length) {
|
||||
byteBuffer.position(length + position());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toByteArray() {
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(int size) {
|
||||
if (byteBuffer == null || size > byteBuffer.capacity()) {
|
||||
byteBuffer = ByteBuffer.allocate(size);
|
||||
this.byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
byteBuffer.clear();
|
||||
}
|
||||
}
|
||||
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: FileReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FileReader extends FilterReader {
|
||||
private final File mFile;
|
||||
|
||||
public FileReader(File file) throws IOException {
|
||||
super(new StreamReader(new FileInputStream(file)));
|
||||
mFile = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.close();
|
||||
reader = new StreamReader(new FileInputStream(mFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: FilterReader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-23
|
||||
*/
|
||||
public class FilterReader implements Reader {
|
||||
protected Reader reader;
|
||||
|
||||
public FilterReader(Reader in) {
|
||||
this.reader = in;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long total) throws IOException {
|
||||
return reader.skip(total);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
return reader.peek();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
reader.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return reader.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int start, int byteCount) throws IOException {
|
||||
return reader.read(buffer, start, byteCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return reader.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
reader.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
reset();
|
||||
return reader.toInputStream();
|
||||
}
|
||||
}
|
||||
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @link {https://developers.google.com/speed/webp/docs/riff_container#terminology_basics}
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public interface Reader {
|
||||
long skip(long total) throws IOException;
|
||||
|
||||
byte peek() throws IOException;
|
||||
|
||||
void reset() throws IOException;
|
||||
|
||||
int position();
|
||||
|
||||
int read(byte[] buffer, int start, int byteCount) throws IOException;
|
||||
|
||||
int available() throws IOException;
|
||||
|
||||
/**
|
||||
* close io
|
||||
*/
|
||||
void close() throws IOException;
|
||||
|
||||
InputStream toInputStream() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-11
|
||||
*/
|
||||
public class StreamReader extends FilterInputStream implements Reader {
|
||||
private int position;
|
||||
|
||||
public StreamReader(InputStream in) {
|
||||
super(in);
|
||||
try {
|
||||
in.reset();
|
||||
} catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte peek() throws IOException {
|
||||
byte ret = (byte) read();
|
||||
position++;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int ret = super.read(b, off, len);
|
||||
position += Math.max(0, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
super.reset();
|
||||
position = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long ret = super.skip(n);
|
||||
position += ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream toInputStream() throws IOException {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: APNG4Android
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-12
|
||||
*/
|
||||
public interface Writer {
|
||||
void reset(int size);
|
||||
|
||||
void putByte(byte b);
|
||||
|
||||
void putBytes(byte[] b);
|
||||
|
||||
int position();
|
||||
|
||||
void skip(int length);
|
||||
|
||||
byte[] toByteArray();
|
||||
|
||||
void close() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从Asset中读取流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class AssetStreamLoader extends StreamLoader {
|
||||
|
||||
private final Context mContext;
|
||||
private final String mAssetName;
|
||||
|
||||
public AssetStreamLoader(Context context, String assetName) {
|
||||
mContext = context.getApplicationContext();
|
||||
mAssetName = assetName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getAssets().open(mAssetName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.ByteBufferReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* @Description: ByteBufferLoader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-15
|
||||
*/
|
||||
public abstract class ByteBufferLoader implements Loader {
|
||||
public abstract ByteBuffer getByteBuffer();
|
||||
|
||||
@Override
|
||||
public Reader obtain() throws IOException {
|
||||
return new ByteBufferReader(getByteBuffer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.FileReader;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: 从文件加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class FileLoader implements Loader {
|
||||
|
||||
private final File mFile;
|
||||
private Reader mReader;
|
||||
|
||||
public FileLoader(String path) {
|
||||
mFile = new File(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Reader obtain() throws IOException {
|
||||
return new FileReader(mFile);
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @Description: Loader
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019-05-14
|
||||
*/
|
||||
public interface Loader {
|
||||
Reader obtain() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Description: 从资源加载流
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public class ResourceStreamLoader extends StreamLoader {
|
||||
private final Context mContext;
|
||||
private final int mResId;
|
||||
|
||||
|
||||
public ResourceStreamLoader(Context context, int resId) {
|
||||
mContext = context.getApplicationContext();
|
||||
mResId = resId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException {
|
||||
return mContext.getResources().openRawResource(mResId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 Zhou Pengfei
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.signal.glide.common.loader;
|
||||
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* @Author: pengfei.zhou
|
||||
* @CreateDate: 2019/3/28
|
||||
*/
|
||||
public abstract class StreamLoader implements Loader {
|
||||
protected abstract InputStream getInputStream() throws IOException;
|
||||
|
||||
|
||||
public final synchronized Reader obtain() throws IOException {
|
||||
return new StreamReader(getInputStream());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
@@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
@@ -68,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
@@ -127,6 +131,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeGlideCodecs();
|
||||
|
||||
FeatureFlags.init();
|
||||
NotificationChannels.create(this);
|
||||
@@ -154,6 +159,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getFrameRateTracker().begin();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
checkBuildExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
@@ -378,6 +391,35 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
SignalGlideCodecs.setLogProvider(new org.signal.glide.Log.Provider() {
|
||||
@Override
|
||||
public void v(@NonNull String tag, @NonNull String message) {
|
||||
Log.v(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(@NonNull String tag, @NonNull String message) {
|
||||
Log.d(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(@NonNull String tag, @NonNull String message) {
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {
|
||||
Log.e(tag, message, throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -77,6 +78,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
@@ -142,6 +144,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
public void pushFragment(@NonNull Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
|
||||
.replace(android.R.id.content, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
|
||||
|
||||
@Override
|
||||
@@ -169,7 +179,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DONATE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
|
||||
|
||||
tintIcons();
|
||||
}
|
||||
@@ -284,6 +296,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DONATE:
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@@ -292,14 +307,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
|
||||
Bundle args = new Bundle();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
|
||||
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
|
||||
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
|
||||
|
||||
fragmentTransaction.replace(android.R.id.content, fragment);
|
||||
fragmentTransaction.addToBackStack(null);
|
||||
fragmentTransaction.commit();
|
||||
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -91,7 +91,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
|
||||
}
|
||||
|
||||
protected final @NonNull ActionBar requireSupportActionBar() {
|
||||
public final @NonNull ActionBar requireSupportActionBar() {
|
||||
return Objects.requireNonNull(getSupportActionBar());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
public static final String MULTI_SELECT = "multi_select";
|
||||
public static final String REFRESHABLE = "refreshable";
|
||||
public static final String RECENTS = "recents";
|
||||
public static final String TOTAL_CAPACITY = "total_capacity";
|
||||
public static final String SELECTION_LIMIT = "selection_limit";
|
||||
public static final String CURRENT_SELECTION = "current_selection";
|
||||
|
||||
private ConstraintLayout constraintLayout;
|
||||
@@ -208,7 +208,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
|
||||
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
|
||||
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(TOTAL_CAPACITY, NO_LIMIT);
|
||||
selectionLimit = requireActivity().getIntent().getIntExtra(SELECTION_LIMIT, NO_LIMIT);
|
||||
currentSelection = getCurrentSelection();
|
||||
|
||||
updateGroupLimit(getChipCount());
|
||||
|
||||
@@ -27,9 +27,6 @@ import android.content.res.Configuration;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Rational;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
@@ -37,7 +34,6 @@ import android.view.WindowManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -48,7 +44,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
@@ -57,23 +53,17 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.VerifySpan;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
|
||||
|
||||
public class WebRtcCallActivity extends AppCompatActivity {
|
||||
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
|
||||
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
|
||||
|
||||
private static final int STANDARD_DELAY_FINISH = 1000;
|
||||
public static final int BUSY_SIGNAL_DELAY_FINISH = 5500;
|
||||
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
|
||||
@@ -417,8 +407,7 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
callScreen.setStatus(getString(R.string.RedPhone_busy));
|
||||
|
||||
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
|
||||
delayedFinish(WebRtcCallService.BUSY_TONE_LENGTH);
|
||||
}
|
||||
|
||||
private void handleCallConnected(@NonNull WebRtcViewModel event) {
|
||||
@@ -470,37 +459,24 @@ public class WebRtcCallActivity extends AppCompatActivity {
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
String name = recipient.getDisplayName(this);
|
||||
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
|
||||
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
|
||||
}
|
||||
|
||||
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange() {
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(viewModel.getRecipient().getId()));
|
||||
|
||||
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
|
||||
untrustedIdentityExplanation.setText(spannableString);
|
||||
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
startService(intent);
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setView(untrustedIdentityExplanation)
|
||||
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
|
||||
synchronized (SESSION_LOCK) {
|
||||
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
|
||||
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
|
||||
}
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
|
||||
d.dismiss();
|
||||
|
||||
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
|
||||
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
|
||||
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
|
||||
|
||||
startService(intent);
|
||||
})
|
||||
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
|
||||
d.dismiss();
|
||||
handleTerminate(recipient, HangupMessage.Type.NORMAL);
|
||||
})
|
||||
.show();
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
handleTerminate(viewModel.getRecipient().get(), HangupMessage.Type.NORMAL);
|
||||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
|
||||
@@ -156,7 +156,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File stickerDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File stickerDirectory = context.getDir(StickerDatabase.DIRECTORY, Context.MODE_PRIVATE);
|
||||
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
|
||||
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
|
||||
@@ -2,16 +2,25 @@ package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Showed when a build has fully expired (either via the compile-time constant, or remote
|
||||
* deprecation).
|
||||
*/
|
||||
public class ExpiredBuildReminder extends Reminder {
|
||||
|
||||
public ExpiredBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_expired_build),
|
||||
context.getString(R.string.reminder_header_expired_build_details));
|
||||
super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -19,8 +28,17 @@ public class ExpiredBuildReminder extends Reminder {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 0;
|
||||
@Override
|
||||
public List<Action> getActions() {
|
||||
return super.getActions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Importance getImportance() {
|
||||
return Importance.TERMINAL;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return SignalStore.misc().isClientDeprecated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,24 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Reminder that is shown when a build is getting close to expiry (either because of the
|
||||
* compile-time constant, or remote deprecation).
|
||||
*/
|
||||
public class OutdatedBuildReminder extends Reminder {
|
||||
|
||||
public OutdatedBuildReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_outdated_build),
|
||||
getPluralsText(context));
|
||||
super(null, getPluralsText(context));
|
||||
|
||||
setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context));
|
||||
addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now));
|
||||
}
|
||||
|
||||
private static CharSequence getPluralsText(final Context context) {
|
||||
int days = Util.getDaysTillBuildExpiry() - 1;
|
||||
if (days == 0) {
|
||||
return context.getString(R.string.reminder_header_outdated_build_details_today);
|
||||
}
|
||||
return context.getResources().getQuantityString(R.plurals.reminder_header_outdated_build_details, days, days);
|
||||
int days = getDaysUntilExpiry() - 1;
|
||||
return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,7 +32,10 @@ public class OutdatedBuildReminder extends Reminder {
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return Util.getDaysTillBuildExpiry() <= 10;
|
||||
return getDaysUntilExpiry() <= 10;
|
||||
}
|
||||
|
||||
private static int getDaysUntilExpiry() {
|
||||
return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public abstract class Reminder {
|
||||
return Importance.NORMAL;
|
||||
}
|
||||
|
||||
public void addAction(@NonNull Action action) {
|
||||
protected void addAction(@NonNull Action action) {
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public abstract class Reminder {
|
||||
}
|
||||
|
||||
public enum Importance {
|
||||
NORMAL, ERROR
|
||||
NORMAL, ERROR, TERMINAL
|
||||
}
|
||||
|
||||
public final class Action {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -19,7 +17,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -48,7 +45,6 @@ public final class ReminderView extends FrameLayout {
|
||||
initialize();
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.HONEYCOMB)
|
||||
public ReminderView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
@@ -56,14 +52,14 @@ public final class ReminderView extends FrameLayout {
|
||||
|
||||
private void initialize() {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true);
|
||||
progressBar = ViewUtil.findById(this, R.id.reminder_progress);
|
||||
progressText = ViewUtil.findById(this, R.id.reminder_progress_text);
|
||||
container = ViewUtil.findById(this, R.id.container);
|
||||
closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
title = ViewUtil.findById(this, R.id.reminder_title);
|
||||
text = ViewUtil.findById(this, R.id.reminder_text);
|
||||
space = ViewUtil.findById(this, R.id.reminder_space);
|
||||
actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions);
|
||||
progressBar = findViewById(R.id.reminder_progress);
|
||||
progressText = findViewById(R.id.reminder_progress_text);
|
||||
container = findViewById(R.id.container);
|
||||
closeButton = findViewById(R.id.cancel);
|
||||
title = findViewById(R.id.reminder_title);
|
||||
text = findViewById(R.id.reminder_text);
|
||||
space = findViewById(R.id.reminder_space);
|
||||
actionsRecycler = findViewById(R.id.reminder_actions);
|
||||
}
|
||||
|
||||
public void showReminder(final Reminder reminder) {
|
||||
@@ -76,9 +72,26 @@ public final class ReminderView extends FrameLayout {
|
||||
title.setVisibility(GONE);
|
||||
space.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (!reminder.isDismissable()) {
|
||||
space.setVisibility(GONE);
|
||||
}
|
||||
|
||||
text.setText(reminder.getText());
|
||||
container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error
|
||||
: R.drawable.reminder_background_normal);
|
||||
|
||||
switch (reminder.getImportance()) {
|
||||
case NORMAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_normal);
|
||||
break;
|
||||
case ERROR:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_error);
|
||||
break;
|
||||
case TERMINAL:
|
||||
container.setBackgroundResource(R.drawable.reminder_background_terminal);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
setOnClickListener(reminder.getOkListener());
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
/**
|
||||
* Reusable adapter for generic settings list.
|
||||
*/
|
||||
public class BaseSettingsAdapter extends MappingAdapter {
|
||||
public void configureSingleSelect(@NonNull SingleSelectSetting.SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
registerFactory(SingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item));
|
||||
}
|
||||
|
||||
public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) {
|
||||
registerFactory(CustomizableSingleSelectSetting.Item.class,
|
||||
new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModelList;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A simple settings screen that takes its configuration via {@link Configuration}.
|
||||
*/
|
||||
public class BaseSettingsFragment extends Fragment {
|
||||
|
||||
private static final String CONFIGURATION_ARGUMENT = "current_selection";
|
||||
|
||||
private RecyclerView recycler;
|
||||
|
||||
public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) {
|
||||
BaseSettingsFragment fragment = new BaseSettingsFragment();
|
||||
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration);
|
||||
fragment.setArguments(arguments);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.base_settings_fragment, container, false);
|
||||
|
||||
recycler = view.findViewById(R.id.base_settings_list);
|
||||
recycler.setItemAnimator(null);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
BaseSettingsAdapter adapter = new BaseSettingsAdapter();
|
||||
|
||||
recycler.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT));
|
||||
configuration.configure(requireActivity(), adapter);
|
||||
configuration.setArguments(getArguments());
|
||||
configuration.configureAdapter(adapter);
|
||||
|
||||
adapter.submitList(configuration.getSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* A configuration for a settings screen. Utilizes serializable to hide
|
||||
* reflection of instantiating from a fragment argument.
|
||||
*/
|
||||
public static abstract class Configuration implements Serializable {
|
||||
protected transient FragmentActivity activity;
|
||||
protected transient BaseSettingsAdapter adapter;
|
||||
|
||||
public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) {
|
||||
this.activity = activity;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve any runtime information from the fragment's arguments.
|
||||
*/
|
||||
public void setArguments(@Nullable Bundle arguments) {}
|
||||
|
||||
protected void updateSettingsList() {
|
||||
adapter.submitList(getSettings());
|
||||
}
|
||||
|
||||
public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter);
|
||||
|
||||
public abstract @NonNull MappingModelList getSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Adds ability to customize a value for a single select (radio) setting.
|
||||
*/
|
||||
public class CustomizableSingleSelectSetting {
|
||||
|
||||
public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener {
|
||||
void onCustomizeClicked(@NonNull Item item);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
private final TextView summaryText;
|
||||
private final View customize;
|
||||
private final RadioButton radio;
|
||||
private final SingleSelectSetting.ViewHolder delegate;
|
||||
private final Group customizeGroup;
|
||||
private final CustomizableSingleSelectionListener selectionListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) {
|
||||
super(itemView);
|
||||
this.selectionListener = selectionListener;
|
||||
|
||||
radio = findViewById(R.id.customizable_single_select_radio);
|
||||
summaryText = findViewById(R.id.customizable_single_select_summary);
|
||||
customize = findViewById(R.id.customizable_single_select_customize);
|
||||
customizeGroup = findViewById(R.id.customizable_single_select_customize_group);
|
||||
|
||||
delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener) {
|
||||
@Override
|
||||
protected void setChecked(boolean checked) {
|
||||
radio.setChecked(checked);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
delegate.bind(model.singleSelectItem);
|
||||
customizeGroup.setVisibility(radio.isChecked() ? View.VISIBLE : View.GONE);
|
||||
customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model));
|
||||
if (model.getCustomValue() != null) {
|
||||
summaryText.setText(model.getSummaryText());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private SingleSelectSetting.Item singleSelectItem;
|
||||
private Object customValue;
|
||||
private String summaryText;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) {
|
||||
this.customValue = customValue;
|
||||
this.summaryText = summaryText;
|
||||
|
||||
singleSelectItem = new SingleSelectSetting.Item(item, text, isSelected);
|
||||
}
|
||||
|
||||
public @Nullable Object getCustomValue() {
|
||||
return customValue;
|
||||
}
|
||||
|
||||
public @Nullable String getSummaryText() {
|
||||
return summaryText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areItemsTheSame(newItem.singleSelectItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue) && Objects.equals(summaryText, newItem.summaryText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.components.settings;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckedTextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Single select (radio) setting option
|
||||
*/
|
||||
public class SingleSelectSetting {
|
||||
|
||||
public interface SingleSelectSelectionChangedListener {
|
||||
void onSelectionChanged(@NonNull Object selection);
|
||||
}
|
||||
|
||||
public static class ViewHolder extends MappingViewHolder<Item> {
|
||||
|
||||
protected final CheckedTextView text;
|
||||
protected final SingleSelectSelectionChangedListener selectionChangedListener;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull SingleSelectSelectionChangedListener selectionChangedListener) {
|
||||
super(itemView);
|
||||
this.selectionChangedListener = selectionChangedListener;
|
||||
this.text = findViewById(R.id.single_select_item_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Item model) {
|
||||
text.setText(model.text);
|
||||
setChecked(model.isSelected);
|
||||
itemView.setOnClickListener(v -> selectionChangedListener.onSelectionChanged(model.item));
|
||||
}
|
||||
|
||||
protected void setChecked(boolean checked) {
|
||||
text.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Item implements MappingModel<Item> {
|
||||
private final String text;
|
||||
private final Object item;
|
||||
private final boolean isSelected;
|
||||
|
||||
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected) {
|
||||
this.item = item;
|
||||
this.text = text != null ? text : item.toString();
|
||||
this.isSelected = isSelected;
|
||||
}
|
||||
|
||||
public @NonNull String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public @NonNull Object getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Item newItem) {
|
||||
return item.equals(newItem.item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Item newItem) {
|
||||
return Objects.equals(text, newItem.text) && isSelected == newItem.isSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,12 +98,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
||||
}
|
||||
|
||||
public void clearVerticalBoundaries() {
|
||||
setVerticalBoundaries(0, parent.getMeasuredHeight());
|
||||
setVerticalBoundaries(parent.getTop(), parent.getMeasuredHeight() + parent.getTop());
|
||||
}
|
||||
|
||||
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
|
||||
extraPaddingTop = topBoundary;
|
||||
extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary;
|
||||
extraPaddingTop = topBoundary - parent.getTop();
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
|
||||
if (isAnimating) {
|
||||
fling();
|
||||
|
||||
@@ -216,6 +216,8 @@ public class DirectoryHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch("refresh");
|
||||
|
||||
DirectoryResult result;
|
||||
|
||||
if (FeatureFlags.cds()) {
|
||||
@@ -224,6 +226,8 @@ public class DirectoryHelper {
|
||||
result = ContactDiscoveryV1.getDirectoryResult(databaseNumbers, systemNumbers);
|
||||
}
|
||||
|
||||
stopwatch.split("network");
|
||||
|
||||
if (result.getNumberRewrites().size() > 0) {
|
||||
Log.i(TAG, "[getDirectoryResult] Need to rewrite some numbers.");
|
||||
recipientDatabase.updatePhoneNumbers(result.getNumberRewrites());
|
||||
@@ -238,10 +242,16 @@ public class DirectoryHelper {
|
||||
.map(recipientDatabase::getOrInsertFromE164)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
stopwatch.split("process-cds");
|
||||
|
||||
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
|
||||
|
||||
stopwatch.split("update-registered");
|
||||
|
||||
updateContactsDatabase(context, activeIds, true, result.getNumberRewrites());
|
||||
|
||||
stopwatch.split("contacts-db");
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
}
|
||||
@@ -258,6 +268,8 @@ public class DirectoryHelper {
|
||||
} else {
|
||||
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
|
||||
}
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
@@ -300,8 +301,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
|
||||
|
||||
private static final String STATE_REACT_WITH_ANY_PAGE = "STATE_REACT_WITH_ANY_PAGE";
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
@@ -1362,7 +1361,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
private void handleRecentSafetyNumberChange() {
|
||||
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
|
||||
records.addAll(identityRecords.getUntrustedRecords());
|
||||
SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
SafetyNumberChangeDialog.show(getSupportFragmentManager(), records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1383,6 +1382,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled() { }
|
||||
|
||||
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
|
||||
Log.i(TAG, "handleSecurityChange(" + isSecureText + ", " + isDefaultSms + ")");
|
||||
|
||||
@@ -1424,7 +1426,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (stickerLocator != null && draftMedia != null) {
|
||||
Log.d(TAG, "Handling shared sticker.");
|
||||
sendSticker(stickerLocator, draftMedia, 0, true);
|
||||
sendSticker(stickerLocator, Objects.requireNonNull(draftContentType), draftMedia, 0, true);
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
@@ -1684,6 +1686,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
} else if (ExpiredBuildReminder.isEligible()) {
|
||||
reminderView.get().showReminder(new ExpiredBuildReminder(this));
|
||||
reminderView.get().setOnActionClickListener(this::handleReminderAction);
|
||||
} else if (ServiceOutageReminder.isEligible(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
|
||||
reminderView.get().showReminder(new ServiceOutageReminder(this));
|
||||
@@ -1709,6 +1712,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
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);
|
||||
}
|
||||
@@ -2875,7 +2881,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
|
||||
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId()), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
|
||||
sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId(), stickerRecord.getEmoji()), stickerRecord.getContentType(), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() ->
|
||||
DatabaseFactory.getStickerDatabase(getApplicationContext())
|
||||
@@ -2883,9 +2889,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
);
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, Optional.absent(), Optional.absent(), Optional.absent());
|
||||
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
startActivityForResult(intent, MEDIA_SENDER);
|
||||
return;
|
||||
@@ -2896,7 +2902,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
boolean initiating = threadId == -1;
|
||||
TransportOption transport = sendButton.getSelectedTransport();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
Slide stickerSlide = new StickerSlide(this, uri, size, stickerLocator);
|
||||
Slide stickerSlide = new StickerSlide(this, uri, size, stickerLocator, contentType);
|
||||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
@@ -3095,7 +3101,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(this, messageRecord))
|
||||
.show();
|
||||
} else if (messageRecord.isIdentityMismatchFailure()) {
|
||||
SafetyNumberChangeDialog.create(this, messageRecord).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
SafetyNumberChangeDialog.show(this, messageRecord);
|
||||
} else {
|
||||
startActivity(MessageDetailsActivity.getIntentForMessageDetails(this, messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
|
||||
}
|
||||
|
||||
@@ -838,6 +838,7 @@ public class ConversationFragment extends LoggingFragment {
|
||||
|
||||
if (slide.hasSticker()) {
|
||||
composeIntent.putExtra(ConversationActivity.STICKER_EXTRA, slide.asAttachment().getSticker());
|
||||
composeIntent.setType(slide.asAttachment().getContentType());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.thoughtcrime.securesms.conversation.ui.error;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.telecom.Call;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -13,6 +13,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
@@ -28,21 +30,22 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks {
|
||||
|
||||
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
|
||||
|
||||
private static final String RECIPIENT_IDS_EXTRA = "recipient_ids";
|
||||
private static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
private static final String MESSAGE_TYPE_EXTRA = "message_type";
|
||||
private static final String IS_CALL_EXTRA = "is_call";
|
||||
|
||||
private SafetyNumberChangeViewModel viewModel;
|
||||
private SafetyNumberChangeAdapter adapter;
|
||||
private View dialogView;
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull List<IdentityDatabase.IdentityRecord> identityRecords) {
|
||||
List<String> ids = Stream.of(identityRecords)
|
||||
.filterNot(IdentityDatabase.IdentityRecord::isFirstUse)
|
||||
.map(record -> record.getRecipientId().serialize())
|
||||
@@ -53,12 +56,12 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
public static @NonNull SafetyNumberChangeDialog create(Context context, MessageRecord messageRecord) {
|
||||
public static void show(@NonNull FragmentActivity fragmentActivity, @NonNull MessageRecord messageRecord) {
|
||||
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
|
||||
.map(mismatch -> mismatch.getRecipientId(context).serialize())
|
||||
.map(mismatch -> mismatch.getRecipientId(fragmentActivity).serialize())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
@@ -68,7 +71,16 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
arguments.putString(MESSAGE_TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
fragment.show(fragmentActivity.getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
public static void showForCall(@NonNull FragmentManager fragmentManager, @NonNull RecipientId recipientId) {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putStringArray(RECIPIENT_IDS_EXTRA, new String[] { recipientId.serialize() });
|
||||
arguments.putBoolean(IS_CALL_EXTRA, true);
|
||||
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
|
||||
fragment.setArguments(arguments);
|
||||
fragment.show(fragmentManager, SAFETY_NUMBER_DIALOG);
|
||||
}
|
||||
|
||||
private SafetyNumberChangeDialog() { }
|
||||
@@ -93,6 +105,8 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
boolean isCall = requireArguments().getBoolean(IS_CALL_EXTRA, false);
|
||||
|
||||
dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme());
|
||||
@@ -101,8 +115,8 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
|
||||
builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
.setPositiveButton(isCall ? R.string.safety_number_change_dialog__call_anyway : R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
|
||||
.setNegativeButton(android.R.string.cancel, this::handleCancel);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
@@ -151,6 +165,12 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
trustOrVerifyResultLiveData.observeForever(observer);
|
||||
}
|
||||
|
||||
private void handleCancel(@NonNull DialogInterface dialogInterface, int which) {
|
||||
if (getActivity() instanceof Callback) {
|
||||
((Callback) getActivity()).onCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
|
||||
@@ -159,5 +179,6 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
|
||||
public interface Callback {
|
||||
void onSendAnywayAfterSafetyNumberChange();
|
||||
void onMessageResentAfterSafetyNumberChange();
|
||||
void onCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -10,8 +9,8 @@ import androidx.annotation.WorkerThread;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
@@ -21,9 +20,22 @@ import java.util.List;
|
||||
final class MentionsPickerRepository {
|
||||
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
private final GroupDatabase groupDatabase;
|
||||
|
||||
MentionsPickerRepository(@NonNull Context context) {
|
||||
recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<RecipientId> getMembers(@Nullable Recipient recipient) {
|
||||
if (recipient == null || !recipient.isPushV2Group()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(groupDatabase.getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF))
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -32,19 +44,14 @@ final class MentionsPickerRepository {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<RecipientId> recipientIds = Stream.of(mentionQuery.members)
|
||||
.filterNot(m -> m.getMember().isLocalNumber())
|
||||
.map(m -> m.getMember().getId())
|
||||
.toList();
|
||||
|
||||
return recipientDatabase.queryRecipientsForMentions(mentionQuery.query, recipientIds);
|
||||
return recipientDatabase.queryRecipientsForMentions(mentionQuery.query, mentionQuery.members);
|
||||
}
|
||||
|
||||
static class MentionQuery {
|
||||
@Nullable private final String query;
|
||||
@NonNull private final List<GroupMemberEntry.FullMember> members;
|
||||
@Nullable private final String query;
|
||||
@NonNull private final List<RecipientId> members;
|
||||
|
||||
MentionQuery(@Nullable String query, @NonNull List<GroupMemberEntry.FullMember> members) {
|
||||
MentionQuery(@Nullable String query, @NonNull List<RecipientId> members) {
|
||||
this.query = query;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry.FullMember;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
@@ -29,24 +28,26 @@ public class MentionsPickerViewModel extends ViewModel {
|
||||
|
||||
private final SingleLiveEvent<Recipient> selectedRecipient;
|
||||
private final LiveData<List<MappingModel<?>>> mentionList;
|
||||
private final MutableLiveData<LiveGroup> group;
|
||||
private final MutableLiveData<LiveRecipient> liveRecipient;
|
||||
private final MutableLiveData<Query> liveQuery;
|
||||
private final MutableLiveData<Boolean> isShowing;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository, @NonNull MegaphoneRepository megaphoneRepository) {
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
|
||||
@NonNull MegaphoneRepository megaphoneRepository)
|
||||
{
|
||||
this.megaphoneRepository = megaphoneRepository;
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
this.liveQuery = new MutableLiveData<>();
|
||||
this.selectedRecipient = new SingleLiveEvent<>();
|
||||
this.isShowing = new MutableLiveData<>(false);
|
||||
|
||||
group = new MutableLiveData<>();
|
||||
liveQuery = new MutableLiveData<>(Query.NONE);
|
||||
selectedRecipient = new SingleLiveEvent<>();
|
||||
isShowing = new MutableLiveData<>(false);
|
||||
LiveData<Recipient> recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
||||
LiveData<List<RecipientId>> fullMembers = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(recipient, mentionsPickerRepository::getMembers));
|
||||
LiveData<Query> query = Transformations.distinctUntilChanged(liveQuery);
|
||||
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, (q, m) -> new MentionQuery(q.query, m));
|
||||
|
||||
LiveData<List<FullMember>> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
|
||||
LiveData<Query> query = Transformations.distinctUntilChanged(liveQuery);
|
||||
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, (q, m) -> new MentionQuery(q.query, m));
|
||||
|
||||
mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).<MappingModel<?>>map(MentionViewState::new).toList());
|
||||
this.mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).<MappingModel<?>>map(MentionViewState::new).toList());
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<MappingModel<?>>> getMentionList() {
|
||||
@@ -78,11 +79,7 @@ public class MentionsPickerViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void onRecipientChange(@NonNull Recipient recipient) {
|
||||
GroupId groupId = recipient.getGroupId().orNull();
|
||||
if (groupId != null) {
|
||||
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||
group.setValue(liveGroup);
|
||||
}
|
||||
this.liveRecipient.setValue(recipient.live());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,13 +111,13 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
||||
@Override
|
||||
@WorkerThread
|
||||
protected void archiveThreads(Set<Long> threadIds) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).setArchived(threadIds, true);
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).setArchived(threadIds, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@WorkerThread
|
||||
protected void reverseArchiveThreads(Set<Long> threadIds) {
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).setArchived(threadIds, false);
|
||||
DatabaseFactory.getThreadDatabase(getActivity()).setArchived(threadIds, true);
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
|
||||
@@ -56,6 +56,7 @@ import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
@@ -117,6 +118,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
@@ -176,6 +178,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
private ViewGroup megaphoneContainer;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private Drawable archiveDrawable;
|
||||
private LifecycleObserver visibilityLifecycleObserver;
|
||||
|
||||
public static ConversationListFragment newInstance() {
|
||||
return new ConversationListFragment();
|
||||
@@ -214,6 +217,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
cameraFab.show();
|
||||
|
||||
reminderView.setOnDismissListener(this::updateReminders);
|
||||
reminderView.setOnActionClickListener(this::onReminderAction);
|
||||
|
||||
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
|
||||
list.setItemAnimator(new DeleteItemAnimator());
|
||||
@@ -256,7 +260,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
InsightsLauncher.showInsightsModal(requireContext(), requireFragmentManager());
|
||||
}
|
||||
|
||||
SimpleTask.run(getLifecycle(), Recipient::self, this::initializeProfileIcon);
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), Recipient::self, this::initializeProfileIcon);
|
||||
|
||||
if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
|
||||
list.removeItemDecoration(searchAdapterDecoration);
|
||||
@@ -272,6 +276,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
ConversationFragment.prepare(requireContext());
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(visibilityLifecycleObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -283,6 +288,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
ProcessLifecycleOwner.get().getLifecycle().removeObserver(visibilityLifecycleObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
menu.clear();
|
||||
@@ -412,6 +423,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
viewModel.onMegaphoneCompleted(event);
|
||||
}
|
||||
|
||||
private void onReminderAction(@IdRes int reminderActionId) {
|
||||
if (reminderActionId == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private void hideKeyboard() {
|
||||
InputMethodManager imm = ServiceUtil.getInputMethodManager(requireContext());
|
||||
imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0);
|
||||
@@ -508,12 +525,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList);
|
||||
viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState);
|
||||
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() {
|
||||
visibilityLifecycleObserver = new DefaultLifecycleObserver() {
|
||||
@Override
|
||||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
viewModel.onVisible();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private void onSearchResultChanged(@Nullable SearchResult result) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
@@ -10,6 +11,9 @@ import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@@ -44,21 +48,17 @@ public class UnidentifiedAccessUtil {
|
||||
try {
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(recipient);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
|
||||
" | UUID certificate supported? " + recipient.isUuidSupported());
|
||||
|
||||
if (theirUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessCertificate != null)
|
||||
{
|
||||
if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate),
|
||||
new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
@@ -75,13 +75,13 @@ public class UnidentifiedAccessUtil {
|
||||
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
|
||||
try {
|
||||
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
|
||||
byte[] ourUnidentifiedAccessCertificate = TextSecurePreferences.getUnidentifiedAccessCertificate(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(Recipient.self());
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
if (ourUnidentifiedAccessCertificate != null) {
|
||||
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate),
|
||||
new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
@@ -95,6 +95,23 @@ public class UnidentifiedAccessUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getUnidentifiedAccessCertificate(@NonNull Recipient recipient) {
|
||||
CertificateType certificateType;
|
||||
PhoneNumberPrivacyValues.PhoneNumberSharingMode sendPhoneNumberTo = SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode();
|
||||
|
||||
switch (sendPhoneNumberTo) {
|
||||
case EVERYONE: certificateType = CertificateType.UUID_AND_E164; break;
|
||||
case CONTACTS: certificateType = recipient.isSystemContact() ? CertificateType.UUID_AND_E164 : CertificateType.UUID_ONLY; break;
|
||||
case NOBODY : certificateType = CertificateType.UUID_ONLY; break;
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format("Certificate type for %s with setting %s -> %s", recipient.getId(), sendPhoneNumberTo, certificateType));
|
||||
|
||||
return SignalStore.certificateValues()
|
||||
.getUnidentifiedAccessCertificate(certificateType);
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.resolve().getProfileKey());
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
|
||||
@@ -83,10 +84,12 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -118,6 +121,7 @@ public class AttachmentDatabase extends Database {
|
||||
public static final String STICKER_PACK_ID = "sticker_pack_id";
|
||||
public static final String STICKER_PACK_KEY = "sticker_pack_key";
|
||||
static final String STICKER_ID = "sticker_id";
|
||||
static final String STICKER_EMOJI = "sticker_emoji";
|
||||
static final String FAST_PREFLIGHT_ID = "fast_preflight_id";
|
||||
public static final String DATA_RANDOM = "data_random";
|
||||
private static final String THUMBNAIL_RANDOM = "thumbnail_random";
|
||||
@@ -150,7 +154,7 @@ public class AttachmentDatabase extends Database {
|
||||
THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST,
|
||||
FAST_PREFLIGHT_ID, VOICE_NOTE, BORDERLESS, QUOTE, DATA_RANDOM,
|
||||
THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID,
|
||||
STICKER_PACK_KEY, STICKER_ID, DATA_HASH, VISUAL_HASH,
|
||||
STICKER_PACK_KEY, STICKER_ID, STICKER_EMOJI, DATA_HASH, VISUAL_HASH,
|
||||
TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER,
|
||||
UPLOAD_TIMESTAMP };
|
||||
|
||||
@@ -187,6 +191,7 @@ public class AttachmentDatabase extends Database {
|
||||
STICKER_PACK_ID + " TEXT DEFAULT NULL, " +
|
||||
STICKER_PACK_KEY + " DEFAULT NULL, " +
|
||||
STICKER_ID + " INTEGER DEFAULT -1, " +
|
||||
STICKER_EMOJI + " STRING DEFAULT NULL, " +
|
||||
DATA_HASH + " TEXT DEFAULT NULL, " +
|
||||
VISUAL_HASH + " TEXT DEFAULT NULL, " +
|
||||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " +
|
||||
@@ -479,6 +484,41 @@ public class AttachmentDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
public void trimAllAbandonedAttachments() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String selectAllMmsIds = "SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME;
|
||||
String selectDataInUse = "SELECT DISTINCT " + DATA + " FROM " + TABLE_NAME + " WHERE " + QUOTE + " = 0 AND " + MMS_ID + " IN (" + selectAllMmsIds + ")";
|
||||
String where = MMS_ID + " NOT IN (" + selectAllMmsIds + ") AND " + DATA + " NOT IN (" + selectDataInUse + ")";
|
||||
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
public void deleteAbandonedAttachmentFiles() {
|
||||
Set<String> filesOnDisk = new HashSet<>();
|
||||
Set<String> filesInDb = new HashSet<>();
|
||||
|
||||
File attachmentDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
|
||||
for (File file : attachmentDirectory.listFiles()) {
|
||||
filesOnDisk.add(file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA, THUMBNAIL }, null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
filesInDb.add(CursorUtil.requireString(cursor, DATA));
|
||||
filesInDb.add(CursorUtil.requireString(cursor, THUMBNAIL));
|
||||
}
|
||||
}
|
||||
|
||||
filesInDb.addAll(DatabaseFactory.getStickerDatabase(context).getAllStickerFiles());
|
||||
|
||||
Set<String> onDiskButNotInDatabase = SetUtil.difference(filesOnDisk, filesInDb);
|
||||
|
||||
for (String filePath : onDiskButNotInDatabase) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
new File(filePath).delete();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
void deleteAllAttachments() {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
@@ -1196,7 +1236,8 @@ public class AttachmentDatabase extends Database {
|
||||
object.getInt(STICKER_ID) >= 0
|
||||
? new StickerLocator(object.getString(STICKER_PACK_ID),
|
||||
object.getString(STICKER_PACK_KEY),
|
||||
object.getInt(STICKER_ID))
|
||||
object.getInt(STICKER_ID),
|
||||
object.getString(STICKER_EMOJI))
|
||||
: null,
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(object.getString(VISUAL_HASH)),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(object.getString(VISUAL_HASH)) : null,
|
||||
@@ -1231,9 +1272,10 @@ public class AttachmentDatabase extends Database {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)) >= 0
|
||||
? new StickerLocator(cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(STICKER_PACK_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)))
|
||||
? new StickerLocator(CursorUtil.requireString(cursor, STICKER_PACK_ID),
|
||||
CursorUtil.requireString(cursor, STICKER_PACK_KEY),
|
||||
CursorUtil.requireInt(cursor, STICKER_ID),
|
||||
CursorUtil.requireString(cursor, STICKER_EMOJI))
|
||||
: null,
|
||||
MediaUtil.isAudioType(contentType) ? null : BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))),
|
||||
MediaUtil.isAudioType(contentType) ? AudioHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(VISUAL_HASH))) : null,
|
||||
@@ -1311,6 +1353,7 @@ public class AttachmentDatabase extends Database {
|
||||
contentValues.put(STICKER_PACK_ID, attachment.getSticker().getPackId());
|
||||
contentValues.put(STICKER_PACK_KEY, attachment.getSticker().getPackKey());
|
||||
contentValues.put(STICKER_ID, attachment.getSticker().getStickerId());
|
||||
contentValues.put(STICKER_EMOJI, attachment.getSticker().getEmoji());
|
||||
}
|
||||
|
||||
if (dataInfo != null) {
|
||||
|
||||
@@ -850,21 +850,33 @@ public final class GroupDatabase extends Database {
|
||||
}
|
||||
|
||||
public boolean isAdmin(@NonNull Recipient recipient) {
|
||||
return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), recipient.getUuid().get())
|
||||
Optional<UUID> uuid = recipient.getUuid();
|
||||
|
||||
if (!uuid.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), uuid.get())
|
||||
.transform(t -> t.getRole() == Member.Role.ADMINISTRATOR)
|
||||
.or(false);
|
||||
}
|
||||
|
||||
public MemberLevel memberLevel(@NonNull Recipient recipient) {
|
||||
Optional<UUID> uuid = recipient.getUuid();
|
||||
|
||||
if (!uuid.isPresent()) {
|
||||
return MemberLevel.NOT_A_MEMBER;
|
||||
}
|
||||
|
||||
DecryptedGroup decryptedGroup = getDecryptedGroup();
|
||||
|
||||
return DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), recipient.getUuid().get())
|
||||
return DecryptedGroupUtil.findMemberByUuid(decryptedGroup.getMembersList(), uuid.get())
|
||||
.transform(member -> member.getRole() == Member.Role.ADMINISTRATOR
|
||||
? MemberLevel.ADMINISTRATOR
|
||||
: MemberLevel.FULL_MEMBER)
|
||||
.or(() -> DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), recipient.getUuid().get())
|
||||
.or(() -> DecryptedGroupUtil.findPendingByUuid(decryptedGroup.getPendingMembersList(), uuid.get())
|
||||
.transform(m -> MemberLevel.PENDING_MEMBER)
|
||||
.or(() -> DecryptedGroupUtil.findRequestingByUuid(decryptedGroup.getRequestingMembersList(), recipient.getUuid().get())
|
||||
.or(() -> DecryptedGroupUtil.findRequestingByUuid(decryptedGroup.getRequestingMembersList(), uuid.get())
|
||||
.transform(m -> MemberLevel.REQUESTING_MEMBER)
|
||||
.or(MemberLevel.NOT_A_MEMBER)));
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ public class GroupReceiptDatabase extends Database {
|
||||
db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)});
|
||||
}
|
||||
|
||||
void deleteAbandonedRows() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, MMS_ID + " NOT IN (SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME + ")", null);
|
||||
}
|
||||
|
||||
void deleteAllRows() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.delete(TABLE_NAME, null, null);
|
||||
|
||||
@@ -44,6 +44,7 @@ public class MediaDatabase extends Database {
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", "
|
||||
|
||||
@@ -21,9 +21,9 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
@@ -143,6 +143,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
abstract void deleteMessagesInThreadBeforeDate(long threadId, long date);
|
||||
abstract void deleteThreads(@NonNull Set<Long> threadIds);
|
||||
abstract void deleteAllThreads();
|
||||
abstract void deleteAbandonedMessages();
|
||||
|
||||
public abstract SQLiteDatabase beginTransaction();
|
||||
public abstract void endTransaction(SQLiteDatabase database);
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -41,7 +40,6 @@ import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
@@ -79,11 +77,9 @@ import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
@@ -234,6 +230,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID+ ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
@@ -1587,29 +1584,18 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
||||
Cursor cursor = null;
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date;
|
||||
|
||||
try {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = THREAD_ID + " = ? AND (CASE (" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") ";
|
||||
db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId));
|
||||
}
|
||||
|
||||
for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) {
|
||||
where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date;
|
||||
}
|
||||
@Override
|
||||
void deleteAbandonedMessages() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadDatabase.TABLE_NAME + ")";
|
||||
|
||||
where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)");
|
||||
|
||||
cursor = db.query(TABLE_NAME, new String[] {ID}, where, new String[] {threadId+""}, null, null, null);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Log.i(TAG, "Trimming: " + cursor.getLong(0));
|
||||
deleteMessage(cursor.getLong(0));
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -272,6 +272,18 @@ public class MmsSmsDatabase extends Database {
|
||||
return count;
|
||||
}
|
||||
|
||||
public int getMessageCountBeforeDate(long date) {
|
||||
String selection = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " < " + date;
|
||||
|
||||
try (Cursor cursor = queryTables(new String[] { "COUNT(*)" }, selection, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getSecureMessageCountForInsights() {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getSecureMessageCountForInsights();
|
||||
count += DatabaseFactory.getMmsDatabase(context).getSecureMessageCountForInsights();
|
||||
@@ -362,6 +374,29 @@ public class MmsSmsDatabase extends Database {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public long getTimestampForFirstMessageAfterDate(long date) {
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
|
||||
String selection = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + date;
|
||||
|
||||
try (Cursor cursor = queryTables(new String[] { MmsSmsColumns.NORMALIZED_DATE_RECEIVED }, selection, order, "1")) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void deleteMessagesInThreadBeforeDate(long threadId, long trimBeforeDate) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
|
||||
DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
|
||||
}
|
||||
|
||||
public void deleteAbandonedMessages() {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteAbandonedMessages();
|
||||
DatabaseFactory.getMmsDatabase(context).deleteAbandonedMessages();
|
||||
}
|
||||
|
||||
private Cursor queryTables(String[] projection, String selection, String order, String limit) {
|
||||
String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@@ -393,6 +428,7 @@ public class MmsSmsDatabase extends Database {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_EMOJI + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_EMOJI + ", " +
|
||||
"'" + AttachmentDatabase.VISUAL_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VISUAL_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.google.android.gms.common.util.ArrayUtils;
|
||||
import net.sqlcipher.database.SQLiteConstraintException;
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
@@ -28,8 +29,9 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.WakeGroupV2Job;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
@@ -857,10 +859,21 @@ public class RecipientDatabase extends Database {
|
||||
for (SignalGroupV2Record insert : groupV2Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert));
|
||||
|
||||
GroupId.V2 groupId = GroupId.v2(insert.getMasterKey());
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
|
||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new WakeGroupV2Job(insert.getMasterKey()));
|
||||
Log.i(TAG, "Creating restore placeholder for " + groupId);
|
||||
|
||||
DatabaseFactory.getGroupDatabase(context)
|
||||
.create(masterKey,
|
||||
DecryptedGroup.newBuilder()
|
||||
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
|
||||
.build());
|
||||
|
||||
Log.i(TAG, "Scheduling request for latest group info for " + groupId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
needsRefresh.add(recipient.getId());
|
||||
@@ -874,7 +887,8 @@ public class RecipientDatabase extends Database {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(update.getOld().getMasterKey()));
|
||||
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
needsRefresh.add(recipient.getId());
|
||||
@@ -1019,7 +1033,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
private static @NonNull ContentValues getValuesForStorageGroupV2(@NonNull SignalGroupV2Record groupV2) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKey()).toString());
|
||||
values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKeyOrThrow()).toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
|
||||
@@ -1736,7 +1750,6 @@ public class RecipientDatabase extends Database {
|
||||
for (RecipientId id : unregistered) {
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId());
|
||||
values.put(UUID, (String) null);
|
||||
if (update(id, values)) {
|
||||
markDirty(id, DirtyState.DELETE);
|
||||
}
|
||||
|
||||
@@ -968,16 +968,18 @@ public class SmsDatabase extends MessageDatabase {
|
||||
|
||||
@Override
|
||||
void deleteMessagesInThreadBeforeDate(long threadId, long date) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = THREAD_ID + " = ? AND (CASE " + TYPE;
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date;
|
||||
|
||||
for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) {
|
||||
where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date;
|
||||
}
|
||||
db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId));
|
||||
}
|
||||
|
||||
where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)");
|
||||
@Override
|
||||
void deleteAbandonedMessages() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadDatabase.TABLE_NAME + ")";
|
||||
|
||||
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
||||
db.delete(TABLE_NAME, where, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,12 +3,12 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
@@ -23,6 +23,8 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.Closeable;
|
||||
@@ -30,29 +32,30 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class StickerDatabase extends Database {
|
||||
|
||||
private static final String TAG = Log.tag(StickerDatabase.class);
|
||||
|
||||
public static final String TABLE_NAME = "sticker";
|
||||
public static final String _ID = "_id";
|
||||
static final String PACK_ID = "pack_id";
|
||||
private static final String PACK_KEY = "pack_key";
|
||||
private static final String PACK_TITLE = "pack_title";
|
||||
private static final String PACK_AUTHOR = "pack_author";
|
||||
private static final String STICKER_ID = "sticker_id";
|
||||
private static final String EMOJI = "emoji";
|
||||
private static final String COVER = "cover";
|
||||
private static final String PACK_ORDER = "pack_order";
|
||||
private static final String INSTALLED = "installed";
|
||||
private static final String LAST_USED = "last_used";
|
||||
public static final String FILE_PATH = "file_path";
|
||||
public static final String FILE_LENGTH = "file_length";
|
||||
public static final String FILE_RANDOM = "file_random";
|
||||
public static final String TABLE_NAME = "sticker";
|
||||
public static final String _ID = "_id";
|
||||
static final String PACK_ID = "pack_id";
|
||||
private static final String PACK_KEY = "pack_key";
|
||||
private static final String PACK_TITLE = "pack_title";
|
||||
private static final String PACK_AUTHOR = "pack_author";
|
||||
private static final String STICKER_ID = "sticker_id";
|
||||
private static final String EMOJI = "emoji";
|
||||
public static final String CONTENT_TYPE = "content_type";
|
||||
private static final String COVER = "cover";
|
||||
private static final String PACK_ORDER = "pack_order";
|
||||
private static final String INSTALLED = "installed";
|
||||
private static final String LAST_USED = "last_used";
|
||||
public static final String FILE_PATH = "file_path";
|
||||
public static final String FILE_LENGTH = "file_length";
|
||||
public static final String FILE_RANDOM = "file_random";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
PACK_ID + " TEXT NOT NULL, " +
|
||||
@@ -63,6 +66,7 @@ public class StickerDatabase extends Database {
|
||||
COVER + " INTEGER, " +
|
||||
PACK_ORDER + " INTEGER, " +
|
||||
EMOJI + " TEXT NOT NULL, " +
|
||||
CONTENT_TYPE + " TEXT DEFAULT NULL, " +
|
||||
LAST_USED + " INTEGER, " +
|
||||
INSTALLED + " INTEGER," +
|
||||
FILE_PATH + " TEXT NOT NULL, " +
|
||||
@@ -75,7 +79,7 @@ public class StickerDatabase extends Database {
|
||||
"CREATE INDEX IF NOT EXISTS sticker_sticker_id_index ON " + TABLE_NAME + " (" + STICKER_ID + ");"
|
||||
};
|
||||
|
||||
private static final String DIRECTORY = "stickers";
|
||||
public static final String DIRECTORY = "stickers";
|
||||
|
||||
private final AttachmentSecret attachmentSecret;
|
||||
|
||||
@@ -94,6 +98,7 @@ public class StickerDatabase extends Database {
|
||||
contentValues.put(PACK_AUTHOR, sticker.getPackAuthor());
|
||||
contentValues.put(STICKER_ID, sticker.getStickerId());
|
||||
contentValues.put(EMOJI, sticker.getEmoji());
|
||||
contentValues.put(CONTENT_TYPE, sticker.getContentType());
|
||||
contentValues.put(COVER, sticker.isCover() ? 1 : 0);
|
||||
contentValues.put(INSTALLED, sticker.isInstalled() ? 1 : 0);
|
||||
contentValues.put(FILE_PATH, fileInfo.getFile().getAbsolutePath());
|
||||
@@ -101,6 +106,12 @@ public class StickerDatabase extends Database {
|
||||
contentValues.put(FILE_RANDOM, fileInfo.getRandom());
|
||||
|
||||
long id = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
if (id == -1) {
|
||||
String selection = PACK_ID + " = ? AND " + STICKER_ID + " = ? AND " + COVER + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(sticker.getPackId(), sticker.getStickerId(), (sticker.isCover() ? 1 : 0));
|
||||
|
||||
id = databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, selection, args);
|
||||
}
|
||||
|
||||
if (id > 0) {
|
||||
notifyStickerListeners();
|
||||
@@ -187,6 +198,19 @@ public class StickerDatabase extends Database {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @NonNull Set<String> getAllStickerFiles() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
Set<String> files = new HashSet<>();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { FILE_PATH }, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
files.add(CursorUtil.requireString(cursor, FILE_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public @Nullable InputStream getStickerStream(long rowId) throws IOException {
|
||||
String selection = _ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(rowId) };
|
||||
@@ -460,6 +484,7 @@ public class StickerDatabase extends Database {
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(PACK_KEY)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(EMOJI)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LENGTH)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(COVER)) == 1);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
@@ -15,7 +17,11 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ThreadBodyUtil {
|
||||
|
||||
@@ -42,7 +48,8 @@ public final class ThreadBodyUtil {
|
||||
} else if (record.getSlideDeck().getAudioSlide() != null) {
|
||||
return format(context, record, EmojiStrings.AUDIO, R.string.ThreadRecord_voice_message);
|
||||
} else if (MessageRecordUtil.hasSticker(record)) {
|
||||
return format(context, record, EmojiStrings.STICKER, R.string.ThreadRecord_sticker);
|
||||
String emoji = getStickerEmoji(record);
|
||||
return format(context, record, emoji, R.string.ThreadRecord_sticker);
|
||||
}
|
||||
|
||||
boolean hasImage = false;
|
||||
@@ -81,4 +88,11 @@ public final class ThreadBodyUtil {
|
||||
private static @NonNull String getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
return MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody()).toString();
|
||||
}
|
||||
|
||||
private static @NonNull String getStickerEmoji(@NonNull MessageRecord record) {
|
||||
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
|
||||
|
||||
return Util.isEmpty(slide.getEmoji()) ? EmojiStrings.STICKER
|
||||
: slide.getEmoji();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ import net.sqlcipher.database.SQLiteDatabase;
|
||||
import org.jsoup.helper.StringUtil;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientDetails;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -67,6 +68,7 @@ import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -74,6 +76,9 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
private static final String TAG = ThreadDatabase.class.getSimpleName();
|
||||
|
||||
public static final long NO_TRIM_BEFORE_DATE_SET = 0;
|
||||
public static final int NO_TRIM_MESSAGE_COUNT_SET = Integer.MAX_VALUE;
|
||||
|
||||
public static final String TABLE_NAME = "thread";
|
||||
public static final String ID = "_id";
|
||||
public static final String DATE = "date";
|
||||
@@ -256,53 +261,90 @@ public class ThreadDatabase extends Database {
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
public void trimAllThreads(int length, ProgressListener listener) {
|
||||
Cursor cursor = null;
|
||||
int threadCount = 0;
|
||||
int complete = 0;
|
||||
public void trimAllThreads(int length, long trimBeforeDate) {
|
||||
if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
|
||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { ID }, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
trimThreadInternal(CursorUtil.requireLong(cursor, ID), length, trimBeforeDate);
|
||||
}
|
||||
}
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
cursor = this.getConversationList();
|
||||
|
||||
if (cursor != null)
|
||||
threadCount = cursor.getCount();
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
trimThread(threadId, length);
|
||||
|
||||
listener.onProgress(++complete, threadCount);
|
||||
}
|
||||
mmsSmsDatabase.deleteAbandonedMessages();
|
||||
attachmentDatabase.trimAllAbandonedAttachments();
|
||||
groupReceiptDatabase.deleteAbandonedRows();
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
attachmentDatabase.deleteAbandonedAttachmentFiles();
|
||||
|
||||
notifyAttachmentListeners();
|
||||
notifyStickerListeners();
|
||||
notifyStickerPackListeners();
|
||||
}
|
||||
|
||||
public void trimThread(long threadId, int length) {
|
||||
Log.i(TAG, "Trimming thread: " + threadId + " to: " + length);
|
||||
Cursor cursor = null;
|
||||
public void trimThread(long threadId, int length, long trimBeforeDate) {
|
||||
if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
|
||||
return;
|
||||
}
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId);
|
||||
|
||||
if (cursor != null && length > 0 && cursor.getCount() > length) {
|
||||
Log.w(TAG, "Cursor count is greater than length!");
|
||||
cursor.moveToPosition(length - 1);
|
||||
|
||||
long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED));
|
||||
|
||||
Log.i(TAG, "Cut off tweet date: " + lastTweetDate);
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
|
||||
DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate);
|
||||
|
||||
update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
trimThreadInternal(threadId, length, trimBeforeDate);
|
||||
mmsSmsDatabase.deleteAbandonedMessages();
|
||||
attachmentDatabase.trimAllAbandonedAttachments();
|
||||
groupReceiptDatabase.deleteAbandonedRows();
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
attachmentDatabase.deleteAbandonedAttachmentFiles();
|
||||
|
||||
notifyAttachmentListeners();
|
||||
notifyStickerListeners();
|
||||
notifyStickerPackListeners();
|
||||
}
|
||||
|
||||
private void trimThreadInternal(long threadId, int length, long trimBeforeDate) {
|
||||
if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (length != NO_TRIM_MESSAGE_COUNT_SET) {
|
||||
try (Cursor cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId)) {
|
||||
if (cursor != null && length > 0 && cursor.getCount() > length) {
|
||||
cursor.moveToPosition(length - 1);
|
||||
trimBeforeDate = Math.max(trimBeforeDate, cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trimBeforeDate != NO_TRIM_BEFORE_DATE_SET) {
|
||||
Log.i(TAG, "Trimming thread: " + threadId + " before: " + trimBeforeDate);
|
||||
|
||||
DatabaseFactory.getMmsSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate);
|
||||
|
||||
update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,19 +1121,19 @@ public class ThreadDatabase extends Database {
|
||||
Recipient resolved = Recipient.resolved(threadRecipientId);
|
||||
if (resolved.isPushGroup()) {
|
||||
if (resolved.isPushV2Group()) {
|
||||
DecryptedGroup decryptedGroup = DatabaseFactory.getGroupDatabase(context).requireGroup(resolved.requireGroupId().requireV2()).requireV2GroupProperties().getDecryptedGroup();
|
||||
Optional<UUID> inviter = DecryptedGroupUtil.findInviter(decryptedGroup.getPendingMembersList(), Recipient.self().getUuid().get());
|
||||
|
||||
if (inviter.isPresent()) {
|
||||
RecipientId recipientId = RecipientId.from(inviter.get(), null);
|
||||
return Extra.forGroupV2invite(recipientId);
|
||||
} else if (decryptedGroup.getRevision() == 0) {
|
||||
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(decryptedGroup.getMembersList());
|
||||
|
||||
if (foundingMember.isPresent()) {
|
||||
return Extra.forGroupMessageRequest(RecipientId.from(UuidUtil.fromByteString(foundingMember.get().getUuid()), null));
|
||||
MessageRecord.InviteAddState inviteAddState = record.getGv2AddInviteState();
|
||||
if (inviteAddState != null) {
|
||||
RecipientId from = RecipientId.from(inviteAddState.getAddedOrInvitedBy(), null);
|
||||
if (inviteAddState.isInvited()) {
|
||||
Log.i(TAG, "GV2 invite message request from " + from);
|
||||
return Extra.forGroupV2invite(from);
|
||||
} else {
|
||||
Log.i(TAG, "GV2 message request from " + from);
|
||||
return Extra.forGroupMessageRequest(from);
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Falling back to unknown message request state for GV2 message");
|
||||
return Extra.forMessageRequest();
|
||||
} else {
|
||||
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
|
||||
|
||||
@@ -1109,7 +1151,8 @@ public class ThreadDatabase extends Database {
|
||||
} else if (record.isRemoteDelete()) {
|
||||
return Extra.forRemoteDelete();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
return Extra.forSticker();
|
||||
StickerSlide slide = Objects.requireNonNull(((MmsMessageRecord) record).getSlideDeck().getStickerSlide());
|
||||
return Extra.forSticker(slide.getEmoji());
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
|
||||
return Extra.forAlbum();
|
||||
}
|
||||
@@ -1150,10 +1193,6 @@ public class ThreadDatabase extends Database {
|
||||
return query;
|
||||
}
|
||||
|
||||
public interface ProgressListener {
|
||||
void onProgress(int complete, int total);
|
||||
}
|
||||
|
||||
public Reader readerFor(Cursor cursor) {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
@@ -1275,6 +1314,7 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
@JsonProperty private final boolean isRevealable;
|
||||
@JsonProperty private final boolean isSticker;
|
||||
@JsonProperty private final String stickerEmoji;
|
||||
@JsonProperty private final boolean isAlbum;
|
||||
@JsonProperty private final boolean isRemoteDelete;
|
||||
@JsonProperty private final boolean isMessageRequestAccepted;
|
||||
@@ -1283,6 +1323,7 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
|
||||
@JsonProperty("isSticker") boolean isSticker,
|
||||
@JsonProperty("stickerEmoji") String stickerEmoji,
|
||||
@JsonProperty("isAlbum") boolean isAlbum,
|
||||
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
|
||||
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
|
||||
@@ -1291,6 +1332,7 @@ public class ThreadDatabase extends Database {
|
||||
{
|
||||
this.isRevealable = isRevealable;
|
||||
this.isSticker = isSticker;
|
||||
this.stickerEmoji = stickerEmoji;
|
||||
this.isAlbum = isAlbum;
|
||||
this.isRemoteDelete = isRemoteDelete;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
@@ -1299,31 +1341,31 @@ public class ThreadDatabase extends Database {
|
||||
}
|
||||
|
||||
public static @NonNull Extra forViewOnce() {
|
||||
return new Extra(true, false, false, false, true, false, null);
|
||||
return new Extra(true, false, null, false, false, true, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forSticker() {
|
||||
return new Extra(false, true, false, false, true, false, null);
|
||||
public static @NonNull Extra forSticker(@Nullable String emoji) {
|
||||
return new Extra(false, true, emoji, false, false, true, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forAlbum() {
|
||||
return new Extra(false, false, true, false, true, false, null);
|
||||
return new Extra(false, false, null, true, false, true, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forRemoteDelete() {
|
||||
return new Extra(false, false, false, true, true, false, null);
|
||||
return new Extra(false, false, null, false, true, true, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forMessageRequest() {
|
||||
return new Extra(false, false, false, false, false, false, null);
|
||||
return new Extra(false, false, null, false, false, false, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
|
||||
return new Extra(false, false, false, false, false, false, recipientId.serialize());
|
||||
return new Extra(false, false, null, false, false, false, false, recipientId.serialize());
|
||||
}
|
||||
|
||||
public static @NonNull Extra forGroupV2invite(RecipientId recipientId) {
|
||||
return new Extra(false, false, false, false, false, true, recipientId.serialize());
|
||||
return new Extra(false, false, null, false, false, false, true, recipientId.serialize());
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
@@ -1334,6 +1376,10 @@ public class ThreadDatabase extends Database {
|
||||
return isSticker;
|
||||
}
|
||||
|
||||
public @Nullable String getStickerEmoji() {
|
||||
return stickerEmoji;
|
||||
}
|
||||
|
||||
public boolean isAlbum() {
|
||||
return isAlbum;
|
||||
}
|
||||
|
||||
@@ -144,8 +144,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int PINNED_CONVERSATIONS = 69;
|
||||
private static final int MENTION_GLOBAL_SETTING_MIGRATION = 70;
|
||||
private static final int UNKNOWN_STORAGE_FIELDS = 71;
|
||||
private static final int STICKER_CONTENT_TYPE = 72;
|
||||
private static final int STICKER_EMOJI_IN_NOTIFICATIONS = 73;
|
||||
|
||||
private static final int DATABASE_VERSION = 71;
|
||||
private static final int DATABASE_VERSION = 73;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@@ -1013,6 +1015,14 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN storage_proto TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (oldVersion < STICKER_CONTENT_TYPE) {
|
||||
db.execSQL("ALTER TABLE sticker ADD COLUMN content_type TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (oldVersion < STICKER_EMOJI_IN_NOTIFICATIONS) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN sticker_emoji TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
@@ -53,23 +53,24 @@ final class GroupsV2UpdateMessageProducer {
|
||||
/**
|
||||
* Describes a group that is new to you, use this when there is no available change record.
|
||||
* <p>
|
||||
* Invitation and groups you create are the most common cases where no change is available.
|
||||
* Invitation and revision 0 groups are the most common use cases for this.
|
||||
* <p>
|
||||
* When invited, it's possible there's no change available.
|
||||
* <p>
|
||||
* When the revision of the group is 0, the change is very noisy and only the editor is useful.
|
||||
*/
|
||||
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group) {
|
||||
UpdateDescription describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange decryptedGroupChange) {
|
||||
Optional<DecryptedPendingMember> selfPending = DecryptedGroupUtil.findPendingByUuid(group.getPendingMembersList(), selfUuid);
|
||||
if (selfPending.isPresent()) {
|
||||
return updateDescription(selfPending.get().getAddedByUuid(), inviteBy -> context.getString(R.string.MessageRecord_s_invited_you_to_the_group, inviteBy));
|
||||
}
|
||||
|
||||
if (group.getRevision() == 0) {
|
||||
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(group.getMembersList());
|
||||
if (foundingMember.isPresent()) {
|
||||
ByteString foundingMemberUuid = foundingMember.get().getUuid();
|
||||
if (selfUuidBytes.equals(foundingMemberUuid)) {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
|
||||
} else {
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
|
||||
}
|
||||
ByteString foundingMemberUuid = decryptedGroupChange.getEditor();
|
||||
if (!foundingMemberUuid.isEmpty()) {
|
||||
if (selfUuidBytes.equals(foundingMemberUuid)) {
|
||||
return updateDescription(context.getString(R.string.MessageRecord_you_created_the_group));
|
||||
} else {
|
||||
return updateDescription(foundingMemberUuid, creator -> context.getString(R.string.MessageRecord_s_added_you, creator));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
|
||||
if (editorIsYou) {
|
||||
if (newMemberIsYou) {
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_sharable_group_link)));
|
||||
updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group_via_the_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), added -> context.getString(R.string.MessageRecord_you_added_s, added)));
|
||||
}
|
||||
@@ -167,7 +168,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
updates.add(0, updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_added_you, editor)));
|
||||
} else {
|
||||
if (member.getUuid().equals(change.getEditor())) {
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_sharable_group_link, newMember)));
|
||||
updates.add(updateDescription(member.getUuid(), newMember -> context.getString(R.string.MessageRecord_s_joined_the_group_via_the_group_link, newMember)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), member.getUuid(), (editor, newMember) -> context.getString(R.string.MessageRecord_s_added_s, editor, newMember)));
|
||||
}
|
||||
@@ -516,33 +517,33 @@ final class GroupsV2UpdateMessageProducer {
|
||||
case ANY:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, editor)));
|
||||
}
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
groupLinkEnabled = true;
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_sharable_group_link_with_admin_approval)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_sharable_group_link_with_admin_approval, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, editor)));
|
||||
}
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_sharable_group_link)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_sharable_group_link, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_turned_off_the_group_link, editor)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!groupLinkEnabled && change.getNewInviteLinkPassword().size() > 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_sharable_group_link)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link)));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_sharable_group_link, editor)));
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_reset_the_group_link, editor)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -550,18 +551,18 @@ final class GroupsV2UpdateMessageProducer {
|
||||
private void describeUnknownEditorNewGroupInviteLinkAccess(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
switch (change.getNewInviteLinkAccess()) {
|
||||
case ANY:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off)));
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_on_with_admin_approval)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on)));
|
||||
break;
|
||||
case UNSATISFIABLE:
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_turned_off)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off)));
|
||||
break;
|
||||
}
|
||||
|
||||
if (change.getNewInviteLinkPassword().size() > 0) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_sharable_group_link_has_been_reset)));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +573,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||
if (requestingMemberIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group)));
|
||||
} else {
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_sharable_group_link, requesting)));
|
||||
updates.add(updateDescription(member.getUuid(), requesting -> context.getString(R.string.MessageRecord_s_requested_to_join_via_the_group_link, requesting)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class IncomingSticker {
|
||||
|
||||
@@ -10,6 +11,7 @@ public class IncomingSticker {
|
||||
private final String packAuthor;
|
||||
private final int stickerId;
|
||||
private final String emoji;
|
||||
private final String contentType;
|
||||
private final boolean isCover;
|
||||
private final boolean isInstalled;
|
||||
|
||||
@@ -19,6 +21,7 @@ public class IncomingSticker {
|
||||
@NonNull String packAuthor,
|
||||
int stickerId,
|
||||
@NonNull String emoji,
|
||||
@Nullable String contentType,
|
||||
boolean isCover,
|
||||
boolean isInstalled)
|
||||
{
|
||||
@@ -28,6 +31,7 @@ public class IncomingSticker {
|
||||
this.packAuthor = packAuthor;
|
||||
this.stickerId = stickerId;
|
||||
this.emoji = emoji;
|
||||
this.contentType = contentType;
|
||||
this.isCover = isCover;
|
||||
this.isInstalled = isInstalled;
|
||||
}
|
||||
@@ -56,6 +60,10 @@ public class IncomingSticker {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public @Nullable String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public boolean isCover() {
|
||||
return isCover;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.text.style.StyleSpan;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
@@ -41,6 +42,7 @@ import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -177,7 +179,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
if (decryptedGroupV2Context.hasChange() && decryptedGroupV2Context.getGroupState().getRevision() != 0) {
|
||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getChange()));
|
||||
} else {
|
||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState());
|
||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
@@ -185,6 +187,29 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable InviteAddState getGv2AddInviteState() {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(getBody());
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.parseFrom(decoded);
|
||||
DecryptedGroup groupState = decryptedGroupV2Context.getGroupState();
|
||||
boolean invited = DecryptedGroupUtil.findPendingByUuid(groupState.getPendingMembersList(), Recipient.self().requireUuid()).isPresent();
|
||||
|
||||
if (decryptedGroupV2Context.hasChange()) {
|
||||
UUID changeEditor = UuidUtil.fromByteStringOrNull(decryptedGroupV2Context.getChange().getEditor());
|
||||
|
||||
if (changeEditor != null) {
|
||||
return new InviteAddState(invited, changeEditor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "GV2 Message editor could not be determined");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient, @NonNull Function<Recipient, String> stringFunction) {
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getUuid().or(UuidUtil.UNKNOWN_UUID)), () -> stringFunction.apply(recipient.resolve()));
|
||||
}
|
||||
@@ -378,4 +403,23 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
public boolean hasSelfMention() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class InviteAddState {
|
||||
|
||||
private final boolean invited;
|
||||
private final UUID addedOrInvitedBy;
|
||||
|
||||
public InviteAddState(boolean invited, @NonNull UUID addedOrInvitedBy) {
|
||||
this.invited = invited;
|
||||
this.addedOrInvitedBy = addedOrInvitedBy;
|
||||
}
|
||||
|
||||
public @NonNull UUID getAddedOrInvitedBy() {
|
||||
return addedOrInvitedBy;
|
||||
}
|
||||
|
||||
public boolean isInvited() {
|
||||
return invited;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package org.thoughtcrime.securesms.database.model;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -18,6 +20,7 @@ public final class StickerRecord {
|
||||
private final String packKey;
|
||||
private final int stickerId;
|
||||
private final String emoji;
|
||||
private final String contentType;
|
||||
private final long size;
|
||||
private final boolean isCover;
|
||||
|
||||
@@ -26,16 +29,18 @@ public final class StickerRecord {
|
||||
@NonNull String packKey,
|
||||
int stickerId,
|
||||
@NonNull String emoji,
|
||||
@Nullable String contentType,
|
||||
long size,
|
||||
boolean isCover)
|
||||
{
|
||||
this.rowId = rowId;
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
this.emoji = emoji;
|
||||
this.size = size;
|
||||
this.isCover = isCover;
|
||||
this.rowId = rowId;
|
||||
this.packId = packId;
|
||||
this.packKey = packKey;
|
||||
this.stickerId = stickerId;
|
||||
this.emoji = emoji;
|
||||
this.contentType = contentType;
|
||||
this.size = size;
|
||||
this.isCover = isCover;
|
||||
}
|
||||
|
||||
public long getRowId() {
|
||||
@@ -62,6 +67,10 @@ public final class StickerRecord {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public @NonNull String getContentType() {
|
||||
return contentType == null ? MediaUtil.IMAGE_WEBP : contentType;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
@@ -81,11 +90,12 @@ public final class StickerRecord {
|
||||
isCover == that.isCover &&
|
||||
packId.equals(that.packId) &&
|
||||
packKey.equals(that.packKey) &&
|
||||
emoji.equals(that.emoji);
|
||||
emoji.equals(that.emoji) &&
|
||||
Objects.equals(contentType, that.contentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(rowId, packId, packKey, stickerId, emoji, size, isCover);
|
||||
return Objects.hash(rowId, packId, packKey, stickerId, emoji, contentType, size, isCover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FrameRateTracker;
|
||||
@@ -60,6 +61,7 @@ public class ApplicationDependencies {
|
||||
private static GroupsV2Operations groupsV2Operations;
|
||||
private static EarlyMessageCache earlyMessageCache;
|
||||
private static MessageNotifier messageNotifier;
|
||||
private static TrimThreadsByDateManager trimThreadsByDateManager;
|
||||
|
||||
@MainThread
|
||||
public static synchronized void init(@NonNull Application application, @NonNull Provider provider) {
|
||||
@@ -67,9 +69,10 @@ public class ApplicationDependencies {
|
||||
throw new IllegalStateException("Already initialized!");
|
||||
}
|
||||
|
||||
ApplicationDependencies.application = application;
|
||||
ApplicationDependencies.provider = provider;
|
||||
ApplicationDependencies.messageNotifier = provider.provideMessageNotifier();
|
||||
ApplicationDependencies.application = application;
|
||||
ApplicationDependencies.provider = provider;
|
||||
ApplicationDependencies.messageNotifier = provider.provideMessageNotifier();
|
||||
ApplicationDependencies.trimThreadsByDateManager = provider.provideTrimThreadsByDateManager();
|
||||
}
|
||||
|
||||
public static @NonNull Application getApplication() {
|
||||
@@ -257,6 +260,11 @@ public class ApplicationDependencies {
|
||||
return incomingMessageObserver;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull TrimThreadsByDateManager getTrimThreadsByDateManager() {
|
||||
assertInitialization();
|
||||
return trimThreadsByDateManager;
|
||||
}
|
||||
|
||||
private static void assertInitialization() {
|
||||
if (application == null || provider == null) {
|
||||
throw new UninitializedException();
|
||||
@@ -279,6 +287,7 @@ public class ApplicationDependencies {
|
||||
@NonNull EarlyMessageCache provideEarlyMessageCache();
|
||||
@NonNull MessageNotifier provideMessageNotifier();
|
||||
@NonNull IncomingMessageObserver provideIncomingMessageObserver();
|
||||
@NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager();
|
||||
}
|
||||
|
||||
private static class UninitializedException extends IllegalStateException {
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.push.SecurityEventListener;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.service.TrimThreadsByDateManager;
|
||||
import org.thoughtcrime.securesms.util.AlarmSleepTimer;
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCache;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -178,6 +179,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
return new IncomingMessageObserver(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager() {
|
||||
return new TrimThreadsByDateManager(context);
|
||||
}
|
||||
|
||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
private final Context context;
|
||||
|
||||
73
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java
vendored
Normal file
73
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.apng.decode.APNGParser;
|
||||
import org.signal.glide.common.io.ByteBufferReader;
|
||||
import org.signal.glide.common.loader.ByteBufferLoader;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ApngBufferCacheDecoder implements ResourceDecoder<ByteBuffer, APNGDecoder> {
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
||||
return APNGParser.isAPNG(new ByteBufferReader(source));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<APNGDecoder> decode(@NonNull final ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
|
||||
if (!APNGParser.isAPNG(new ByteBufferReader(source))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Loader loader = new ByteBufferLoader() {
|
||||
@Override
|
||||
public ByteBuffer getByteBuffer() {
|
||||
source.position(0);
|
||||
return source;
|
||||
}
|
||||
};
|
||||
|
||||
return new FrameSeqDecoderResource(new APNGDecoder(loader, null), source.limit());
|
||||
}
|
||||
|
||||
private static class FrameSeqDecoderResource implements Resource<APNGDecoder> {
|
||||
private final APNGDecoder decoder;
|
||||
private final int size;
|
||||
|
||||
FrameSeqDecoderResource(@NonNull APNGDecoder decoder, int size) {
|
||||
this.decoder = decoder;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Class<APNGDecoder> getResourceClass() {
|
||||
return APNGDecoder.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull APNGDecoder get() {
|
||||
return this.decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle() {
|
||||
this.decoder.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java
vendored
Normal file
48
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
|
||||
import org.signal.glide.apng.APNGDrawable;
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
|
||||
public class ApngFrameDrawableTranscoder implements ResourceTranscoder<APNGDecoder, Drawable> {
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<Drawable> transcode(@NonNull Resource<APNGDecoder> toTranscode, @NonNull Options options) {
|
||||
APNGDecoder decoder = toTranscode.get();
|
||||
APNGDrawable drawable = new APNGDrawable(decoder);
|
||||
|
||||
drawable.setAutoPlay(false);
|
||||
drawable.setLoopLimit(0);
|
||||
|
||||
return new DrawableResource<Drawable>(drawable) {
|
||||
@Override
|
||||
public @NonNull Class<Drawable> getResourceClass() {
|
||||
return Drawable.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycle() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java
vendored
Normal file
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.apng.decode.APNGParser;
|
||||
import org.signal.glide.common.io.StreamReader;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ApngStreamCacheDecoder implements ResourceDecoder<InputStream, APNGDecoder> {
|
||||
|
||||
private final ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder;
|
||||
|
||||
public ApngStreamCacheDecoder(ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder) {
|
||||
this.byteBufferDecoder = byteBufferDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return APNGParser.isAPNG(new StreamReader(source));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<APNGDecoder> decode(@NonNull final InputStream source, int width, int height, @NonNull Options options) throws IOException {
|
||||
byte[] data = Util.readFully(source);
|
||||
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
|
||||
return byteBufferDecoder.decode(byteBuffer, width, height, options);
|
||||
}
|
||||
}
|
||||
|
||||
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java
vendored
Normal file
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.EncodeStrategy;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceEncoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
|
||||
import org.signal.glide.apng.decode.APNGDecoder;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class EncryptedApngCacheEncoder extends EncryptedCoder implements ResourceEncoder<APNGDecoder> {
|
||||
|
||||
private static final String TAG = Log.tag(EncryptedApngCacheEncoder.class);
|
||||
|
||||
private final byte[] secret;
|
||||
|
||||
public EncryptedApngCacheEncoder(@NonNull byte[] secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull EncodeStrategy getEncodeStrategy(@NonNull Options options) {
|
||||
return EncodeStrategy.SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean encode(@NonNull Resource<APNGDecoder> data, @NonNull File file, @NonNull Options options) {
|
||||
try {
|
||||
Loader loader = data.get().getLoader();
|
||||
InputStream input = loader.obtain().toInputStream();
|
||||
OutputStream output = createEncryptedOutputStream(secret, file);
|
||||
|
||||
Util.copy(input, output);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EncryptedBitmapCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, Bitmap> {
|
||||
|
||||
private static final String TAG = EncryptedBitmapCacheDecoder.class.getSimpleName();
|
||||
|
||||
private final StreamBitmapDecoder streamBitmapDecoder;
|
||||
private final byte[] secret;
|
||||
|
||||
public EncryptedBitmapCacheDecoder(@NonNull byte[] secret, @NonNull StreamBitmapDecoder streamBitmapDecoder) {
|
||||
this.secret = secret;
|
||||
this.streamBitmapDecoder = streamBitmapDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull File source, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<Bitmap> decode(@NonNull File source, int width, int height, @NonNull Options options)
|
||||
throws IOException
|
||||
{
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return streamBitmapDecoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,6 @@ public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements Re
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
|
||||
Log.i(TAG, "Encrypted resource encoder running: " + file.toString());
|
||||
|
||||
Bitmap bitmap = data.get();
|
||||
Bitmap.CompressFormat format = getFormat(bitmap, options);
|
||||
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);
|
||||
|
||||
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java
vendored
Normal file
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EncryptedCacheDecoder<DecodeType> extends EncryptedCoder implements ResourceDecoder<File, DecodeType> {
|
||||
|
||||
private static final String TAG = Log.tag(EncryptedCacheDecoder.class);
|
||||
|
||||
private final byte[] secret;
|
||||
private final ResourceDecoder<InputStream, DecodeType> decoder;
|
||||
|
||||
public EncryptedCacheDecoder(byte[] secret, ResourceDecoder<InputStream, DecodeType> decoder) {
|
||||
this.secret = secret;
|
||||
this.decoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull File source, @NonNull Options options) throws IOException {
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return decoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<DecodeType> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return decoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,6 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<Inp
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
@Override
|
||||
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
|
||||
Log.i(TAG, "Encrypted cache encoder running: " + file.toString());
|
||||
|
||||
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
|
||||
|
||||
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.thoughtcrime.securesms.glide.cache;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class EncryptedGifCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, GifDrawable> {
|
||||
|
||||
private static final String TAG = EncryptedGifCacheDecoder.class.getSimpleName();
|
||||
|
||||
private final byte[] secret;
|
||||
private final StreamGifDecoder gifDecoder;
|
||||
|
||||
public EncryptedGifCacheDecoder(@NonNull byte[] secret, @NonNull StreamGifDecoder gifDecoder) {
|
||||
this.secret = secret;
|
||||
this.gifDecoder = gifDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull File source, @NonNull Options options) {
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.handles(inputStream, options);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Resource<GifDrawable> decode(@NonNull File source, int width, int height, @NonNull Options options) throws IOException {
|
||||
Log.i(TAG, "Encrypted GIF cache decoder running...");
|
||||
try (InputStream inputStream = createEncryptedInputStream(secret, source)) {
|
||||
return gifDecoder.decode(inputStream, width, height, options);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public abstract class GroupId {
|
||||
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!";
|
||||
private static final int MMS_BYTE_LENGTH = 16;
|
||||
private static final int V1_MMS_BYTE_LENGTH = 16;
|
||||
private static final int V1_BYTE_LENGTH = 16;
|
||||
private static final int V2_BYTE_LENGTH = GroupIdentifier.SIZE;
|
||||
|
||||
private final String encodedId;
|
||||
@@ -46,6 +47,13 @@ public abstract class GroupId {
|
||||
return new GroupId.V1(gv1GroupIdBytes);
|
||||
}
|
||||
|
||||
public static @NonNull GroupId.V1 v1Exact(byte[] gv1GroupIdBytes) throws BadGroupIdException {
|
||||
if (gv1GroupIdBytes.length != V1_BYTE_LENGTH) {
|
||||
throw new BadGroupIdException();
|
||||
}
|
||||
return new GroupId.V1(gv1GroupIdBytes);
|
||||
}
|
||||
|
||||
public static GroupId.V1 createV1(@NonNull SecureRandom secureRandom) {
|
||||
return v1orThrow(Util.getSecretBytes(secureRandom, V1_MMS_BYTE_LENGTH));
|
||||
}
|
||||
@@ -54,15 +62,11 @@ public abstract class GroupId {
|
||||
return mms(Util.getSecretBytes(secureRandom, MMS_BYTE_LENGTH));
|
||||
}
|
||||
|
||||
public static GroupId.V2 v2orThrow(@NonNull byte[] bytes) {
|
||||
try {
|
||||
return v2(bytes);
|
||||
} catch (BadGroupIdException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static GroupId.V2 v2(@NonNull byte[] bytes) throws BadGroupIdException {
|
||||
/**
|
||||
* Private because it's too easy to pass the {@link GroupMasterKey} bytes directly to this as they
|
||||
* are the same length as the {@link GroupIdentifier}.
|
||||
*/
|
||||
private static GroupId.V2 v2(@NonNull byte[] bytes) throws BadGroupIdException {
|
||||
if (bytes.length != V2_BYTE_LENGTH) {
|
||||
throw new BadGroupIdException();
|
||||
}
|
||||
@@ -70,7 +74,11 @@ public abstract class GroupId {
|
||||
}
|
||||
|
||||
public static GroupId.V2 v2(@NonNull GroupIdentifier groupIdentifier) {
|
||||
return v2orThrow(groupIdentifier.serialize());
|
||||
try {
|
||||
return v2(groupIdentifier.serialize());
|
||||
} catch (BadGroupIdException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static GroupId.V2 v2(@NonNull GroupMasterKey masterKey) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
@@ -188,7 +189,11 @@ final class GroupManagerV2 {
|
||||
groupDatabase.onAvatarUpdated(groupId, avatar != null);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
|
||||
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, null, null);
|
||||
DecryptedGroupChange groupChange = DecryptedGroupChange.newBuilder(GroupChangeReconstruct.reconstructGroupChange(DecryptedGroup.newBuilder().build(), decryptedGroup))
|
||||
.setEditor(UuidUtil.toByteString(selfUuid))
|
||||
.build();
|
||||
|
||||
RecipientAndThread recipientAndThread = sendGroupUpdate(masterKey, decryptedGroup, groupChange, null);
|
||||
|
||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient,
|
||||
recipientAndThread.threadId,
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
|
||||
intent.putExtra(EXTRA_RECIPIENT_ID, recipientId);
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, ContactSelectionListFragment.NO_LIMIT);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, ContactSelectionListFragment.NO_LIMIT);
|
||||
|
||||
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(currentGroupsMemberOf));
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -55,8 +54,8 @@ public class CreateGroupActivity extends ContactSelectionActivity {
|
||||
: ContactsCursorLoader.DisplayMode.FLAG_PUSH;
|
||||
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, FeatureFlags.groupsV2create() ? FeatureFlags.gv2GroupCapacity() - 1
|
||||
: ContactSelectionListFragment.NO_LIMIT);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, FeatureFlags.groupsV2create() ? FeatureFlags.gv2GroupCapacity() - 1
|
||||
: ContactSelectionListFragment.NO_LIMIT);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -199,6 +199,16 @@ final class ManageGroupRepository {
|
||||
return totalCapacity;
|
||||
}
|
||||
|
||||
public int getSelectionLimit() {
|
||||
if (totalCapacity == ContactSelectionListFragment.NO_LIMIT) {
|
||||
return totalCapacity;
|
||||
}
|
||||
|
||||
boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
|
||||
|
||||
return totalCapacity - (containsSelf ? 1 : 0);
|
||||
}
|
||||
|
||||
public int getRemainingCapacity() {
|
||||
return totalCapacity - members.size();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ManageGroupViewModel extends ViewModel {
|
||||
@@ -321,7 +320,7 @@ public class ManageGroupViewModel extends ViewModel {
|
||||
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
|
||||
intent.putExtra(AddMembersActivity.GROUP_ID, manageGroupRepository.getGroupId().toString());
|
||||
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
|
||||
intent.putExtra(ContactSelectionListFragment.TOTAL_CAPACITY, capacity.getTotalCapacity() - 1);
|
||||
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMIT, capacity.getSelectionLimit());
|
||||
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, capacity.getMembersWithoutSelf());
|
||||
fragment.startActivityForResult(intent, resultCode);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ final class GroupStateMapper {
|
||||
|
||||
private static final String TAG = Log.tag(GroupStateMapper.class);
|
||||
|
||||
static final int LATEST = Integer.MAX_VALUE;
|
||||
static final int PLACEHOLDER_REVISION = -1;
|
||||
static final int LATEST = Integer.MAX_VALUE;
|
||||
static final int PLACEHOLDER_REVISION = -1;
|
||||
static final int RESTORE_PLACEHOLDER_REVISION = -2;
|
||||
|
||||
private static final Comparator<ServerGroupLogEntry> BY_REVISION = (o1, o2) -> Integer.compare(o1.getRevision(), o2.getRevision());
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||
@@ -63,9 +65,20 @@ public final class GroupsV2StateProcessor {
|
||||
|
||||
private static final String TAG = Log.tag(GroupsV2StateProcessor.class);
|
||||
|
||||
public static final int LATEST = GroupStateMapper.LATEST;
|
||||
public static final int LATEST = GroupStateMapper.LATEST;
|
||||
|
||||
/**
|
||||
* Used to mark a group state as a placeholder when there is partial knowledge (title and avater)
|
||||
* gathered from a group join link.
|
||||
*/
|
||||
public static final int PLACEHOLDER_REVISION = GroupStateMapper.PLACEHOLDER_REVISION;
|
||||
|
||||
/**
|
||||
* Used to mark a group state as a placeholder when you have no knowledge at all of the group
|
||||
* e.g. from a group master key from a storage service restore.
|
||||
*/
|
||||
public static final int RESTORE_PLACEHOLDER_REVISION = GroupStateMapper.RESTORE_PLACEHOLDER_REVISION;
|
||||
|
||||
private final Context context;
|
||||
private final JobManager jobManager;
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
@@ -174,7 +187,8 @@ public final class GroupsV2StateProcessor {
|
||||
|
||||
if (inputGroupState == null) {
|
||||
try {
|
||||
inputGroupState = queryServer(localState, revision == LATEST && localState == null);
|
||||
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
||||
inputGroupState = queryServer(localState, latestRevisionOnly);
|
||||
} catch (GroupNotAMemberException e) {
|
||||
if (localState != null && signedGroupChange != null) {
|
||||
try {
|
||||
@@ -209,7 +223,13 @@ public final class GroupsV2StateProcessor {
|
||||
}
|
||||
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries());
|
||||
determineProfileSharing(inputGroupState, newLocalState);
|
||||
if (localState != null && localState.getRevision() == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
Log.i(TAG, "Inserting single update message for restore placeholder");
|
||||
insertUpdateMessages(timestamp, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||
} else {
|
||||
insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries());
|
||||
}
|
||||
persistLearnedProfileKeys(inputGroupState);
|
||||
|
||||
GlobalGroupState remainingWork = advanceGroupStateResult.getNewGlobalGroupState();
|
||||
@@ -293,30 +313,52 @@ public final class GroupsV2StateProcessor {
|
||||
jobManager.add(new AvatarGroupsV2DownloadJob(groupId, newLocalState.getAvatar()));
|
||||
}
|
||||
|
||||
boolean fullMemberPostUpdate = GroupProtoUtil.isMember(Recipient.self().getUuid().get(), newLocalState.getMembersList());
|
||||
boolean trustedAdder = false;
|
||||
determineProfileSharing(inputGroupState, newLocalState);
|
||||
}
|
||||
|
||||
if (newLocalState.getRevision() == 0) {
|
||||
Optional<DecryptedMember> foundingMember = DecryptedGroupUtil.firstMember(newLocalState.getMembersList());
|
||||
private void determineProfileSharing(@NonNull GlobalGroupState inputGroupState,
|
||||
@NonNull DecryptedGroup newLocalState)
|
||||
{
|
||||
if (inputGroupState.getLocalState() != null) {
|
||||
boolean wasAMemberAlready = DecryptedGroupUtil.findMemberByUuid(inputGroupState.getLocalState().getMembersList(), Recipient.self().getUuid().get()).isPresent();
|
||||
|
||||
if (foundingMember.isPresent()) {
|
||||
UUID foundingMemberUuid = UuidUtil.fromByteString(foundingMember.get().getUuid());
|
||||
Recipient foundingRecipient = Recipient.externalPush(context, foundingMemberUuid, null, false);
|
||||
|
||||
if (foundingRecipient.isSystemContact() || foundingRecipient.isProfileSharing()) {
|
||||
Log.i(TAG, "Group 'adder' is trusted. contact: " + foundingRecipient.isSystemContact() + ", profileSharing: " + foundingRecipient.isProfileSharing());
|
||||
trustedAdder = true;
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Could not find founding member during gv2 create. Not enabling profile sharing.");
|
||||
if (wasAMemberAlready) {
|
||||
Log.i(TAG, "Skipping profile sharing detection as was already a full member before update");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (fullMemberPostUpdate && trustedAdder) {
|
||||
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
|
||||
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
|
||||
Optional<DecryptedMember> selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), Recipient.self().getUuid().get());
|
||||
|
||||
if (selfAsMemberOptional.isPresent()) {
|
||||
DecryptedMember selfAsMember = selfAsMemberOptional.get();
|
||||
int revisionJoinedAt = selfAsMember.getJoinedAtRevision();
|
||||
|
||||
Optional<Recipient> addedByOptional = Stream.of(inputGroupState.getServerHistory())
|
||||
.map(ServerGroupLogEntry::getChange)
|
||||
.filter(c -> c != null && c.getRevision() == revisionJoinedAt)
|
||||
.findFirst()
|
||||
.map(c -> Optional.fromNullable(UuidUtil.fromByteStringOrNull(c.getEditor()))
|
||||
.transform(a -> Recipient.externalPush(context, UuidUtil.fromByteStringOrNull(c.getEditor()), null, false)))
|
||||
.orElse(Optional.absent());
|
||||
|
||||
if (addedByOptional.isPresent()) {
|
||||
Recipient addedBy = addedByOptional.get();
|
||||
|
||||
Log.i(TAG, String.format("Added as a full member of %s by %s", groupId, addedBy.getId()));
|
||||
|
||||
if (addedBy.isSystemContact() || addedBy.isProfileSharing()) {
|
||||
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact() + ", profileSharing: " + addedBy.isProfileSharing());
|
||||
Log.i(TAG, "Added to a group and auto-enabling profile sharing");
|
||||
recipientDatabase.setProfileSharing(Recipient.externalGroup(context, groupId).getId(), true);
|
||||
} else {
|
||||
Log.i(TAG, "Added to a group, but not enabling profile sharing, as 'adder' is not trusted");
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Could not find founding member during gv2 create. Not enabling profile sharing.");
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Added to a group, but not enabling profile sharing. fullMember: " + fullMemberPostUpdate + ", trustedAdded: " + trustedAdder);
|
||||
Log.i(TAG, String.format("Added to %s, but not enabling profile sharing as not a fullMember.", groupId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,19 @@ public abstract class BaseJob extends Job {
|
||||
}
|
||||
|
||||
protected void log(@NonNull String tag, @NonNull String message) {
|
||||
Log.i(tag, JobLogger.format(this, message));
|
||||
log(tag, "", JobLogger.format(this, message));
|
||||
}
|
||||
|
||||
protected void log(@NonNull String tag, @NonNull String extra, @NonNull String message) {
|
||||
Log.i(tag, JobLogger.format(this, extra, message));
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String message) {
|
||||
warn(tag, message, null);
|
||||
warn(tag, "", message, null);
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String event, @NonNull String message) {
|
||||
warn(tag, event, message, null);
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @Nullable Throwable t) {
|
||||
@@ -63,6 +71,10 @@ public abstract class BaseJob extends Job {
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String message, @Nullable Throwable t) {
|
||||
Log.w(tag, JobLogger.format(this, message), t);
|
||||
warn(tag, "", message, t);
|
||||
}
|
||||
|
||||
protected void warn(@NonNull String tag, @NonNull String extra, @NonNull String message, @Nullable Throwable t) {
|
||||
Log.w(tag, JobLogger.format(this, extra, message), t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.UuidMigrationJob;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -100,7 +101,6 @@ public final class JobManagerFactories {
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory());
|
||||
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
|
||||
put(WakeGroupV2Job.KEY, new WakeGroupV2Job.Factory());
|
||||
put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
@@ -139,6 +139,7 @@ public final class JobManagerFactories {
|
||||
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
|
||||
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
|
||||
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
|
||||
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());
|
||||
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
|
||||
|
||||
// Dead jobs
|
||||
@@ -151,6 +152,7 @@ public final class JobManagerFactories {
|
||||
put("Argon2TestJob", new FailingJob.Factory());
|
||||
put("Argon2TestMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("WakeGroupV2Job", new FailingJob.Factory());
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ public final class PushDecryptMessageJob extends BaseJob {
|
||||
}
|
||||
|
||||
private @NonNull List<Job> handleMessage(@NonNull SignalServiceEnvelope envelope) throws NoSenderException {
|
||||
Log.i(TAG, "Processing message ID " + envelope.getTimestamp());
|
||||
try {
|
||||
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
|
||||
SignalServiceAddress localAddress = new SignalServiceAddress(Optional.of(TextSecurePreferences.getLocalUuid(context)), Optional.of(TextSecurePreferences.getLocalNumber(context)));
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
@@ -162,7 +163,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
ApplicationDependencies.getJobManager().cancelAllInQueue(TypingSendJob.getQueue(threadId));
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
log(TAG, String.valueOf(message.getSentTimeMillis()), "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +174,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
log(TAG, String.valueOf(message.getSentTimeMillis()), "Sending message: " + messageId);
|
||||
|
||||
if (!groupRecipient.resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
|
||||
RecipientUtil.shareProfileIfFirstSecureMessage(context, groupRecipient);
|
||||
@@ -253,7 +254,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
RetrieveProfileJob.enqueue(mismatchRecipientIds);
|
||||
}
|
||||
} catch (UntrustedIdentityException | UndeliverableMessageException e) {
|
||||
warn(TAG, e);
|
||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);
|
||||
database.markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
@@ -299,7 +300,8 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
boolean isRecipientUpdate = destinations.size() != DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId).size();
|
||||
boolean isRecipientUpdate = Stream.of(DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
|
||||
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations)
|
||||
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user