diff --git a/app/build.gradle b/app/build.gradle index a9fa89133..0dc518ac3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -80,8 +80,8 @@ protobuf { } } -def canonicalVersionCode = 702 -def canonicalVersionName = "4.70.5" +def canonicalVersionCode = 704 +def canonicalVersionName = "4.71.1" def postFixSize = 10 def abiPostFix = ['universal' : 0, @@ -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' diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java index 786c61628..c660f644a 100644 --- a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java +++ b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java @@ -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 { + private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class); + public FlipperSqlCipherAdapter(Context context) { super(context); } @Override public List 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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a069a466..a93978503 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -640,6 +640,8 @@ + + { + 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); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java b/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java new file mode 100644 index 000000000..37f60d90c --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/ACTLChunk.java @@ -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(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java b/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java new file mode 100644 index 000000000..cf1040320 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java @@ -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 { + + 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 chunks = APNGParser.parse(reader); + List 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); + } + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java b/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java new file mode 100644 index 000000000..fd1ca2706 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java @@ -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 { + public final byte blend_op; + public final byte dispose_op; + byte[] ihdrData; + List imageChunks = new ArrayList<>(); + List 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 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; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java new file mode 100644 index 000000000..79dff419d --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java @@ -0,0 +1,145 @@ +/* + * 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) { + if (!(e instanceof FormatException)) { + e.printStackTrace(); + } + } + return false; + } + + public static List parse(APNGReader reader) throws IOException { + if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) { + throw new FormatException(); + } + + List 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; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/Chunk.java b/app/src/main/java/org/signal/glide/apng/decode/Chunk.java new file mode 100644 index 000000000..192cc0fd6 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/Chunk.java @@ -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 { + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java b/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java new file mode 100644 index 000000000..9e74d806f --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java @@ -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. + *

+ * 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 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(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java b/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java new file mode 100644 index 000000000..1618c59df --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/FDATChunk.java @@ -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(); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java new file mode 100644 index 000000000..bd7a60fee --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IDATChunk.java @@ -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"); +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java new file mode 100644 index 000000000..f0cbd8008 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IENDChunk.java @@ -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"); +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java b/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java new file mode 100644 index 000000000..eebd9d278 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/IHDRChunk.java @@ -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: + *

+ * 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); + } +} diff --git a/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java b/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java new file mode 100644 index 000000000..65715f1e4 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/decode/StillFrame.java @@ -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 { + + 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; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/io/APNGReader.java b/app/src/main/java/org/signal/glide/apng/io/APNGReader.java new file mode 100644 index 000000000..293ed2461 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/io/APNGReader.java @@ -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 __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; + } +} diff --git a/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java b/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java new file mode 100644 index 000000000..25a7c2764 --- /dev/null +++ b/app/src/main/java/org/signal/glide/apng/io/APNGWriter.java @@ -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); + } +} diff --git a/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java b/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java new file mode 100644 index 000000000..57ba5a68b --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/FrameAnimationDrawable.java @@ -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 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 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(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/decode/Frame.java b/app/src/main/java/org/signal/glide/common/decode/Frame.java new file mode 100644 index 000000000..e7fd5e963 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/decode/Frame.java @@ -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 { + 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); +} diff --git a/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java b/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java new file mode 100644 index 000000000..2834ee618 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/decode/FrameSeqDecoder.java @@ -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 { + private static final String TAG = FrameSeqDecoder.class.getSimpleName(); + private final int taskId; + + private final Loader mLoader; + private final Handler workerHandler; + protected List frames = new ArrayList<>(); + protected int frameIndex = -1; + private int playCount; + private Integer loopLimit = null; + private Set 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 cacheBitmaps = new HashSet<>(); + protected Map 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 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; + } +} diff --git a/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java b/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java new file mode 100644 index 000000000..6a7aae089 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/executor/FrameDecoderExecutor.java @@ -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 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(); + } +} + diff --git a/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java b/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java new file mode 100644 index 000000000..7ed9cfa10 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/ByteBufferReader.java @@ -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()); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java b/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java new file mode 100644 index 000000000..f60688fb8 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/ByteBufferWriter.java @@ -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(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/FileReader.java b/app/src/main/java/org/signal/glide/common/io/FileReader.java new file mode 100644 index 000000000..1f21184d4 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/FileReader.java @@ -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)); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/FilterReader.java b/app/src/main/java/org/signal/glide/common/io/FilterReader.java new file mode 100644 index 000000000..08abbc912 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/FilterReader.java @@ -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(); + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/Reader.java b/app/src/main/java/org/signal/glide/common/io/Reader.java new file mode 100644 index 000000000..6be530b2b --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/Reader.java @@ -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; +} diff --git a/app/src/main/java/org/signal/glide/common/io/StreamReader.java b/app/src/main/java/org/signal/glide/common/io/StreamReader.java new file mode 100644 index 000000000..7eb08f51f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/StreamReader.java @@ -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; + } +} diff --git a/app/src/main/java/org/signal/glide/common/io/Writer.java b/app/src/main/java/org/signal/glide/common/io/Writer.java new file mode 100644 index 000000000..84600a08f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/io/Writer.java @@ -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; +} diff --git a/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java new file mode 100644 index 000000000..d62ac7200 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/AssetStreamLoader.java @@ -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); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java b/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java new file mode 100644 index 000000000..05bf8d36e --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/ByteBufferLoader.java @@ -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()); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/FileLoader.java b/app/src/main/java/org/signal/glide/common/loader/FileLoader.java new file mode 100644 index 000000000..e861aa7ed --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/FileLoader.java @@ -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); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/Loader.java b/app/src/main/java/org/signal/glide/common/loader/Loader.java new file mode 100644 index 000000000..9a38babb2 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/Loader.java @@ -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; +} diff --git a/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java new file mode 100644 index 000000000..5d6db5a84 --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/ResourceStreamLoader.java @@ -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); + } +} diff --git a/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java b/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java new file mode 100644 index 000000000..0103ca80f --- /dev/null +++ b/app/src/main/java/org/signal/glide/common/loader/StreamLoader.java @@ -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()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 176b6472d..b6b7ffa05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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; @@ -127,6 +129,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi initializePendingMessages(); initializeBlobProvider(); initializeCleanup(); + initializeGlideCodecs(); FeatureFlags.init(); NotificationChannels.create(this); @@ -378,6 +381,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))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index e5c5297d6..ca252ff42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java index 6ab7bf04e..170dac472 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java @@ -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()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index dea984158..24dac2be5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 578eb6d5a..34f416a81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -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 output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsAdapter.java new file mode 100644 index 000000000..a9769f39f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsAdapter.java @@ -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)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java new file mode 100644 index 000000000..02a7fd9f5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java @@ -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(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java new file mode 100644 index 000000000..70c227edc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java @@ -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 { + 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 { + private SingleSelectSetting.Item singleSelectItem; + private Object customValue; + private String summaryText; + + public 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); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java new file mode 100644 index 000000000..7300bcd8b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java @@ -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 { + + 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 { + private final String text; + private final Object item; + private final boolean isSelected; + + public 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; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 76cb4a365..b33b619ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -300,8 +300,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 +1360,7 @@ public class ConversationActivity extends PassphraseRequiredActivity private void handleRecentSafetyNumberChange() { List records = identityRecords.getUnverifiedRecords(); records.addAll(identityRecords.getUntrustedRecords()); - SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG); + SafetyNumberChangeDialog.show(getSupportFragmentManager(), records); } @Override @@ -1383,6 +1381,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 +1425,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); } @@ -2875,7 +2876,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 +2884,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 +2897,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 +3096,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())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 8525f44df..1723288e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -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()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java index c3c95a28a..a323827ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeDialog.java @@ -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 identityRecords) { + public static void show(@NonNull FragmentManager fragmentManager, @NonNull List identityRecords) { List 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 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(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepository.java index 016537925..124f4dfaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerRepository.java @@ -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 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 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 members; + @Nullable private final String query; + @NonNull private final List members; - MentionQuery(@Nullable String query, @NonNull List members) { + MentionQuery(@Nullable String query, @NonNull List members) { this.query = query; this.members = members; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java index a2a0cde48..34d60392f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/mentions/MentionsPickerViewModel.java @@ -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 selectedRecipient; private final LiveData>> mentionList; - private final MutableLiveData group; + private final MutableLiveData liveRecipient; private final MutableLiveData liveQuery; private final MutableLiveData 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 = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData); + LiveData> fullMembers = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(recipient, mentionsPickerRepository::getMembers)); + LiveData query = Transformations.distinctUntilChanged(liveQuery); + LiveData mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, (q, m) -> new MentionQuery(q.query, m)); - LiveData> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers)); - LiveData query = Transformations.distinctUntilChanged(liveQuery); - LiveData mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, (q, m) -> new MentionQuery(q.query, m)); - - mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).>map(MentionViewState::new).toList()); + this.mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).>map(MentionViewState::new).toList()); } @NonNull LiveData>> 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()); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java index 849ec6e25..ed4395274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java @@ -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 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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index e51a4bb8e..f60e91285 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -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 filesOnDisk = new HashSet<>(); + Set 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 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 975bfa714..3ad8510e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -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 = 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 = 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))); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 959a71fbc..4fb767f78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index 0316809ac..3bc355628 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -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 + ", " diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index d6674d7c2..d6dfceb63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -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 threadIds); abstract void deleteAllThreads(); + abstract void deleteAbandonedMessages(); public abstract SQLiteDatabase beginTransaction(); public abstract void endTransaction(SQLiteDatabase database); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 3af51c901..6274398d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 573336a22..03ef88f96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -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 + ", " + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 99a745af6..fe8893cc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -1736,7 +1736,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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 07c82ff5e..5fcc14e44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java index 871f25b9e..77588d49d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/StickerDatabase.java @@ -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,7 @@ 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.Util; import java.io.Closeable; @@ -30,29 +31,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 +65,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 +78,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 +97,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()); @@ -187,6 +191,19 @@ public class StickerDatabase extends Database { return cursor; } + public @NonNull Set getAllStickerFiles() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + Set 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 +477,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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java index 7611e76be..37a0983cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadBodyUtil.java @@ -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(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 2c43fe55e..61bfba6a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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,92 @@ 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; + } + + long trimDate = trimBeforeDate; + + 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); + trimDate = Math.max(trimDate, cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED))); + } + } + } + + if (trimDate != 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 +1123,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 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 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 +1153,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 +1195,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 +1316,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 +1325,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 +1334,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 +1343,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 +1378,10 @@ public class ThreadDatabase extends Database { return isSticker; } + public @Nullable String getStickerEmoji() { + return stickerEmoji; + } + public boolean isAlbum() { return isAlbum; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 0407b4fdc..284c8bbd2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index 614c23fd2..3f0de33ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -53,23 +53,24 @@ final class GroupsV2UpdateMessageProducer { /** * Describes a group that is new to you, use this when there is no available change record. *

- * 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. + *

+ * When invited, it's possible there's no change available. + *

+ * 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 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 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 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))); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/IncomingSticker.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/IncomingSticker.java index 4614bf57c..1f69a78db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/IncomingSticker.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/IncomingSticker.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 30a510f61..19906c3fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -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 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; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java index 8db2525d3..2586ea620 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/StickerRecord.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 33ae68d03..1f76dbd21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index f5436f7a1..74349fe1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java new file mode 100644 index 000000000..90ad63825 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java @@ -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 { + + @Override + public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) { + return APNGParser.isAPNG(new ByteBufferReader(source)); + } + + @Override + public @Nullable Resource 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 { + private final APNGDecoder decoder; + private final int size; + + FrameSeqDecoderResource(@NonNull APNGDecoder decoder, int size) { + this.decoder = decoder; + this.size = size; + } + + @Override + public @NonNull Class 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(); + } + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java new file mode 100644 index 000000000..f12af6a81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java @@ -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 { + + @Override + public @Nullable Resource transcode(@NonNull Resource toTranscode, @NonNull Options options) { + APNGDecoder decoder = toTranscode.get(); + APNGDrawable drawable = new APNGDrawable(decoder); + + drawable.setAutoPlay(false); + drawable.setLoopLimit(0); + + return new DrawableResource(drawable) { + @Override + public @NonNull Class getResourceClass() { + return Drawable.class; + } + + @Override + public int getSize() { + return 0; + } + + @Override + public void recycle() { + } + + @Override + public void initialize() { + super.initialize(); + } + }; + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java new file mode 100644 index 000000000..0cb7cd021 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java @@ -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 { + + private final ResourceDecoder byteBufferDecoder; + + public ApngStreamCacheDecoder(ResourceDecoder byteBufferDecoder) { + this.byteBufferDecoder = byteBufferDecoder; + } + + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) { + return APNGParser.isAPNG(new StreamReader(source)); + } + + @Override + public @Nullable Resource 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); + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java new file mode 100644 index 000000000..852cc8912 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java @@ -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 { + + 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 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; + } +} + diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java deleted file mode 100644 index 6f99927a1..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapCacheDecoder.java +++ /dev/null @@ -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 { - - 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 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); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java index 7541e73d9..38864e70a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedBitmapResourceEncoder.java @@ -33,8 +33,6 @@ public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements Re @SuppressWarnings("EmptyCatchBlock") @Override public boolean encode(@NonNull Resource 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java new file mode 100644 index 000000000..92315a720 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java @@ -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 extends EncryptedCoder implements ResourceDecoder { + + private static final String TAG = Log.tag(EncryptedCacheDecoder.class); + + private final byte[] secret; + private final ResourceDecoder decoder; + + public EncryptedCacheDecoder(byte[] secret, ResourceDecoder 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 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); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java index 1d546ccf5..1744efcae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheEncoder.java @@ -30,8 +30,6 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder { - - 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 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); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index d2a5a12fc..965a9f00d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 11690c563..3af96db4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -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; @@ -209,6 +211,7 @@ public final class GroupsV2StateProcessor { } updateLocalDatabaseGroupState(inputGroupState, newLocalState); + determineProfileSharing(inputGroupState, newLocalState); insertUpdateMessages(timestamp, advanceGroupStateResult.getProcessedLogEntries()); persistLearnedProfileKeys(inputGroupState); @@ -293,30 +296,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 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 selfAsMemberOptional = DecryptedGroupUtil.findMemberByUuid(newLocalState.getMembersList(), Recipient.self().getUuid().get()); + + if (selfAsMemberOptional.isPresent()) { + DecryptedMember selfAsMember = selfAsMemberOptional.get(); + int revisionJoinedAt = selfAsMember.getJoinedAtRevision(); + + Optional 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)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java index b84e92e97..f22115973 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BaseJob.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index e070e7ccc..668b3578b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -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; @@ -139,6 +140,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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index 92f798c11..2ebb3e4bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -151,6 +151,7 @@ public final class PushDecryptMessageJob extends BaseJob { } private @NonNull List 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))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index fd5ed5644..7eee96ab7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -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 addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List 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> unidentifiedAccess = Stream.of(destinations) .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 10c372af0..d1a199075 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -113,12 +113,12 @@ public class PushMediaSendJob extends PushSendJob { OutgoingMediaMessage message = database.getOutgoingMessage(messageId); if (database.isSent(messageId)) { - warn(TAG, "Message " + messageId + " was already sent. Ignoring."); + warn(TAG, String.valueOf(message.getSentTimeMillis()), "Message " + messageId + " was already sent. Ignoring."); return; } try { - log(TAG, "Sending message: " + messageId); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Sending message: " + messageId); RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient()); @@ -139,13 +139,13 @@ public class PushMediaSendJob extends PushSendJob { } if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { - log(TAG, "Marking recipient as UD-unrestricted following a UD send."); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-unrestricted following a UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); } else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { - log(TAG, "Marking recipient as UD-enabled following a UD send."); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-enabled following a UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.ENABLED); } else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { - log(TAG, "Marking recipient as UD-disabled following a non-UD send."); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Marking recipient as UD-disabled following a non-UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED); } @@ -158,7 +158,7 @@ public class PushMediaSendJob extends PushSendJob { DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(messageId); } - log(TAG, "Sent message: " + messageId); + log(TAG, String.valueOf(message.getSentTimeMillis()), "Sent message: " + messageId); } catch (InsecureFallbackApprovalException ifae) { warn(TAG, "Failure", ifae); @@ -231,13 +231,13 @@ public class PushMediaSendJob extends PushSendJob { return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), mediaMessage).getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { - warn(TAG, e); + warn(TAG, String.valueOf(message.getSentTimeMillis()), e); throw new InsecureFallbackApprovalException(e); } catch (FileNotFoundException e) { - warn(TAG, e); + warn(TAG, String.valueOf(message.getSentTimeMillis()), e); throw new UndeliverableMessageException(e); } catch (IOException e) { - warn(TAG, e); + warn(TAG, String.valueOf(message.getSentTimeMillis()), e); throw new RetryLaterException(e); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 6e7003889..c093e039b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -338,6 +338,8 @@ public final class PushProcessMessageJob extends BaseJob { return; } + Log.i(TAG, "Processing message ID " + content.getTimestamp()); + if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent(); @@ -1658,14 +1660,15 @@ public final class PushProcessMessageJob extends BaseJob { String packId = Hex.toStringCondensed(sticker.get().getPackId()); String packKey = Hex.toStringCondensed(sticker.get().getPackKey()); int stickerId = sticker.get().getStickerId(); - StickerLocator stickerLocator = new StickerLocator(packId, packKey, stickerId); + String emoji = sticker.get().getEmoji(); + StickerLocator stickerLocator = new StickerLocator(packId, packKey, stickerId, emoji); StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context); StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false); if (stickerRecord != null) { return Optional.of(new UriAttachment(stickerRecord.getUri(), stickerRecord.getUri(), - MediaUtil.IMAGE_WEBP, + stickerRecord.getContentType(), AttachmentDatabase.TRANSFER_PROGRESS_DONE, stickerRecord.getSize(), StickerSlide.WIDTH, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 9185a1d5e..8dd0cc68a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -21,11 +21,14 @@ import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.PartProgressEvent; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.CertificateType; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; @@ -45,8 +48,8 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; @@ -57,10 +60,12 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -283,9 +288,11 @@ public abstract class PushSendJob extends SendJob { byte[] packId = Hex.fromStringCondensed(stickerAttachment.getSticker().getPackId()); byte[] packKey = Hex.fromStringCondensed(stickerAttachment.getSticker().getPackKey()); int stickerId = stickerAttachment.getSticker().getStickerId(); + StickerRecord record = DatabaseFactory.getStickerDatabase(context).getSticker(stickerAttachment.getSticker().getPackId(), stickerId, false); + String emoji = record != null ? record.getEmoji() : null; SignalServiceAttachment attachment = getAttachmentPointerFor(stickerAttachment); - return Optional.of(new SignalServiceDataMessage.Sticker(packId, packKey, stickerId, attachment)); + return Optional.of(new SignalServiceDataMessage.Sticker(packId, packKey, stickerId, emoji, attachment)); } catch (IOException e) { Log.w(TAG, "Failed to decode sticker id/key", e); return Optional.absent(); @@ -327,23 +334,34 @@ public abstract class PushSendJob extends SendJob { protected void rotateSenderCertificateIfNecessary() throws IOException { try { - byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context); + Collection requiredCertificateTypes = SignalStore.phoneNumberPrivacy() + .getRequiredCertificateTypes(); - if (certificateBytes == null) { - throw new InvalidCertificateException("No certificate was present."); + Log.i(TAG, "Ensuring we have these certificates " + requiredCertificateTypes); + + for (CertificateType certificateType : requiredCertificateTypes) { + + byte[] certificateBytes = SignalStore.certificateValues() + .getUnidentifiedAccessCertificate(certificateType); + + if (certificateBytes == null) { + throw new InvalidCertificateException(String.format("No certificate %s was present.", certificateType)); + } + + SenderCertificate certificate = new SenderCertificate(certificateBytes); + + if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) { + throw new InvalidCertificateException(String.format(Locale.US, "Certificate %s is expired, or close to it. Expires on: %d, currently: %d", certificateType, certificate.getExpiration(), System.currentTimeMillis())); + } + Log.d(TAG, String.format("Certificate %s is valid", certificateType)); } - SenderCertificate certificate = new SenderCertificate(certificateBytes); - - if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) { - throw new InvalidCertificateException("Certificate is expired, or close to it. Expires on: " + certificate.getExpiration() + ", currently: " + System.currentTimeMillis()); - } - - Log.d(TAG, "Certificate is valid."); + Log.d(TAG, "All certificates are valid."); } catch (InvalidCertificateException e) { - Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e); - RotateCertificateJob certificateJob = new RotateCertificateJob(context); - certificateJob.onRun(); + Log.w(TAG, "A certificate was invalid at send time. Fetching new ones.", e); + if (!ApplicationDependencies.getJobManager().runSynchronously(new RotateCertificateJob(), 5000).isPresent()) { + throw new IOException("Timeout rotating certificate"); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 18cbbea57..fb2104552 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -76,12 +76,12 @@ public class PushTextSendJob extends PushSendJob { SmsMessageRecord record = database.getSmsMessage(messageId); if (!record.isPending() && !record.isFailed()) { - warn(TAG, "Message " + messageId + " was already sent. Ignoring."); + warn(TAG, String.valueOf(record.getDateSent()), "Message " + messageId + " was already sent. Ignoring."); return; } try { - log(TAG, "Sending message: " + messageId); + log(TAG, String.valueOf(record.getDateSent()), "Sending message: " + messageId); RecipientUtil.shareProfileIfFirstSecureMessage(context, record.getRecipient()); @@ -101,13 +101,13 @@ public class PushTextSendJob extends PushSendJob { } if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN && profileKey == null) { - log(TAG, "Marking recipient as UD-unrestricted following a UD send."); + log(TAG, String.valueOf(record.getDateSent()), "Marking recipient as UD-unrestricted following a UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); } else if (unidentified && accessMode == UnidentifiedAccessMode.UNKNOWN) { - log(TAG, "Marking recipient as UD-enabled following a UD send."); + log(TAG, String.valueOf(record.getDateSent()), "Marking recipient as UD-enabled following a UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.ENABLED); } else if (!unidentified && accessMode != UnidentifiedAccessMode.DISABLED) { - log(TAG, "Marking recipient as UD-disabled following a non-UD send."); + log(TAG, String.valueOf(record.getDateSent()), "Marking recipient as UD-disabled following a non-UD send."); DatabaseFactory.getRecipientDatabase(context).setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED); } @@ -116,15 +116,15 @@ public class PushTextSendJob extends PushSendJob { expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn()); } - log(TAG, "Sent message: " + messageId); + log(TAG, String.valueOf(record.getDateSent()), "Sent message: " + messageId); } catch (InsecureFallbackApprovalException e) { - warn(TAG, "Failure", e); + warn(TAG, String.valueOf(record.getDateSent()), "Failure", e); database.markAsPendingInsecureSmsFallback(record.getId()); ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException e) { - warn(TAG, "Failure", e); + warn(TAG, String.valueOf(record.getDateSent()), "Failure", e); RecipientId recipientId = Recipient.external(context, e.getIdentifier()).getId(); database.addMismatchedIdentity(record.getId(), recipientId, e.getIdentityKey()); database.markAsSentFailed(record.getId()); @@ -164,7 +164,7 @@ public class PushTextSendJob extends PushSendJob { Optional profileKey = getProfileKey(messageRecipient); Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, messageRecipient); - log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent()); + log(TAG, String.valueOf(message.getDateSent()), "Have access key to use: " + unidentifiedAccess.isPresent()); SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(message.getDateSent()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 98801a20f..553f40e48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -71,18 +71,22 @@ public class RefreshAttributesJob extends BaseJob { registrationLockV1 = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context); } + boolean phoneNumberDiscoverable = SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable(); + SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut()); Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() + + "\n Phone number discoverable : " + phoneNumberDiscoverable + "\n Capabilities:" + "\n Storage? " + capabilities.isStorage() + "\n GV2? " + capabilities.isGv2() + - "\n UUID? " + capabilities.isUuid()) ; + "\n UUID? " + capabilities.isUuid()); SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages, registrationLockV1, registrationLockV2, unidentifiedAccessKey, universalUnidentifiedAccess, - capabilities); + capabilities, + phoneNumberDiscoverable); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java index dbd7f177c..33ca75786 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateCertificateJob.java @@ -1,35 +1,37 @@ package org.thoughtcrime.securesms.jobs; - -import android.content.Context; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.CertificateType; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; -public class RotateCertificateJob extends BaseJob { +public final class RotateCertificateJob extends BaseJob { public static final String KEY = "RotateCertificateJob"; - private static final String TAG = RotateCertificateJob.class.getSimpleName(); + private static final String TAG = Log.tag(RotateCertificateJob.class); - public RotateCertificateJob(Context context) { + public RotateCertificateJob() { this(new Job.Parameters.Builder() .setQueue("__ROTATE_SENDER_CERTIFICATE__") .addConstraint(NetworkConstraint.KEY) .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build()); - setContext(context); } private RotateCertificateJob(@NonNull Job.Parameters parameters) { @@ -57,10 +59,25 @@ public class RotateCertificateJob extends BaseJob { } synchronized (RotateCertificateJob.class) { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - byte[] certificate = accountManager.getSenderCertificate(); + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + Collection certificateTypes = SignalStore.phoneNumberPrivacy() + .getAllCertificateTypes(); - TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate); + Log.i(TAG, "Rotating these certificates " + certificateTypes); + + for (CertificateType certificateType: certificateTypes) { + byte[] certificate; + + switch (certificateType) { + case UUID_AND_E164: certificate = accountManager.getSenderCertificate(); break; + case UUID_ONLY : certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); break; + default : throw new AssertionError(); + } + + Log.i(TAG, String.format("Successfully got %s certificate", certificateType)); + SignalStore.certificateValues() + .setUnidentifiedAccessCertificate(certificateType, certificate); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java index 353bb216d..1aa932063 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java @@ -88,9 +88,9 @@ public class SmsSendJob extends SendJob { } try { - log(TAG, "Sending message: " + messageId + " (attempt " + runAttempt + ")"); + log(TAG, String.valueOf(record.getDateSent()), "Sending message: " + messageId + " (attempt " + runAttempt + ")"); deliver(record); - log(TAG, "Sent message: " + messageId); + log(TAG, String.valueOf(record.getDateSent()), "Sent message: " + messageId); } catch (UndeliverableMessageException ude) { warn(TAG, ude); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId()); @@ -149,8 +149,8 @@ public class SmsSendJob extends SendJob { getSmsManagerFor(message.getSubscriptionId()).sendMultipartTextMessage(recipient, null, messages, sentIntents, deliveredIntents); } catch (NullPointerException | IllegalArgumentException npe) { warn(TAG, npe); - log(TAG, "Recipient: " + recipient); - log(TAG, "Message Parts: " + messages.size()); + log(TAG, String.valueOf(message.getDateSent()), "Recipient: " + recipient); + log(TAG, String.valueOf(message.getDateSent()), "Message Parts: " + messages.size()); try { for (int i=0;i REGULAR_CERTIFICATE = Collections.singletonList(CertificateType.UUID_AND_E164); + private static final Collection PRIVACY_CERTIFICATE = Collections.singletonList(CertificateType.UUID_ONLY); + private static final Collection BOTH_CERTIFICATES = Collections.unmodifiableCollection(Arrays.asList(CertificateType.UUID_AND_E164, CertificateType.UUID_ONLY)); + + PhoneNumberPrivacyValues(@NonNull KeyValueStore store) { + super(store); + } + + @Override + void onFirstEverAppLaunch() { + // TODO [ALAN] PhoneNumberPrivacy: During registration, set the attribute to so that new registrations start out as not listed + //getStore().beginWrite() + // .putInteger(LISTING_MODE, PhoneNumberListingMode.UNLISTED.ordinal()) + // .apply(); + } + + public @NonNull PhoneNumberSharingMode getPhoneNumberSharingMode() { + if (!FeatureFlags.phoneNumberPrivacy()) return PhoneNumberSharingMode.EVERYONE; + return PhoneNumberSharingMode.values()[getInteger(SHARING_MODE, PhoneNumberSharingMode.EVERYONE.ordinal())]; + } + + public void setPhoneNumberSharingMode(@NonNull PhoneNumberSharingMode phoneNumberSharingMode) { + putInteger(SHARING_MODE, phoneNumberSharingMode.ordinal()); + } + + public @NonNull PhoneNumberListingMode getPhoneNumberListingMode() { + if (!FeatureFlags.phoneNumberPrivacy()) return PhoneNumberListingMode.LISTED; + return PhoneNumberListingMode.values()[getInteger(LISTING_MODE, PhoneNumberListingMode.LISTED.ordinal())]; + } + + public void setPhoneNumberListingMode(@NonNull PhoneNumberListingMode phoneNumberListingMode) { + putInteger(LISTING_MODE, phoneNumberListingMode.ordinal()); + } + + /** + * If you respect {@link #getPhoneNumberSharingMode}, then you will only ever need to fetch and store + * these certificates types. + */ + public Collection getRequiredCertificateTypes() { + switch (getPhoneNumberSharingMode()) { + case EVERYONE: return REGULAR_CERTIFICATE; + case CONTACTS: return BOTH_CERTIFICATES; + case NOBODY : return PRIVACY_CERTIFICATE; + default : throw new AssertionError(); + } + } + + /** + * All certificate types required according to the feature flags. + */ + public Collection getAllCertificateTypes() { + return FeatureFlags.phoneNumberPrivacy() ? BOTH_CERTIFICATES : REGULAR_CERTIFICATE; + } + + /** + * Serialized, do not change ordinal/order + */ + public enum PhoneNumberSharingMode { + EVERYONE, + CONTACTS, + NOBODY + } + + /** + * Serialized, do not change ordinal/order + */ + public enum PhoneNumberListingMode { + LISTED, + UNLISTED; + + public boolean isDiscoverable() { + return this == LISTED; + } + + public boolean isUnlisted() { + return this == UNLISTED; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index f7cd61041..ed14f118d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -4,7 +4,11 @@ import androidx.annotation.NonNull; public final class SettingsValues extends SignalStoreValues { - public static final String LINK_PREVIEWS = "settings.link_previews"; + public static final String LINK_PREVIEWS = "settings.link_previews"; + public static final String KEEP_MESSAGES_DURATION = "settings.keep_messages_duration"; + + public static final String THREAD_TRIM_LENGTH = "pref_trim_length"; + public static final String THREAD_TRIM_ENABLED = "pref_trim_threads"; SettingsValues(@NonNull KeyValueStore store) { super(store); @@ -24,4 +28,29 @@ public final class SettingsValues extends SignalStoreValues { public void setLinkPreviewsEnabled(boolean enabled) { putBoolean(LINK_PREVIEWS, enabled); } + + public @NonNull KeepMessagesDuration getKeepMessagesDuration() { + return KeepMessagesDuration.fromId(getInteger(KEEP_MESSAGES_DURATION, 0)); + } + + public void setKeepMessagesForDuration(@NonNull KeepMessagesDuration duration) { + putInteger(KEEP_MESSAGES_DURATION, duration.getId()); + } + + public boolean isTrimByLengthEnabled() { + return getBoolean(THREAD_TRIM_ENABLED, false); + } + + public void setThreadTrimByLengthEnabled(boolean enabled) { + putBoolean(THREAD_TRIM_ENABLED, enabled); + } + + public int getThreadTrimLength() { + return getInteger(THREAD_TRIM_LENGTH, 500); + } + + public void setThreadTrimLength(int length) { + putInteger(THREAD_TRIM_LENGTH, length); + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index a948b78a3..81887e2aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -13,32 +13,36 @@ public final class SignalStore { private static final SignalStore INSTANCE = new SignalStore(); - private final KeyValueStore store; - private final KbsValues kbsValues; - private final RegistrationValues registrationValues; - private final PinValues pinValues; - private final RemoteConfigValues remoteConfigValues; - private final StorageServiceValues storageServiceValues; - private final UiHints uiHints; - private final TooltipValues tooltipValues; - private final MiscellaneousValues misc; - private final InternalValues internalValues; - private final EmojiValues emojiValues; - private final SettingsValues settingsValues; + private final KeyValueStore store; + private final KbsValues kbsValues; + private final RegistrationValues registrationValues; + private final PinValues pinValues; + private final RemoteConfigValues remoteConfigValues; + private final StorageServiceValues storageServiceValues; + private final UiHints uiHints; + private final TooltipValues tooltipValues; + private final MiscellaneousValues misc; + private final InternalValues internalValues; + private final EmojiValues emojiValues; + private final SettingsValues settingsValues; + private final CertificateValues certificateValues; + private final PhoneNumberPrivacyValues phoneNumberPrivacyValues; private SignalStore() { - this.store = ApplicationDependencies.getKeyValueStore(); - this.kbsValues = new KbsValues(store); - this.registrationValues = new RegistrationValues(store); - this.pinValues = new PinValues(store); - this.remoteConfigValues = new RemoteConfigValues(store); - this.storageServiceValues = new StorageServiceValues(store); - this.uiHints = new UiHints(store); - this.tooltipValues = new TooltipValues(store); - this.misc = new MiscellaneousValues(store); - this.internalValues = new InternalValues(store); - this.emojiValues = new EmojiValues(store); - this.settingsValues = new SettingsValues(store); + this.store = ApplicationDependencies.getKeyValueStore(); + this.kbsValues = new KbsValues(store); + this.registrationValues = new RegistrationValues(store); + this.pinValues = new PinValues(store); + this.remoteConfigValues = new RemoteConfigValues(store); + this.storageServiceValues = new StorageServiceValues(store); + this.uiHints = new UiHints(store); + this.tooltipValues = new TooltipValues(store); + this.misc = new MiscellaneousValues(store); + this.internalValues = new InternalValues(store); + this.emojiValues = new EmojiValues(store); + this.settingsValues = new SettingsValues(store); + this.certificateValues = new CertificateValues(store); + this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store); } public static void onFirstEverAppLaunch() { @@ -52,6 +56,8 @@ public final class SignalStore { misc().onFirstEverAppLaunch(); internalValues().onFirstEverAppLaunch(); settings().onFirstEverAppLaunch(); + certificateValues().onFirstEverAppLaunch(); + phoneNumberPrivacy().onFirstEverAppLaunch(); } public static @NonNull KbsValues kbsValues() { @@ -98,6 +104,14 @@ public final class SignalStore { return INSTANCE.settingsValues; } + public static @NonNull CertificateValues certificateValues() { + return INSTANCE.certificateValues; + } + + public static @NonNull PhoneNumberPrivacyValues phoneNumberPrivacy() { + return INSTANCE.phoneNumberPrivacyValues; + } + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() { return new GroupsV2AuthorizationSignalStoreCache(getStore()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java index c9268994e..731f00487 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreview.java @@ -75,6 +75,9 @@ public class LinkPreview { } public @NonNull String getDescription() { + if (description.equals(title)) { + return ""; + } return description; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index 8f5dfa048..9ee8995bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -212,12 +212,9 @@ final class MessageRequestRepository { void unblockAndAccept(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestUnblocked) { executor.execute(() -> { - Recipient recipient = liveRecipient.resolve(); - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + Recipient recipient = liveRecipient.resolve(); RecipientUtil.unblock(context, recipient); - recipientDatabase.setProfileSharing(liveRecipient.getId(), true); - liveRecipient.refresh(); List messageIds = DatabaseFactory.getThreadDatabase(context) .setEntireThreadRead(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 685881fc2..47df39184 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -39,7 +39,7 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 17; + public static final int CURRENT_VERSION = 18; private static final class Version { static final int LEGACY = 1; @@ -59,6 +59,7 @@ public class ApplicationMigrations { static final int PIN_REMINDER = 15; static final int VERSIONED_PROFILE = 16; static final int PIN_OPT_OUT = 17; + static final int TRIM_SETTINGS = 18; } /** @@ -241,6 +242,10 @@ public class ApplicationMigrations { jobs.put(Version.PIN_OPT_OUT, new PinOptOutMigration()); } + if (lastSeenVersion < Version.TRIM_SETTINGS) { + jobs.put(Version.TRIM_SETTINGS, new TrimByLengthSettingsMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java new file mode 100644 index 000000000..1153ce87a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.migrations; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +import static org.thoughtcrime.securesms.keyvalue.SettingsValues.THREAD_TRIM_ENABLED; +import static org.thoughtcrime.securesms.keyvalue.SettingsValues.THREAD_TRIM_LENGTH; + +public class TrimByLengthSettingsMigrationJob extends MigrationJob { + + private static final String TAG = Log.tag(TrimByLengthSettingsMigrationJob.class); + + public static final String KEY = "TrimByLengthSettingsMigrationJob"; + + TrimByLengthSettingsMigrationJob() { + this(new Parameters.Builder().build()); + } + + private TrimByLengthSettingsMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + boolean isUiBlocking() { + return false; + } + + @Override + void performMigration() throws Exception { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ApplicationDependencies.getApplication()); + if (preferences.contains(THREAD_TRIM_ENABLED)) { + SignalStore.settings().setThreadTrimByLengthEnabled(preferences.getBoolean(THREAD_TRIM_ENABLED, false)); + //noinspection ConstantConditions + SignalStore.settings().setThreadTrimLength(Integer.parseInt(preferences.getString(THREAD_TRIM_LENGTH, "500"))); + + preferences.edit() + .remove(THREAD_TRIM_ENABLED) + .remove(THREAD_TRIM_LENGTH) + .apply(); + } + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull TrimByLengthSettingsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new TrimByLengthSettingsMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java index 503696758..590ffeafa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/UuidMigrationJob.java @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; import java.io.IOException; import java.util.UUID; @@ -58,7 +57,6 @@ public class UuidMigrationJob extends MigrationJob { ensureSelfRecipientExists(context); fetchOwnUuid(context); - rotateSealedSenderCerts(context); } @Override @@ -78,14 +76,6 @@ public class UuidMigrationJob extends MigrationJob { TextSecurePreferences.setLocalUuid(context, localUuid); } - private static void rotateSealedSenderCerts(@NonNull Context context) throws IOException { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - byte[] certificate = accountManager.getSenderCertificate(); - - TextSecurePreferences.setUnidentifiedAccessCertificate(context, certificate); - } - - public static class Factory implements Job.Factory { @Override public @NonNull UuidMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index bcca70879..c95bcb078 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.graphics.Bitmap; import androidx.annotation.NonNull; + +import android.graphics.drawable.Drawable; import android.util.Log; import com.bumptech.glide.Glide; @@ -27,14 +29,18 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl; +import org.thoughtcrime.securesms.glide.cache.ApngBufferCacheDecoder; +import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder; import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; +import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; -import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapCacheDecoder; +import org.thoughtcrime.securesms.glide.cache.ApngStreamCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder; +import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder; -import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder; +import org.signal.glide.apng.decode.APNGDecoder; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.stickers.StickerRemoteUri; @@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader; import java.io.File; import java.io.InputStream; +import java.nio.ByteBuffer; @GlideModule public class SignalGlideModule extends AppGlideModule { @@ -54,7 +61,6 @@ public class SignalGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setLogLevel(Log.ERROR); -// builder.setDiskCache(new NoopDiskCacheFactory()); } @Override @@ -63,14 +69,25 @@ public class SignalGlideModule extends AppGlideModule { byte[] secret = attachmentSecret.getModernKey(); registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance()); - registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); - registry.prepend(File.class, Bitmap.class, new EncryptedBitmapCacheDecoder(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - registry.prepend(File.class, GifDrawable.class, new EncryptedGifCacheDecoder(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); + registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); + registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); + registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); + registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); + + ApngBufferCacheDecoder apngBufferCacheDecoder = new ApngBufferCacheDecoder(); + ApngStreamCacheDecoder apngStreamCacheDecoder = new ApngStreamCacheDecoder(apngBufferCacheDecoder); + + registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder); + registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder); + registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret)); + registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder)); + registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder()); + + registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java index de42c7d56..f4920301a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/StickerSlide.java @@ -13,17 +13,23 @@ import org.thoughtcrime.securesms.blurhash.BlurHash; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.util.MediaUtil; +import java.util.Objects; + public class StickerSlide extends Slide { public static final int WIDTH = 512; public static final int HEIGHT = 512; + private final StickerLocator stickerLocator; + public StickerSlide(@NonNull Context context, @NonNull Attachment attachment) { super(context, attachment); + this.stickerLocator = Objects.requireNonNull(attachment.getSticker()); } - public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_WEBP, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false)); + public StickerSlide(Context context, Uri uri, long size, @NonNull StickerLocator stickerLocator, @NonNull String contentType) { + super(context, constructAttachmentFromUri(context, uri, contentType, size, WIDTH, HEIGHT, true, null, null, stickerLocator, null, null, false, false, false)); + this.stickerLocator = Objects.requireNonNull(attachment.getSticker()); } @Override @@ -41,8 +47,17 @@ public class StickerSlide extends Slide { return true; } + @Override + public boolean isBorderless() { + return true; + } + @Override public @NonNull String getContentDescription() { return context.getString(R.string.Slide_sticker); } + + public @Nullable String getEmoji() { + return stickerLocator.getEmoji(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 1cc60e1f3..664d8ca7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -346,9 +347,10 @@ public class DefaultMessageNotifier implements MessageNotifier { return; } - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - List notifications = notificationState.getNotifications(); - Recipient recipient = notifications.get(0).getRecipient(); + NotificationPrivacyPreference notificationPrivacy = TextSecurePreferences.getNotificationPrivacy(context); + SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, notificationPrivacy); + List notifications = notificationState.getNotifications(); + Recipient recipient = notifications.get(0).getRecipient(); int notificationId; if (Build.VERSION.SDK_INT >= 23) { @@ -371,7 +373,10 @@ public class DefaultMessageNotifier implements MessageNotifier { boolean isSingleNotificationContactJoined = notifications.size() == 1 && notifications.get(0).isJoin(); - if (!KeyCachingService.isLocked(context) && RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) { + if (notificationPrivacy.isDisplayMessage() && + !KeyCachingService.isLocked(context) && + RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) + { ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId), @@ -422,8 +427,9 @@ public class DefaultMessageNotifier implements MessageNotifier { return; } - MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - List notifications = notificationState.getNotifications(); + NotificationPrivacyPreference notificationPrivacy = TextSecurePreferences.getNotificationPrivacy(context); + MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, notificationPrivacy); + List notifications = notificationState.getNotifications(); builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount()); builder.setMostRecentSender(notifications.get(0).getIndividualRecipient()); @@ -438,7 +444,9 @@ public class DefaultMessageNotifier implements MessageNotifier { long timestamp = notifications.get(0).getTimestamp(); if (timestamp != 0) builder.setWhen(timestamp); - builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY)); + if (notificationPrivacy.isDisplayMessage()) { + builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY)); + } ListIterator iterator = notifications.listIterator(notifications.size()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 7d5954ada..5d147200d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -7,6 +7,9 @@ import android.graphics.Color; import android.graphics.Typeface; import android.os.Bundle; import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.TextAppearanceSpan; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; @@ -14,6 +17,7 @@ import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.autofill.HintConstants; @@ -37,6 +41,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.keyvalue.KbsValues; +import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.PinValues; import org.thoughtcrime.securesms.keyvalue.SettingsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -45,13 +50,13 @@ import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.KbsConstants; import org.thoughtcrime.securesms.lock.v2.RegistrationLockUtil; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; @@ -67,8 +72,10 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private static final String TAG = Log.tag(AppProtectionPreferenceFragment.class); - private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked"; - private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more"; + private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked"; + private static final String PREFERENCE_UNIDENTIFIED_LEARN_MORE = "pref_unidentified_learn_more"; + private static final String PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER = "pref_who_can_see_phone_number"; + private static final String PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER = "pref_who_can_find_by_phone_number"; private CheckBoxPreference disablePassphrase; @@ -99,6 +106,18 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment this.findPreference(PREFERENCE_UNIDENTIFIED_LEARN_MORE).setOnPreferenceClickListener(new UnidentifiedLearnMoreClickListener()); disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener()); + if (FeatureFlags.phoneNumberPrivacy()) { + Preference whoCanSeePhoneNumber = this.findPreference(PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER); + Preference whoCanFindByPhoneNumber = this.findPreference(PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER); + + whoCanSeePhoneNumber.setPreferenceDataStore(null); + whoCanSeePhoneNumber.setOnPreferenceClickListener(new PhoneNumberPrivacyWhoCanSeeClickListener()); + + whoCanFindByPhoneNumber.setPreferenceDataStore(null); + whoCanFindByPhoneNumber.setOnPreferenceClickListener(new PhoneNumberPrivacyWhoCanFindClickListener()); + } else { + this.findPreference("category_phone_number_privacy").setVisible(false); + } SwitchPreferenceCompat linkPreviewPref = (SwitchPreferenceCompat) this.findPreference(SettingsValues.LINK_PREVIEWS); linkPreviewPref.setChecked(SignalStore.settings().isLinkPreviewsEnabled()); @@ -138,6 +157,9 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment signalPinReminders.setEnabled(false); registrationLockV2.setEnabled(false); } + + initializePhoneNumberPrivacyWhoCanSeeSummary(); + initializePhoneNumberPrivacyWhoCanFindSummary(); } @Override @@ -164,6 +186,27 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)); } + private void initializePhoneNumberPrivacyWhoCanSeeSummary() { + Preference preference = findPreference(PREFERENCE_WHO_CAN_SEE_PHONE_NUMBER); + + switch (SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode()) { + case EVERYONE: preference.setSummary(R.string.PhoneNumberPrivacy_everyone); break; + case CONTACTS: preference.setSummary(R.string.PhoneNumberPrivacy_my_contacts); break; + case NOBODY : preference.setSummary(R.string.PhoneNumberPrivacy_nobody); break; + default : throw new AssertionError(); + } + } + + private void initializePhoneNumberPrivacyWhoCanFindSummary() { + Preference preference = findPreference(PREFERENCE_WHO_CAN_FIND_BY_PHONE_NUMBER); + + switch (SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode()) { + case LISTED : preference.setSummary(R.string.PhoneNumberPrivacy_everyone); break; + case UNLISTED: preference.setSummary(R.string.PhoneNumberPrivacy_nobody); break; + default : throw new AssertionError(); + } + } + private void initializeVisibility() { if (TextSecurePreferences.isPasswordDisabled(getContext())) { findPreference("pref_enable_passphrase_temporary").setVisible(false); @@ -504,4 +547,87 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment } } } + + private final class PhoneNumberPrivacyWhoCanSeeClickListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + PhoneNumberPrivacyValues phoneNumberPrivacyValues = SignalStore.phoneNumberPrivacy(); + + final PhoneNumberPrivacyValues.PhoneNumberSharingMode[] value = { phoneNumberPrivacyValues.getPhoneNumberSharingMode() }; + + new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.preferences_app_protection__see_my_phone_number) + .setCancelable(true) + .setSingleChoiceItems(items(requireContext()), value[0].ordinal(), (dialog, which) -> value[0] = PhoneNumberPrivacyValues.PhoneNumberSharingMode.values()[which]) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberSharingMode = value[0]; + phoneNumberPrivacyValues.setPhoneNumberSharingMode(phoneNumberSharingMode); + Log.i(TAG, String.format("PhoneNumberSharingMode changed to %s. Scheduling storage value sync", phoneNumberSharingMode)); + StorageSyncHelper.scheduleSyncForDataChange(); + initializePhoneNumberPrivacyWhoCanSeeSummary(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return true; + } + + private CharSequence[] items(Context context) { + return new CharSequence[]{ + titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_see_description)), + titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_my_contacts), context.getString(R.string.PhoneNumberPrivacy_my_contacts_see_description)), + context.getString(R.string.PhoneNumberPrivacy_nobody) }; + } + + } + + private final class PhoneNumberPrivacyWhoCanFindClickListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + PhoneNumberPrivacyValues phoneNumberPrivacyValues = SignalStore.phoneNumberPrivacy(); + + final PhoneNumberPrivacyValues.PhoneNumberListingMode[] value = { phoneNumberPrivacyValues.getPhoneNumberListingMode() }; + + new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.preferences_app_protection__find_me_by_phone_number) + .setCancelable(true) + .setSingleChoiceItems(items(requireContext()), + value[0].ordinal(), + (dialog, which) -> value[0] = PhoneNumberPrivacyValues.PhoneNumberListingMode.values()[which]) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + PhoneNumberPrivacyValues.PhoneNumberListingMode phoneNumberListingMode = value[0]; + phoneNumberPrivacyValues.setPhoneNumberListingMode(phoneNumberListingMode); + Log.i(TAG, String.format("PhoneNumberListingMode changed to %s. Scheduling storage value sync", phoneNumberListingMode)); + StorageSyncHelper.scheduleSyncForDataChange(); + ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); + initializePhoneNumberPrivacyWhoCanFindSummary(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return true; + } + + private CharSequence[] items(Context context) { + return new CharSequence[]{ + titleAndDescription(context, context.getString(R.string.PhoneNumberPrivacy_everyone), context.getString(R.string.PhoneNumberPrivacy_everyone_find_description)), + context.getString(R.string.PhoneNumberPrivacy_nobody) }; + } + } + + /** Adds a detail row for radio group descriptions. */ + private static CharSequence titleAndDescription(@NonNull Context context, @NonNull String header, @NonNull String description) { + SpannableStringBuilder builder = new SpannableStringBuilder(); + + builder.append("\n"); + builder.append(header); + builder.append("\n"); + + builder.setSpan(new TextAppearanceSpan(context, android.R.style.TextAppearance_Small), builder.length(), builder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + builder.append(description); + builder.append("\n"); + + return builder; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java index da5410e98..4ceb22881 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java @@ -1,35 +1,69 @@ package org.thoughtcrime.securesms.preferences; +import android.annotation.SuppressLint; import android.os.Bundle; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; -import androidx.preference.EditTextPreference; import androidx.preference.Preference; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.components.settings.BaseSettingsAdapter; +import org.thoughtcrime.securesms.components.settings.BaseSettingsFragment; +import org.thoughtcrime.securesms.components.settings.CustomizableSingleSelectSetting; +import org.thoughtcrime.securesms.components.settings.SingleSelectSetting; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; +import org.thoughtcrime.securesms.keyvalue.SettingsValues; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Trimmer; +import org.thoughtcrime.securesms.util.MappingModelList; +import org.thoughtcrime.securesms.util.StringUtil; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; + +import java.text.NumberFormat; public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { - private static final String TAG = Log.tag(StoragePreferenceFragment.class); + private Preference keepMessages; + private Preference trimLength; @Override - public void onCreate(Bundle paramBundle) { + public void onCreate(@Nullable Bundle paramBundle) { super.onCreate(paramBundle); - findPreference(TextSecurePreferences.THREAD_TRIM_NOW) - .setOnPreferenceClickListener(new TrimNowClickListener()); - findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH) - .setOnPreferenceChangeListener(new TrimLengthValidationListener()); + findPreference("pref_storage_clear_message_history") + .setOnPreferenceClickListener(new ClearMessageHistoryClickListener()); + + trimLength = findPreference(SettingsValues.THREAD_TRIM_LENGTH); + trimLength.setOnPreferenceClickListener(p -> { + getApplicationPreferencesActivity().requireSupportActionBar().setTitle(R.string.preferences__conversation_length_limit); + getApplicationPreferencesActivity().pushFragment(BaseSettingsFragment.create(new ConversationLengthLimitConfiguration())); + return true; + }); + + keepMessages = findPreference(SettingsValues.KEEP_MESSAGES_DURATION); + keepMessages.setOnPreferenceClickListener(p -> { + getApplicationPreferencesActivity().requireSupportActionBar().setTitle(R.string.preferences__keep_messages); + getApplicationPreferencesActivity().pushFragment(BaseSettingsFragment.create(new KeepMessagesConfiguration())); + return true; + }); StoragePreferenceCategory storageCategory = (StoragePreferenceCategory) findPreference("pref_storage_category"); FragmentActivity activity = requireActivity(); @@ -41,19 +75,24 @@ public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { } @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { addPreferencesFromResource(R.xml.preferences_storage); } @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__storage); + ((ApplicationPreferencesActivity) requireActivity()).requireSupportActionBar().setTitle(R.string.preferences__storage); FragmentActivity activity = requireActivity(); ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity); viewModel.refreshStorageBreakdown(activity.getApplicationContext()); + + keepMessages.setSummary(SignalStore.settings().getKeepMessagesDuration().getStringResource()); + + trimLength.setSummary(SignalStore.settings().isTrimByLengthEnabled() ? getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(SignalStore.settings().getThreadTrimLength())) + : getString(R.string.preferences_storage__none)); } @Override @@ -61,49 +100,197 @@ public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } - private class TrimNowClickListener implements Preference.OnPreferenceClickListener { + private @NonNull ApplicationPreferencesActivity getApplicationPreferencesActivity() { + return (ApplicationPreferencesActivity) requireActivity(); + } + + private class ClearMessageHistoryClickListener implements Preference.OnPreferenceClickListener { @Override - public boolean onPreferenceClick(Preference preference) { - final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity()); + public boolean onPreferenceClick(@NonNull Preference preference) { new AlertDialog.Builder(requireActivity()) - .setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now) - .setMessage(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages, - threadLengthLimit, threadLengthLimit)) - .setPositiveButton(R.string.ApplicationPreferencesActivity_delete, (dialog, which) -> Trimmer.trimAllThreads(getActivity(), threadLengthLimit)) + .setTitle(R.string.preferences_storage__clear_message_history) + .setMessage(R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device) + .setPositiveButton(R.string.delete, (d, w) -> showAreYouReallySure()) .setNegativeButton(android.R.string.cancel, null) .show(); return true; } + + private void showAreYouReallySure() { + new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history) + .setMessage(R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone) + .setPositiveButton(R.string.preferences_storage__delete_all_now, (d, w) -> SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication()).deleteAllConversations())) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } } - private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener { + public static class KeepMessagesConfiguration extends BaseSettingsFragment.Configuration implements SingleSelectSetting.SingleSelectSelectionChangedListener { - TrimLengthValidationListener() { - EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH); - onPreferenceChange(preference, preference.getText()); + @Override + public void configureAdapter(@NonNull BaseSettingsAdapter adapter) { + adapter.configureSingleSelect(this); } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (newValue == null || ((String)newValue).trim().length() == 0) { - return false; + public @NonNull MappingModelList getSettings() { + KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration(); + return Stream.of(KeepMessagesDuration.values()) + .map(duration -> new SingleSelectSetting.Item(duration, activity.getString(duration.getStringResource()), duration.equals(currentDuration))) + .collect(MappingModelList.toMappingModelList()); + } + + @Override + public void onSelectionChanged(@NonNull Object selection) { + KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration(); + KeepMessagesDuration newDuration = (KeepMessagesDuration) selection; + + if (newDuration.ordinal() > currentDuration.ordinal()) { + new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_storage__delete_older_messages) + .setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, activity.getString(newDuration.getStringResource()))) + .setPositiveButton(R.string.delete, (d, w) -> updateTrimByTime(newDuration)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else { + updateTrimByTime(newDuration); + } + } + + private void updateTrimByTime(@NonNull KeepMessagesDuration newDuration) { + SignalStore.settings().setKeepMessagesForDuration(newDuration); + updateSettingsList(); + ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary(); + } + } + + public static class ConversationLengthLimitConfiguration extends BaseSettingsFragment.Configuration implements CustomizableSingleSelectSetting.CustomizableSingleSelectionListener { + + private static final int CUSTOM_LENGTH = -1; + + @Override + public void configureAdapter(@NonNull BaseSettingsAdapter adapter) { + adapter.configureSingleSelect(this); + adapter.configureCustomizableSingleSelect(this); + } + + @Override + public @NonNull MappingModelList getSettings() { + int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() : 0; + int[] options = activity.getResources().getIntArray(R.array.conversation_length_limit); + boolean hasSelection = false; + MappingModelList settings = new MappingModelList(); + + for (int option : options) { + boolean isSelected = option == trimLength; + String text = option == 0 ? activity.getString(R.string.preferences_storage__none) + : activity.getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(option)); + + settings.add(new SingleSelectSetting.Item(option, text, isSelected)); + + hasSelection = hasSelection || isSelected; } - int value; - try { - value = Integer.parseInt((String)newValue); - } catch (NumberFormatException nfe) { - Log.w(TAG, nfe); - return false; + int currentValue = SignalStore.settings().getThreadTrimLength(); + settings.add(new CustomizableSingleSelectSetting.Item(CUSTOM_LENGTH, + activity.getString(R.string.preferences_storage__custom), + !hasSelection, + currentValue, + activity.getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(currentValue)))); + return settings; + } + + @SuppressLint("InflateParams") + @Override + public void onCustomizeClicked(@Nullable CustomizableSingleSelectSetting.Item item) { + boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled(); + int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0; + + View view = LayoutInflater.from(activity).inflate(R.layout.customizable_setting_edit_text, null, false); + EditText editText = view.findViewById(R.id.customizable_setting_edit_text); + if (trimLength > 0) { + editText.setText(String.valueOf(trimLength)); } - if (value < 1) { - return false; - } + AlertDialog dialog = new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_Storage__custom_conversation_length_limit) + .setView(view) + .setPositiveButton(android.R.string.ok, (d, w) -> onSelectionChanged(Integer.parseInt(editText.getText().toString()))) + .setNegativeButton(android.R.string.cancel, (d, w) -> updateSettingsList()) + .create(); - preference.setSummary(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_messages_per_conversation, value, value)); - return true; + dialog.setOnShowListener(d -> { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(editText.getText())); + editText.requestFocus(); + editText.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(@NonNull Editable sequence) { + CharSequence trimmed = StringUtil.trimSequence(sequence); + if (TextUtils.isEmpty(trimmed)) { + sequence.replace(0, sequence.length(), ""); + } else { + try { + Integer.parseInt(trimmed.toString()); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + return; + } catch (NumberFormatException e) { + String onlyDigits = trimmed.toString().replaceAll("[^\\d]", ""); + if (!onlyDigits.equals(trimmed.toString())) { + sequence.replace(0, sequence.length(), onlyDigits); + } + } + } + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + } + + @Override + public void beforeTextChanged(@NonNull CharSequence sequence, int start, int count, int after) {} + + @Override + public void onTextChanged(@NonNull CharSequence sequence, int start, int before, int count) {} + }); + }); + + dialog.show(); + } + + @Override + public void onSelectionChanged(@NonNull Object selection) { + boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled(); + int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0; + int newTrimLength = (Integer) selection; + + if (newTrimLength > 0 && (!trimLengthEnabled || newTrimLength < trimLength)) { + new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_storage__delete_older_messages) + .setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages, NumberFormat.getInstance().format(newTrimLength))) + .setPositiveButton(R.string.delete, (d, w) -> updateTrimByLength(newTrimLength)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else if (newTrimLength == CUSTOM_LENGTH) { + onCustomizeClicked(null); + } else { + updateTrimByLength(newTrimLength); + } + } + + private void updateTrimByLength(int length) { + boolean restrictingChange = !SignalStore.settings().isTrimByLengthEnabled() || length < SignalStore.settings().getThreadTrimLength(); + + SignalStore.settings().setThreadTrimByLengthEnabled(length > 0); + SignalStore.settings().setThreadTrimLength(length); + updateSettingsList(); + + if (SignalStore.settings().isTrimByLengthEnabled() && restrictingChange) { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + + long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration() + : ThreadDatabase.NO_TRIM_BEFORE_DATE_SET; + + SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication()).trimAllThreads(length, trimBeforeDate)); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index 1baf640dd..3ba9f7f6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -174,6 +174,7 @@ public class RecipientUtil { } DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false); + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true); ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); StorageSyncHelper.scheduleSyncForDataChange(); ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index 44f0c16fc..12e5483ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.jobs.RotateCertificateJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.pin.PinState; import org.thoughtcrime.securesms.push.AccountManagerFactory; @@ -174,7 +175,7 @@ public final class CodeVerificationRequest { private static void handleSuccessfulRegistration(@NonNull Context context) { JobManager jobManager = ApplicationDependencies.getJobManager(); jobManager.add(new DirectoryRefreshJob(false)); - jobManager.add(new RotateCertificateJob(context)); + jobManager.add(new RotateCertificateJob()); DirectoryRefreshListener.schedule(context); RotateSignedPreKeyListener.schedule(context); @@ -220,7 +221,8 @@ public final class CodeVerificationRequest { registrationLockV2, unidentifiedAccessKey, universalUnidentifiedAccess, - AppCapabilities.getCapabilities(true)); + AppCapabilities.getCapabilities(true), + SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable()); UUID uuid = UuidUtil.parseOrThrow(response.getUuid()); boolean hasPin = response.isStorageCapable(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CallState.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CallState.java index 8bbf6f993..1f1037e0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CallState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/CallState.java @@ -10,10 +10,10 @@ public enum CallState { /** Idle, setting up objects */ IDLE, - /** Dialing. Outgoing call is signaling the remote peer */ + /** Dialing. Outgoing call is signaling the remote peer */ DIALING, - /** Answering. Incoming call is responding to remote peer */ + /** Answering. Incoming call is responding to remote peer */ ANSWERING, /** Remote ringing. Outgoing call, ICE negotiation is complete */ @@ -25,10 +25,9 @@ public enum CallState { /** Connected. Incoming/Outgoing call, the call is connected */ CONNECTED, - /** Terminated. Incoming/Outgoing call, the call is terminated */ + /** Terminated. Incoming/Outgoing call, the call is terminated */ TERMINATED, - /** Busy. Outgoing call received a busy notification */ + /** Busy. Outgoing call received a busy notification */ RECEIVED_BUSY; - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java index 9106fb108..dedf1580d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/RemotePeer.java @@ -40,6 +40,10 @@ public final class RemotePeer implements Remote, Parcelable return callId; } + public void setCallId(@NonNull CallId callId) { + this.callId = callId; + } + public @NonNull CallState getState() { return callState; } @@ -73,21 +77,19 @@ public final class RemotePeer implements Remote, Parcelable return remotePeer != null && this.callId.equals(remotePeer.callId); } - public void dialing(@NonNull CallId callId) { + public void dialing() { if (callState != CallState.IDLE) { throw new IllegalStateException("Cannot transition to DIALING from state: " + callState); } - this.callId = callId; this.callState = CallState.DIALING; } - public void answering(@NonNull CallId callId) { + public void answering() { if (callState != CallState.IDLE) { throw new IllegalStateException("Cannot transition to ANSWERING from state: " + callState); } - this.callId = callId; this.callState = CallState.ANSWERING; } @@ -99,14 +101,6 @@ public final class RemotePeer implements Remote, Parcelable this.callState = CallState.REMOTE_RINGING; } - public void receivedBusy() { - if (callState != CallState.DIALING) { - Log.w(TAG, "RECEIVED_BUSY from unexpected state: " + callState); - } - - this.callState = CallState.RECEIVED_BUSY; - } - public void localRinging() { if (callState != CallState.ANSWERING) { throw new IllegalStateException("Cannot transition to LOCAL_RINGING from state: " + callState); @@ -123,6 +117,14 @@ public final class RemotePeer implements Remote, Parcelable this.callState = CallState.CONNECTED; } + public void receivedBusy() { + if (callState != CallState.DIALING) { + Log.w(TAG, "RECEIVED_BUSY from unexpected state: " + callState); + } + + this.callState = CallState.RECEIVED_BUSY; + } + @Override public int describeContents() { return 0; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java index b06c75571..9f8c0805d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/RotateSenderCertificateListener.java @@ -4,7 +4,6 @@ package org.thoughtcrime.securesms.service; import android.content.Context; import android.content.Intent; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.RotateCertificateJob; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -22,7 +21,7 @@ public class RotateSenderCertificateListener extends PersistentAlarmManagerListe @Override protected long onAlarm(Context context, long scheduledTime) { - ApplicationDependencies.getJobManager().add(new RotateCertificateJob(context)); + ApplicationDependencies.getJobManager().add(new RotateCertificateJob()); long nextTime = System.currentTimeMillis() + INTERVAL; TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java new file mode 100644 index 000000000..895362509 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.service; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +public class TrimThreadsByDateManager extends TimedEventManager { + + private static final String TAG = Log.tag(TrimThreadsByDateManager.class); + + private final ThreadDatabase threadDatabase; + private final MmsSmsDatabase mmsSmsDatabase; + + public TrimThreadsByDateManager(@NonNull Application application) { + super(application, "TrimThreadsByDateManager"); + + threadDatabase = DatabaseFactory.getThreadDatabase(application); + mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(application); + + scheduleIfNecessary(); + } + + @Override + protected @Nullable TrimEvent getNextClosestEvent() { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + if (keepMessagesDuration == KeepMessagesDuration.FOREVER) { + return null; + } + + long trimBeforeDate = System.currentTimeMillis() - keepMessagesDuration.getDuration(); + + if (mmsSmsDatabase.getMessageCountBeforeDate(trimBeforeDate) > 0) { + Log.i(TAG, "Messages exist before date, trim immediately"); + return new TrimEvent(0); + } + + long timestamp = mmsSmsDatabase.getTimestampForFirstMessageAfterDate(trimBeforeDate); + + if (timestamp == 0) { + return null; + } + + return new TrimEvent(Math.max(0, keepMessagesDuration.getDuration() - (System.currentTimeMillis() - timestamp))); + } + + @Override + protected void executeEvent(@NonNull TrimEvent event) { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + + int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() + : ThreadDatabase.NO_TRIM_MESSAGE_COUNT_SET; + + long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration() + : ThreadDatabase.NO_TRIM_BEFORE_DATE_SET; + + Log.i(TAG, "Trimming all threads with length: " + trimLength + " before: " + trimBeforeDate); + threadDatabase.trimAllThreads(trimLength, trimBeforeDate); + } + + @Override + protected long getDelayForEvent(@NonNull TrimEvent event) { + return event.delay; + } + + @Override + protected void scheduleAlarm(@NonNull Application application, long delay) { + setAlarm(application, delay, TrimThreadsByDateAlarm.class); + } + + public static class TrimThreadsByDateAlarm extends BroadcastReceiver { + + private static final String TAG = Log.tag(TrimThreadsByDateAlarm.class); + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + Log.d(TAG, "onReceive()"); + ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary(); + } + } + + public static class TrimEvent { + final long delay; + + public TrimEvent(long delay) { + this.delay = delay; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index ddad832e0..e0e69fd32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -12,6 +12,7 @@ import android.os.IBinder; import android.os.ResultReceiver; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; +import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -47,6 +48,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TelephonyUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver; import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager; @@ -68,7 +70,6 @@ import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import java.io.IOException; @@ -102,6 +103,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String EXTRA_SPEAKER = "audio_speaker"; public static final String EXTRA_BLUETOOTH = "audio_bluetooth"; public static final String EXTRA_REMOTE_PEER = "remote_peer"; + public static final String EXTRA_REMOTE_PEER_KEY = "remote_peer_key"; public static final String EXTRA_REMOTE_DEVICE = "remote_device"; public static final String EXTRA_OFFER_OPAQUE = "offer_opaque"; public static final String EXTRA_OFFER_SDP = "offer_sdp"; @@ -161,6 +163,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public static final String ACTION_ENDED_RX_OFFER_WHILE_ACTIVE = "ENDED_RX_OFFER_WHILE_ACTIVE"; public static final String ACTION_CALL_CONCLUDED = "CALL_CONCLUDED"; + public static final int BUSY_TONE_LENGTH = 2000; + private CameraState localCameraState = CameraState.UNKNOWN; private boolean microphoneEnabled = true; private boolean remoteVideoEnabled = false; @@ -181,12 +185,14 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private IncomingPstnCallReceiver callReceiver; private UncaughtExceptionHandlerManager uncaughtExceptionHandlerManager; - @Nullable private CallManager callManager; - @Nullable private RemotePeer activePeer; - @Nullable private TextureViewRenderer localRenderer; - @Nullable private TextureViewRenderer remoteRenderer; - @Nullable private EglBase eglBase; - @Nullable private Camera camera; + @Nullable private CallManager callManager; + @Nullable private RemotePeer activePeer; + @Nullable private RemotePeer busyPeer; + @Nullable private SparseArray peerMap; + @Nullable private TextureViewRenderer localRenderer; + @Nullable private TextureViewRenderer remoteRenderer; + @Nullable private EglBase eglBase; + @Nullable private Camera camera; private final ExecutorService serviceExecutor = Executors.newSingleThreadExecutor(); private final ExecutorService networkExecutor = Executors.newSingleThreadExecutor(); @@ -257,7 +263,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, else if (intent.getAction().equals(ACTION_ENDED_RX_OFFER_EXPIRED)) handleEndedReceivedOfferExpired(intent); else if (intent.getAction().equals(ACTION_ENDED_RX_OFFER_WHILE_ACTIVE)) handleEndedReceivedOfferWhileActive(intent); else if (intent.getAction().equals(ACTION_CALL_CONCLUDED)) handleCallConcluded(intent); - }); return START_NOT_STICKY; @@ -322,15 +327,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - - // Initializers - private void initializeResources() { this.messageSender = ApplicationDependencies.getSignalServiceMessageSender(); this.accountManager = ApplicationDependencies.getSignalServiceAccountManager(); this.lockManager = new LockManager(this); this.audioManager = new SignalAudioManager(this); this.bluetoothStateManager = new BluetoothStateManager(this, this); + this.peerMap = new SparseArray<>(); this.messageSender.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); this.accountManager.setSoTimeoutMillis(TimeUnit.SECONDS.toMillis(10)); @@ -340,7 +343,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } catch (CallException e) { callFailure("Unable to create Call Manager: ", e); } - } private void registerIncomingPstnCallReceiver() { @@ -383,8 +385,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - // Handlers - private void handleReceivedOffer(Intent intent) { CallId callId = getCallId(intent); RemotePeer remotePeer = getRemotePeer(intent); @@ -399,7 +399,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.i(TAG, "handleReceivedOffer(): id: " + callId.format(remoteDevice)); if (TelephonyUtil.isAnyPstnLineBusy(this)) { - Log.i(TAG, "handleReceivedOffer(): PSTN line is busy."); + Log.i(TAG, "PSTN line is busy."); intent.putExtra(EXTRA_BROADCAST, true); handleSendBusy(intent); insertMissedCall(remotePeer, true); @@ -407,7 +407,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } if (remotePeer.getRecipient() == null || !RecipientUtil.isCallRequestAccepted(getApplicationContext(), remotePeer.getRecipient())) { - Log.i(TAG, "handleReceivedOffer(): Caller is untrusted."); + Log.w(TAG, "Caller is untrusted."); intent.putExtra(EXTRA_BROADCAST, true); intent.putExtra(EXTRA_HANGUP_TYPE, HangupMessage.Type.NEED_PERMISSION.getCode()); handleSendHangup(intent); @@ -415,12 +415,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, return; } + peerMap.append(remotePeer.hashCode(), remotePeer); + Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); + isRemoteVideoOffer = offerType == OfferMessage.Type.VIDEO_CALL; CallManager.CallMediaType callType = getCallMediaTypeFromOfferType(offerType); long messageAgeSec = Math.max(serverDeliveredTimestamp - serverReceivedTimestamp, 0) / 1000; - Log.i(TAG, "handleReceivedOffer(): messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp); + Log.i(TAG, "messageAgeSec: " + messageAgeSec + ", serverReceivedTimestamp: " + serverReceivedTimestamp + ", serverDeliveredTimestamp: " + serverDeliveredTimestamp); try { callManager.receivedOffer(callId, remotePeer, remoteDevice, opaque, sdp, messageAgeSec, callType, 1, isMultiRing, true); @@ -430,15 +433,19 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleOutgoingCall(Intent intent) { + Log.i(TAG, "handleOutgoingCall():"); + RemotePeer remotePeer = getRemotePeer(intent); if (remotePeer.getState() != CallState.IDLE) { throw new IllegalStateException("Dialing from non-idle?"); } - Log.i(TAG, "handleOutgoingCall():"); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); + peerMap.append(remotePeer.hashCode(), remotePeer); + Log.i(TAG, "add remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); + initializeVideo(); OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); @@ -480,6 +487,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, try { callManager.hangup(); DatabaseFactory.getSmsDatabase(this).insertMissedCall(activePeer.getId()); + terminate(activePeer); } catch (CallException e) { callFailure("hangup() failed: ", e); } @@ -591,7 +599,19 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleStartOutgoingCall(Intent intent) { - Log.i(TAG, "handleStartOutgoingCall(): callId: " + activePeer.getCallId()); + Log.i(TAG, "handleStartOutgoingCall():"); + + if (activePeer != null) { + throw new IllegalStateException("handleStartOutgoingCall(): activePeer already set"); + } + + RemotePeer remotePeer = getRemotePeerFromMap(intent); + activePeer = remotePeer; + activePeer.dialing(); + Log.i(TAG, "assign activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); + + AudioManager androidAudioManager = ServiceUtil.getAudioManager(this); + androidAudioManager.setSpeakerphoneOn(false); sendMessage(WebRtcViewModel.State.CALL_OUTGOING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); lockManager.updatePhoneState(getInCallPhoneState()); @@ -609,9 +629,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); - LinkedList deviceList = new LinkedList(); - deviceList.add(SignalServiceAddress.DEFAULT_DEVICE_ID); - try { callManager.proceed(activePeer.getCallId(), WebRtcCallService.this, @@ -635,11 +652,19 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleStartIncomingCall(Intent intent) { - if (activePeer.getState() != CallState.ANSWERING) { - throw new IllegalStateException("StartIncoming while non-ANSWERING"); + Log.i(TAG, "handleStartIncomingCall():"); + + if (activePeer != null) { + throw new IllegalStateException("handleStartIncomingCall(): activePeer already set"); } - Log.i(TAG, "handleStartIncomingCall(): callId: " + activePeer.getCallId()); + RemotePeer remotePeer = getRemotePeerFromMap(intent); + activePeer = remotePeer; + activePeer.answering(); + Log.i(TAG, "assign activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); + + AudioManager androidAudioManager = ServiceUtil.getAudioManager(this); + androidAudioManager.setSpeakerphoneOn(false); initializeVideo(); @@ -648,12 +673,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer, retrieveTurnServers().addListener(new SuccessOnlyListener>(this.activePeer.getState(), this.activePeer.getCallId()) { @Override public void onSuccessContinue(List iceServers) { - boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); boolean hideIp = !activePeer.getRecipient().isSystemContact() || isAlwaysTurn; - LinkedList deviceList = new LinkedList<>(); - try { callManager.proceed(activePeer.getCallId(), WebRtcCallService.this, @@ -677,7 +699,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleAcceptCall(Intent intent) { - if (activePeer != null && activePeer.getState() != CallState.LOCAL_RINGING) { Log.w(TAG, "handleAcceptCall(): Ignoring for inactive call."); return; @@ -705,7 +726,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, String sdp = intent.getStringExtra(EXTRA_OFFER_SDP); OfferMessage.Type offerType = OfferMessage.Type.fromCode(intent.getStringExtra(EXTRA_OFFER_TYPE)); - Log.i(TAG, "handleSendOffer: id: " + callId.format(remoteDevice)); + Log.i(TAG, "handleSendOffer(): id: " + callId.format(remoteDevice)); OfferMessage offerMessage = new OfferMessage(callId.longValue(), sdp, offerType, opaque); Integer destinationDeviceId = broadcast ? null : remoteDevice; @@ -722,7 +743,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, byte[] opaque = intent.getByteArrayExtra(EXTRA_ANSWER_OPAQUE); String sdp = intent.getStringExtra(EXTRA_ANSWER_SDP); - Log.i(TAG, "handleSendAnswer: id: " + callId.format(remoteDevice)); + Log.i(TAG, "handleSendAnswer(): id: " + callId.format(remoteDevice)); AnswerMessage answerMessage = new AnswerMessage(callId.longValue(), sdp, opaque); Integer destinationDeviceId = broadcast ? null : remoteDevice; @@ -738,7 +759,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); ArrayList iceCandidates = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); - Log.i(TAG, "handleSendIceCandidates: id: " + callId.format(remoteDevice)); + Log.i(TAG, "handleSendIceCandidates(): id: " + callId.format(remoteDevice)); LinkedList iceUpdateMessages = new LinkedList(); for (IceCandidateParcel parcel : iceCandidates) { @@ -760,7 +781,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, boolean isLegacy = intent.getBooleanExtra(EXTRA_HANGUP_IS_LEGACY, true); int deviceId = intent.getIntExtra(EXTRA_HANGUP_DEVICE_ID, 0); - Log.i(TAG, "handleSendHangup: id: " + callId.format(remoteDevice)); + Log.i(TAG, "handleSendHangup(): id: " + callId.format(remoteDevice)); HangupMessage hangupMessage = new HangupMessage(callId.longValue(), type, deviceId, isLegacy); Integer destinationDeviceId = broadcast ? null : remoteDevice; @@ -775,7 +796,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); boolean broadcast = intent.getBooleanExtra(EXTRA_BROADCAST, false); - Log.i(TAG, "handleSendBusy: id: " + callId.format(remoteDevice)); + Log.i(TAG, "handleSendBusy(): id: " + callId.format(remoteDevice)); BusyMessage busyMessage = new BusyMessage(callId.longValue()); Integer destinationDeviceId = broadcast ? null : remoteDevice; @@ -805,7 +826,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Integer remoteDevice = intent.getIntExtra(EXTRA_REMOTE_DEVICE, -1); ArrayList iceCandidateParcels = intent.getParcelableArrayListExtra(EXTRA_ICE_CANDIDATES); - Log.i(TAG, "handleReceivedIceCandidates: id: " + callId.format(remoteDevice) + ", count: " + iceCandidateParcels.size()); + Log.i(TAG, "handleReceivedIceCandidates(): id: " + callId.format(remoteDevice) + ", count: " + iceCandidateParcels.size()); LinkedList iceCandidates = new LinkedList(); for (IceCandidateParcel parcel : iceCandidateParcels) { @@ -849,16 +870,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleLocalRinging(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Recipient recipient = remotePeer.getRecipient(); - Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId()); - if (!remotePeer.callIdEquals(activePeer)) { Log.w(TAG, "handleLocalRinging(): Ignoring for inactive call."); return; } + Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId()); + activePeer.localRinging(); lockManager.updatePhoneState(LockManager.PhoneState.INTERACTIVE); @@ -883,30 +904,29 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleRemoteRinging(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - Recipient recipient = remotePeer.getRecipient(); - - Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); + RemotePeer remotePeer = getRemotePeerFromMap(intent); if (!remotePeer.callIdEquals(activePeer)) { Log.w(TAG, "handleRemoteRinging(): Ignoring for inactive call."); return; } + Log.i(TAG, "handleRemoteRinging(): call_id: " + remotePeer.getCallId()); + activePeer.remoteRinging(); sendMessage(WebRtcViewModel.State.CALL_RINGING, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } private void handleCallConnected(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - - Log.i(TAG, "handleCallConnected: call_id: " + remotePeer.getCallId()); + RemotePeer remotePeer = getRemotePeerFromMap(intent); if (!remotePeer.callIdEquals(activePeer)) { Log.w(TAG, "handleCallConnected(): Ignoring for inactive call."); return; } + Log.i(TAG, "handleCallConnected(): call_id: " + remotePeer.getCallId()); + audioManager.startCommunication(activePeer.getState() == CallState.REMOTE_RINGING); bluetoothStateManager.setWantsConnection(true); @@ -947,7 +967,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, return; } - Log.i(TAG, "handleRemoteVideoEnable: call_id: " + activePeer.getCallId()); + Log.i(TAG, "handleRemoteVideoEnable(): call_id: " + activePeer.getCallId()); remoteVideoEnabled = enable; sendMessage(WebRtcViewModel.State.CALL_CONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); @@ -1007,46 +1027,49 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private void handleLocalHangup(Intent intent) { if (activePeer == null) { + if (busyPeer != null) { + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, busyPeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + busyPeer = null; + } + Log.w(TAG, "handleLocalHangup(): Ignoring for inactive call."); return; } Log.i(TAG, "handleLocalHangup(): call_id: " + activePeer.getCallId()); - if (activePeer.getState() == CallState.RECEIVED_BUSY) { - sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - terminate(); - } else { - accountManager.cancelInFlightRequests(); - messageSender.cancelInFlightRequests(); + accountManager.cancelInFlightRequests(); + messageSender.cancelInFlightRequests(); + try { + callManager.hangup(); sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - - try { - callManager.hangup(); - } catch (CallException e) { - callFailure("hangup() failed: ", e); - } + terminate(activePeer); + } catch (CallException e) { + callFailure("hangup() failed: ", e); } } private void handleEndedReceivedOfferExpired(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Log.i(TAG, "handleEndedReceivedOfferExpired(): call_id: " + remotePeer.getCallId()); + insertMissedCall(remotePeer, true); + + terminate(remotePeer); } private void handleEndedReceivedOfferWhileActive(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - - Log.i(TAG, "handleEndedReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId()); + RemotePeer remotePeer = getRemotePeerFromMap(intent); if (activePeer == null) { - Log.w(TAG, "handleEndedReceivedOfferWhileActive(): ignoring call with null activePeer"); + Log.w(TAG, "handleEndedReceivedOfferWhileActive(): Ignoring for inactive call."); return; } + Log.i(TAG, "handleEndedReceivedOfferWhileActive(): call_id: " + remotePeer.getCallId()); + switch (activePeer.getState()) { case DIALING: case REMOTE_RINGING: setCallInProgressNotification(TYPE_OUTGOING_RINGING, activePeer); break; @@ -1062,10 +1085,12 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } insertMissedCall(remotePeer, true); + + terminate(remotePeer); } private void handleEndedRemoteHangup(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Log.i(TAG, "handleEndedRemoteHangup(): call_id: " + remotePeer.getCallId()); @@ -1082,80 +1107,102 @@ public class WebRtcCallService extends Service implements CallManager.Observer, if (incomingBeforeAccept) { insertMissedCall(remotePeer, true); } + + terminate(remotePeer); } private void handleEndedRemoteHangupAccepted(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); + + Log.i(TAG, "handleEndedRemoteHangupAccepted(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { sendMessage(WebRtcViewModel.State.CALL_ACCEPTED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } + + terminate(remotePeer); } private void handleEndedRemoteHangupBusy(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); + + Log.i(TAG, "handleEndedRemoteHangupBusy(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { sendMessage(WebRtcViewModel.State.CALL_ONGOING_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } + + terminate(remotePeer); } private void handleEndedRemoteHangupDeclined(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); + + Log.i(TAG, "handleEndedRemoteHangupDeclined(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { sendMessage(WebRtcViewModel.State.CALL_DECLINED_ELSEWHERE, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } - } - private void delayedBusyFinish(CallId callId) { - if (activePeer != null && callId.equals(activePeer.getCallId())) { - Log.i(TAG, "delayedBusyFinish(): calling terminate()"); - terminate(); - } + terminate(remotePeer); } private void handleEndedRemoteBusy(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); - CallId callId = remotePeer.getCallId(); + RemotePeer remotePeer = getRemotePeerFromMap(intent); - Log.i(TAG, "handleEndedRemoteBusy(): call_id: " + callId); + Log.i(TAG, "handleEndedRemoteBusy(): call_id: " + remotePeer.getCallId()); - if (!remotePeer.callIdEquals(activePeer)) { - Log.w(TAG, "handleEndedRemoteBusy(): Ignoring for inactive call."); - return; + if (remotePeer.callIdEquals(activePeer)) { + activePeer.receivedBusy(); + busyPeer = activePeer; + + OutgoingRinger ringer = new OutgoingRinger(this); + ringer.start(OutgoingRinger.Type.BUSY); + Util.runOnMainDelayed(() -> { + ringer.stop(); + busyPeer = null; + }, BUSY_TONE_LENGTH); + + sendMessage(WebRtcViewModel.State.CALL_BUSY, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } - activePeer.receivedBusy(); - sendMessage(WebRtcViewModel.State.CALL_BUSY, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); - - audioManager.startOutgoingRinger(OutgoingRinger.Type.BUSY); - Util.runOnMainDelayed(() -> { - delayedBusyFinish(callId); - }, WebRtcCallActivity.BUSY_SIGNAL_DELAY_FINISH); + terminate(remotePeer); } private void handleEndedRemoteNeedPermission(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Log.i(TAG, "handleEndedRemoteNeedPermission(): call_id: " + remotePeer.getCallId()); if (remotePeer.callIdEquals(activePeer)) { sendMessage(WebRtcViewModel.State.CALL_NEEDS_PERMISSION, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } + + terminate(remotePeer); } private void handleEndedRemoteGlare(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Log.i(TAG, "handleEndedRemoteGlare(): call_id: " + remotePeer.getCallId()); - handleEndedRemoteBusy(intent); + + if (remotePeer.callIdEquals(activePeer)) { + sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, remotePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); + } + + boolean incomingBeforeAccept = remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING; + if (incomingBeforeAccept) { + insertMissedCall(remotePeer, true); + } + + terminate(remotePeer); } private void handleEndedFailure(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + RemotePeer remotePeer = getRemotePeerFromMap(intent); Log.i(TAG, "handleEndedFailure(): call_id: " + remotePeer.getCallId()); + if (remotePeer.callIdEquals(activePeer)) { sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } @@ -1163,6 +1210,8 @@ public class WebRtcCallService extends Service implements CallManager.Observer, if (remotePeer.getState() == CallState.ANSWERING || remotePeer.getState() == CallState.LOCAL_RINGING) { insertMissedCall(remotePeer, true); } + + terminate(remotePeer); } private void handleEndedTimeout(Intent intent) { @@ -1190,22 +1239,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } private void handleCallConcluded(Intent intent) { - RemotePeer remotePeer = getRemotePeer(intent); + Log.i(TAG, "handleCallConcluded():"); - Log.i(TAG, "handleCallConcluded(): call_id: " + remotePeer.getCallId()); - if (!remotePeer.callIdEquals(activePeer)) { - Log.w(TAG, "handleCallConcluded(): Ignoring for inactive call."); - return; - } + RemotePeer remotePeer = getRemotePeerFromMap(intent); - boolean terminateAlreadyScheduled = activePeer.getState() == CallState.RECEIVED_BUSY; - if (!terminateAlreadyScheduled) { - terminate(); - } + Log.i(TAG, "delete remotePeer callId: " + remotePeer.getCallId() + " key: " + remotePeer.hashCode()); + + peerMap.delete(remotePeer.hashCode()); } - /// Helper Methods - private boolean isIdle() { return activePeer == null; } @@ -1222,7 +1264,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, camera = new Camera(WebRtcCallService.this, WebRtcCallService.this, eglBase); localCameraState = camera.getCameraState(); - }); } @@ -1231,11 +1272,16 @@ public class WebRtcCallService extends Service implements CallManager.Observer, CallNotificationBuilder.getCallInProgressNotification(this, type, remotePeer.getRecipient())); } - private synchronized void terminate() { - Log.i(TAG, "terminate()"); + private synchronized void terminate(RemotePeer remotePeer) { + Log.i(TAG, "terminate():"); if (activePeer == null) { - Log.i(TAG, "terminate(): skipping with no active peer"); + Log.i(TAG, "skipping with no active peer"); + return; + } + + if (!remotePeer.callIdEquals(activePeer)) { + Log.i(TAG, "skipping remotePeer is not active peer"); return; } @@ -1264,11 +1310,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } this.localCameraState = CameraState.UNKNOWN; - this.activePeer = null; this.microphoneEnabled = true; this.remoteVideoEnabled = false; this.enableVideoOnCreate = false; + Log.i(TAG, "clear activePeer callId: " + activePeer.getCallId() + " key: " + activePeer.hashCode()); + this.activePeer = null; + lockManager.updatePhoneState(LockManager.PhoneState.IDLE); } @@ -1356,8 +1404,29 @@ public class WebRtcCallService extends Service implements CallManager.Observer, return remotePeer; } + private static @NonNull int getRemotePeerKey(Intent intent) { + if (!intent.getExtras().containsKey(EXTRA_REMOTE_PEER_KEY)) { + throw new AssertionError("No RemotePeer key in intent!"); + } + + // The default of -1 should never be applied since the key exists. + int remotePeerKey = intent.getIntExtra(EXTRA_REMOTE_PEER_KEY, -1); + + return remotePeerKey; + } + + private @NonNull RemotePeer getRemotePeerFromMap(Intent intent) { + int remotePeerKey = getRemotePeerKey(intent); + RemotePeer remotePeer = peerMap.get(remotePeerKey); + if (remotePeer == null) { + throw new AssertionError("No RemotePeer in map for key: " + remotePeerKey + "!"); + } + + return remotePeer; + } + private void callFailure(String message, Throwable error) { - Log.w(TAG, message, error); + Log.w(TAG, "callFailure(): " + message, error); if (activePeer != null) { sendMessage(WebRtcViewModel.State.CALL_DISCONNECTED, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); @@ -1373,7 +1442,9 @@ public class WebRtcCallService extends Service implements CallManager.Observer, Log.w(TAG, "No call manager, not reseting. Error message: " + message , error); } - terminate(); + terminate(activePeer); + + peerMap.clear(); } private static @NonNull CallManager.CallMediaType getCallMediaTypeFromOfferType(@NonNull OfferMessage.Type offerType) { @@ -1645,7 +1716,6 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } else if (error instanceof IOException) { sendMessage(WebRtcViewModel.State.NETWORK_FAILURE, activePeer, localCameraState, remoteVideoEnabled, bluetoothAvailable, microphoneEnabled, isRemoteVideoOffer); } - } } @@ -1663,34 +1733,32 @@ public class WebRtcCallService extends Service implements CallManager.Observer, } } - // CallManager observer callbacks - @Override public void onStartCall(Remote remote, CallId callId, Boolean isOutgoing, CallManager.CallMediaType callMediaType) { - Log.i(TAG, "onStartCall: callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType); - - if (activePeer != null) { - throw new IllegalStateException("activePeer already set for START_OUTGOING_CALL"); - } + Log.i(TAG, "onStartCall(): callId: " + callId + ", outgoing: " + isOutgoing + ", type: " + callMediaType); if (remote instanceof RemotePeer) { - activePeer = (RemotePeer)remote; + RemotePeer remotePeer = (RemotePeer) remote; + if (peerMap.get(remotePeer.hashCode()) == null) { + Log.w(TAG, "remotePeer not found in map with key: " + remotePeer.hashCode() + "! Dropping."); + try { + callManager.drop(callId); + } catch (CallException e) { + callFailure("callManager.drop() failed: ", e); + } + } + + remotePeer.setCallId(callId); Intent intent = new Intent(this, WebRtcCallService.class); - AudioManager audioManager = ServiceUtil.getAudioManager(this); - audioManager.setSpeakerphoneOn(false); - if (isOutgoing) { - intent.setAction(ACTION_START_OUTGOING_CALL); - activePeer.dialing(callId); + intent.setAction(ACTION_START_OUTGOING_CALL); } else { - intent.setAction(ACTION_START_INCOMING_CALL); - activePeer.answering(callId); + intent.setAction(ACTION_START_INCOMING_CALL); } - intent.putExtra(EXTRA_REMOTE_PEER, activePeer) - .putExtra(EXTRA_CALL_ID, callId.longValue()); + intent.putExtra(EXTRA_REMOTE_PEER_KEY, remotePeer.hashCode()); startService(intent); } else { @@ -1701,11 +1769,15 @@ public class WebRtcCallService extends Service implements CallManager.Observer, @Override public void onCallEvent(Remote remote, CallEvent event) { if (remote instanceof RemotePeer) { - RemotePeer remotePeer = (RemotePeer)remote; - Intent intent = new Intent(this, WebRtcCallService.class); + RemotePeer remotePeer = (RemotePeer) remote; + if (peerMap.get(remotePeer.hashCode()) == null) { + throw new AssertionError("remotePeer not found in map!"); + } - Log.i(TAG, "onCallEvent: call_id: " + remotePeer.getCallId() + ", event: " + event); - intent.putExtra(EXTRA_REMOTE_PEER, remotePeer); + Log.i(TAG, "onCallEvent(): call_id: " + remotePeer.getCallId() + ", state: " + remotePeer.getState() + ", event: " + event); + + Intent intent = new Intent(this, WebRtcCallService.class); + intent.putExtra(EXTRA_REMOTE_PEER_KEY, remotePeer.hashCode()); switch (event) { case LOCAL_RINGING: @@ -1790,11 +1862,13 @@ public class WebRtcCallService extends Service implements CallManager.Observer, public void onCallConcluded(Remote remote) { if (remote instanceof RemotePeer) { RemotePeer remotePeer = (RemotePeer)remote; - Intent intent = new Intent(this, WebRtcCallService.class); Log.i(TAG, "onCallConcluded: call_id: " + remotePeer.getCallId()); + + Intent intent = new Intent(this, WebRtcCallService.class); intent.setAction(ACTION_CALL_CONCLUDED) - .putExtra(EXTRA_REMOTE_PEER, remotePeer); + .putExtra(EXTRA_REMOTE_PEER_KEY, remotePeer.hashCode()); + startService(intent); } else { throw new AssertionError("Received remote is not instanceof RemotePeer"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java index 19857fcbb..67987a204 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareActivity.java @@ -314,6 +314,8 @@ public class ShareActivity extends PassphraseRequiredActivity intent.putExtra(ConversationActivity.MEDIA_EXTRA, shareData.getMedia()); } else if (shareData != null && shareData.isForPrimitive()) { Log.i(TAG, "Shared data is a primitive type."); + } else if (shareData == null && stickerExtra != null) { + intent.setType(getIntent().getType()); } else { Log.i(TAG, "Shared data was not external."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java index ecb99e2a8..328528e11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerLocator.java @@ -3,23 +3,27 @@ package org.thoughtcrime.securesms.stickers; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; public class StickerLocator implements Parcelable { private final String packId; private final String packKey; private final int stickerId; + private final String emoji; - public StickerLocator(@NonNull String packId, @NonNull String packKey, int stickerId) { + public StickerLocator(@NonNull String packId, @NonNull String packKey, int stickerId, @Nullable String emoji) { this.packId = packId; this.packKey = packKey; this.stickerId = stickerId; + this.emoji = emoji; } private StickerLocator(Parcel in) { packId = in.readString(); packKey = in.readString(); stickerId = in.readInt(); + emoji = in.readString(); } public @NonNull String getPackId() { @@ -30,10 +34,14 @@ public class StickerLocator implements Parcelable { return packKey; } - public @NonNull int getStickerId() { + public int getStickerId() { return stickerId; } + public @Nullable String getEmoji() { + return emoji; + } + @Override public int describeContents() { return 0; @@ -44,6 +52,7 @@ public class StickerLocator implements Parcelable { dest.writeString(packId); dest.writeString(packKey); dest.writeInt(stickerId); + dest.writeString(emoji); } public static final Creator CREATOR = new Creator() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManifest.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManifest.java index 77b5c5c3c..fd215d451 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManifest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerManifest.java @@ -4,6 +4,7 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; import java.util.ArrayList; @@ -66,18 +67,20 @@ public final class StickerManifest { private final String packKey; private final int id; private final String emoji; + private final String contentType; private final Optional uri; - public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji) { - this(packId, packKey, id, emoji, null); + public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji, @Nullable String contentType) { + this(packId, packKey, id, emoji, contentType, null); } - public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji, @Nullable Uri uri) { - this.packId = packId; - this.packKey = packKey; - this.id = id; - this.emoji = emoji; - this.uri = Optional.fromNullable(uri); + public Sticker(@NonNull String packId, @NonNull String packKey, int id, @NonNull String emoji, @Nullable String contentType, @Nullable Uri uri) { + this.packId = packId; + this.packKey = packKey; + this.id = id; + this.emoji = emoji; + this.contentType = contentType; + this.uri = Optional.fromNullable(uri); } public @NonNull String getPackId() { @@ -96,6 +99,10 @@ public final class StickerManifest { return emoji; } + public @Nullable String getContentType() { + return contentType; + } + public Optional getUri() { return uri; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java index 58b1b03fe..702a1c998 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackPreviewRepository.java @@ -126,11 +126,11 @@ public final class StickerPackPreviewRepository { @NonNull String packKey, @NonNull SignalServiceStickerManifest.StickerInfo remoteSticker) { - return new StickerManifest.Sticker(packId, packKey, remoteSticker.getId(), remoteSticker.getEmoji()); + return new StickerManifest.Sticker(packId, packKey, remoteSticker.getId(), remoteSticker.getEmoji(), remoteSticker.getContentType()); } private StickerManifest.Sticker toSticker(@NonNull StickerRecord record) { - return new StickerManifest.Sticker(record.getPackId(), record.getPackKey(), record.getStickerId(), record.getEmoji(), record.getUri()); + return new StickerManifest.Sticker(record.getPackId(), record.getPackKey(), record.getStickerId(), record.getEmoji(), record.getContentType(), record.getUri()); } static class StickerManifestResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java index 14612f9e9..c4fbdf0a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPreviewPopup.java @@ -10,6 +10,8 @@ import android.widget.ImageView; import android.widget.PopupWindow; import android.widget.TextView; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -37,6 +39,7 @@ final class StickerPreviewPopup extends PopupWindow { void presentSticker(@NonNull Object stickerGlideModel, @Nullable String emoji) { emojiText.setText(emoji); glideRequests.load(stickerGlideModel) + .diskCacheStrategy(DiskCacheStrategy.NONE) .into(image); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java index fe9613b2c..15bd613be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerRolloverTouchListener.java @@ -14,12 +14,15 @@ import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.ViewUtil; import org.whispersystems.libsignal.util.Pair; +import java.lang.ref.WeakReference; + public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchListener { private final StickerPreviewPopup popup; private final RolloverEventListener eventListener; private final RolloverStickerRetriever stickerRetriever; - private boolean hoverMode; + private WeakReference currentView; + private boolean hoverMode; StickerRolloverTouchListener(@NonNull Context context, @NonNull GlideRequests glideRequests, @@ -29,6 +32,8 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis this.eventListener = eventListener; this.stickerRetriever = stickerRetriever; this.popup = new StickerPreviewPopup(context, glideRequests); + this.currentView = new WeakReference<>(null); + popup.setAnimationStyle(R.style.StickerPopupAnimation); } @@ -45,15 +50,19 @@ public class StickerRolloverTouchListener implements RecyclerView.OnItemTouchLis hoverMode = false; popup.dismiss(); eventListener.onStickerPopupEnded(); + currentView.clear(); break; default: for (int i = 0, len = recyclerView.getChildCount(); i < len; i++) { View child = recyclerView.getChildAt(i); if (ViewUtil.isPointInsideView(recyclerView, motionEvent.getRawX(), motionEvent.getRawY()) && - ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY())) + ViewUtil.isPointInsideView(child, motionEvent.getRawX(), motionEvent.getRawY()) && + child != currentView.get()) { showStickerForView(recyclerView, child); + currentView = new WeakReference<>(child); + break; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java index eba2ca391..277a5210c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import java.util.Arrays; import java.util.Collection; @@ -55,16 +56,18 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger> update) { if (!update.isPresent()) { return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index e92d0c03a..792d47412 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -49,20 +49,21 @@ public final class FeatureFlags { private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2); - private static final String USERNAMES = "android.usernames"; - private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; - private static final String REMOTE_DELETE = "android.remoteDelete"; - private static final String GROUPS_V2_OLD_1 = "android.groupsv2"; - private static final String GROUPS_V2_OLD_2 = "android.groupsv2.2"; - private static final String GROUPS_V2 = "android.groupsv2.3"; - private static final String GROUPS_V2_CREATE = "android.groupsv2.create.3"; - private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion"; - private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion"; - private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize"; - private static final String CDS_VERSION = "android.cdsVersion"; - private static final String INTERNAL_USER = "android.internalUser"; - private static final String MENTIONS = "android.mentions"; - private static final String VERIFY_V2 = "android.verifyV2"; + private static final String USERNAMES = "android.usernames"; + private static final String ATTACHMENTS_V3 = "android.attachmentsV3.2"; + private static final String REMOTE_DELETE = "android.remoteDelete"; + private static final String GROUPS_V2_OLD_1 = "android.groupsv2"; + private static final String GROUPS_V2_OLD_2 = "android.groupsv2.2"; + private static final String GROUPS_V2 = "android.groupsv2.3"; + private static final String GROUPS_V2_CREATE_VERSION = "android.groupsv2.createVersion"; + private static final String GROUPS_V2_JOIN_VERSION = "android.groupsv2.joinVersion"; + private static final String GROUPS_V2_LINKS_VERSION = "android.groupsv2.manageGroupLinksVersion"; + private static final String GROUPS_V2_CAPACITY = "global.groupsv2.maxGroupSize"; + private static final String CDS_VERSION = "android.cdsVersion"; + private static final String INTERNAL_USER = "android.internalUser"; + private static final String MENTIONS = "android.mentions"; + private static final String VERIFY_V2 = "android.verifyV2"; + private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -73,7 +74,7 @@ public final class FeatureFlags { ATTACHMENTS_V3, REMOTE_DELETE, GROUPS_V2, - GROUPS_V2_CREATE, + GROUPS_V2_CREATE_VERSION, GROUPS_V2_CAPACITY, GROUPS_V2_JOIN_VERSION, GROUPS_V2_LINKS_VERSION, @@ -103,7 +104,7 @@ public final class FeatureFlags { */ private static final Set HOT_SWAPPABLE = Sets.newHashSet( ATTACHMENTS_V3, - GROUPS_V2_CREATE, + GROUPS_V2_CREATE_VERSION, GROUPS_V2_JOIN_VERSION, VERIFY_V2, CDS_VERSION @@ -208,7 +209,7 @@ public final class FeatureFlags { /** Attempt groups v2 creation. */ public static boolean groupsV2create() { return groupsV2LatestFlag() && - getBoolean(GROUPS_V2_CREATE, false) && + getVersionFlag(GROUPS_V2_CREATE_VERSION) == VersionFlag.ON && !SignalStore.internalValues().gv2DoNotCreateGv2Groups(); } @@ -279,6 +280,14 @@ public final class FeatureFlags { return getBoolean(VERIFY_V2, false); } + /** + * Whether the user can choose phone number privacy settings, and; + * Whether to fetch and store the secondary certificate + */ + public static boolean phoneNumberPrivacy() { + return getVersionFlag(PHONE_NUMBER_PRIVACY_VERSION) == VersionFlag.ON; + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); @@ -442,10 +451,10 @@ public final class FeatureFlags { return forced; } - String remote = (String) REMOTE_VALUES.get(key); - if (remote != null) { + Object remote = REMOTE_VALUES.get(key); + if (remote instanceof String) { try { - return Integer.parseInt(remote); + return Integer.parseInt((String) remote); } catch (NumberFormatException e) { Log.w(TAG, "Expected an int for key '" + key + "', but got something else! Falling back to the default."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java new file mode 100644 index 000000000..0413eefbe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Collector; +import com.annimon.stream.function.BiConsumer; +import com.annimon.stream.function.Function; +import com.annimon.stream.function.Supplier; + +import java.util.ArrayList; + +public class MappingModelList extends ArrayList> { + + public static @NonNull Collector, MappingModelList, MappingModelList> toMappingModelList() { + return new Collector, MappingModelList, MappingModelList>() { + @Override + public @NonNull Supplier supplier() { + return MappingModelList::new; + } + + @Override + public @NonNull BiConsumer> accumulator() { + return MappingModelList::add; + } + + @Override + public @NonNull Function finisher() { + return mappingModels -> mappingModels; + } + }; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index e879ebf28..43d672b85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -51,8 +51,6 @@ public class TextSecurePreferences { public static final String MMSC_USERNAME_PREF = "pref_apn_mmsc_username"; private static final String MMSC_CUSTOM_PASSWORD_PREF = "pref_apn_mmsc_custom_password"; public static final String MMSC_PASSWORD_PREF = "pref_apn_mmsc_password"; - public static final String THREAD_TRIM_LENGTH = "pref_trim_length"; - public static final String THREAD_TRIM_NOW = "pref_trim_now"; public static final String ENABLE_MANUAL_MMS_PREF = "pref_enable_manual_mms"; private static final String LAST_VERSION_CODE_PREF = "last_version_code"; @@ -74,7 +72,6 @@ public class TextSecurePreferences { private static final String SMS_DELIVERY_REPORT_PREF = "pref_delivery_report_sms"; public static final String MMS_USER_AGENT = "pref_mms_user_agent"; private static final String MMS_CUSTOM_USER_AGENT = "pref_custom_mms_user_agent"; - private static final String THREAD_TRIM_ENABLED = "pref_trim_threads"; private static final String LOCAL_NUMBER_PREF = "pref_local_number"; private static final String LOCAL_UUID_PREF = "pref_local_uuid"; private static final String LOCAL_USERNAME_PREF = "pref_local_username"; @@ -182,7 +179,6 @@ public class TextSecurePreferences { private static final String NEEDS_MESSAGE_PULL = "pref_needs_message_pull"; private static final String UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF = "pref_unidentified_access_certificate_rotation_time"; - private static final String UNIDENTIFIED_ACCESS_CERTIFICATE = "pref_unidentified_access_certificate_uuid"; public static final String UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access"; public static final String SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators"; private static final String UNIDENTIFIED_DELIVERY_ENABLED = "pref_unidentified_delivery_enabled"; @@ -598,26 +594,6 @@ public class TextSecurePreferences { setLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, value); } - public static void setUnidentifiedAccessCertificate(Context context, byte[] value) { - setStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, Base64.encodeBytes(value)); - } - - public static byte[] getUnidentifiedAccessCertificate(Context context) { - return parseCertificate(getStringPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE, null)); - } - - private static byte[] parseCertificate(String raw) { - try { - if (raw != null) { - return Base64.decode(raw); - } - } catch (IOException e) { - Log.w(TAG, e); - } - - return null; - } - public static boolean isUniversalUnidentifiedAccess(Context context) { return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false); } @@ -1035,14 +1011,6 @@ public class TextSecurePreferences { setStringPreference(context, LED_BLINK_PREF_CUSTOM, pattern); } - public static boolean isThreadLengthTrimmingEnabled(Context context) { - return getBooleanPreference(context, THREAD_TRIM_ENABLED, false); - } - - public static int getThreadTrimLength(Context context) { - return Integer.parseInt(getStringPreference(context, THREAD_TRIM_LENGTH, "500")); - } - public static boolean isSystemEmojiPreferred(Context context) { return getBooleanPreference(context, SYSTEM_EMOJI_PREF, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java b/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java deleted file mode 100644 index eac7575f0..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.widget.Toast; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.ThreadDatabase; - -public class Trimmer { - - public static void trimAllThreads(Context context, int threadLengthLimit) { - new TrimmingProgressTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadLengthLimit); - } - - private static class TrimmingProgressTask extends AsyncTask implements ThreadDatabase.ProgressListener { - private ProgressDialog progressDialog; - private Context context; - - public TrimmingProgressTask(Context context) { - this.context = context; - } - - @Override - protected void onPreExecute() { - progressDialog = new ProgressDialog(context); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(false); - progressDialog.setTitle(R.string.trimmer__deleting); - progressDialog.setMessage(context.getString(R.string.trimmer__deleting_old_messages)); - progressDialog.setMax(100); - progressDialog.show(); - } - - @Override - protected Void doInBackground(Integer... params) { - DatabaseFactory.getThreadDatabase(context).trimAllThreads(params[0], this); - return null; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - double count = progress[1]; - double index = progress[0]; - - progressDialog.setProgress((int)Math.round((index / count) * 100.0)); - } - - @Override - protected void onPostExecute(Void result) { - progressDialog.dismiss(); - Toast.makeText(context, - R.string.trimmer__old_messages_successfully_deleted, - Toast.LENGTH_LONG).show(); - } - - @Override - public void onProgress(int complete, int total) { - this.publishProgress(complete, total); - } - } -} diff --git a/app/src/main/res/drawable/ic_heart_outline_24.xml b/app/src/main/res/drawable/ic_heart_outline_24.xml new file mode 100644 index 000000000..0654b6e60 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_heart_solid_24.xml b/app/src/main/res/drawable/ic_heart_solid_24.xml new file mode 100644 index 000000000..8b0e33a84 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart_solid_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_20.xml b/app/src/main/res/drawable/ic_open_20.xml new file mode 100644 index 000000000..ef2c78bbf --- /dev/null +++ b/app/src/main/res/drawable/ic_open_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_outline_24.xml b/app/src/main/res/drawable/ic_settings_outline_24.xml new file mode 100644 index 000000000..dad0eb5d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_outline_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/base_settings_fragment.xml b/app/src/main/res/layout/base_settings_fragment.xml new file mode 100644 index 000000000..adaa315d9 --- /dev/null +++ b/app/src/main/res/layout/base_settings_fragment.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/layout/customizable_setting_edit_text.xml b/app/src/main/res/layout/customizable_setting_edit_text.xml new file mode 100644 index 000000000..049b4eb78 --- /dev/null +++ b/app/src/main/res/layout/customizable_setting_edit_text.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/layout/customizable_single_select_item.xml b/app/src/main/res/layout/customizable_single_select_item.xml new file mode 100644 index 000000000..91b5dadc9 --- /dev/null +++ b/app/src/main/res/layout/customizable_single_select_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/donate_preference_widget.xml b/app/src/main/res/layout/donate_preference_widget.xml new file mode 100644 index 000000000..70ed9737f --- /dev/null +++ b/app/src/main/res/layout/donate_preference_widget.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/group_manage_fragment.xml b/app/src/main/res/layout/group_manage_fragment.xml index 5da6d4f6d..e3ef23d12 100644 --- a/app/src/main/res/layout/group_manage_fragment.xml +++ b/app/src/main/res/layout/group_manage_fragment.xml @@ -573,7 +573,7 @@ android:gravity="center_vertical|start" android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding" android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding" - android:text="@string/ManageGroupActivity_sharable_group_link" + android:text="@string/ManageGroupActivity_group_link" android:textAlignment="viewStart" android:textAppearance="@style/Signal.Text.Body" /> diff --git a/app/src/main/res/layout/profile_create_fragment.xml b/app/src/main/res/layout/profile_create_fragment.xml index b2ade4f09..6619d483a 100644 --- a/app/src/main/res/layout/profile_create_fragment.xml +++ b/app/src/main/res/layout/profile_create_fragment.xml @@ -165,7 +165,8 @@ android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/name_container" /> + app:layout_constraintTop_toBottomOf="@id/name_container" + tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/profile_overview_username_label" + tools:visibility="visible" /> + app:srcCompat="@drawable/ic_compose_solid_24" + tools:visibility="visible" /> + diff --git a/app/src/main/res/layout/sticker_keyboard_page_list_item.xml b/app/src/main/res/layout/sticker_keyboard_page_list_item.xml index cb56ced74..379a30977 100644 --- a/app/src/main/res/layout/sticker_keyboard_page_list_item.xml +++ b/app/src/main/res/layout/sticker_keyboard_page_list_item.xml @@ -3,7 +3,4 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/sticker_keyboard_page_image" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="4dp"> - - \ No newline at end of file + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/sticker_preview_activity.xml b/app/src/main/res/layout/sticker_preview_activity.xml index 4b9a62a17..7c7f79b9a 100644 --- a/app/src/main/res/layout/sticker_preview_activity.xml +++ b/app/src/main/res/layout/sticker_preview_activity.xml @@ -17,7 +17,7 @@ قم بتحديث Signal لاستخدام روابط المجموعة - إنّ نسخة Signal التي تستعملها لا تدعم روابط المجموعة القابلة للمشاركة. قم بالتحديث لأحدث نسخة لتستطيع الإنضمام إلى هذه المجموعة باستخدام رابط. قم بتحديث Signal إضافة @@ -962,25 +961,10 @@ غير %1$s الأشخاص الذين يستطيعون تعديل عضوية المجموعة إلى \"%2$s\". تم تغيير من يمكنهم تعديل عضوية المجموعة ليصبح : \"%1$s\". - لقد فعلت رابط المجموعة القابل للمشاركة. - لقد فعلت رابط المجموعة القابل للمشاركة بعد موافقة أحد المشرفين. - لقد عطلت رابط المجموعة القابل للمشاركة. - قام %1$s بتفعيل رابط المجموعة القابل للمشاركة. - قام %1$sبتفعيل رابط المجموعة القابل للمشاركة بعد موافقة أحد المشرفين. - قام %1$s بتعطيل رابط المجموعة القابل للمشاركة. - تمّ تفعيل رابط المجموعة القابل للمشاركة. - تمّ تفعيل رابط المجموعة القابل للمشاركة بعد موافقة أحد المشرفين. - تمّ تعطيل رابط المجموعة القابل للمشاركة. - لقد قمت بإعادة تعيين رابط المجموعة القابل للمشاركة. - قام %1$s بإعادة تعيين رابط المجموعة القابل للمشاركة. - تمت إعادة تعيين رابط المجموعة القابل للمشاركة. - لقد قمت بالانضمام للمجموعة من خلال رابط المجموعة القابل للمشاركة. - قام %1$s بالانضمام للمجموعة من خلال رابط المجموعة القابل للمشاركة. لقد أرسلت طلب للانضمام للمجموعة. - أرسل %1$sطلباً للانضمام من خلال رابط المجموعة القابل للمشاركة. قبل %1$sطلبك للانضمام للمجموعة. قبل %1$s طلب %2$s انضمام للمجموعة. @@ -1919,13 +1903,9 @@ كلمة السر MMSC تقارير تسليم رسائل SMS طلب تقرير تسليم لكل رسالة SMS ترسلها - حذف الرسائل القديمة بالمحادثة عند تجاوز أقصى طول محدد. - احذف الرسائل القديمة المحادثات والوسائط التخزين حد طول المحادثة - تقليم كل المحادثات الآن - فحص وتقليم كل المحادثات وفق الحد الأقصى الأجهزة المتصلة فاتح داكن @@ -1955,13 +1935,14 @@ عند استخدام واي فاي عند استخدام التجوال تنزلي تلقائي للوسائط - تقليم الرسالة استخدام سعة التخزين الصور الفيديوهات الملفات صوت مراجعة سعة التخزين + لا شيء + مخصص استخدام الرموز التعبيرية بالنظام تعطيل الرموز التعبيرية المدمجة في Signal تمرير جميع المكالمات عبر خادوم Signal لتجنّب الإفصاح عن عنوان بروتوكول الانترنت الخاص بك إلى جهة الاتصال. تفعيل الخاصيّة سيقلل من جودة المكالمة. @@ -2320,6 +2301,7 @@ تسجيل Signal - رمز التّحقق لِ Android أبداً مجهول + لا أحد قفل الشاشة منع الوصول إلى Signal عبر قفل الشاشة أو بصمة الإصبع نفاذ مهلة قفل الشاشة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 247a3b7d5..412a4326b 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -1081,12 +1081,12 @@ MMSC Şifrəsi SMS çatdırılma hesabatları Hər göndərdiyin SMS üçün çatdırılma hesabatı tələb et - Müəyyən uzunluğu keçən köhnə mesajları avtomatik olaraq sil - Köhnə mesajları sil + + Söhbətlər və media Söhbətləşmənin uzunluq limiti - Bütün söhbətləri indi təmizlə - Bütün söhbətləri yoxla və onları uzunluq limitlərinə görə idarə et + + Bağlantısı olan cihazlar Açıq Tünd @@ -1110,7 +1110,7 @@ Wi-Fi istifadə edərkən Rominq zamanı Medianın avtomatik yüklənməsi - Mesajların təmizlənməsi + Səs Sistem smaylikini istifadə et Signal-ın daxili smaylik dəstəyini deaktiv et diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 9f63a6c3c..d625e7664 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1122,12 +1122,12 @@ MMSC Парола SMS потвърждения за получаване Поискай потвърждение при получаване за всяко изпратено SMS съобщение - Автоматично изтрий най-старите съобщения, когато разговорът достигне определена дължина. - Изтрий старите съобщения + + Чат и мултимедия Граница за дължина на разговора - Скъси всички разговори сега - Сканирай всички разговори и приложи ограничението за дължина на разговори + + Свързани устройства Светла Тъмна @@ -1151,7 +1151,7 @@ Когато се използва Wi-Fi Когато се използва роуминг Автоматично изтегляне на мултимедия - Съкращаване на съобщенията + Аудио Използване на вградените emoji-та Деактивирай вградена в Signal подръжка на emoji-та diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index b39b59efb..975716571 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1510,13 +1510,9 @@ এমএমএসসি পাসওয়ার্ড এসএমএস বিতরণ রিপোর্ট আপনার প্রেরিত প্রতিটি এসএমএস বার্তার জন্য একটি বিতরণ রিপোর্টের জন্য অনুরোধ করুন - কোনও কথোপকথন একটি নির্দিষ্ট দৈর্ঘ্য ছাড়িয়ে গেলে স্বয়ংক্রিয়ভাবে পুরানো বার্তাগুলি মুছে দিন - পুরানো বার্তা মুছে দিন চ্যাট আর মিডিয়া স্টোরেজ কথোপকথন দৈর্ঘ্য সীমা - এখন সমস্ত কথোপকথন সংক্ষিপ্ত করুন - সমস্ত কথোপকথন স্ক্যান করুন এবং কথোপকথন দৈর্ঘ্যের সীমাটি প্রয়োগ করুন সংযুক্ত ডিভাইস সমূহ আলো অন্ধকার @@ -1541,13 +1537,13 @@ ওয়াইফাই ব্যবহার করার সময় রোমিং করার সময় মিডিয়া অটো-ডাউনলোড - বার্তা ছাঁটাই স্টোরেজ ব্যবহার ছবিসমূহ ভিডিওসমূহ ফাইলসমূহ শব্দ স্টোরেজ পর্যালোচনা + কিছুই না সিস্টেম ইমোজি ব্যবহার করুন Signal এর অন্তর্নির্মিত ইমোজি সমর্থন অক্ষম করুন আপনার যোগাযোগের আইপি ঠিকানাটি প্রকাশ না করার জন্য Signal সার্ভারের মাধ্যমে কল সমূহ রিলে করুন। সক্ষম করা কল এর মান হ্রাস পাবে| diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 1ae6a0fce..1a398fc27 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -467,7 +467,6 @@ Zahtjevi za članstvo na čekanju Nema zahtjeva za članstvo. - Osobe s ovog popisa pokušavaju pristupiti grupi putem dobijenog linka za grupu. Uvršten/a \"%1$s\" Odbijen/a \"%1$s\" @@ -512,7 +511,7 @@ Izmijeni informacije o grupi Odredite ko može promijeniti naziv i sliku grupe, te vrijeme za nestajanje poruka. Odredite ko može uvrstiti ili pozvati nove članove. - Link grupe: za dijeljenje + Link grupe Blokiraj grupu Prestani s blokiranjem grupe Napusti grupu @@ -638,7 +637,6 @@ Uskoro: linkovi za grupe Ažurirajte Signal da biste mogli koristiti grupne linkove Pristupanje grupi putem linka nije još podržano u Signalu. To će biti moguće u jednoj od narednih verzija. - Verzija Signala koju koristite ne podržava dijeljenje grupnih linkova. Instalirajte zadnju verziju da biste se mogli priključiti ovoj grupi putem linka. Ažurirajte Signal Link za grupu nije ispravan @@ -886,25 +884,10 @@ %1$s je promijenio/la ovlasti za upravljanje članstvom u grupi na \"%2$s\". Ko može upravljati članstvom u grupi promijenjeno je u \"%1$s\". - Aktivirali ste mogućnost dijeljenja linka za grupu. - Aktivirali ste mogućnost dijeljenja linka za grupu uz administratorovo odobrenje. - Isključili ste mogućnost dijeljenja linka za grupu. - %1$s je aktivirao/la mogućnost dijeljenja linka za grupu. - %1$s je aktivirao/la mogućnost dijeljenja linka za grupu uz administratorovo odobrenje. - %1$s je isključio/la mogućnost dijeljenja linka za grupu. - Mogućnost dijeljenja linka za grupu aktivirana je. - Mogućnost dijeljenja linka za grupu aktivirana je uz administratorovo odobrenje. - Mogućnost dijeljenja linka za grupu isključena je. - Kreirali ste iznova link za grupu. - %1$s je iznova kreirao/la link za grupu. - Link za grupu kreiran je iznova. - Pristupili ste grupi putem dobijenog linka za grupu. - %1$s pristupio/la je grupi putem dobijenog linka za grupu. Poslali ste zahtjev za pristupanje grupi. - %1$s traži odobrenje da pristupi grupi putem dobijenog linka za grupu. %1$s odobrio/la je Vaš zahtjev za pristupanje grupi. %1$s odobrio/la je zahtjev za pristupanje grupi koji je uputio/la %2$s. @@ -1804,13 +1787,9 @@ MMSC lozinka SMS izvještaj o isporučenju Traži izvještaj o isporučenju za svaku SMS poruku koju pošaljete - Automatski izbriši starije poruke nakon što prekorače utvrđenu dužinu - Izbriši stare poruke Poruke i datoteke Memorija Maksimalni broj poruka - Skrati sve konverzacije odmah - Pregledaj sve konverzacije i primijeni ograničenje dužine Povezani uređaji Svijetla Tamna @@ -1840,13 +1819,13 @@ Prilikom korištenja Wi-Fi konekcije Prilikom roaminga Automatsko preuzimanje datoteka - Skraćivanje poruke Korištenje memorije Slike Videozapisi Datoteke Zvuk Provjerite memoriju + Nijedno Koristi sistemske emoji sličice Onemogući Signalovu ugrađenu podršku za emoji sličice Preusmjeri sve pozive na Signal server kako bi Vaša IP adresa bila skrivena od sagovornika. Aktiviranjem ove opcije smanjit će se kvalitet poziva. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 76fb86c47..3a0066603 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -449,7 +449,6 @@ Sol·licituds d\'adhesió pendents No hi ha cap sol·licitud d\'adhesió per mostrar. - La gent d\'aquesta llista intenta afegir-se a aquest grup per l\'enllaç de grup que es pot compartir. S\'hi ha afegit %1$s S\'ha rebutjat %1$s @@ -491,7 +490,7 @@ Edita la informació del grup Trieu qui pot editar el nom del grup, l\'avatar i els missatges efímers. Trieu qui pot afegir-hi o convidar-hi membres nous. - Enllaç de grup que es pot compartir + Enllaç del grup Bloca el grup Desbloca el grup Abandona el grup @@ -611,7 +610,6 @@ Aviat hi haurà enllaços de grup Actualitzeu el Signal per usar els enllaços de grup Afegir-se a un grup per un enllaç encara no és possible amb el Signal. Aquesta funció s\'activarà en una actualització propera. - La versió del Signal que useu no admet enllaços de grup compartibles. Actualitzeu a la versió més recent per afegir-vos a aquest grup per l\'enllaç. Actualitza el Signal L\'enllaç de grup no és vàlid. @@ -847,25 +845,10 @@ %1$s ha canviat qui pot editar els membres del grup a \"%2$s\". S\'ha canviat qui pot editar els membres del grup a \"%1$s\". - Heu activat l\'enllaç de grup que es pot compartir. - Heu activat l\'enllaç de grup que es pot compartir amb aprovació de l\'administrador/a. - Heu desactivat l\'enllaç de grup que es pot compartir. - %1$s ha activat l\'enllaç de grup que es pot compartir. - %1$s ha activat l\'enllaç de grup que es pot compartir amb aprovació de l\'administrador/a. - %1$s ha desactivat l\'enllaç de grup que es pot compartir. - S\'ha activat l\'enllaç de grup que es pot compartir. - S\'ha activat l\'enllaç de grup que es pot compartir amb aprovació de l\'administrador/a. - S\'ha desactivat l\'enllaç de grup que es pot compartir. - Heu restablert l\'enllaç de grup que es pot compartir. - %1$s ha restablert l\'enllaç de grup que es pot compartir. - S\'ha restablert l\'enllaç de grup que es pot compartir. - Us heu afegit al grup per l\'enllaç de grup que es pot compartir. - %1$ss\'ha afegit al grup per l\'enllaç de grup que es pot compartir. Heu enviat la sol·licitud per afegir-vos al grup. - %1$sha demanat afegir-s\'hi per l\'enllaç de grup que es pot compartir. %1$s ha aprovat la sol·licitud per afegir-vos al grup. %1$s ha aprovat una sol·licitud de %2$sper afegir-se al grup. @@ -1744,13 +1727,9 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no Contrasenya de MMSC Informes de lliurament d\'SMS Demana un informe de lliurament per cada missatge SMS que envieu. - Suprimeix automàticament els missatges antics si la conversa excedeix d\'una mida concreta. - Suprimeix els missatges antics Xats i multimèdia Emmagatzematge Límit de la mida de la conversa - Escapça totes les converses - Escaneja totes les converses i aplica-hi els límits de mida Dispositius enllaçats Clar Fosc @@ -1780,13 +1759,14 @@ S\'ha rebut un missatge d\'intercanvi de claus per a una versió del protocol no En usar Wi-Fi En itinerància Baixada automàtica de contingut multimèdia - Escapçament de missatges Ús de l\'emmagatzematge Fotografies Vídeos Fitxers Àudio Revisa l\'emmagatzematge + Cap + Personalitzat Usa els emoji del sistema Desactiva els emojis inclosos al Signal Retransmet totes les trucades a través del servidor del Signal per evitar revelar l\'adreça IP al contacte. Activar-ho reduirà la qualitat de la trucada. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0a3c2c275..e08a592e2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -485,6 +485,8 @@ Čekající žádosti o členství Ždáné žádosti o členství k zobrazení + Přidán \"%1$s\" + Odmítnut \"%1$s\" Hotovo Tohoto člověka nemůžete přidat do zastaralých skupin. @@ -525,10 +527,12 @@ Zmizení zpráv Nevyřízené skupinové pozvánky + Požadavky & pozvánky členů Přidat členy Upravit informace o skupině Vyberte kdo může měnit jméno a obrázek skupiny a časovač mizejících zpráv. Vyberte, kdo může přidat nebo pozvat nové členy. + Skupinový odkaz Zablokovat skupinu Odblokovat skupinu Opustit skupinu @@ -615,11 +619,23 @@ Zakázáno Výchozí + Sdílený skupinový odkaz + Spravovat & sdílet + Skupinový odkaz Sdílet + Obnovit odkaz + Požadavky členů + Schválit nové členy Povoleno Zakázáno Výchozí + Obnovit skupinový odkaz + Požadovat, aby správce schválil nové členy, kteří se připojují přes skupinový odkaz. + Opravdu chcete obnovit skupinový odkaz? Pomocí současného odkazu se pak už lidé nebudou schopni ke skupině připojit. + QR kód + Lidé, kteří naskenují tento kód, budou schopni připojit se k vaší skupině. Správci stále budou muset schválit nové členy, pokud jste to tak nastavili. + Sdílet kód Chcete zrušit pozvání, které jste zaslali %1$s? @@ -632,17 +648,29 @@ Už jste členem Připojit Žádost o připojení + Nelze se připojit ke skupině. Prosím zkuste to znovu později Došlo k chybě v síti. Tento odkaz na skupinu není aktivní Nelze získat informace o skupině, zkuste to, prosím, později. Připojit se k této skupině a sdílet vaše jméno a fotografii s jejími členy? Správce této skupiny musí schválit váš požadavek na přístup, než budete moci do této skupiny vstoupit. Součástí požadavku je poskytnutí vašeho jména a fotografie členům skupiny. + + Skupina · %1$d člen + Skupina · %1$d členové + Skupina · %1$d členů + Skupina · %1$d členů + + Těšte se na skupinové odkazy Aktualizujte Signal pro používání skupinových odkazů - Verze Signalu, kterou používáte, nepodporuje sdílené skupinové odkazy. Aktualizujte na nejnovější verzi, abyste se mohli připojit k této skupině pomocí odkazu. + Připojení ke skupině pomocí odkazu ještě není v Signalu podporováno. Tato funkce bude zahrnuta v nadcházející aktualizaci. Aktualizovat Signal + Skupinový odkaz je neplatný + Přidat \"%1$s\" do skupiny? + Odmítnout požadavek od \"%1$s\"? Přidat + Odmítnout Skupinový avatar Avatar @@ -900,10 +928,17 @@ Odeslali jste požadavek na vstup do skupiny. + %1$s schválil váš požadavek na připojení ke skupině. + %1$s schválil požadavek na připojení ke skupině od %2$s. + Schválili jste požadavek na připojení ke skupině od %1$s. Požadavek na vstup do skupiny byl schválen. + Požadavek na připojení ke skupině od %1$s byl schválen. Požadavek na vstup do skupiny byl zamítnut administrátorem. + %1$s odmítl požadavek na připojení ke skupině od %2$s. Požadavek na vstup do skupiny od %1$s byl zamítnut. + Zrušili jste svůj požadavek na připojení ke skupině. + %1$s zrušili svůj požadavek na připojení ke skupině. Váš bezpečnostní kód s %s se změnil. Označili jste vaše bezpečnostní kódy pro komunikaci s %s jako ověřené. @@ -1241,6 +1276,7 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Uživatelská jména nesmí začínat číslicí. Uživatelské jméno je neplatné. Uživatelská jména musí mít %1$d až %2$dznaků. + Uživatelská jména v Signalu jsou volitelná. Pokud si vytvoříte uživatelské jméno, ostatní uživatelé Signalu vás budou schopni podle něj najít a kontaktovat bez znalosti vašeho telefonního čísla. Váš kontakt používá starou verzi aplikace Signal. Před kontrolou bezpečnostních kódů jej prosím požádejte, aby si aplikaci aktualizoval. Váš kontakt používá novější verzi aplikace Signal s nekompatibilním formátem QR kódu. Prosím aktualizujte svou verzi, abyste mohli QR kódy porovnat. @@ -1804,13 +1840,9 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. MMSC Heslo Doručenky SMS U každé odeslané zprávy požadovat potvrzení o doručení - Automaticky mazat starší zprávy jakmile konverzace přesáhne nastavenou délku - Smazat staré zprávy Konverzace a média Úložiště Limit délky konverzací - Zkrátit všechny konverzace teď - Prohledat všechny konverzace a uplatnit limit délky. Propojená zařízení Světlý Tmavý @@ -1840,13 +1872,13 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Při použití WiFi Při roamingu Automatické stahování multimédií - Zkracování zpráv Využití úložiště Fotografie Videa Soubory Audio Zkontrolovat úložiště + Nic Použít systémové smajlíky Zakázat použití interních smajlíků aplikace Signal Přenášet všechna volání přes servery Signal, aby nedošlo k odhalení vaší IP adresy volanému. Povolením dojde ke zhoršení kvality hovoru. @@ -1874,6 +1906,7 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Zmínky Upozornit mne Přijmout upozornění při zmínce ve ztišených rozhovorech + Nastavit uživatelské jméno @@ -2267,6 +2300,8 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Odebrat Zkopírováno do schránky Správce + Schválit + Odmítnout Starší a nové skupiny Co jsou starší skupiny? @@ -2275,8 +2310,11 @@ Obdržen požadavek na výměnu klíčů pro neplatnou verzi protokolu. Starší skupiny nemohou být převedeny na nové, ale můžete vytvořit novou skupinu se stejnými členy. Pro vytvoření nové skupiny je třeba, aby všichni členové skupiny aktualizovali na poslední verzi Signal. + Sdílet přes Signal Kopírovat + QR kód Sdílet Zkopírováno do schránky + Odkaz není v současné době aktivní diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 27d2f795f..362654d37 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -618,7 +618,6 @@ Send neges heb ei ddiogelu? Profwyd gwall rhwydwaith. Diweddarwch Signal i ddefnyddio dolenni grŵp - Nid yw\'r fersiwn o Signal rydych yn ei ddefnyddio\'n cynnal dolenni grwpiau. Diweddarwch i\'r fersiwn diweddaraf i ymuno â\'r grŵp drwy ddolen. Diweddaru Signal Ychwanegu @@ -782,8 +781,8 @@ Send neges heb ei ddiogelu? Gosododd %1$s amserydd y neges diflannu i %2$s. Mae\'r amserydd neges sy\'n diflannu wedi\'i osod i %1$s. - Newidiodd %$s eu henwau proffil i %2$s. - Newidiodd %$s eu henwau proffil o %2$s i %3$s. + Newidiodd %1$s eu henwau proffil i %2$s. + Newidiodd %1$s eu henwau proffil o %2$s i %3$s. Newidiodd %1$s eu proffil. Crëwyd y grŵp gennych chi. @@ -1777,13 +1776,9 @@ Send neges heb ei ddiogelu? Cyfrinair MMSC Adroddiadau trosglwyddo SMS Gofyn am adroddiad trosglwyddo pob neges SMS rydych chi\'n ei anfon - Dileu negeseuon hŷn yn ddiofyn pan fydd y sgwrs yn fwy na maint penodol - Dileu hen negeseuon Sgyrsiau a chyfryngau Storfa Maint uchaf sgyrsiau - Tocio bob sgwrs nawr - Sganio drwy\'r holl sgyrsiau a gorfodi terfynau hyd sgwrsio Dyfeisiau cysylltiedig Golau Tywyll @@ -1813,13 +1808,13 @@ Send neges heb ei ddiogelu? Wrth ddefnyddio diwifr Wrth grwydro Awto lwytho cyfryngau - Tocio sgyrsiau Storfa wedi ei ddefnyddio Lluniau Fideos Ffeiliau Sain Adolygu\'r storfa + Dim Defnyddio gwenogluniau\'r system Analluogi cefnogaeth gwenogluniau Signal Ail-alw pob galwad drwy\'r gweinydd Signal i osgoi datgelu eich cyfeiriad IP i\'ch cyswllt. Bydd galluogi yn lleihau ansawdd galwadau. diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index d07531035..564899d0a 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -450,7 +450,7 @@ Afventende medlemsanmodninger Ingen medlemsanmodninger - Personer på denne liste forsøger at deltage i gruppen via det delbare gruppelink + Personer på denne liste forsøger at deltage i gruppen via gruppelinket. \"%1$s\" tilføjet %1$s\" afvist @@ -492,7 +492,7 @@ Rediger gruppeinformation Vælg hvem som kan ændre navn og avatar for gruppen, og beskeder med tidsudløb Vælg hvem der kan tilføje eller invitere nye medlemmer - Delbart gruppelink + Gruppelink Blokér gruppe Ophæv blokering af gruppe Forlad gruppe @@ -612,7 +612,7 @@ Gruppelinks kommer snart Opdatér Signal for at bruge gruppelinks Deltagelse i en gruppe via et link understøttes endnu ikke af Signal. Funktionen frigives i en kommende opdatering - Signal versionen du bruger, understøtter ikke delbare gruppelinks. Opdater til den nyeste version for at deltage i gruppen via linket + Signal-versionen som du anvender, understøtter ikke gruppelinks. Opdater til den nyeste version for at deltage i denne gruppe via link. Opdatér Signal Gruppelink er ikke gyldigt @@ -848,25 +848,25 @@ %1$s ændrede, hvem som kan redigere medlemskab af gruppen til \"%2$s\" Hvem som kan redigere medlemskaber af gruppen er blevet ændret til \"%1$s\" - Du aktiverede det delbare gruppelink - Du aktiverede det delbare gruppelink med administratorgodkendelse - Du déaktiverede det delbare gruppelink - %1$s aktiverede det delbare gruppelink - %1$s aktiverede det delbare gruppelink med administratorgodkendelse - %1$s déaktiverede det delbare gruppelink - Det delbare gruppelink blev aktiveret - Det delbare gruppelink blev aktiveret med administratorgodkendelse - Det delbare gruppelink blev déaktiveret + Du aktiverede gruppelinket med administratorgodkendelse slået fra. + Du aktiverede gruppelinket med administratorgodkendelse slået til. + Du deaktiverede gruppelinket. + %1$s aktiverede gruppelinket med administratorgodkendelse slået fra. + %1$s aktiverede gruppelinket med administratorgodkendelse slået til. + %1$s deaktiverede gruppelinket. + Gruppelinket blev aktiveret med administratorgodkendelse slået fra. + Gruppelinket blev aktiveret med administratorgodkendelse slået til. + Gruppelinket blev deaktiveret. - Du nulstiller det delbare gruppelink - %1$s nulstiller det delbare gruppelink - Det delbare gruppelink blev nulstillet + Du nulstiller gruppelinket. + %1$s nulstiller gruppelinket. + Gruppelink blev nulstillet. - Du deltager i gruppen via det delbare gruppelink - %1$s deltager i gruppen via det delbare gruppelink + Du deltager i gruppen via gruppelinket. + %1$s deltager i gruppen via gruppelinket. Du sendte en anmodning om at deltage i gruppen - %1$s anmodede om at deltage via det delbare gruppelink + %1$s anmodede om at deltage via gruppelinket. %1$s godkendte din anmodning om at deltage i gruppen %1$s godkendte en anmodning fra %2$s om at deltage i gruppen @@ -1447,6 +1447,7 @@ Modtog en nøgle besked, for en ugyldig protokol-version Sikkerhedsnummer ændringer Send alligevel + Ring alligevel Følgende personer kan have geninstalleret eller skiftet enheder. Bekræft dit sikkerhedsnummer med dem for at sikre privatlivets fred Vis Tidligere bekræftet @@ -1750,13 +1751,11 @@ Modtog en nøgle besked, for en ugyldig protokol-version MMSC Kodeord Leveringsrapporter for SMS Modtag leveringsrapporter for hver SMS besked du sender - Sletter automatisk ældre beskeder når antallet overstiger det specificerede - Slet gamle beskeder Chat og medier Hukommelse Max. grænse for samtaler - Trim alle samtaler - Skan alle samtaler og reducér antallet til det maksimalt ønskede + Bevar beskeder + Ryd beskedhistorik Forbundne enheder Lyst Mørkt @@ -1786,17 +1785,34 @@ Modtog en nøgle besked, for en ugyldig protokol-version Ved brug af WiFi Ved roaming Hent automatisk multimedier - Trimning af beskeder + Beskedhistorik Brug af hukommelse Billeder Videoer Filer Lyd Vis hukommelse + Slet ældre beskeder? + Ryd beskedhistorik? + Dette sletter al beskedhistorik og medier permanent fra din enhed, der er ældre end %1$s. + Dette vil reducere alle samtaler permanent til de %1$s seneste beskeder. + Dette sletter al beskedhistorik og medier permanent fra din enhed. + Er du sikker på, du vil slette al beskedhistorik? + Al beskedhistorik fjernes permanent. Denne handling kan ikke fortrydes. + Slet det hele nu + For evigt + 1 år + 6 måneder + 30 dage + Ingen + %1$s beskeder + Tilpasset + Tilpas begrænsning af samtalelængde Anvend system emoji Deaktivér Signal´s indbyggede emoji understøttelse Videresend alle opkald gennem Signal serveren, for at undgå at afsløre din IP-adresse for din kontakt. Opkaldskvaliteten vil blive forringet Videresend altid opkald + Hvem kan… App adgang Kommunikation Chats @@ -1821,6 +1837,7 @@ Modtog en nøgle besked, for en ugyldig protokol-version Underret mig Modtag meddelelser, når du er omtalt i udsatte chats Opret et brugernavn + Tilpas mulighed @@ -2119,6 +2136,14 @@ Modtog en nøgle besked, for en ugyldig protokol-version Signal registration - Verifikationskode for Android Aldrig Ukendt + Se mit telefonnummer + Find mig via telefonnummer + Alle + Mine kontakter + Ingen + Dit telefonnummer vil være synligt for alle personer og grupper, du sender beskeder til. + Enhver, der har dit telefonnummer i sine kontakter, vil se dig som en kontakt på Signal. Andre vil være i stand til at finde dig i søgning. + Kun dine kontakter kan se dit telefonnummer på Signal. Skærmlås Lås adgang til Signal med Android skærmlås eller fingeraftryk Timeout for inaktiv skærmlås diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index abec158cf..2a194a47e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -449,7 +449,7 @@ Ausstehende Beitrittsanfragen Keine Beitrittsanfragen vorhanden. - Personen auf dieser Liste versuchen der Gruppe über den teilbaren Gruppen-Link beizutreten. + Personen auf dieser Liste versuchen der Gruppe über den Gruppen-Link beizutreten. »%1$s« hinzugefügt »%1$s« abgelehnt @@ -491,7 +491,7 @@ Gruppendetails bearbeiten Wähle aus, wer Gruppenname, Avatar und verschwindende Nachrichten bearbeiten kann. Wähle aus, wer neue Mitglieder hinzufügen oder einladen kann. - Teilbarer Gruppen-Link + Gruppen-Link Gruppe blockieren Gruppe freigeben Gruppe verlassen @@ -847,25 +847,25 @@ %1$s hat die Bearbeitungsberechtigten für die Gruppenmitgliedschaft zu »%2$s« geändert. Die Bearbeitungsberechtigten für die Gruppenmitgliedschaft wurden zu »%1$s« geändert. - Du hast den teilbaren Gruppen-Link aktiviert. - Du hast den teilbaren Gruppen-Link mit Admin-Bestätigung aktiviert. - Du hast den teilbaren Gruppen-Link deaktiviert. - %1$s hat den teilbaren Gruppen-Link aktiviert. - %1$s hat den teilbaren Gruppen-Link mit Admin-Bestätigung aktiviert. - %1$s hat den teilbaren Gruppen-Link deaktiviert. - Der teilbare Gruppen-Link wurde aktiviert. - Der teilbare Gruppen-Link wurde mit Admin-Bestätigung aktiviert. - Der teilbare Gruppen-Link wurde deaktiviert. + Du hast den Gruppen-Link ohne Admin-Bestätigung aktiviert. + Du hast den Gruppen-Link mit Admin-Bestätigung aktiviert. + Du hast den Gruppen-Link deaktiviert. + »%1$s« hat den Gruppen-Link ohne Admin-Bestätigung aktiviert. + »%1$s« hat den Gruppen-Link mit Admin-Bestätigung aktiviert. + »%1$s« hat den Gruppen-Link deaktiviert. + Der Gruppen-Link ohne Admin-Bestätigung wurde aktiviert. + Der Gruppen-Link mit Admin-Bestätigung wurde aktiviert. + Der Gruppen-Link wurde deaktiviert. - Du hast den teilbaren Gruppen-Link zurückgesetzt. - %1$s hat den teilbaren Gruppen-Link zurückgesetzt. - Der teilbare Gruppen-Link wurde zurückgesetzt. + Du hast den Gruppen-Link zurückgesetzt. + »%1$s« hat den Gruppen-Link zurückgesetzt. + Der Gruppen-Link wurde zurückgesetzt. - Du bist der Gruppe über den teilbaren Gruppen-Link beigetreten. - %1$s ist der Gruppe über den teilbaren Gruppen-Link beigetreten. + Du bist der Gruppe über den Gruppen-Link beigetreten. + »%1$s« ist der Gruppe über den Gruppen-Link beigetreten. Du hast eine Gruppenbeitrittsanfrage versendet. - %1$s hat eine Beitrittsanfrage über den teilbaren Gruppen-Link gestellt. + »%1$s« hat eine Beitrittsanfrage über den Gruppen-Link gestellt. %1$s hat deine Gruppenbeitrittsanfrage bestätigt. %1$s hat eine Gruppenbeitrittsanfrage von %2$s bestätigt. @@ -1436,6 +1436,7 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangen Änderungen der Sicherheitsnummer Trotzdem versenden + Trotzdem anrufen Die folgenden Personen haben Signal vielleicht erneut installiert oder das Gerät gewechselt. Verifiziert eure gemeinsame Sicherheitsnummer zur Sicherstellung der Privatsphäre. Anzeigen Zuvor verifiziert @@ -1739,13 +1740,11 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenMMSC-Passwort SMS-Zustellberichte Zustellbericht für jede gesendete SMS anfordern - Alte Nachrichten automatisch löschen, sobald eine Unterhaltung die angegebene Länge überschreitet - Alte Nachrichten löschen Unterhaltungen und Medieninhalte Speicher Höchstzahl an Nachrichten - Alle Unterhaltungen jetzt kürzen - Alle Unterhaltungen prüfen und Längenbegrenzung anwenden + Nachrichten behalten + Nachrichtenverlauf löschen Gekoppelte Geräte Hell Dunkel @@ -1775,17 +1774,34 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenBei WLAN-Verbindung Bei Roaming Medieninhalte autom. herunterladen - Unterhaltungen kürzen + Nachrichtenverlauf Speicherbelegung Bilder Videos Dateien Audio Speicherinhalte überprüfen + Ältere Nachrichten löschen? + Nachrichtenverlauf löschen? + Dies wird alle Nachrichtenverläufe und Medien, die älter als %1$s sind, dauerhaft von deinem Gerät löschen. + Dies löscht sofort alle Nachrichten bis auf die letzten %1$s Nachrichten je Unterhaltung. + Dies wird alle Nachrichtenverläufe und Medien dauerhaft von deinem Gerät löschen. + Möchtest du wirklich alle Nachrichtenverläufe löschen? + Alle Nachrichtenverläufe werden dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden. + Alle jetzt löschen + Für immer + 1 Jahr + 6 Monate + 30 Tage + Keine + %1$s Nachrichten + Personalisiert + personalisierte Höchstzahl an Nachrichten System-Emojis verwenden Integrierte Emojis deaktivieren und stattdessen System-Emojis verwenden Alle Anrufe über den Signal-Server leiten, um die eigene IP-Adresse gegenüber Kontakten nicht offenzulegen. Dies verringert die Anrufqualität. Anrufe immer indirekt + Wer kann… App-Zugriff Kommunikation Unterhaltungen @@ -1810,6 +1826,7 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenMich benachrichtigen Benachrichtigungen erhalten, wenn du in stummgeschalteten Unterhaltungen erwähnt wirst Benutzername einrichten + Einstellungen anpassen @@ -2108,6 +2125,14 @@ Schlüsselaustausch-Nachricht für eine ungültige Protokollversion empfangenSignal-Registrierung – Verifikationscode für Android Nie Unbekannt + meine Telefonnummer sehen + mich über die Telefonnummer finden + Jeder + Meine Kontakte + Niemand + Deine Telefonnummer ist sichtbar für alle Personen und Gruppen, mit denen du dich unterhälst. + Jeder, der deine Telefonnummer in seinen Kontakten hat, sieht dich als Kontakt in Signal. Andere können dich per Suche finden. + Nur deine Kontakte sehen deine Telefonnummer in Signal. Bildschirmsperre Zugriff auf Signal mit Android-Bildschirmsperre oder Fingerabdruck sperren Autom. Sperre bei Inaktivität diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 7ebe2c9d9..7f2347106 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -449,7 +449,7 @@ Αιτήματα μελών σε εκκρεμότητα Δεν υπάρχουν αιτήματα μελών. - Τα άτομα σε αυτή τη λίστα έχουν προσπαθήσει να μπουν στην ομάδα μέσω του συνδέσμου για είσοδο στην ομάδα. + Τα άτομα σε αυτή τη λίστα έχουν προσπαθήσει να μπουν στην ομάδα μέσω του συνδέσμου της ομάδας. Ο/Η \"%1$s\" προστέθηκε Ο/Η \"%1$s\" απορρίφθηκε @@ -491,7 +491,7 @@ Επεξεργασία πληροφοριών ομάδας Επέλεξε ποιός μπορεί να επεξεργαστεί το όνομα της ομάδας, το εικονίδιο, και τα μηνύματα που εξαφανίζονται. Επέλεξε ποιος μπορεί να προσθέσει ή να προσκαλέσει νέα μέλη. - Σύνδεσμος για είσοδο στην ομάδα + Σύνδεσμος ομάδας Φραγή ομάδας Κατάργηση φραγής ομάδας Αποχώρηση απ\' την ομάδα @@ -611,7 +611,7 @@ Οι σύνδεσμοι ομάδων έρχονται σύντομα Αναβάθμσε το Signal για να χρησιμοποιήσεις συνδέσμους σε ομάδες. Η εγγραφή σε ομάδα μέσω συνδέσμου δεν υποστηρίζεται ακόμα από το Signal. Η δυνατότητα αυτή θα είναι διαθέσιμη σε επόμενη ενημέρωση. - Η έκδοση Signal που χρησιμοποιείς δεν υποστηρίζει μοιραζόμενους συνδέσμους σε ομάδες. Αναβάθμισε στη τελευταία έκδοση για να μπεις στην ομάδα μέσω συνδέσμου. + Η έκδοση Signal που χρησιμοποιείς δεν υποστηρίζει συνδέσμους ομαδών. Αναβάθμισε στη τελευταία έκδοση για να μπεις στην ομάδα μέσω του συνδέσμου. Αναβάθμιση του Signal Ο σύνδεσμος ομάδας δεν είναι έγκυρος @@ -847,25 +847,25 @@ Ο/Η %1$s άλλαξε το ποιός μπορεί να επεξεργάζεται τα μέλη της ομάδας σε \"%2$s\". Άλλαξε το ποιός μπορεί να επεξεργάζεται τα μέλη της ομάδας σε \"%1$s\". - Ενεργοποίησες το σύνδεσμο για είσοδο στην ομάδα. - Ενεργοποίησες το σύνδεσμο για είσοδο στην ομάδα με έγκριση διαχειριστή. - Απενεργοποίησες το σύνδεσμο για είσοδο στον ομάδα - Ο/Η %1$s ενεργοποίησε το σύνδεσμο για είσοδο στην ομάδα. - Ο/Η %1$s ενεργοποίησε το σύνδεσμο για είσοδο στην ομάδα με έγκριση διαχειριστή. - Ο/Η %1$sαπενεργοποίησε το σύνδεσμο για είσοδο στην ομάδα. - Ο σύνδεσμος για είσοδο στην ομάδα ενεργοποιήθηκε. - Ο σύνδεσμος για είσοδο στην ομάδα με έγκριση διαχειριστή ενεργοποιήθηκε. - Ο σύνδεσμος για είσοδο στην ομάδα απενεργοποιήθηκε. + Ενεργοποίησες το σύνδεσμο της ομάδας με απενεργοποιημένη την έγκριση διαχειριστή. + Ενεργοποίησες το σύνδεσμο της ομάδας με ενεργοποιημένη την έγκριση διαχειριστή. + Απενεργοποίησες το σύνδεσμο της ομάδας. + Ο/Η %1$s ενεργοποίησε το σύνδεσμο της ομάδας με απενεργοποιημένη την έγκριση διαχειριστή. + Ο/Η %1$s ενεργοποίησε το σύνδεσμο της ομάδας με ενεργοποιημένη την έγκριση διαχειριστή. + Ο/Η %1$s απενεργοποίησε το σύνδεσμο της ομάδας. + Ενεργοποιήθηκε ο σύνδεσμος της ομάδας με απενεργοποιημένη την έγκριση διαχειριστή. + Ενεργοποιήθηκε ο σύνδεσμος της ομάδας με ενεργοποιημένη την έγκριση διαχειριστή. + Ο σύνδεσμος της ομάδας απενεργοποιήθηκε. - Επανέφερες το σύνδεσμο για είσοδο στην ομάδα - Ο/Η %1$s επανέφερε το σύνδεσμο για είσοδο στην ομάδα - Ο σύνδεσμος για είσοδο στην ομάδα επαναφέρθηκε. + Επανέφερες το σύνδεσμο της ομάδας. + Ο/Η %1$s επανέφερε το σύνδεσμο της ομάδας. + Ο σύνδεσμος της ομάδας επαναφέρθηκε. - Μπήκες στην ομάδα μέσω του συνδέσμου για είσοδο στην ομάδα. - Ο/Η %1$s μπήκε στην ομάδα μέσω του συνδέσμου για είσοδο στην ομάδα. + Μπήκες στην ομάδα μέσω του συνδέσμου της ομάδας. + Ο/Η %1$s μπήκε στην ομάδα μέσω του συνδέσμου της ομάδας. Έστειλες αίτημα για να μπείς στην ομάδα. - Ο/Η %1$s ζήτησε να μπει στην ομάδα μέσω του συνδέσμου για είσοδο στην ομάδα. + Ο/Η %1$s ζήτησε να μπει στην ομάδα μέσω του συνδέσμου της ομάδας. Ο/Η %1$s ενέκρινε το αίτημά σου να μπεις στην ομάδα. Ο/Η %1$s ενέκρινε το αίτημα του/της %2$s να μπει στην ομάδα. @@ -1442,6 +1442,7 @@ Αλλαγές αριθμού ασφαλείας Αποστόλη παρ\' όλα αυτά + Κλήση παρ\' όλα αυτά Τα παρακάτω άτομα ίσως επανεγκατεστησαν το Signal ή άλλαξαν συσκευή. Επαλήθευσε τους αριθμούς ασφαλείας με αυτά, για να διασφαλίσεις την ιδιωτικότητα. Εμφάνιση Προηγουμένως επαληθευμένοι @@ -1745,13 +1746,11 @@ Κωδικός MMSC Αναφορές παράδοσης SMS Αίτημα αναφοράς παράδοσης για κάθε SMS που στέλνεις - Αυτόματη διαγραφή παλαιότερων μηνυμάτων όταν μια συνομιλία ξεπεράσει ένα ορισμένο μήκος - Διαγραφή παλιών μηνυμάτων Συνομιλίες και πολυμέσα Αποθηκευτικός χώρος Όριο μεγέθους συνομιλίας - Περικοπή όλων των συνομιλιών τώρα - Σκανάρισμα όλων των συνομιλιών και επιβολή του ορίου μήκους των συνομιλιών + Διατήρηση μηνυμάτων + Εκκαθάριση ιστορικού μηνυμάτων Συνδεμένες συσκευές Φωτεινό Σκοτεινό @@ -1781,17 +1780,34 @@ Όταν χρησιμοποιείται WiFi Κατά το roaming Αυτόματη λήψη πολυμέσων - Περικοπή μηνυμάτων + Ιστορικό μηνυμάτων Χρήση αποθηκευτικού χώρου Φωτογραφίες Βίντεο Αρχεία Ήχος Ανασκόπηση χώρου + Διαγραφή παλαιότερων μηνυμάτων; + Εκκαθάριση του ιστορικού μηνυμάτων; + Αυτό θα διαγράψει μόνιμα όλο το ιστορικό μηνυμάτων και τα πολυμέσα από τη συσκευή σου, τα οποία είναι παλαιότερα από %1$s. + Αυτό θα περικόψει όλες τις συνομιλίες στα %1$s πιο πρόσφατα μηνύματα. + Αυτό θα διαγράψει μόνιμα όλο το ιστορικό μηνυμάτων και τα πολυμέσα από τη συσκευή σου. + Είσαι σίγουρος/η πως θέλεις να διαγράψεις όλο το ιστορικό των μηνυμάτων; + Όλο το ιστορικό των μηνυμάτων θα σβηστεί μόνιμα. Αυτή η ενέργεια δεν μπορεί να ανακληθεί. + Διαγραφή όλων τώρα + Για πάντα + 1 έτος + 6 μήνες + 30 ημέρες + Κανένα + %1$s μηνύματα + Προσαρμοσμένο + Προσαρμοσμένο όριο μήκους συνομιλίας Χρήση emoji συστήματος Απενεργοποίηση της ενσωματωμένης υποστήριξης για emoji του Signal Αναμετάδοση όλων των κλήσεων μέσω του σέρβερ Signal για να αποφύγεις την αποκάλυψη της διεύθυνσης IP σου στην επαφή. Η ενεργοποίηση θα ρίξει την ποιότητα των κλήσεων. Αναμετάδοση κλήσεων πάντα + Ποιοί μπορούν… Πρόσβαση εφαρμογής Επικοινωνία Συνομιλίες @@ -1816,6 +1832,7 @@ Να ειδοποιούμαι Λήψη ειδοποιήσεων όταν σε αναφέρουν σε σιγασμένες συνομιλίες Ορισμός ονόματος χρήστη` + Προσαρμογή ρύθμισης @@ -2115,6 +2132,14 @@ Εγγραφή Signal - Κωδικός επιβεβαίωσης για Android Ποτέ Άγνωστο + Να δουν τον αριθμό τηλεφώνου μου + Να με βρουν με τον αριθμό τηλεφώνου μου + Όλοι + Οι επαφές μου + Κανένας + Ο αριθμός τηλεφώνου σου θα είναι ορατός σε όλα τα άτομα και τις ομάδες που στέλνεις μηνύματα. + Οποιοσδήποτε έχει τον αριθμό τηλεφώνου σου στις επαφές του/της, θα μπορεί να σε δει ως επαφή στο Signal. Άλλοι θα μπορούν να σε βρουν στην αναζήτηση. + Μόνο οι επαφές σου θα βλέπουν τον αριθμό τηλεφώνου σου στο Signal. Κλείδωμα οθόνης Κλείδωμα της πρόσβασης στο Signal με την οθόνη κλειδώματος του Android ή με δακτυλικό αποτύπωμα Χρονικό όριο αδράνειας κλειδώματος οθόνης diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index fc3f47ea3..e0ed6934a 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -449,7 +449,6 @@ Pritraktotaj petoj por aniĝi Neniu peto. - Homoj en tiu listo provas anigi tiun grupon per kunhavita grupligilo. „%1$s“ aldonita „%1$s“ malaprobita @@ -491,7 +490,7 @@ Modifi grupinformon Elekti tiun, kiu povas ŝanĝi la nomon, la foton kaj la agordojn pri la memvisôntaj mesaĝoj de la grupo. Elekti tiun, kiu povas aldoni aŭ inviti novajn anojn. - Kunhavebla grupligilo + Grupligilo Bloki la grupon Malbloki la grupon Forlasi la grupon @@ -611,7 +610,6 @@ Grupligiloj baldaŭ disponeblos Ĝisdatigu Signal-on por uzi grupligilojn Anigi grupon pere de ligilo ankoraŭ ne eblas en Signal. Tio alvenos per baldaŭa ĝisdatigo. - La versio de Signal, kiun vi uzas ne subtenas kunhaveblajn grupligilojn. Ĝisdatigu al lasta versio por anigi tiun grupon per ligilo. Ĝisdatigi Signal-on Grupligilo ne validas @@ -847,25 +845,10 @@ %1$s ŝanĝis, kiu povas modifi la grupan membrecon al „%2$s“. Tiuj, kiuj povas modifi la grupan membrecon ŝanĝiĝis al „%1$s“. - Vi ŝaltis kunhaveblan grupligilon. - Vi ŝaltis kunhaveblan grupligilon kun administranto-aprobo. - Vi malŝaltis kunhaveblan grupligilon. - %1$s ŝaltis kunhaveblan grupligilon. - %1$s ŝaltis kunhaveblan grupligilon kun administranto-aprobo. - %1$s malŝaltis kunhaveblan grupligilon. - Oni ŝaltis kunhaveblan grupligilon. - Oni ŝaltis kunhaveblan grupligilon kun administranto-aprobo. - Oni malŝaltis kunhaveblan grupligilon. - Vi restarigis la kunhavigitan grupligilon. - %1$s restarigis la kunhavigitan grupligilon. - Oni restarigis la kunhavigitan grupligilon. - Vi anigis la grupon per kunhavebla grupligilo. - %1$s anigis la grupon per kunhavebla grupligilo. Vi sendis peton por anigi la grupon. - %1$s sendis peton por anigi la grupon per kunhavebla grupligilo. %1$s aprobis vian peton por anigi la grupon. %1$s aprobis peton por anigi la grupon el %2$s. @@ -1749,13 +1732,9 @@ Ricevis mesaĝon pri interŝanĝo de ŝlosiloj por nevalida protokola versio. MMSC-pasvorto SMS-a livero-raportoj Peti livero-raporton por ĉiu SMS-mesaĝo, kiun vi sendas - Aŭtomate forigi malnovajn mesaĝojn, kiam interparolo transpasas indikitan nombron - Forigi malnovajn mesaĝojn Interparoloj kaj aŭdvidaĵoj Konservejo Maksimumo de mesaĝoj en interparolo - Mallongigi ĉiujn interparolojn nun - Trairi ĉiujn interparolojn por efektivigi la maksimuman nombron de mesaĝoj Ligitaj aparatoj Hela Malhela @@ -1785,13 +1764,13 @@ Ricevis mesaĝon pri interŝanĝo de ŝlosiloj por nevalida protokola versio. Kiam uziĝas vifio Dum retmigrado Aŭdvidaĵa aŭtomata elŝuto - Limigo de mesaĝo-tenperiodo Uzado de la konservejo Fotoj Videaĵoj Dosieroj Sonaĵoj Kontroli la konservejon + Neniu Uzi sisteman emoĝiaron Malebligi emoĝiaron de Signal Trapasigi ĉiujn alvokojn tra la Signal-servilo por eviti sciigi vian IP-adreson al via kontakto. Tio malaltigos la alvokan kvaliton. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1c0b17d1a..9dd7f46f1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -84,9 +84,9 @@ ¿Bloquear y abandonar «%1$s»? ¿Bloquear a %1$s? - No podrás enviar ni recibir más mensajes en este grupo y sus participantes no podrán agregarte de nuevo al grupo. - Participantes del grupo no podrán agregarte de nuevo. - Participantes del grupo podrán agregarte de nuevo. + No podrás enviar ni recibir más mensajes en este chat en grupo y sus participantes no podrán agregarte de nuevo al grupo. + Participantes del chat en grupo no podrán agregarte de nuevo. + Participantes del chat en grupo podrán agregarte de nuevo. Podréis chatear y llamaros mutuamente. Tu nombre y foto de perfil se compartirá con esta persona. Las personas bloqueadas no podrán llamarte o enviarte mensajes. ¿Desbloquear a %1$s? @@ -114,7 +114,7 @@ Contactos recientes Personas en Signal - Grupos de Signal + Chats en grupo de Signal Puedes compartir con un máximo de %d chats. Selecciona contactos de Signal No hay contactos de Signal @@ -125,7 +125,7 @@ Eliminar ¿Eliminar foto de perfil? - ¿Eliminar imagen de grupo? + ¿Eliminar imagen de chat en grupo? No se ha encontrado un navegador web. No se encontró la aplicación de email. @@ -143,7 +143,7 @@ Chats recientes Personas - Grupos + Chats en Grupo Buscar número de teléfono Buscar por alias (nombre de usuari*) @@ -166,7 +166,7 @@ Enviado parcialmente, toca para más detalles Fallo al enviar Se recibió un mensaje de intercambio de claves, toca para proceder. - %1$s ha abandonado el grupo. + %1$s ha abandonado el chat en grupo. Fallo al enviar. Toca para enviar sin cifrar ¿Enviar como SMS no cifrado? ¿Enviar como MMS no cifrado? @@ -190,15 +190,15 @@ Lo sentimos, ha habido un fallo al adjuntar el archivo. ¡Esta persona no tiene un número que pueda recibir SMS o un correo válido! ¡El mensaje está vacío! - Participantes del grupo + Participantes del chat en grupo ¡Destinatari* inválid*! Añadido a la pantalla de inicio Llamadas no disponibles Este dispositivo no es compatible con la función de realizar llamadas. - ¿Abandonar grupo? - ¿Estás seguro de que quieres dejar este grupo? + ¿Abandonar chat en grupo? + ¿Estás seguro de que quieres abandonar este chat en grupo? Seleccionar nuev* admin - Antes de abandonar, selecciona un* nuev* admin para este grupo. + Antes de abandonar, selecciona un* nuev* admin para este chat en grupo. Seleccionar admin SMS no seguro MMS no seguro @@ -206,16 +206,16 @@ Pasémonos a Signal: %1$s Por favor, selecciona una persona ¿Desbloquear a esta persona? - ¿Desbloquear este grupo? + ¿Desbloquear este chat en grupo? Podrás volver a recibir mensajes y llamadas de esta persona. - Participantes del grupo podrán agregarte de nuevo. + Participantes del chat en grupo podrán agregarte de nuevo. Desbloquear El adjunto excede los límites de tamaño para el mensaje. Cámara no disponible ¡No se ha podido grabar la nota de voz! - No puedes enviar mensajes a este grupo porque ya no eres participante. + No puedes enviar mensajes a este chat en grupo porque ya no eres participante. No hay ninguna aplicación disponible para abrir este enlace. - Se ha enviado tu solicitud de unirte al grupo al admin. Te llegará una notificación con su respuesta. + Se ha enviado tu solicitud de unirte al chat en grupo al admin. Te llegará una notificación con su respuesta. Cancelar solicitud Para enviar notas de voz y hacer llamadas, permite a Signal acceder al micrófono. Signal necesita acceso al micrófono para enviar notas de voz. Por favor, ve a la aplicación «Ajustes», selecciona Signal en el menú «Aplicaciones y notificaciones» y en «Permisos» activa «Micrófono». @@ -236,14 +236,14 @@ ¡Novedad!: Dilo con cromos (stickers) Cancelar ¿Eliminar el chat? - ¿Eliminar y abandonar grupo? + ¿Eliminar y abandonar chat en grupo? Este chat se eliminará de todos tus dispositivos. - Abandonarás este grupo, que se eliminará de todos tus dispositivos. + Abandonarás este chat en grupo, que se eliminará de todos tus dispositivos. Eliminar Eliminar y abandonar Signal necesita acceder a tu micrófono para llamar a %1$s. Signal necesita acceder a tu micrófono y cámara para llamar a %1$s. - Ahora con más opciones en «Ajustes del grupo» + Ahora con más opciones en «Ajustes del chat en grupo» %d mensaje no leído @@ -324,14 +324,14 @@ Identidad verificada - Algunas personas no pueden agregarse a grupos antiguos. + Algunas personas no pueden agregarse a chats en grupo antiguos. Perfil Fallo al establecer la foto de perfil Problema al crear el perfil Foto de perfil Completa tu perfil - Tu perfil está cifrado de punto a punto. Sólo es visible para tus contactos, para personas con las que inicies o aceptes un chat nuevo y cuando te unas a nuevos grupos. Nadie más tiene acceso, ni siquiera l*s creador*s de Signal. + Tu perfil está cifrado de punto a punto. Sólo es visible para tus contactos, para personas con las que inicies o aceptes un chat nuevo y cuando te unas a nuevos chats en grupo. Nadie más tiene acceso, ni siquiera l*s creador*s de Signal. Configurar avatar de perfil Usar personalizado: %s @@ -388,16 +388,16 @@ ¿Agregar participante? ¿Agregar a %1$s a «%2$s»? Se ha agregado a %1$s a «%2$s». - Agregar al grupo - Agregar a grupos - No se puede agregar a esta persona a grupos del sistema antiguo. + Agregar al chat en grupo + Agregar a chats en grupo + No se puede agregar a esta persona a chats en grupo del sistema antiguo. Selecciona nuev* admin Hecho Has abandonado «%1$s». - ¿Deseas compartir tu nombre y foto de perfil con este grupo? - ¿Deseas hacer visible tu nombre y foto de perfil a tod*s l*s participantes presentes y futuros de este grupo? + ¿Deseas compartir tu nombre y foto de perfil con este chat en grupo? + ¿Deseas hacer visible tu nombre y foto de perfil a tod*s l*s participantes presentes y futuros de este chat en grupo? Hacer visible @@ -411,16 +411,16 @@ Invitación enviada %d invitaciones enviadas - No se puede agregar automáticamente a %1$s al grupo. Ha recibido una invitación y no podrá ver ningún mensaje hasta que la acepte. + No se puede agregar automáticamente a %1$s al chat en grupo. Ha recibido una invitación y no podrá ver ningún mensaje hasta que la acepte. Saber más - No se puede agregar automáticamente a estas personas al grupo. Han recibido una invitación y no podrán ver ningún mensaje hasta que la acepten. + No se puede agregar automáticamente a estas personas al chat en grupo. Han recibido una invitación y no podrán ver ningún mensaje hasta que la acepten. Desactivar Envía enlaces con vista previa Ahora se puede adjuntar la imagen para la vista previa de enlaces en los mensajes que envíes, trasmitida directamente desde la propia página web. Vista previa no disponible - Este enlace de grupo no está disponible + El enlace a chat en grupo no está disponible %1$s · %2$s @@ -435,7 +435,7 @@ No tienes invitaciones pendientes. Invitaciones de otr*s participantes No hay invitaciones pendientes de otr*s participantes. - No se pueden mostrar los detalles de personas invitadas por otr*s participantes del grupo. Sólo compartirán sus detalles con el grupo y verán los mensajes que se envíen a partir del momento en que decidan participar. + No se pueden mostrar los detalles de personas invitadas por otr*s participantes del chat en grupo. Sólo compartirán sus detalles con el grupo y verán los mensajes que se envíen a partir del momento en que decidan participar. Retirar invitación Retirar invitaciones @@ -449,52 +449,52 @@ Solicitudes pendientes No hay solicitudes que mostrar. - Las personas en esta lista han solicitado unirse al grupo vía enlace. + Las personas en esta lista han solicitado unirse al chat en grupo vía enlace. «%1$s» agregad* «%1$s» denegad* Hecho - No se puede agregar a esta persona a grupos del sistema antiguo. + No se puede agregar a esta persona a chats en grupo del sistema antiguo. ¿Agregar %1$s a «%2$s»? ¿Agregar %3$d participantes a «%2$s»? - Nombra este grupo - Crear grupo + Nombra este chat en grupo + Crear chat en grupo Crear Participantes - Nombre del grupo (necesario) + Nombre del chat en grupo (necesario) Este campo es necesario. - Los chats de grupo requieren al menos dos participantes. - Fallo al crear el grupo. + Los chats en grupo requieren al menos dos participantes. + Fallo al crear el chat en grupo. Inténtalo de nuevo más tarde. - Has seleccionado una persona que no usa Signal, así que este grupo será de MMS. + Has seleccionado una persona que no usa Signal, así que este chat en grupo será de MMS. Eliminar Contacto SMS - ¿Expulsar a %1$s del grupo? + ¿Expulsar a %1$s del chat en grupo? - %d participante no soporta el nuevo sistema de grupos. Este será un grupo del sistema antiguo. - %d participantes no soportan el nuevo sistema de grupos. Este será un grupo del sistema antiguo. + %d participante no soporta el nuevo sistema de chats en grupo. Este será un chat en grupo del sistema antiguo. + %d participantes no soportan el nuevo sistema de chats en grupo. Este será un chat en grupo del sistema antiguo. - Se creará un grupo del sistema antiguo porque «%1$s» usa una versión antigua de Signal. Puedes crear un grupo en el nuevo sistema después que esa persona actualice Signal. También puedes no agregar a esa persona al grupo. + Se creará un chat en grupo del sistema antiguo porque «%1$s» usa una versión antigua de Signal. Puedes crear un chat en grupo en el nuevo sistema después que esa persona actualice Signal. También puedes no agregar a esa persona al grupo. Se creará un grupo del sistema antiguo porque %1$d participante usa una versión antigua de Signal. Puedes crear un grupo en el nuevo sistema después que esa persona actualice Signal. También puedes eliminar a esa persona del grupo. - Se creará un grupo del sistema antiguo porque %1$d participantes usan una versión antigua de Signal. Puedes crear un grupo en el nuevo sistema después que esas personas actualicen Signal. También puedes no agregar a esas personas al grupo. + Se creará un chat en grupo del sistema antiguo porque %1$d participantes usan una versión antigua de Signal. Puedes crear un chat en grupo en el nuevo sistema después que esas personas actualicen Signal. También puedes no agregar a esas personas al grupo. Desaparición de mensajes Invitaciones pendientes Solicitudes e invitaciones Agregar participantes - Editar detalles del grupo - Selecciona quién puede modificar el nombre e imagen del grupo y la desaparición de mensajes. + Editar detalles del chat en grupo + Selecciona quién puede modificar el nombre e imagen del chat en grupo y la desaparición de mensajes. Selecciona quién puede agregar o invitar a más participantes. - Enlace para compartir grupo - Bloquear grupo - Desbloquear grupo - Abandonar grupo + Enlace del chat en grupo + Bloquear chat en grupo + Desbloquear chat en grupo + Abandonar chat en grupo Silenciar notificaciones Notificaciones personalizadas Menciones @@ -513,14 +513,14 @@ %d participantes agregad*s. No dispones de permisos para hacerlo - Alguien que has agregado no soporta los nuevos grupos ya que debe actualizar Signal. - Fallo al actualizar el grupo - No eres participante del grupo - Fallo al actualizar el grupo, por favor inténtalo más tarde - Fallo al actualizar el grupo por un problema de red, por favor inténtalo más tarde + Alguien que has agregado no soporta los nuevos chats en grupo ya que debe actualizar Signal. + Fallo al actualizar el chat en grupo + No eres participante del chat en grupo + Fallo al actualizar el chat en grupo, por favor inténtalo más tarde + Fallo al actualizar el chat en grupo por un problema de red, por favor inténtalo más tarde Editar nombre e imagen - Grupo antiguo - Este es un grupo que usa el sistema antiguo de Signal. Para acceder a características como admins de grupo, crea un grupo nuevo. + Chat en grupo antiguo + Este es un chat en grupo que usa el sistema antiguo de Signal. Para acceder a características como admins de grupo, crea un chat en grupo nuevo. Notificar cuando alguien me mencione ¿Deseas recibir notificationes si te mencionan en un chat silenciado? @@ -541,13 +541,13 @@ Hasta %1$s Inactivo Activo - Agregar a un grupo - Ver todos los grupos + Agregar a un chat en grupo + Ver todos los chats en grupo Mostrar todos - No hay grupos en común + No hay chats en grupo en común %d grupo en común - %d grupos en común + %d chats en grupo en común Editar nombre y foto Mensaje @@ -570,9 +570,9 @@ Desactivada Por defecto - Enlace para compartir grupo + Enlace para compartir chat en grupo Gestionar y compartir - Enlace del grupo + Enlace del chat en grupo Compartir Restablecer enlace Solicitudes de participantes @@ -580,12 +580,12 @@ Activada Desactivada Por defecto - Restablecer enlace del grupo + Restablecer enlace del chat en grupo Un* admin debe aprobar l*s nuev*s participantes que se unan vía enlace. - ¿Deseas restablecer el enlace para compartir el grupo? Si lo restableces, nadie podrá unirse al grupo usando el enlace actual. + ¿Deseas restablecer el enlace para compartir el chat en grupo? Si lo restableces, nadie podrá unirse al chat usando el enlace actual. Código QR - Las personas que escanéen este código podrán unirse al grupo. L*s admins deberán aprobar nuev*s participantes si se ha activado esa opción. + Las personas que escanéen este código podrán unirse al chat en grupo. L*s admins deberán aprobar nuev*s participantes si se ha activado esa opción. Compartir código ¿Deseas retirar la invitación enviada a %1$s? @@ -597,30 +597,30 @@ Ya participas Unirse Solicitar unirse - Fallo al unirse al grupo. Por favor, inténtalo más tarde + Fallo al unirse al chat en grupo. Por favor, inténtalo más tarde Fallo en la conexión de red. - Este enlace de grupo no está disponible - No ha sido posible recuperar los detalles del grupo, por favor inténtalo más tarde - ¿Deseas unirte a este grupo y compartir tu nombre y foto de perfil con sus participantes? - Un* admin del grupo debe aprobar tu solicitud antes de poder unirte. Al solicitar unirte al grupo, tu nombre y foto de perfil se compartirán con sus participantes. + Este enlace al chat en grupo no está disponible + No ha sido posible recuperar los detalles del chat en grupo, por favor inténtalo más tarde + ¿Deseas unirte a este chat en grupo y compartir tu nombre y foto de perfil con sus participantes? + Un* admin del chat en grupo debe aprobar tu solicitud antes de poder unirte. Al solicitar unirte al grupo, tu nombre y foto de perfil se compartirán con sus participantes. - Grupo · %1$d participante - Grupo · %1$d participantes + Chat en grupo · %1$d participante + Chat en grupo · %1$d participantes - Los enlaces de compartir grupos están al llegar - Actualiza Signal para usar enlaces de chats de grupo - La función de unirse a un grupo vía enlace todavía no está competamente implementada. Se activará en una próxima actualización de Signal. - La versión de Signal que usas no soporta enlaces para compartir chats de grupo. Actualiza a la versión más actual para unirte a este grupo vía enlace. + Los enlaces de compartir chats en grupo están al llegar + Actualiza Signal para usar enlaces a chats en grupo + La función de unirse a un chat en grupo vía enlace todavía no está completamente implementada. Se activará en una próxima actualización de Signal. + La versión de Signal que usas no soporta enlaces para compartir chats en grupo. Actualiza a la versión más actual para unirte a este chat en grupo vía enlace. Actualiza Signal - Enlace de grupo inválido + Enlace a chat en grupo inválido - ¿Agregar a %1$s al grupo? + ¿Agregar a %1$s al chat en grupo? ¿Denegar la solicitud de %1$s? Agregar Denegar - Avatar del grupo + Avatar del chat en grupo Foto Enturbiar caras @@ -748,13 +748,13 @@ Desconocid* Has recibido un mensaje cifrado con una versión de Signal antigua que ya no está disponible. Por favor, solicita a esta persona a actualizar a la versión más reciente y a reenviar el mensaje. - Has abandonado el grupo. - Has actualizado el grupo. - Se actualizó el grupo. + Has abandonado el chat en grupo. + Has actualizado el chat en grupo. + Se actualizó el chat en grupo. Has llamado Llamada recibida Llamada perdida - %s ha actualizado el grupo. + %s ha actualizado el chat en grupo. %s ha llamado Has llamado a %s Llamada perdida de %s @@ -769,22 +769,22 @@ %1$sha cambiado su nombre de perfil de %2$s a %3$s. %1$s ha cambiado su nombre de perfil. - Has creado el grupo. - Grupo actualizado. + Has creado el chat en grupo. + Chat en grupo actualizado. Has agregado a «%1$s». %1$s ha agregado a «%2$s». - %1$s te ha agregado al grupo. - Te has incorporado al grupo. - %1$s se han unido al grupo. + %1$s te ha agregado al chat en grupo. + Te has incorporado al chat en grupo. + %1$s se han unido al chat en grupo. Has expulsado a «%1$s». %1$s ha expulsado a «%2$s». - %1$s te ha expulsado del grupo. - Has abandonado el grupo. - %1$s ha abandonado el grupo. - Ya no participas en el grupo. - %1$s ya no participa en el grupo. + %1$s te ha expulsado del chat en grupo. + Has abandonado el chat en grupo. + %1$s ha abandonado el chat en grupo. + Ya no participas en el chat en grupo. + %1$s ya no participa en el chat en grupo. Has promovido a %1$s a admin. %1$s ha promovido a %2$s a admin. @@ -797,87 +797,87 @@ %1$s ya no es admin. Ya no eres admin. - Has invitado a «%1$s» al grupo. - «%1$s» te ha invitado al grupo. + Has invitado a «%1$s» al chat en grupo. + «%1$s» te ha invitado al chat en grupo. «%1$s» ha invitado a 1 persona al grupo. - «%1$s» ha invitado a %2$d personas al grupo. + «%1$s» ha invitado a %2$d personas al chat en grupo. - Te han invitado al grupo. + Te han invitado al chat en grupo. - Se ha invitado a 1 persona al grupo. - Se han invitado a %1$d personas al grupo. + Se ha invitado a 1 persona al chat en grupo. + Se han invitado a %1$d personas al chat en grupo. - Has retirado una invitación al grupo. - Has retirado %1$d invitaciones al grupo. + Has retirado una invitación al chat en grupo. + Has retirado %1$d invitaciones al chat en grupo. - «%1$s» ha retirado una invitación al grupo. - «%1$s» ha retirado %2$d invitaciones al grupo. + «%1$s» ha retirado una invitación al chat en grupo. + «%1$s» ha retirado %2$d invitaciones al chat en grupo. - Alguien ha rechazado la invitación a participar en el grupo. - Has rechazado la invitación a participar en el grupo. - %1$s retiró tu invitación del chat de grupo. - Un* admin retiró tu invitavión del chat de grupo. + Alguien ha rechazado la invitación a participar en el chat en grupo. + Has rechazado la invitación a participar en el chat en grupo. + %1$s retiró tu invitación del chat en grupo. + Un* admin retiró tu invitavión del chat en grupo. Se ha retirado una invitación para el grupo. - Se han retirado %1$d invitaciones para el grupo. + Se han retirado %1$d invitaciones al chat en grupo. - Has aceptado la invitación a participar en el grupo. - «%1$s» ha aceptado la invitación al grupo. + Has aceptado la invitación a participar en el chat en grupo. + «%1$s» ha aceptado la invitación al chat en grupo. Has agregado a «%1$s». %1$s ha agregado a «%2$s». - Has modificado el nombre del grupo a «%1$s». - «%1$s» ha modificado el nombre del grupo a «%2$s». - El nombre del grupo ha cambiado a «%1$s». + Has modificado el nombre del chat en grupo a «%1$s». + «%1$s» ha modificado el nombre del chat en grupo a «%2$s». + El nombre del chat en grupo ha cambiado a «%1$s». - Has modificado la imagen del grupo. - «%1$s» ha modificado la imagen del grupo. - La imagen del grupo ha cambiado. + Has modificado la imagen del chat en grupo. + «%1$s» ha modificado la imagen del chat en grupo. + La imagen del chat en grupo ha cambiado. - Has actualizado quién puede editar los detalles del grupo a «%1$s». - %1$s ha actualizado quién puede editar los detalles del grupo a «%2$s». - Ahora, «%1$s» pueden editar los detalles del grupo. + Has actualizado quién puede editar los detalles del chat en grupo a «%1$s». + %1$s ha actualizado quién puede editar los detalles del chat en grupo a «%2$s». + Ahora, «%1$s» pueden editar los detalles del chat en grupo. - Has actualizado quién puede editar la lista de participantes del grupo a «%1$s». - %1$s ha actualizado quién puede editar la lista de participantes del grupo a «%2$s». - Ahora, «%1$s» pueden editar la lista de participantes del grupo. + Has actualizado quién puede editar la lista de participantes del chat en grupo a «%1$s». + %1$s ha actualizado quién puede editar la lista de participantes del chat en grupo a «%2$s». + Ahora, «%1$s» pueden editar la lista de participantes del chat en grupo. - Has activado la opción de compartir el grupo vía enlace. - Has activado la opción de compartir el grupo vía enlace tras confirmación de admin. - Has desactivado la función de compartir el grupo vía enlace. - %1$s ha activado la función de compartir el grupo vía enlace. - %1$s ha activado la opción de compartir el grupo vía enlace tras confirmación de admin. - %1$s ha desactivado la función de compartir el grupo vía enlace. - Se ha activado la opción de compartir el grupo vía enlace. - Se ha activado la opción de compartir el grupo vía enlace tras confirmación de admin. - Se ha desactivado la función de compartir el grupo vía enlace. + Has activado el enlace para compartir el chat en grupo sin necesitar confirmación del admin. + Has activado el enlace para compartir el chat en grupo tras confirmación del admin. + Has desactivado el enlace para compartir el chat en grupo. + %1$s ha activado el enlace para compartir el chat en grupo sin necesitar confirmación del admin. + %1$s ha activado el enlace para compartir el chat en grupo tras confirmación del admin. + %1$s ha desactivado el enlace para compartir el chat en grupo. + Se ha activado el enlace para compartir el chat en grupo sin necesitar confirmación del admin. + Se ha activado el enlace para compartir el chat en grupo tras confirmación del admin. + Se ha desactivado el enlace para compartir el chat en grupo. - Has restablecido el enlace para compartir el grupo. - %1$s ha restablecido el enlace para compartir el grupo. - El enlace para compartir el grupo ha sido restablecido. + Has restablecido el enlace para compartir el chat en grupo. + %1$s ha restablecido el enlace para compartir el chat en grupo. + Se ha restablecido el enlace para compartir el chat en grupo. - Te has unido al grupo vía enlace. - %1$s se ha unido al grupo vía enlace. + Te has unido al chat en grupo vía enlace. + %1$s se ha unido al chat en grupo vía enlace. - Has enviado una solicitud para unirte al grupo. - %1$s ha solicitado unirse al grupo vía enlace. + Has enviado una solicitud para unirte al chat en grupo. + %1$s ha solicitado unirse al grupo vía enlace. - %1$s ha aprobado tu solicitud de unirte al grupo. - %1$s ha aprobado la solicitud de %2$s de unirse al grupo. - Has aprobado la solicitud de unirse al grupo de %1$s. - Se ha aprobado tu solicitud de unirte al grupo. - Se ha aprobado la solicitud de %1$s de unirse al grupo. + %1$s ha aprobado tu solicitud de unirte al chat en grupo. + %1$s ha aprobado la solicitud de %2$s de unirse al chat en grupo. + Has aprobado la solicitud de unirse al chat en grupo de %1$s. + Se ha aprobado tu solicitud de unirte al chat en grupo. + Se ha aprobado la solicitud de %1$s de unirse al chat en grupo. - Un* admin ha denegado tu solicitud de unirte al grupo. - %1$s ha denegado la solicitud de %2$s de unirse al grupo. - Se ha denegado la solicitud de %1$s de unirse al grupo. - Has retirado tu solicitud de unirte al grupo. - %1$s ha retirado su solicitud de unirse al grupo. + Un* admin ha denegado tu solicitud de unirte al chat en grupo. + %1$s ha denegado la solicitud de %2$s de unirse al chat en grupo. + Se ha denegado la solicitud de %1$s de unirse al chat en grupo. + Has retirado tu solicitud de unirte al chat en grupo. + %1$s ha retirado su solicitud de unirse al chat en grupo. Tus cifras de seguridad con %s han cambiado. Has marcado tus cifras de seguridad con %s como verificadas. @@ -891,8 +891,8 @@ Desbloquear ¿Deseas que %1$s te envíe mensajes y así compartir tu nombre y foto de perfil? Esta persona no sabrá que has visto sus mensajes hasta que aceptes. ¿Deseas que %1$s te envíe mensajes y así compartir tu nombre y foto de perfil? Esta persona no sabrá que has visto sus mensajes hasta que aceptes. - ¿Unirse a este grupo y compartir tu nombre y foto de perfil con sus participantes? No sabrán que has visto sus mensajes hasta que aceptes. - ¿Desbloquear este grupo y compartir tu nombre y foto de perfil con sus participantes? No recibirás ningún mensaje del grupo hasta que lo desbloquees. + ¿Unirse a este chat en grupo y compartir tu nombre y foto de perfil con sus participantes? No sabrán que has visto sus mensajes hasta que aceptes. + ¿Desbloquear este chat en grupo y compartir tu nombre y foto de perfil con sus participantes? No recibirás ningún mensaje del grupo hasta que lo desbloquees. Participa en %1$s Participa en %1$s y %2$s Participa en %1$s, %2$s y %3$s @@ -905,8 +905,8 @@ %1$d participantes (+%2$d pendientes) - %d grupo adicional - %d grupos adicionales + %d chat en grupo adicional + %d chats en grupo adicionales ¡Las claves no coinciden! @@ -989,13 +989,13 @@ Bloquear - Fallo al abandonar el grupo + Fallo al abandonar el chat en grupo Desbloquear Activada Desactivada Disponible una vez que se ha enviado o recibido un mensaje - Grupo sin nombre + Chat en grupo sin nombre Respondiendo … Finalizando llamada … @@ -1147,8 +1147,8 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Bloqueo de registro: Idioma: - Grupo actualizado - Abandonó el grupo + Chat en grupo actualizado + Abandonó el chat en grupo Sesión segura restablecida. Borrador: Has llamado @@ -1170,8 +1170,8 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Identidad no verificada Ha sido mposible procesar el mensaje Solicitud de chat - %1$s te ha agregado al grupo - %1$s te ha invitado al grupo + %1$s te ha agregado al chat en grupo + %1$s te ha invitado al chat en grupo Foto GIF Nota de voz @@ -1374,7 +1374,7 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Introduce un nombre o número Invitar a Signal - Nuevo grupo + Nuevo chat en grupo Eliminar texto introducido Mostrar teclado @@ -1390,8 +1390,8 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Alias no encontrado %1$s no usa Signal. Asegúrate de introducir el alias correcto. De acuerdo - Este grupo está completo - No necesitas agregarte a ti mism* al grupo + Este chat en grupo está completo + No necesitas agregarte a ti mism* al chat en grupo No hay personas bloqueadas @@ -1447,6 +1447,7 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Cambios en cifras de seguridad Enviar de todas formas + Llamar de todas formas Las siguentes personas pueden haber reinstalado Signal o cambiado su dispositivo. Verifica tus cifras de seguridas con ellas para asegurar vuestra privacidad. Ver Identidad previamente verificada @@ -1549,17 +1550,17 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Reenviando … - %1$s se ha unido al grupo. - %1$s se han unido al grupo. + %1$s se ha unido al chat en grupo. + %1$s se han unido al chat en grupo. - Ahora el nombre del grupo es «%1$s». + Ahora el nombre del chat en grupo es «%1$s». - ¿Hacer visibles para este grupo el nombre y la foto de perfil? + ¿Hacer visibles para este chat en grupo el nombre y la foto de perfil? Desbloquear - Signal necesita una configuración de MMS válida para enviar los mensajes multimedia y de grupo a través del proveedor móvil. Tu dispositivo no proporciona esta información, algo que ocasionalmente ocurre con dispositivos bloqueados y otras configuraciones restrictivas. - Para enviar mensajes multimedia y de grupo, toca «Aceptar» y completa las configuraciones solicitadas. Las configuraciones de MMS para tu proveedor generalmente se pueden localizar buscando por «APN de proveedor». Sólo necesitarás hacer esto una vez. + Signal necesita una configuración de MMS válida para enviar los mensajes multimedia y de chat en grupo a través del proveedor móvil. Tu dispositivo no proporciona esta información, algo que ocasionalmente ocurre con dispositivos bloqueados y otras configuraciones restrictivas. + Para enviar mensajes multimedia y de chat en grupo, toca «Aceptar» y completa las configuraciones solicitadas. Las configuraciones de MMS para tu proveedor generalmente se pueden localizar buscando por «APN de proveedor». Sólo necesitarás hacer esto una vez. Nombre (necesario) Apellido(s) (opcional) @@ -1567,8 +1568,8 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Alias (nombre de usuari*) Crear alias (nombre de usuari*) - Editar nombre e imagen de grupo - Nombre del grupo + Editar nombre e imagen de chat en grupo + Nombre del chat en grupo Archivos multimedia compartidos @@ -1750,13 +1751,11 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Contraseña de MMSC Confirmación de entrega de SMS Solicita la confirmación de entrega de SMS para todos los SMS enviados. - Elimina automáticamente los mensajes más antiguos cuando el chat exceda el número de mensajes especificado - Eliminar mensajes antiguos Chats y multimedia Almacenamiento Límite de longitud de chat - Recortar ahora todos los chats - Escanear todos los chats e imponer límite de longitud + Preservar mensajes + Eliminar historial de mensajes Dispositivos enlazados Claro Oscuro @@ -1786,17 +1785,34 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Al usar Wi-Fi En itinerancia (roaming) Auto-descarga de archivos multimedia - Recorte de mensajes en chats + Historial de mensajes Uso de almacenamiento Fotos Vídeos Archivos Audio Comprobar almacenamiento + ¿Eliminar mensajes antiguos? + ¿Eliminar historial de mensajes? + Esto eliminará permanentemente todos los mensajes y adjuntos de este dispositivo con más antigüedad de %1$s. + Esto recortará permanentemente todos los chat y mantendrá los %1$s mensajes más recientes. + Esto eliminará permanentemente todos los mensajes y adjuntos de este dispositivo. + ¿Seguro que deseas eliminar todo tu historial de mensajes? + El historial completo de mensajes se eliminará permanentemente. Esta acción no se puede deshacer. + Borrar todos ahora + cero días + 1 año + 6 meses + 30 días + Ninguna + %1$s mensajes + Personalizado + Límite de longitud de chat personalizado Usar emoji del sistema Desactivar soporte de emoji integrado de Signal Redirige todas las llamadas a través del servidor de Signal para evitar revelar tu dirección IP. Activar la opción reducirá la calidad de las llamadas. Redirigir llamadas siempre + Quién puede … Acceso a la aplicación Comunicación Chats @@ -1821,13 +1837,14 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Notificarme Recibe notificaciones si te mencionan en un chat silenciado Selecciona un alias + Personalizar opción Nuevo mensaje para … - Agregar al grupo + Agregar al chat en grupo Llamar @@ -1884,9 +1901,9 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Silenciar notificaciones Añadir archivo adjunto - Editar grupo - Ajustes del grupo - Abandonar grupo + Editar chat en grupo + Ajustes del chat en grupo + Abandonar chat en grupo Todos los archivos multimedia Ajustes de chat Añadir a la pantalla de inicio @@ -1901,7 +1918,7 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Chat Transmisión - Nuevo grupo + Nuevo chat en grupo Ajustes Bloquear Marcar todos como leídos @@ -2062,7 +2079,7 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Te lo volveremos a recordar. Confirmar tu PIN será obligatorio en %1$d días. ¡Ya puedes @mencionar en grupos de chat! - Llama la atención de alguien en un chat de grupo del nuevo sistema al escribir @ + Llama la atención de alguien en un chat en grupo del nuevo sistema al escribir @ Icono de transporte Cargando … @@ -2120,6 +2137,14 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Registro de Signal - Código de verificación para Android Nunca Desconocido + Ver mi número de teléfono + Encontrarme por mi número de teléfono + Tod*s + Mis contactos + Nadie + Tu número de teléfono será visible para la gente a quién envíes mensajes y para l*s participantes de tus chats en grupo. + Cualquiera que tenga tu número de teléfono entre sus contactos podrá ver que usas Signal. Otras te encontrarán si buscan tu número. + Sólo las personas entre tus contactos podrán ver que puedes comunicarte a través de Signal. Bloqueo de pantalla Bloquea el acceso a Signal con el código de bloqueo de Android o la huella dactilar. Tiempo de inactividad para el bloqueo de pantalla @@ -2183,18 +2208,18 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Bloquear Desbloquear Añadir a contactos - Agregar a un grupo - Agregar a otro grupo + Agregar a un chat en grupo + Agregar a otro chat en grupo Ver cifras de seguridad Promover a admin Retirar permisos de admin - Expulsar del grupo + Expulsar del chat en grupo Mensaje Llamada de voz Llamada no segura Vídeollamada ¿Retirar los permisos de admin a %1$s? - %1$s podrá editar los detalles de este grupo y modificar la lista de participantes + %1$s podrá editar los detalles de este chat en grupo y modificar la lista de participantes ¿Expulsar a %1$s de «%2$s»? Eliminar Copiado al portapapeles @@ -2202,12 +2227,12 @@ Se recibió un mensaje de intercambio de claves para una versión no válida del Aprobar Denegar - Grupos nuevos y antiguos - ¿Qué son los grupos antiguos? - Los grupos antiguos no son compatibles con las características de los grupos nuevos como admins o actualizaciones más descriptivas. - ¿Cómo uso los nuevos grupos? - Los grupos antiguos no pueden convertirse en grupos nuevos, pero puedes crear un grupo nuevo con la misma gente. - Para crear un nuevo grupo, tod*s l*s participantes deben tener su versión de Signal actualizada. + Chats en grupo nuevos y antiguos + ¿Qué son los chats en grupo antiguos? + Los chats en grupo antiguos no son compatibles con las características de los chats en grupo nuevos como admins o actualizaciones más descriptivas. + ¿Cómo uso los nuevos chats en grupo? + Los chats en grupo antiguos no pueden convertirse en chats en grupo nuevos, pero puedes crear un grupo nuevo con la misma gente. + Para crear un nuevo chat en grupo, tod*s l*s participantes deben tener su versión de Signal actualizada. Compartir vía Signal Copiar diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 8d10581dd..87c2587ba 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -566,8 +566,6 @@ Esines võrgu viga. Uuenda Signal-it grupilinkide kasutamiseks - See Signali versioon, mida kasutad, ei toeta jagatavaid grupilinke. -Uuenda viimasele versioonile, et selle grupiga lingi abil liituda. Uuenda Signal-it Lisa @@ -1672,13 +1670,9 @@ Uuenda viimasele versioonile, et selle grupiga lingi abil liituda. MMSC parool SMSi kohaletoimetamise rapordid Taotle kohaletoimetamise raporti iga SMS-sõnumi kohta, mida saadad - Kustuta automaatselt vanad sõnumid, kui vestlus ületab määratud pikkuse - Kustuta vanad sõnumid Vestlused ja meedia Mäluruum Vestluse pikkuse limiit - Piira kõik vestlused nüüd - Skaneeri kõiki vestlusi ja jõusta vestluspikkuse limiite Lingitud seadmed Hele Tume @@ -1708,13 +1702,14 @@ Uuenda viimasele versioonile, et selle grupiga lingi abil liituda. Kui kasutad Wi-Fit Kui oled välismaal Meedia automaatne allalaadimine - Sõnumi piiramine Mäluruumi kasutus Fotod Videod Failid Audio Vaata mäluruum üle + Puudub + Kohandatud Kasuta süsteemi emojisid Keela Signalisse sisseehitatud emoji-tugi Vahenda kõik kõned Signali serveri kaudu, et vältida oma IP-aadressi levitamist oma kontaktile. Lubamine vähendab kõnekvaliteeti. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 8ef217f97..fc3038a98 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -1624,13 +1624,9 @@ Inportatu \'SMSBackup and Restorekin\' bateragarria den enkriptatu gabeko babesk MMSC Pasahitza SMS banaketaren txostenak Bidaltzen duzun SMS mezu bakoitzeko banaketa txosten bat eskatu - Automatikoki ezabatu mezu zaharragoak solasaldiak luzera jakin bat gaindituz gero? - Mezu zaharrak ezabatu Txatak eta multimedia Biltegia Solasaldiaren luzera muga - Murriztu solasaldi guztien luzera orain - Eskaneatu solasaldi guztiak eta betearazi luzera mugak. Lotutako gailuak Argia Iluna @@ -1660,13 +1656,14 @@ Inportatu \'SMSBackup and Restorekin\' bateragarria den enkriptatu gabeko babesk Wi-Fia erabiltzerakoan Ibiltaritzako datuak erabiltzerakoan Multimedia edukien deskargatze automatikoa - Mezu murrizketa Biltegiaren erabilera Argazkiak Bideoak Fitxategiak Audioa Berrikusi biltegia + Bat ere ez + Pertsonalizatua Erabili sistemaren emojiak Desaktibatu Signalen emoji integratuak Birtransmititu dei guztiak Signal zerbitzari baten bitartez; horrela ez diozu erakutsiko zure IP helbidea kontaktuari. Aktibatzeak deiaren kalitatea gutxiagotuko du. @@ -1983,6 +1980,7 @@ Inportatu \'SMSBackup and Restorekin\' bateragarria den enkriptatu gabeko babesk Signal Errejistratzea - Androiderako Egiaztatze Kodea Inoiz ez Ezezaguna + Inor ez Blokeo-pantaila Blokeatu Signal-era sartzea Androiden pantaila-blokeoa edo hatz-markak erabiliz Blokeo-pantailaren inaktibitate denbora-muga diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index ab9a546b8..44a37f623 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -48,6 +48,7 @@ پین ایجاد شد. پین غیرفعال شد. مخفی سازی + پنهان کردن یادآور؟ %d دقیقه @@ -420,9 +421,16 @@ هیچ پیش‌نمایش پیوندی در دسترس نیست این پیوند گروه فعال نیست + %1$s · %2$s + + %1$d عضو + %1$d عضو + دعوت‌های در حال انتظار + درخواست‌ها + دعوت‌ها افرادی که شما دعوت کردید شما هیچ دعوت در حال انتظاری ندارید دعوت‌های اعضای دیگر گروه @@ -439,6 +447,8 @@ اشکال در لغو دعوت‌ها + درخواست‌‌های عضویت در حال انتظار + هیچ درخواست عضویت برای نمایش وجود ندارد انجام شد این فرد نمی‌تواند به گروه‌های قدیمی اضافه شود. @@ -572,7 +582,6 @@ این پیوند گروه فعال نیست برای استفاده از پیوند‌های گروه Signal را به‌روزرسانی کنید - نسخهٔ Signal که استفاده می‌کنید از پیوند‌های با قابلیت اشتراک‌گذاری پشتیبانی نمی‌کند. برای پیوستن به این گروه از طریق پیوند به آخرین نسخه به‌روزرسانی کنید. بروزرسانی Signal افزودن @@ -1676,13 +1685,9 @@ رمز عبور MMSC گزارش‌های تحویل پیامک درخواست گزارش تحویل برای هر پیامک ارسالی از جانب شما - حذف خودکار پیام‌های قدیمی زمانی که یک مکالمه از طول مشخص شده بیشتر می‌شود - حذف پیام‌های قدیمی گفتگوها و رسانه‌ها حافظه محدودیت طول مکالمه - همین حالا همهٔ مکالمه‌ها را پیرایش کن - اسکن کردن تمام مکالمه‌ها و اعمال محدودیت طول مکالمه دستگاه‌های پیوند داده شده روشن تاریک @@ -1712,13 +1717,14 @@ هنگام استفاده از Wi-Fi هنگام رومینگ دریافت خودکار رسانه - پیرایش پیام حافظهٔ مورد استفاده تصاویر ویدئو‌ها فایل‌ها صوت بازبینی حافظه + هیچکدام + دلخواه استفاده از شکلک‌های سیستم غیرفعال کردن پشتیبانی از شکلک‌های داخل Signal گذراندن تمامی تماس‌ها از سرور Signal به منظور جلوگیری از آشکار شدن نشانی اینترنتی شما برای مخاطبانتان. فعال‌سازی این گزینه باعث کاهش کیفیت تماس می‌گردد. @@ -2044,6 +2050,7 @@ ثبت‌نام Signal - کد تأیید برای اندروید هرگز ناشناخته + هیچ کس قفل صفحه قفل دسترسی Signal با قفل صفحهٔ اندروید یا اثر انگشت قفل خودکار صفحه به دلیل عدم فعالیت diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 78b5e64dd..e3e78351d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -449,7 +449,6 @@ Odottavat jäsenpyynnöt Ei odottavia jäsenpyyntöjä. - Tässä luettelossa olevat henkilöt yrittävät liittyä ryhmään jaettavan ryhmälinkin kautta. Lisäsi \"%1$s\" Hylkäsi \"%1$s\" @@ -491,7 +490,7 @@ Muokkaa ryhmän tietoja Valitse ketkä voivat muokata ryhmän nimeä, kuvaketta ja katoavien viestien ajastinta. Valitse ketkä voivat lisätä tai kutsua uusia jäseniä. - Jaettava ryhmälinkki + Ryhmälinkki Estä ryhmä Poista ryhmän esto Poistu ryhmästä @@ -611,7 +610,6 @@ Ryhmälinkit tulossa pian Päivitä Signal käyttääksesi ryhmälinkkejä Signal ei vielä tue ryhmään liittymistä linkin kautta. Tämä ominaisuus julkaistaan tulevassa päivityksessä. - Käyttämäsi versio Signalista ei tue jaettavia ryhmälinkkejä. Päivitä uusimpaan versioon liittyäksesi tähän ryhmään linkin kautta. Päivitä Signal Ryhmälinkki ei ole kelvollinen @@ -847,25 +845,10 @@ %1$s muutti kuka voi muokata ryhmän jäsenyyttä käyttäjiin \"%2$s\". Ryhmän jäsenyyttä voi nyt muokata \"%1$s\". - Otit käyttöön jaettavan ryhmälinkin. - Otit käyttöön jaettavan ryhmälinkin ylläpitäjän luvalla. - Poistit jaettavan ryhmälinkin käytöstä. - %1$s otti käyttöön jaettavan ryhmälinkin. - %1$s otti käyttöön jaettavan ryhmälinkin ylläpitäjän luvalla. - %1$s poisti jaettavan ryhmälinkin käytöstä. - Jaettava ryhmälinkki on otettu käyttöön. - Jaettava ryhmälinkki on otettu käyttöön ylläpitäjän luvalla. - Jaettava ryhmälinkki on poistettu käytöstä. - Sinä uudelleenasetit jaettavan ryhmälinkin. - %1$s uudelleenasetti jaettavan ryhmälinkin. - Jaettava ryhmälinkki on asetettu uudelleen. - Liityit ryhmään jaettavan ryhmälinkin kautta. - %1$s liittyi ryhmään jaettavan ryhmälinkin kautta. Lähetit pyynnön liittyä ryhmään. - %1$s pyytää liittyä ryhmään jaettavan ryhmälinkin kautta. %1$s hyväksyi pyyntösi liittyä ryhmään. %1$s hyväksyi henkilön %2$s pyynnön liittyä ryhmään. @@ -1744,13 +1727,9 @@ Vastaanotetiin avaintenvaihtoviesti, joka kuuluu väärälle protokollaversiolle MMSC-salasana SMS toimitusvahvistukset Pyydä vahvistus jokaisen lähetetyn tekstiviestin toimituksesta - Poista vanhimpia viestejä automaattisesti, kun keskustelu ylittää määritetyn pituusrajan - Vanhojen viestien poisto Keskustelut ja liitetiedostot Tallennustila Keskustelun pituusraja - Karsi kaikki keskustelut nyt - Käy läpi kaikki keskustelut ja pakota niihin pituusrajat Yhdistetyt laitteet Vaalea Tumma @@ -1780,13 +1759,14 @@ Vastaanotetiin avaintenvaihtoviesti, joka kuuluu väärälle protokollaversiolle Wi-Fi-verkossa Roaming-tilassa Liitetiedostojen automaattinen lataus - Keskustelujen karsiminen Tallennustilan käyttö Kuvat Videot Tiedostot Äänitiedosto Tarkista tallennustila + Ei mitään + Mukautettu Käytä järjestelmän hymiöitä Ota Signalin oletushymiöt pois käytöstä Välitä kaikki puhelut Signal-palvelimen kautta välttääksesi IP-osoitteesi paljastumista yhteystiedollesi. Tämä toiminto heikentää puhelun laatua. @@ -2113,6 +2093,7 @@ Vastaanotetiin avaintenvaihtoviesti, joka kuuluu väärälle protokollaversiolle Signalin rekisteröinti - vahvistuskoodi Androidille ei koskaan Tuntematon + Ei kenellekkään Näytön lukitus Lukitse Signal Androidin näytön lukituksella tai sormenjäljellä Automaattisen näytön lukituksen odotusaika diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5c3e35bd7..c3062413d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -196,7 +196,7 @@ Les appels ne sont pas pris en charge Cet appareil ne semble pas prendre en charge les actions de numérotation. Quitter le groupe ? - Voulez-vous vraiment quitter ce groupe ? + Souhaitez-vous vraiment quitter ce groupe ? Choisir un nouvel administrateur Avant de quitter le groupe, vous devez choisir au moins un nouvel administrateur pour ce groupe. Choisir un administrateur @@ -449,7 +449,6 @@ Demandes de membres en attente Il n’y a aucune demande de membre à afficher. - Des personnes de cette liste tentent de se joindre à ce groupe grâce au lien de groupe partageable. « %1$s » a été ajouté « %1$s » a été refusé @@ -491,7 +490,7 @@ Modifier les renseignements du groupe Choisissez qui peut modifier le nom, l’avatar et les messages éphémères du groupe. Choisissez qui peut ajouter ou inviter de nouveaux membres. - Lien de groupe partageable + Lien de groupe Bloquer le groupe Débloquer le groupe Quitter le groupe @@ -582,7 +581,7 @@ Valeur par défaut Réinitialisation du lien de groupe Exiger qu’un administrateur approuve les nouveaux membres qui se joignent grâce au lien de groupe. - Voulez-vous vraiment réinitialiser le lien du groupe ? Personne ne pourra plus se joindre au groupe grâce au lien actuel. + Souhaitez-vous vraiment réinitialiser le lien du groupe ? Personne ne pourra plus se joindre au groupe grâce au lien actuel. Code QR Les personnes qui balaieront ce code pourront se joindre à votre groupe. Les administrateurs devront quand même approuver les nouveaux membres si vous avez activé ce paramètre. @@ -611,7 +610,6 @@ Les liens de groupe arrivent bientôt Mettez Signal à jour pour utiliser les liens de groupe Il n’est pas encore possible dans Signal de se joindre à un groupe grâce à un lien. Cette fonction sera proposée dans une mise à jour future. - La version de Signal que vous utilisez ne prend pas en charge les liens de groupe partageables. Installez la dernière version de Signal pour vous joindre à ce groupe à partir d’un lien. Mettre Signal à jour Le lien de groupe est invalide @@ -847,25 +845,13 @@ %1$s a changé qui peut modifier les membres du groupe en « %2$s ». Qui peut modifier les membres du groupe a été changé en « %1$s ». - Vous avez activé le lien de groupe partageable. - Vous avez activé le lien de groupe partageable avec approbation d’un administrateur. - Vous avez désactivé le lien de groupe partageable. - %1$s a activé le lien de groupe partageable. - %1$s a activé le lien de groupe partageable avec approbation d’un administrateur. - %1$s a désactivé le lien de groupe partageable. - Le lien de groupe partageable a été activé. - Le lien de groupe partageable a été activé avec approbation d’un administrateur. - Le lien de groupe partageable a été désactivé. + Vous avez activé le lien de groupe partageable avec approbation d’un administrateur. + Vous avez désactivé le lien de groupe partageable. - Vous avez réinitialisé le lien de groupe partageable. - %1$s a réinitialisé le lien de groupe partageable. - Le lien de groupe partageable a été réinitialisé. - Vous êtes joint au groupe grâce au lien de groupe partageable. - %1$s s’est joint au groupe grâce au lien de groupe partageable. + Vous avez rejoint le groupe grâce au lien de groupe. Vous avez demandé à vous joindre au groupe. - %1$s a demandé à se joindre grâce au lien de groupe partageable. %1$s a approuvé votre demande de vous joindre au groupe. %1$s a approuvé une demande de %2$s de se joindre au groupe. @@ -1740,13 +1726,9 @@ Mot de passe MMSC Relevés de remise des textos Demander un relevé de remise pour chaque texto envoyé - Supprimer automatiquement les messages plus anciens dès qu’une conversation dépasse une certaine longueur - Supprimer les anciens messages Conversations et médias Mémoire Limite de taille des conversations - Réduire toutes les conversations maintenant - Analyser toutes les conversations et imposer des limites de longueur de conversation Appareils reliés Clair Sombre @@ -1776,13 +1758,15 @@ En utilisant le WI-FI En itinérance Téléchargement automatique des médias - Élagage des messages Utilisation de la mémoire Photos Vidéos Fichiers Son État de la mémoire + 1 an + Aucune + Personnalisé Utiliser les émojis du système Désactiver la prise en charge des émojis intégrés à Signal Relayer tous les appels par le serveur Signal pour éviter de divulguer votre adresse IP à votre contact. L’activation de cette option réduira la qualité des appels. @@ -2109,6 +2093,7 @@ Inscription à Signal – Code de confirmation pour Android Jamais Inconnu + Personne Verrouillage de l’écran Verrouiller l’accès à Signal avec le verrouillage de l’écran d’Android ou une empreinte Délai d’inactivité avant verrouillage de l’écran diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 90f9cc608..6a38ed60a 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -957,11 +957,11 @@ a shonrú Príobháideachas MMSC URL Focal faire MMSC - Scrios seanteachtaireachtaí + Comhráite agus meáin Fadtheorainn comhrá - Bearr gach comhrá anois - Scan trí gach comhrá agus fadtheorainn a chuir i bhfeidhm + + Gléasanna nasctha Geal Dorcha @@ -981,7 +981,7 @@ a shonrú Nuair atá feidhm á baint as Wi-Fi Nuair atá tú ag fánaíocht Íosluchtaigh go huathoibríoch meáin - Bearradh teachtaireachta + Bain feidhm as emoji an chórais Cuir glaonna ar aghaidh i gcónaí Riochtain aipe diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index b38bd6e81..39d873089 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -1413,13 +1413,13 @@ Contrasinal de MMSC Informes de entrega de SMS Solicita un informe de entrega para cada SMS que envías - Borra automaticamente as mensaxes máis antigas unha vez que a conversa supera unha determinada lonxitude - Borrar as mensaxes antigas + + Conversas e multimedia Almacenamento Lonxitude máxima das conversas - Recortar as mensaxes agora - Escanea todas as conversas e axusta o seu tamaño + + Dispositivos vinculados Claro Escuro @@ -1447,7 +1447,7 @@ Usando conexión wifi En itinerancia Descarga automática multimedia - Recorte das mensaxes + Uso do almacenamento Fotografías Vídeos diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index b1d0b6aca..47e08356d 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -1292,13 +1292,13 @@ MMSC પાસવર્ડ SMS ડિલિવરી અહેવાલો તમે મોકલો છો તે દરેક SMS મેસેજ માટે ડિલિવરી રિપોર્ટની વિનંતી કરો - એકવાર સંવાદ ની ઉલ્લેખિત લંબાઈથી વધુ થઈ જાય ત્યારે જૂના મેસેજ આપમેળે કાઢી નાખો - જૂના મેસેજ કાઢી નાખો + + ચેટ અને મીડિયા સ્ટોરેજ સંવાદ ની લંબાઈ મર્યાદા - બધા સંવાદ ને હવે ટ્રિમ કરો - બધા સંવાદ દ્વારા સ્કેન કરો અને સંવાદ ની લંબાઈ મર્યાદા લાગુ કરો + + લિંક થયેલ ડિવાઇસ લાઈટ ડાર્ક @@ -1322,7 +1322,7 @@ Wi-Fi નો ઉપયોગ કરતી વખતે રોમિંગ કરતી વખતે મીડિયા સ્વત:ડાઉનલોડ - મેસેજ સુવ્યવસ્થિત + સ્ટોરેજ વપરાશ ફોટા વિડિયો diff --git a/app/src/main/res/values-ha/strings.xml b/app/src/main/res/values-ha/strings.xml index 2d57cc24d..8981f8951 100644 --- a/app/src/main/res/values-ha/strings.xml +++ b/app/src/main/res/values-ha/strings.xml @@ -1296,13 +1296,13 @@ Lambobin sirrin MMSC Sakamakon sakon SMS Tambayi sakamakon ko wanne sakon SMS da ka/kika tura. - Sarrafa gogewar tsofaffin sakonni da kansu da zarar yafi tsawon da aka tsara - Goge tsofaffin sakonni + + Hira da midiya Ajiya Adadin tsahon hira - Rage ko wanne hira yanzu - Binciki ko wace hira kuma tabbatar da adadin tsawon su + + Na\'urori da aka hada Haske Duhu @@ -1326,7 +1326,7 @@ Sanda ake amfani da WiFi Sanda ake zama cikin hanyar internet a bayan fage Saukar da midiya mai sarrafa kanta - Ragewar tsahon sako + Kididdigan ajiya Hotuna Bidiyoyi diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 319ad3c47..98734aa1e 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1303,13 +1303,13 @@ MMSC पासवर्ड SMS वितरण रिपोर्ट आपके द्वारा भेजे जाने वाले प्रत्येक SMS मेसेज के लिए डिलीवरी रिपोर्ट का अनुरोध करें - वार्तालाप एक निर्दिष्ट लंबाई से अधिक हो जाने के बाद स्वचालित रूप से पुराने मेसेज को हटा दें - पुराने मेसेज हटाएं + + चैट और मीडिया स्टॉरेज वार्तालाप लंबाई सीमा - अब सभी वार्तालापों को ट्रिम करें - सभी वार्तालापों के माध्यम से स्कैन करें और बातचीत की लंबाई सीमा लागू करें + + जुड़े हुए उपकरण रोशनी अँधेरा @@ -1333,7 +1333,7 @@ Wi-Fi का उपयोग करते समय रोमिंग करते समय मीडिया ऑटो डाउनलोड - मेसेज ट्रिमिंग + स्टॉरेज का उपयोग तस्वीरें वीडियो diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index f6437f0e4..029048910 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1329,13 +1329,13 @@ Uvezi nekriptiranu kopiju. Kompatibilno s \'SMSBackup And Restore\'. MMSC lozinka SMS potvrde isporuke Traži potvrdu isporuke za svaki poslani SMS - Automatski obriši starije poruke nakon što razgovor pređe određenu duljinu - Obriši stare poruke + + Razgovori i media Pohrana Maksimalna duljina razgovora - Skrati sve razgovore odmah - Skeniraj sve razgovore i primijeni ograničenje duljine razgovora + + Povezani uređaji Svijetla Tamna @@ -1359,7 +1359,7 @@ Uvezi nekriptiranu kopiju. Kompatibilno s \'SMSBackup And Restore\'. Prilikom korištenja Wi-Fi Prilikom roaminga Automatsko preuzimanje media sadržaja - Skraćivanje poruke + Datoteke Zvuk Koristi emotikone sustava diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 35bea346a..5a400dba2 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -450,7 +450,6 @@ Függőben lévő tagsági kérések Nincs megjeleníthető tagsági kérés - A listán szereplő felhasználók megosztható csoporthivatkozások használatával szeretnének csatlakozni. \"%1$s\" hozzáadva \"%1$s\" elutasítva @@ -492,7 +491,7 @@ Csoportinfó szerkesztése Add meg, hogy ki változtathatja meg a csoport nevét, képét és az eltűnő üzenetekkel kapcsolatos beállításokat! Add meg, hogy ki adhat hozzá vagy hívhat meg új tagokat! - Megosztható csoporthivatkozás + Csoporthivatkozás Csoport letiltása Csoport blokkolásának feloldása Kilépés a csoportból @@ -612,7 +611,6 @@ A csoporthivatkozások hamarosan megérkeznek Frissítsd a Signalt a csoporthivatkozások használatához A csoporthivatkozások általi belépést egyelőre nem támogatja a Signal. Ez a funkció egy jövőbeli frissítéssel lesz elérhető. - Az általad használt verziójú Signal nem támogatja a megosztható csoporthivatkozásokat. Frissíts a legújabb verzióra, hogy a hivatkozás használatával csatlakozhass ehhez a csoporthoz! Signal frissítése A csoporthivatkozás érvénytelen @@ -848,25 +846,10 @@ %1$s megváltoztatta a csoport tagjainak szerkeszthetőségét erre: \"%2$s\". A csoporttagságot szerkeszthetők köre megváltozott erre: \"%1$s\" - Bekapcsoltad a megosztható csoporthivatkozást. - Bekapcsoltad a megosztható csoporthivatkozást adminisztrátori jóváhagyással. - Kikapcsoltad a megosztható csoporthivatkozást. - %1$s bekapcsolta a megosztható csoporthivatkozást. - %1$s bekapcsolta a megosztható csoporthivatkozást adminisztrátori jóváhagyással. - %1$s kikapcsolta a megosztható csoporthivatkozást. - A megosztható csoporthivatkozás be lett kapcsolva. - A megosztható csoporthivatkozás be lett kapcsolva adminisztrátori jóváhagyással. - A megosztható csoporthivatkozás ki lett kapcsolva. - Lecserélted a megosztható csoporthivatkozást. - %1$s lecserélte a megosztható csoporthivatkozást. - A megosztható csoporthivatkozás le lett cserélve. - Beléptél a csoportba egy megosztható csoporthivatkozás használatával. - %1$s belépett a csoportba egy megosztható csoporthivatkozás használatával. Elküldted csatlakozási igényedet a csoportnak. - %1$s csatlakozni szeretne megosztható csoporthivatkozás használatával. %1$s jóváhagyta csatlakozási igényedet a csoporthoz. %1$s jóváhagyott egy csatlakozási igényt tőle: %2$s. @@ -1753,13 +1736,9 @@ Kulcs-csere üzenet érkezett érvénytelen protokoll verzióhoz. MMSC jelszó SMS kézbesítési jelentések Kézbesítési jelentés kérése az összes elküldött SMS-ről - Régi üzenetek automatikus törlése, amint a beszélgetés üzeneteinek száma meghaladja a beállított értéket - Régi üzenetek törlése Csevegések és média Tárolás Beszélgetés hosszának korlátja - Összes beszélgetés csonkítása most - Az összes beszélgetés vizsgálata, és a beszélgetésekre vonatkozó hosszkorlátozás érvényesítése Társított eszközök Világos Sötét @@ -1789,13 +1768,14 @@ Kulcs-csere üzenet érkezett érvénytelen protokoll verzióhoz. Wi-Fi használata során Barangoláskor Médiafájlok automatikus letöltése - Beszélgetések csonkítása Tárhelyhasználat Fotók Videók Fájlok Hang Tárhely áttekintése + Egyik sem + Egyéni Rendszer emojik használata A Signal beépített emoji-támogatásának letiltása A hívások átjátszása a Signal szerverein annak érdekében, hogy IP címed telefonhívás során is rejtve maradjon partnered előtt. Engedélyezésével a hívások minősége gyengébb lesz. @@ -2122,6 +2102,7 @@ Kulcs-csere üzenet érkezett érvénytelen protokoll verzióhoz. Signal regisztráció - megerősítő kód Androidhoz Soha Ismeretlen + Senki Képernyőzár Signal-hoz való hozzáférés védelme Android képernyőzárral vagy ujjlenyomattal Inaktív képernyő lezárása előtti türelmi idő diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index fa4dd89da..5622c9f1b 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1452,13 +1452,9 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid. Kata Sandi MMSC Laporan pengiriman SMS Minta laporan pengiriman untuk tiap SMS yang dikirim - Otomatis menghapus pesan lama saat melebihi batas percakapan tertentu - Hapus pesan lama Obrolan dan media Penyimpanan Batas panjang percakapan - Pangkas seluruh percakapan - Pindai seluruh rangkaian percakapan dan terapkan batas panjang percakapan Perangkat terhubung Terang Gelap @@ -1483,13 +1479,15 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid. Ketika menggunakan Wi-Fi Ketika roaming Otomatis unduh media - Penyingkatan pesan Penggunaan media penyimpanan Foto Video Berkas Audio Tinjau penyimpanan + 1 tahun + Kosong + Spesifik Gunakan emoji sistem Nonaktifkan dukungan emoji Signal bawaan Sampaikan semua panggilan melalui peladen Signal untuk menghindari pengungkapan alamat IP Anda ke kontak Anda. Mengaktifkannya akan mengurangi kualitas panggilan. @@ -1789,6 +1787,7 @@ Menerima pesan pertukaran kunci untuk versi protokol yang tidak valid. Pendaftaran Signal - Kode Verifikasi untuk Android Tidak pernah Tidak dikenal + Tak seorangpun Layar terkunci Kunci akses Signal Anda dengan kunci layar Android atau sidik jari Batas waktu kunci layar tanpa aktivitas diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c761cc872..37ceead22 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -215,7 +215,7 @@ Impossibile registrare il messaggio! Non puoi inviare messaggi a questo gruppo perché non sei più un membro. Sul tuo dispositivo non sono presenti app per gestire questo link. - La tua richiesta di unirti è stata inviata all\'amministratore del gruppo. Riceverai una notifica quando interverranno. + La tua richiesta di unirti è stata inviata agli amministratori del gruppo. Riceverai una notifica quando interverranno. Annulla richiesta Per poter mandare un messaggio audio, permetti a Signal di accedere al tuo microfono. Signal richiede l\'autorizzazione all\'uso del microfono per inviare messaggi audio, ma è stata negata in modo permanente. Si prega di aprire il menu delle impostazioni dell\'app, selezionare \"Autorizzazioni\" e abilitare \"Microfono\". @@ -449,7 +449,6 @@ Richieste di diventare membro in attesa Nessuna richiesta di diventare membro da mostrare. - Le persone in questo elenco stanno tentando di unirsi a questo gruppo tramite il link del gruppo condivisibile. Aggiunto \"%1$s\" Rifiutato \"%1$s\" @@ -491,7 +490,7 @@ Modifica informazioni gruppo Scegli chi può modificare il nome del gruppo, l\'avatar e il timer dei messaggi a scomparsa. Scegli chi può aggiungere o invitare nuovi membri. - Link del gruppo condivisibile + Link del gruppo Blocca gruppo Sblocca gruppo Abbandona il gruppo @@ -611,7 +610,6 @@ Link dei gruppi presto disponibili Aggiorna Signal per utilizzare i link dei gruppi Unirsi a un gruppo tramite un link non è ancora supportato da Signal. Questa funzione verrà rilasciata in un prossimo aggiornamento. - La versione di Signal che stai utilizzando non supporta i link dei gruppi condivisibili. Aggiorna alla versione più recente per unirti a questo gruppo tramite link. Aggiorna Signal Il link del gruppo non è valido @@ -847,25 +845,15 @@ %1$s ha cambiato chi può modificare l\'appartenenza al gruppo in \"%2$s\". Chi può modificare l\'appartenenza al gruppo è stato cambiato in \"%1$s\". - Hai attivato il link del gruppo condivisibile. - Hai attivato il link del gruppo condivisibile con l\'approvazione degli amministratori. - Hai disattivato il link del gruppo condivisibile. - %1$s ha attivato il link del gruppo condivisibile. - %1$s ha attivato il link del gruppo condivisibile con l\'approvazione degli amministratori. - %1$s ha disattivato il link del gruppo condivisibile. - Il link del gruppo condivisibile è stato attivato. - Il link del gruppo condivisibile è stato attivato con l\'approvazione degli amministratori. - Il link del gruppo condivisibile è stato disattivato. + Hai attivato il link del gruppo senza l\'approvazione degli amministratori. + Hai attivato il link del gruppo con l\'approvazione degli amministratori. + Hai disattivato il link del gruppo. - Hai reimpostato il link del gruppo condivisibile. - %1$s ha reimpostato il link del gruppo condivisibile. - Il link del gruppo condivisibile è stato reimpostato. + Hai reimpostato il link del gruppo. - Ti sei unito al gruppo tramite il link del gruppo condivisibile. - %1$s si è unito al gruppo tramite il link del gruppo condivisibile. + Ti sei unito al gruppo tramite il link del gruppo. Hai inviato una richiesta a unirsi al gruppo. - %1$s ha richiesto di unirsi tramite il link del gruppo condivisibile. %1$s ha approvato la tua richiesta di unirti al gruppo. %1$s ha approvato una richiesta di unirsi al gruppo da %2$s. @@ -1750,13 +1738,9 @@ MMSC Password Rapporti di consegna SMS Richiedi un rapporto di consegna per ogni SMS inviato - Elimina automaticamente i messaggi più vecchi quando una conversazione supera una certa lunghezza. - Elimina i messaggi vecchi Chat e download Memoria Limite di lunghezza conversazione - Riduci immediatamente tutte le conversazioni - Analizza tutte le conversazioni e applica il limite di lunghezza Dispositivi collegati Chiaro Scuro @@ -1786,13 +1770,15 @@ Via Wi-Fi In roaming Download automatico file - Cancellazione messaggi Utilizzo memoria Foto Video File Audio Esamina memoria + 1 anno + Nessuno + Personalizzato Usa le emoji di sistema Disattiva le emoji di Signal Ritrasmetti le chiamate attraverso i server di Signal per non rivelare il tuo indirizzo IP ai tuoi contatti. Abilitandolo verrà ridotta la qualità della chiamata. @@ -2119,6 +2105,8 @@ Registrazione Signal - Codice di verifica per Android Mai Sconosciuto + Nessuno + Solo i tuoi contatti vedranno il tuo numero di telefono su Signal. Blocco schermo Blocca l\'accesso a Signal con il blocco schermo di Android o la tua impronta digitale Tempo di inattività per blocco schermo diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index fcef839bb..f2b0a492c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -485,7 +485,6 @@ בקשות ממתינות של חברי קבוצה אין חברי קבוצה להראות. - אנשים ברשימה זו מנסים להצטרף אל קבוצה זו באמצעות קישור הקבוצה בר־השיתוף. \"%1$s\" התווסף/ה \"%1$s\" נדחה/נדחתה @@ -533,7 +532,7 @@ ערוך מידע קבוצה בחר מי יכול לערוך את השם, היצגן וההודעות הנעלמות של הקבוצה. בחר מי יכול להוסיף או להזמין חברי קבוצה חדשים. - קישור קבוצה בר־שיתוף + קישור קבוצה חסום קבוצה בטל חסימת קבוצה עזוב קבוצה @@ -665,7 +664,6 @@ קישורי קבוצה מגיעים בקרוב עדכן את Signal כדי להשתמש בקישורי קבוצה הצטרפות אל קבוצה באמצעות קישור אינה נתמכת עדין על ידי Signal. מאפיין זה ישוחרר בעדכון עתידי. - הגרסה של Signal שאתה משתמש בה אינה תומכת בקישורי קבוצה שניתנים לשיתוף. עדכן אל הגרסה האחרונה כדי להצטרף אל קבוצה זו באמצעות קישור. עדכן את Signal קישור הקבוצה בלתי תקף @@ -925,25 +923,10 @@ %1$s שינה מי יכול לערוך חברות קבוצה אל \"%2$s\". מי יכול לערוך חברות קבוצה השתנה אל \"%1$s\". - הפעלת את קישור הקבוצה בר־השיתוף. - הפעלת את קישור הקבוצה בר־השיתוף עם אישור מנהלן. - כיבית את קישור הקבוצה בר־השיתוף. - %1$s הפעיל/ה את קישור הקבוצה בר־השיתוף. - %1$s הפעיל/ה את קישור הקבוצה בר־השיתוף עם אישור מנהלן. - %1$s כיבה/כיבתה את קישור הקבוצה בר־השיתוף. - קישור הקבוצה בר־השיתוף הופעל. - קישור הקבוצה בר־השיתוף הופעל עם אישור מנהלן. - קישור הקבוצה בר־השיתוף כובה. - איפסת את קישור הקבוצה בר־השיתוף. - %1$s איפס/ה את קישור הקבוצה בר־השיתוף. - קישור הקבוצה בר־השיתוף אופס. - הצטרפת אל הקבוצה באמצעות קישור הקבוצה בר־השיתוף. - %1$s הצטרפ/ה אל הקבוצה באמצעות קישור הקבוצה בר־השיתוף. שלחת בקשה להצטרף אל הקבוצה. - %1$s ביקש/ה להצטרף באמצעות קישור הקבוצה בר־השיתוף. %1$s אישר/ה את בקשתך להצטרף אל הקבוצה. %1$s אישר/ה בקשה להצטרף אל הקבוצה מאת %2$s. @@ -1858,13 +1841,9 @@ סיסמת MMSC דוחות מסירת מסרונים בקש דוח מסירה לכל מסרון שאתה שולח - מחק באופן אוטומטי הודעות ישנות יותר ברגע ששיחה חורגת מאורך שצוין - מחק הודעות ישנות התכתבויות ומדיה אחסון מגבלת אורך שיחה - קצץ את כל השיחות כעת - סרוק את כל השיחות ואכוף מגבלות אורך שיחה מכשירים מקושרים בהירה כהה @@ -1894,13 +1873,14 @@ בעת שימוש ב־Wi-Fi בעת נדידה הורדה אוטומטית של מדיה - קיצוץ הודעה שימוש באחסון תמונות סרטונים קבצים שמע סקור אחסון + אין + מותאם אישית השתמש באימוג\'י של מערכת השבת תמיכת אימוג\'י מובנית של Signal שדר את כל השיחות דרך שרת Signal כדי להימנע מחשיפת כתובת ה־IP שלך לאיש הקשר שלך. אפשור יפחית איכות שיחה. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index fc5cf3b2b..f3e6984af 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -431,7 +431,6 @@ 保留中のメンバー申請 メンバー申請はありません。 - このリストの人々が、共有グループリンク経由でグループ参加を申請しています。 「%1$s」を追加しました 「%1$s」を拒否しました @@ -470,7 +469,7 @@ グループの情報を編集 グループ名、アバターおよび消えるメッセージの時間を編集できる人を選択してください。 新しいメンバーを追加したり招待できる人を選択してください。 - 共有グループリンク + グループリンク グループをブロック グループのブロック解除 グループを抜ける @@ -584,7 +583,6 @@ グループリンクのご案内 グループリンクを使用するためSignalをアップデート リンク経由でのグループ参加は、Signalではまだ対応していません。この機能は今後のアップデートで提供されます。 - このバージョンのSignalは、共有グループリンクをサポートしていません。リンク経由でこのグループに参加するには、最新バージョンにアップデートしてください。 Signalをアップデート グループリンクが正しくありません @@ -808,25 +806,13 @@ %1$s がグループ情報を編集できるユーザを「%2$s」に変更しました。 グループ情報を編集できるユーザーが「%1$s」に変更されました。 - 共有グループリンクを有効にしました。 - 管理者承認制の共有グループリンクを有効にしました。 - 共有グループリンクを無効にしました。 - %1$s が共有グループリンクを有効にしました。 - %1$s が管理者承認制の共有グループリンクを有効にしました。 - %1$s が共有グループリンクを無効にしました。 - 共有グループリンクが有効になりました。 - 管理者承認制の共有グループリンクが有効になりました。 - 共有グループリンクが無効になりました。 + グループリンクを無効にしました。 - 共有グループリンクをリセットしました。 - %1$s が共有グループリンクをリセットしました。 - 共有グループリンクがリセットされました。 + グループリンクをリセットしました。 - 共有グループリンクからグループに参加しました。 - %1$s が共有グループリンクからグループに参加しました。 + グループリンクからグループに参加しました。 グループ参加を申請しました。 - %1$s が共有グループリンクから参加を申請しました。 %1$s がグループ参加申請を承認しました。 %1$s が %2$s からのグループ参加申請を承認しました。 @@ -1686,13 +1672,9 @@ MMSCパスワード SMS配信レポート 送信するすべてのSMSで配信レポートを要求します - 会話が指定した長さを超えると古いメッセージを自動で消去します - 古いメッセージの削除 チャットとメディア ストレージ 会話の長さ制限 - 全会話で古いメッセージを削減 - すべての会話をスキャンして会話の長さ制限を適用します リンク済み端末 ライト ダーク @@ -1710,7 +1692,7 @@ Signalユーザにプライベートメッセージと通話の自由を デバッグログを提出 Wi-Fi通話互換モード - この端末がWi-FiでのSMS/MMS送信を使用している場合に有効にします (Wi-Fi通話の有効時のみ) + この端末がWi-Fi経由のSMS/MMS送信を使用している場合は有効にしてください (Wi-Fi通話の有効時のみ有効になります) プライベートキーボード 既読通知 既読通知を無効にすると、相手の既読通知も受け取れません。 @@ -1722,13 +1704,15 @@ Wi-Fi利用時 ローミング時 メディアの自動ダウンロード - メッセージのトリミング ストレージ使用量 写真 動画 ファイル 音声 ストレージを確認 + 1年 + なし + カスタム システムの絵文字を使う Signal独自の絵文字を無効にします すべての通話をSignalサーバで中継して、相手にIPアドレスを知られることを防ぎます。ただし通話の品質は下がります。 @@ -2047,6 +2031,7 @@ Signal登録 - Android用認証コード なし 不明 + 無人 画面ロック SignalをAndroidの画面ロックや指紋認証でロックします 画面ロックの無操作タイムアウト diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index 47084bddb..372ebbb7a 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -1283,13 +1283,13 @@ Sandi MMSC Laporan pangiriman SMS Tedhaken laporan kangge saben SMS ingkang dipunkirimaken - Brusak pesen ingkang dangu sacara otomatis kala obrolan sampun langkung wates ingkang dipuntetapaken - Brusak pesen-pesen dangu + + Obrolan lan media Penyimpenan Wates panjangipun obrolan - Kirangi sedaya obrolan sakmenika - Scan sedaya obrolan lan tumindakaken wates panjang obrolan + + Pirantos ingkang dipuntautaken Pajar Peteng @@ -1313,7 +1313,7 @@ Kala ngginakaken Wi-Fi Kala roaming Pangunduhan media sacara otomatis - Pangirangan pesen + Pangginaan penyimpenan Foto Video diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index 2e9492cdb..08d09e1a1 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -1515,13 +1515,9 @@ ពាក្យសម្ងាត់ MMSC របាយការណ៍បញ្ជូនSMS ស្នើសុំរបាយការណ៍បញ្ជូនសម្រាប់សារSMSមួយៗដែលអ្នកផ្ញើ - លុបសារចាស់ៗដោយស្វ័យប្រវត្តិ នៅពេលការសន្ទនាលើសការកំណត់ - លុបសារចាស់ៗ ជជែក និងឯកសារ ឧបករណ៍ផ្ទុក ប្រវែងការសន្ទនាមានកំណត់ - តម្រឹមការសន្ទនាទាំងអស់ឥលូវនេះ - វិភាគតាមរយៈការសន្ទនាទាំងអស់ និងពង្រឹងដែនកំណត់ប្រវែងសន្ទនា ភ្ជាប់ឧបករណ៍ ភ្លឺ ងងឹត @@ -1546,13 +1542,13 @@ នៅពេលប្រើប្រាស់Wi-Fi នៅពេលរ៉ូមីង ឯកសារទាញយកដោយស្វ័យប្រវត្តិ - តម្រឹមសារ ការប្រើប្រាស់ឧបករណ៍ផ្ទុក រូបភាព វីដេអូ ឯកសារ សំឡេង បង្ហាញឧបករណ៍ផ្ទុក + គ្មាន ប្រើរូបអារម្មណ៍emoji នៅក្នុងប្រព័ន្ធ បិទមិនប្រើរូបអារម្មណ៍emoji របស់ Signal បញ្ជូនការហៅទាំងអស់តាមរយៈម៉ាស៊ីនមេ Signal ដើម្បីជៀសវាងការបង្ហាញអាសយដ្ឋាន អាយភី របស់អ្នក ទៅកាន់លេខទំនាក់ទំនងរបស់អ្នក។ ការអនុញ្ញាតនេះនឹងកាត់បន្ថយគុណភាពការហៅ។ diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index f1aab8018..97117da28 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -1336,13 +1336,13 @@ ಎಂಎಂಎಸ್‌ಸಿ ಪಾಸ್ವರ್ಡ್ ಎಸ್‌ಎಂಎಸ್ ಡೆಲಿವರಿ ವರದಿಗಳು ನೀವು ಕಳುಹಿಸುವ ಪ್ರತಿ ಎಸ್.ಎಮ್.ಎಸ್ ಸಂದೇಶಕ್ಕೂ ವಿತರಣಾ ವರದಿಯನ್ನು ವಿನಂತಿಸಿ - ಸಂಭಾಷಣೆಯು ನಿಗದಿತ ಉದ್ದವನ್ನು ಮೀರಿದ ನಂತರ ಹಳೆಯ ಸಂದೇಶಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಅಳಿಸಿ - ಹಳೆಯ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ + + ಚಾಟ್‌‌ ಗಳು ಹಾಗೂ ಮೀಡಿಯಾ ಸ್ಟೊರೇಜ್ ಸಂಭಾಷಣೆಯ ಉದ್ದ ಮಿತಿ - ಎಲ್ಲಾ ಸಂಭಾಷಣೆಗಳನ್ನೂ ಈಗ ಟ್ರಿಮ್ ಮಾಡಿ - ಎಲ್ಲ ಸಂಭಾಷಣೆಗಳನ್ನೂ ಸ್ಕ್ಯಾನ್ ಮಾಡಿ ಮತ್ತು ಸಂಭಾಷಣೆಯ ಉದ್ದದ ಮಿತಿಗಳನ್ನು ಜಾರಿಗೊಳಿಸಿ + + ಲಿಂಕ್ ಮಾಡಿರುವ ಸಾಧನಗಳು ತಿಳಿ ಗಾಢ @@ -1367,7 +1367,7 @@ ವೈಫೈ ಬಳಸುತ್ತಿರುವಾಗ ರೋಮಿಂಗ್-ನಲ್ಲಿರುವಾಗ ಮೀಡಿಯಾ ಆಟೋ ಡೌನ್‌ಲೋಡ್ - ಸಂದೇಶದ ಟ್ರಿಮ್ಮಿಂಗ್ + ಸ್ಟೊರೇಜ್ ಬಳಕೆ ಫೋಟೋಗಳು ವೀಡಿಯೊಗಳು diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d9fb2f6d5..5f7d06b31 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1333,13 +1333,13 @@ MMSC 비밀번호 SMS 전송 확인 SMS 메시지를 보낼 때마다 전송 확인 요청 - 대화 저장 한도가 초과되면 이전 메시지 자동 삭제 - 이전 메시지 삭제 + + 대화 및 미디어 저장 공간 대화 저장 한도 - 모든 대화 삭제 - 모든 대화 스캔 후 메시지 저장 한도 적용 + + 연결된 기기 밝게 어둡게 @@ -1365,7 +1365,7 @@ Wi-Fi 사용 시 로밍 시 미디어 자동 다운로드 - 대화 삭제 + 저장 공간 사용 사진 동영상 diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 6b3d9f427..ff19cfa36 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -1023,12 +1023,12 @@ Peyvborîna MMSCê Rapora gihiştina SMSê Rapora gihiştinê ji bo her SMSê ku te şand daxwaz bike - Peyamên kevin xwe-bi-xwe jê dibe piştî ku axaftin ji mezinahiya diyarkirî dirêjtir bibe - Peyamên kevin jê bibe + + Gotûbêj û medya Sînora direjiya axaftinê - Hemû axaftinan qut bike - Hemû gotûbêjan sken bike û zorê bide ku sînorên dirêjiya axaftinê + + Amûrên girêdayî Sivik Tarî @@ -1052,7 +1052,7 @@ Dema bikaranîna dataya Wi-Fiyê Dema gerrê Xwebixwe-daxistina medyayê - Qusandina peyamê + Deng Emojiya pergalê bi kar bîne Piştgiriya emojiyê a daxilî ya Signalê neçalak bike diff --git a/app/src/main/res/values-lg/strings.xml b/app/src/main/res/values-lg/strings.xml index 7944ef622..d78e0b892 100644 --- a/app/src/main/res/values-lg/strings.xml +++ b/app/src/main/res/values-lg/strings.xml @@ -632,12 +632,12 @@ gy\'olonze (%s) sintuufu. mmsc passiwadi oekiraga mnti obubaka butuuse saba ekiraga nti bulu bubaka bwowereza butuuse - buterevu sagula obubaka mu mboozi kasita buyitirira - sagula obubaka obukadde + + emboozi nebifannyi,vidiyo Ekkomo ku mboozi - sala ku mboozi zona kati - yita mu mboozi zonna otekeko ekkomo ku buwanvu bwazo. + + Abyampuliziganya ebikwataganye Ekitangaala @@ -656,7 +656,7 @@ gy\'olonze (%s) sintuufu. Bwoba okozesa WI-Fi Bwoba ku roaming buterevu ebifananyi ne video bye gyayo - Okuyimpaya obubaka + Kozesa system emoji Gyako emoji support eyazimbibwa mu Signal diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index eaa588ebc..eea048cb4 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -485,7 +485,7 @@ Laukiantys narių prašymai Nėra rodytinų narių prašymų. - Šiame sąraše esantys žmonės bando prisijungti prie šios grupės per bendrinamą grupės nuorodą. + Šiame sąraše esantys žmonės bando prisijungti prie šios grupės per grupės nuorodą. Pridėtas naudotojas „%1$s“ Naudotojas „%1$s“ atmestas @@ -533,7 +533,7 @@ Taisyti grupės informaciją Pasirinkite, kas gali taisyti grupės pavadinimą, avatarą bei išnykstančias žinutes. Pasirinkite, kas gali pridėti ar pakviesti naujus narius. - Bendrinama grupės nuoroda + Grupės nuoroda Užblokuoti grupę Atblokuoti grupę Išeiti iš grupės @@ -665,7 +665,7 @@ Greitu metu pasirodys grupių nuorodos Atnaujinkite Signal norėdami naudoti grupių nuorodas Signal kol kas nepalaiko prisijungimo prie grupių per nuorodas. Ši ypatybė bus išleista būsimajame atnaujinime. - Jūsų naudojama Signal versija nepalaiko bendrinamų grupės nuorodų. Norėdami prisijungti prie šios grupės per nuorodą, atnaujinkite Signal iki naujausios versijos. + Jūsų naudojama Signal versija nepalaiko grupės nuorodų. Norėdami prisijungti prie šios grupės per nuorodą, atnaujinkite Signal iki naujausios versijos. Atnaujinti Signal Grupės nuoroda negalioja @@ -925,25 +925,25 @@ %1$s pakeitė asmenis, kurie gali keisti grupės narystes į „%2$s“. Buvo pakeista, kad „%1$s“ gali keisti grupės narystes. - Jūs įjungėte bendrinamą grupės nuorodą. - Jūs įjungėte bendrinamą grupės nuorodą su administratoriaus patvirtinimais. - Jūs išjungėte bendrinamą grupės nuorodą. - %1$s įjungė bendrinamą grupės nuorodą. - %1$s įjungė bendrinamą grupės nuorodą su administratoriaus patvirtinimais. - %1$s išjungė bendrinamą grupės nuorodą. - Bendrinama grupės nuoroda įjungta. - Įjungta bendrinama grupės nuoroda su administratoriaus patvirtinimais. - Bendrinama grupės nuoroda išjungta. + Jūs įjungėte grupės nuorodą su išjungtais administratoriaus patvirtinimais. + Jūs įjungėte grupės nuorodą su įjungtais administratoriaus patvirtinimais. + Jūs išjungėte grupės nuorodą. + %1$s įjungė grupės nuorodą su išjungtais administratoriaus patvirtinimais. + %1$s įjungė grupės nuorodą su įjungtais administratoriaus patvirtinimais. + %1$s išjungė grupės nuorodą. + Įjungta grupės nuoroda su išjungtais administratoriaus patvirtinimais. + Įjungta grupės nuoroda su įjungtais administratoriaus patvirtinimais. + Grupės nuoroda išjungta. - Jūs atstatėte bendrinamą grupės nuorodą. - %1$s atstatė bendrinamą grupės nuorodą. - Bendrinama grupės nuoroda atstatyta. + Jūs atstatėte grupės nuorodą. + %1$s atstatė grupės nuorodą. + Grupės nuoroda atstatyta. - Jūs prisijungėte prie grupės per bendrinamą grupės nuorodą. - %1$s prisijungė prie grupės per bendrinamą grupės nuorodą. + Jūs prisijungėte prie grupės per grupės nuorodą. + %1$s prisijungė prie grupės per grupės nuorodą. Jūs išsiuntėte prašymą prisijungti prie grupės. - %1$s paprašė prisijungti per bendrinamą grupės nuorodą. + %1$s paprašė prisijungti per grupės nuorodą. %1$s patvirtino jūsų prašymą prisijungti prie grupės. %1$s patvirtino naudotojo %2$s prašymą prisijungti prie grupės. @@ -1539,6 +1539,7 @@ Saugumo numerio pasikeitimai Vis tiek siųsti + Vis tiek skambinti Gali būti, kad šie žmonės iš naujo įdiegė programėlę ar pakeitė įrenginį. Patvirtinkite savo saugumo numerį su kiekvienu iš jų, kad užtikrintumėte privatumą. Rodyti Anksčiau patvirtinta(-s) @@ -1858,13 +1859,11 @@ MMSC slaptažodis SMS pristatymo ataskaitos Reikalauti pristatymo ataskaitos kiekvienai jūsų siunčiamai SMS žinutei - Automatiškai ištrinti senesnes žinutes, kai pokalbis viršija nurodytą ilgį - Ištrinti senas žinutes Pokalbiai ir medija Saugykla Pokalbio ilgio riba - Išvalyti visus pokalbius dabar - Peržiūrėti visus pokalbius ir priverstinai taikyti pokalbio ilgio ribas + Saugoti žinutes + Išvalyti žinučių istoriją Susieti įrenginiai Šviesus Tamsus @@ -1894,17 +1893,33 @@ Naudojant belaidį ryšį (Wi-Fi) Naudojant tarptinklinį ryšį Automatinis medijos atsisiuntimas - Žinučių išvalymas + Žinučių istorija Saugyklos naudojimas Nuotraukos Vaizdo įrašai Failai Garso įrašai Peržiūrėti saugyklą + Ištrinti senesnes žinutes? + Išvalyti žinučių istoriją? + Tai visam laikui išvalys visus pokalbius iki %1$s naujausių žinučių. + Tai visam laikui ištrins visą žinučių istoriją ir mediją iš jūsų įrenginio. + Ar tikrai norite ištrinti visą žinučių istoriją? + Visa žinučių istorija bus pašalinta visam laikui. Šio veiksmo negalima bus atšaukti. + Ištrinti viską dabar + Amžinai + 1 metus + 6 mėnesius + 30 dienų + Nėra + %1$s žinučių + Tinkintai + Tinkinta pokalbio ilgio riba Naudoti sistemos jaustukus Išjungti Signal įtaisytą jaustukų palaikymą Retransliuoti visus skambučius per Signal serverį, kad būtų išvengta jūsų IP adreso atskleidimo jūsų adresatui. Įjungus, pablogės skambučių kokybė. Visada retransliuoti skambučius + Kas gali… Programėlės prieiga Susisiekimas Pokalbiai @@ -1929,6 +1944,7 @@ Pranešti Gauti pranešimus, kai jus pamini pokalbiuose, kuriuose pranešimai yra išjungti Nusistatyti naudotojo vardą + Tinkinti parinktį @@ -2243,6 +2259,11 @@ Signal registracija - Patvirtinimo kodas, skirtas Android Niekada Nežinoma + Matyti mano telefono numerį + Rasti mane pagal telefono numerį + Visi + Mano adresatai + Niekas Ekrano užraktas Užrakinti prieigą prie Signal, naudojant Android ekrano užraktą ar piršto atspaudą Ekrano užrakto neveiklumui skirtas laikas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index adfed71ae..6db885672 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -1513,13 +1513,9 @@ Saņemts nederīgas protokola versijas atslēgas apmaiņas ziņojums. MMSC Parole SMS piegādes atskaites Pieprasīt piegādes atskaiti par katru nosūtīto īsziņu - Automātiski dzēst vecas ziņas, kad saruna pārsniedz konkrētu garumu - Dzēst vecas ziņas Tērzēšana un multivide Krātuve Sarunu garuma limits - Saīsināt visas sarunas tagad - Skatīt visas sarunas un uzspiest sarunu garumu limitus tagad Piesaistās ierīces Gaišs Tumšs @@ -1546,13 +1542,13 @@ Saņemts nederīgas protokola versijas atslēgas apmaiņas ziņojums. Izmantojot WiFi Izmantojot viesabonēšanau Multivides automātiskā lejupielāde - Ziņu apgriešana Krātuves lietojums Attēli Video Faili Audio Apskatīt krātuvi + Neviens Lietot sistēmas emoji Izslēgt Signal emoji Pārraidiet visus zvanus caur Signal serveri, lai izvairītos no IP adreses atklāšanas kontaktpersonai. Aktivizēšana samazinās zvanu kvalitāti. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 74a75230a..36194fcdf 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -471,7 +471,7 @@ MMSC лозинка SMS потврда за достава Побарајте доставна потврда за секоја испратена SMS порака - Избришете стари пораки + Максимална должина на разговор Светла Темна diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 8a8e9b7b5..dd434a1f8 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -1579,13 +1579,9 @@ grūpp ap‌ḍē MMSC പാസ്‌വേഡ് SMS ഡെലിവറി റിപ്പോർട്ടുകൾ നിങ്ങൾ അയയ്ക്കുന്ന ഓരോ SMS സന്ദേശത്തിനും ഒരു ഡെലിവറി റിപ്പോർട്ട് അഭ്യർത്ഥിക്കുക - ഒരു സംഭാഷണം ഒരു നിശ്ചിത ദൈർഘ്യം കവിഞ്ഞാൽ പഴയ സന്ദേശങ്ങൾ യാന്ത്രികമായി ഇല്ലാതാക്കുക - പഴയ സന്ദേശങ്ങൾ ഇല്ലാതാക്കുക ചാറ്റുകളും മീഡിയയും സ്റ്റോറേജ് സംഭാഷണ ദൈർഘ്യ പരിധി - എല്ലാ സംഭാഷണങ്ങളും ഇപ്പോൾ ട്രിം ചെയ്യുക - എല്ലാ സംഭാഷണങ്ങളിലൂടെയും സ്‌കാൻ ചെയ്‌ത് സംഭാഷണ ദൈർഘ്യ പരിധി നടപ്പിലാക്കുക ലിങ്കുചെയ്‌ത ഉപകരണങ്ങൾ ലൈറ്റ് ഡാർക്ക് @@ -1613,13 +1609,13 @@ grūpp ap‌ḍē വൈഫൈ ഉപയോഗിക്കുമ്പോൾ റോമിംഗ് ചെയ്യുമ്പോൾ മീഡിയ യാന്ത്രിക-ഡൗൺലോഡ് - സന്ദേശം ട്രിമ്മിംഗ് സ്റ്റോറേജ് ഉപയോഗം ഫോട്ടോ വീഡിയോ ഫയലുകൾ ഓഡിയോ സ്റ്റോറജ് അവലോകനം ചെയ്യുക + ഒന്നുമില്ല സിസ്റ്റം ഇമോജി ഉപയോഗിക്കുക Signal-ന്റെ അന്തർനിർമ്മിത ഇമോജി സപ്പോർട്ട് പ്രവർത്തനരഹിതമാക്കുക നിങ്ങളുടെ കോൺ‌ടാക്റ്റിലേക്ക് നിങ്ങളുടെ ഐ‌പി വിലാസം വെളിപ്പെടുത്തുന്നത് ഒഴിവാക്കാൻ സിഗ്നൽ സെർവർ വഴി എല്ലാ കോളുകളും റിലേ ചെയ്യുക. പ്രവർത്തനക്ഷമമാക്കുന്നത് കോൾ നിലവാരം കുറയ്ക്കും. diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 20237c918..54a7d7963 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -1375,13 +1375,13 @@ MMSC संकेतशब्द SMS पोचवणी अहवाल आपण पाठविणाऱ्या प्रत्येक संदेशावर पोचवणी अहवालाची विनंती करा - एकदा संभाषण एका निर्दिष्ट लांबीच्या पुढे गेले की स्वयंचलितपणे जुने संदेश हटवा - जुने संदेश हटवा + + चॅट आणि मिडिया संचयन संभाषण लांबी मर्यादा - आता संभाषण ट्रिम करा - सर्व संभाषणातून स्कॅन करा आणि संभाषण लांबी मर्यादा अंमलात आणा + + लिंक केलेले डिव्हाईस फिकट गडद @@ -1405,7 +1405,7 @@ Wi-Fi वापरताना रोमिंग असताना मिडिया स्वयं-डाऊनलोड करा - संदेश ट्रि्मिंग + संचयन वापर फोटो व्हिडिओ diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ec05318f1..8534b10c9 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1155,12 +1155,12 @@ Menerima mesej pertukaran kunci untuk versi protokol yang tidak sah. Kata Laluan MMSC Laporan hantaran SMS Minta laporan penghantaran untuk setiap mesej SMS yang anda hantar - Memadam mesej lama apabila perbualan melebihi panjang yang ditentukan secara automatik - Padam mesej lama + + Sembang dan media Had panjang perbualan - Potong semua perbualan sekarang - Mengimbas semua perbualan dan melaksanakan had panjang perbualan + + Peranti dipaut Cerah Gelap @@ -1184,7 +1184,7 @@ Menerima mesej pertukaran kunci untuk versi protokol yang tidak sah. Semasa mengguna WI-Fi Semasa perayauan Memuat turun media secara automatik - Memotong mesej + Audio Mengguna emoji sistem Menyahdayakan sokongan emoji terbina dalam Signal diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index fdc62e809..ea7078895 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -969,13 +969,13 @@ MMSC စကားဝှက် SMS ပို့ပြီးကြောင်း အသိပေးခြင်း SMS တစ်ခုပို့ပြီးတိုင်း ပို့ပြီးကြောင်း အသိပေးပါ။ - စကားဝိုင်းမှ သတ်မှတ်ထားသည့် ပမာဏကျော်သည်နှင့် တစ်ပြိုင်နက် စာအဟောင်းများကို အလိုအလျောက် ဖျက်ပါ - စာတိုအဟောင်းများကို ဖျက်ပါ။ + + စကားစမြည်များနှင့် ရုပ်/သံ/ပုံများ သိမ်းဆည်းရာ စာလုံးရေ အကန့်အသတ် - စကားဝိုင်းများအားလုံးကို အခု ညှိပါ - စကားဝိုင်းများအားလုံးကို သေချာကြည့်ပြီး စကားဝိုင်း၏ အရှည်ကန့်သတ်ချက်ကို အတည်ပြုပါ + + ချိတ်ဆက်ထားသော စက်ပစ္စည်းများ အလင်း အမှောင် @@ -997,7 +997,7 @@ ဝိုင်ဖိုင်ကို အသုံးပြုသည့်အခါ Roaming လုပ်သည့်အခါ ရုပ်/သံ/ပုံကို အလိုအလျောက် ဒေါင်းလုတ်လုပ်ပါ - စာများကို ညှိခြင်း + အသံ emoji စနစ်ကို သုံးပါ Signal ရဲ့ emoji ကို ပိတ်ပါ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 25df9679d..a1d3c21f4 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -1621,13 +1621,9 @@ Mottok nøkkelutvekslingsmelding for ugyldig protokollversion. MMSC-passord SMS-leveringsrapporter Be om leveringsrapport når du sender SMS - Slett gamle meldinger automatisk når en samtale overskrider valgt lengde - Slett gamle meldinger Samtaler og medier Lagring Lengdegrense for samtaler - Rydd opp i alle samtaler nå - Søk gjennom alle samtaler og tving lengdegrenser for samtaler Sammenkoblede enheter Lys Mørk @@ -1657,13 +1653,14 @@ Mottok nøkkelutvekslingsmelding for ugyldig protokollversion. Ved bruk av Wi-Fi I fremmednett Automatisk nedlasting av medier - Automatisk sletting Lagringsbruk Bilder Videoer Filer Lyd Gå gjennom lagring + Ingen + Selvvalgt Bruk systemets emoji Slå av innebygd emoji-støtte i Signal Videresend alle samtaler gjennom Signal serveren for å unngå å avsløre IP-adressen din til kontakten din. Aktivering vill redusere samtalekvaliteten. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 821557ae2..3826e3eca 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -450,7 +450,7 @@ Groepsuitnodigingen in afwachting Er zijn geen groeps uitnodigingen om weer te geven. - Personen in deze lijst proberen lid te worden van deze groep via een deelbare groepslink. + Personen opdeze lijst proberen lid te worden van deze groep via de groepslink. \"%1$s\" toegevoegd \"%1$s\" afgewezen @@ -492,7 +492,7 @@ Groepsinformatie aanpassen Kies wie de naam van de groep, de groepsafbeelding en de timer voor zelfwissende berichten mag aanpassen. Kies wie nieuwe leden mag toevoegen of uitnodigen. - Deelbare groepslink + Groepslink Groepsgesprek blokkeren Groep deblokkeren Groep verlaten @@ -612,7 +612,7 @@ Groepslinks komen binnenkort Werk Signal bij om groepskoppelingen te kunnen gebruiken Lid worden van een groep is nog niet ondersteund in Signal. Deze functie zal in een komende versie uitgeleverd worden. - De versie van Signal die je gebruikt ondersteunt geen deelbare groepskoppelingen. Werk Signal bij naar de meest recente versie om via een koppeling lid te kunen worden van een groep. + De versie van Signal die je gebruikt ondersteunt geen groepslinks. Werk Signal bij naar de meest recente versie om via een link lid te kunen worden van een groep. Signal bijwerken Ongeldige groepslink @@ -848,25 +848,23 @@ %1$s heeft de instelling voor wie de groepslidmaatschap kan aanpassen op ‘%2$s’ ingesteld. De instelling voor wie de groepslidmaatschap kan aanpassen is op ‘%1$s’ ingesteld. - Je hebt de deelbare groepslink ingeschakeld. - Je hebt de deelbare groepslink met beheerderstoestemming ingeschakeld. - Je hebt de deelbare groepslink uitgeschakeld. - %1$s heeft de deelbare groepslink ingeschakeld. - %1$s heeft de deelbare groepslink met beheerderstoestemming ingeschakeld. - %1$s heeft de deelbare groepslink uitgeschakeld. - De deelbare groepslink is ingeschakeld. - De deelbare groepslink met beheerderstoestemming is ingeschakeld. - De deelbare groepslink is uitgeschakeld. + Je hebt de groepslink ingeschakeld, beheerderstoestemming is uitgeschakeld. + Je hebt de groepslink met beheerderstoestemming ingeschakeld. + Je hebt de groepslink uitgeschakeld. + %1$s heeft de groepslink uitgeschakeld. + De groepslink is ingeschakeld. beheerderstoestemming is uitgeschakeld. + De groepslink met beheerderstoestemming is ingeschakeld. + De groepslink is uitgeschakeld. - Je hebt de deelbare groepslink vernieuwd. - %1$s heeft de deelbare groepslink vernieuwd. - De deelbare groepslink is vernieuwd. + Je hebt de groepslink vernieuwd. + %1$s heeft de groepslink vernieuwd. + De groepslink is vernieuwd. - Je bent via de deelbare groepslink lid geworden van de groep. - %1$s is lid geworden via de deelbare groepslink. + Je bent via de groepslink lid geworden van de groep. + %1$s is lid geworden via de groepslink. Je hebt een verzoek verstuurd om lid te worden van de groep. - %1$s heeft verzocht om via de deelbare groepslink lid te worden. + %1$s heeft verzocht om via de groepslink lid te worden. %1$s heeft je verzoek om lid te worden van de groep toegestaan. %1$s heeft het verzoek van %2$s om lid te worden van de groep toegestaan. @@ -1450,6 +1448,7 @@ Tot slot moet Signal de telefoonstatus kunnen lezen om te voorkomen dat Signal-o Veiligheidsnummer veranderingen Toch verzenden + Toch bellen De volgende personen hebben mogelijk de app opnieuw geïnstalleerd of hebben een ander apparaat in gebruik genomen. Verifieer jullie veiligheidsnummers om zeker te zijn dat je met de juiste personen communiceert. Weergeven Was voorheen geverifieerd @@ -1712,8 +1711,8 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Wijzig je wachtwoord Schermvergrendeling met wachtwoord inschakelen Vergrendel scherm en meldingen met een wachtwoord - Schermafdrukken blokkeren - Bescherm je gesprekken tegen schermopnames door andere apps, zowel als Signal geopend is als in de lijst van recent gebruikte apps. + Meekijkpreventie + Verberg Signal in de lijst van recent geopende apps en bescherm je gesprekken tegen schermopnames door andere apps door schermafdrukken te blokkeren Signal automatisch vergrendelen na een bepaalde periode van inactiviteit Vergrendeling bij inactiviteit Inactiviteitsduur voor vergrendeling @@ -1755,13 +1754,11 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi MMSC-wachtwoord Sms-ontvangstbevestigingen Vraag een ontvangstbevestiging voor ieder verzonden sms-bericht - Wis automatisch oudere berichten wanneer een gesprek meer dan een bepaald aantal berichten bevat - Oude berichten wissen Gesprekken en media Opslag Gesprekslengtelimiet - Oude berichten nu verwijderen - Scan alle gesprekken en pas de maximale gesprekslengte toe + Berichten bewaren + Berichtengeschiedenis opschonen Gekoppelde apparaten Licht Donker @@ -1791,17 +1788,30 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Bij wifi-verbinding Bij roaming Media automatisch downloaden - Bewaartermijn beperken + Berichtengeschiedenis Opslaggebruik Afbeeldingen Video\'s Documenten Audio Alle bestanden bekijken + Oude berichten verwijderen? + Berichtengeschiedenis opschonen? + Dit zalalle berichten ouder dan %1$s permanent van je apparaat verwijderen. + Nu alles verwijderen + Voor altijd + 1 jaar + 6 maanden + 30 dagen + Niets + %1$s berichten + Aangepast + Aangepaste gesprekslengtelimiet Gebruik systeem-emoji Schakel de ingebouwde emoji-ondersteuning van Signal uit Om te voorkomen dat je gesprekspartner je IP-adres kan achterhalen worden Signal-oproepen met niet-contactpersonen altijd omgeleid via de Signal-server. Door deze optie in te schakelen wordt dat ook gedaan voor Signal-oproepen met contacten wie wel in je contactenlijst staan. Dit leidt echter tot een verminderde geluids- en videokwaliteit. Alle oproepen omleiden + Wie kan… Toegang tot app Communicatie Gesprekken @@ -1826,6 +1836,7 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Geef me een melding Ontvang meldingen wanneer je gementioned wordt in een gedempt gesprek Gebruikersnaam configureren + Optie personaliseren @@ -2124,6 +2135,12 @@ Signal zal nu toestemming vragen om je contactenlijst te lezen, om na te gaan wi Signal-registratie - Verificatiecode voor Android Nooit Onbekend + Mijn telefoonnummer zien + Vind me via mijn telefoonnummer + Iedereen + Mijn contactpersonen + Niemand + Je telefoonnummer zal zichtbaar zijn voor iedereen en elke groep waar je een bericht naar stuurt Schermvergrendeling Vergrendel Signal met de Android-schermvergrendeling of vingerafdruk Inactiviteitsduur voor schermvergrendeling diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index c08d890b5..27a9db42e 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -48,6 +48,7 @@ PIN oppretta. PIN skrudd av. Skjul + Gøym påminning? %d minutt @@ -214,6 +215,7 @@ Klarte ikkje å ta opp lydar. Du kan ikkje senda meldingar til denne gruppa sidan du ikkje lenger er medlem. Du har inga program på denne eininga som kan handtera denne lenka. + Førespurnaden din om å bli med er sendt til gruppeadministratoren. Du får eit varsel når dei gjer noko med det. Avslå førespurnad La Signal få tilgang til mikrofonen for å senda lydmeldingar. Signal treng tilgang til mikrofonen for å senda lydklipp, men tilgangen er permanent avslått. Opna app-innstillingsmenyen og vel «Tilgang» – eventuelt «Tillatelser» – og skru på «Mikrofon». @@ -341,6 +343,7 @@ Viss kontoen din har lenka einingar, så vil nye notat bli synkroniserte.Ta bilde Vel frå galleri Fjern bilde + Du må gi tillating til bruk av kameraet for å ta eit bilde. No %dm @@ -449,6 +452,8 @@ Dei er invitert inn, og ser inga gruppemeldingar før dei godtek. Klarte ikkje trekka tilbake invitasjonar + Ventande medlemsførespurnader + Ingen medlemsførespurnader La til «%1$s» Avslo «%1$s» @@ -485,11 +490,12 @@ Dei er invitert inn, og ser inga gruppemeldingar før dei godtek. Forsvinnande meldingar Ventande gruppeinvitasjonar + Medlemsførespurnader og invitasjonar Legg til medlemmar Endra gruppeinfo Vel kven som kan endra gruppenamnet, -bildet og forsvinnande meldingar. Vel kven som kan legga til eller invitera nye medlemmar. - Delbar gruppelenke + Gruppelenke Blokker gruppa Ikkje blokker gruppe Forlat gruppe @@ -609,7 +615,6 @@ Dei er invitert inn, og ser inga gruppemeldingar før dei godtek. Gruppelenker kjem snart Oppdater Signal for å bruka gruppelenker Signal støtter ikkje enno å bli med i grupper via lenker. Denne funksjonaliteten kjem i ei seinare utgåve. - Signal-utgåva di støttar ikkje delbare gruppelenker. Oppdater til siste utgåve for å bli med denne gruppa via ei lenke. Oppdater Signal Gruppelenka er ugyldig @@ -845,25 +850,10 @@ Dei er invitert inn, og ser inga gruppemeldingar før dei godtek. %1$s endra kven som kan redigera gruppemedlemskap til «%2$s». Kven som kan endra gruppemedlemskap er endra til «%1$s». - Du aktiverte den delbare gruppelenka. - Du aktiverte den delbare gruppelenka med admin-godkjenning. - Du deaktiverte den delbare gruppelenka. - %1$s aktiverte den delbare gruppelenka. - %1$s aktiverte den delbare gruppelenka med admin-godkjenning. - %1$s deaktiverte den delbare gruppelenka. - Den delbare gruppelenka er aktivert. - Den delbare gruppelenka er aktivert med admin-godkjenning. - Den delbare gruppelenka er deaktivert. - Du nullstilte den delbare gruppelenka. - %1$s nullstilte den delbare gruppelenka. - Den delbare gruppelenka er nullstilt. - Du blei med i gruppa via den delbare gruppelenka. - %1$s blei med i gruppa via den delbare gruppelenka. Du spurde om å bli med i gruppa. - %1$s spurde om å bli med via den delbare gruppelenka. %1$s godkjende førespurnaden din om å bli med i gruppa. %1$s godkjende førespurnaden frå %2$s om å bli med i gruppa. @@ -875,6 +865,7 @@ Dei er invitert inn, og ser inga gruppemeldingar før dei godtek. %1$s avslo ein førespurnad frå %2$s om å bli med i gruppa. Ein førespurnad frå %1$s om å bli med i gruppa er avslått. Du trekte tilbake førespurnaden om å bli med i gruppa. + %1$s trekte tilbake førespurnaden om å bli med i gruppa. Tryggleiksnummeret ditt med %s har endra seg. Du markerte at tryggleiksnummeret ditt med %s er stadfesta. @@ -1202,6 +1193,7 @@ Mottok nøkkelutvekslingsmelding for ugyldig protokollversjon. Brukarnamn kan ikkje begynna med siffer. Brukarnamnet er ugyldig. Brukarnamn må vera mellom %1$d og %2$d teikn lange. + Brukarnamn er valfrie på Signal. Viss du vil laga eit brukarnamn, så kan andre Signal-brukarar finna deg med dette og kontakta deg utan å kjenna til telefonnummeret ditt. Kontakten din køyrer ein gammal versjon av Signal. Ver venleg og be dei om å oppdatera før du stadfestar tryggleiksnummeret ditt. Kontakten din køyrer ein nyare versjon av Signal med eit inkompatibelt QR-kodeformat. Ver venleg og oppdater for å samanlikna. @@ -1747,13 +1739,9 @@ Ved registrering sender me litt kontaktinformasjon til tenaren. Informasjonen ve MMSC-passord SMS-leveringsrapportar Be om leveringsrapport når du sender SMS - Slett gamle meldingar automatisk når ein samtale overskrid vald lengd - Slett gamle meldingar Samtalar og media Lagring Lengdgrense for samtalar - Rydd opp i alle samtalar no - Søk gjennom alle samtalar og tving lengdgrenser for samtalar Tilkopla einingar Lys Mørk @@ -1783,13 +1771,14 @@ Ved registrering sender me litt kontaktinformasjon til tenaren. Informasjonen ve Ved bruk av Wifi I framandnett Automatisk nedlasting av media - Meldingsforkorting Lagringsbruk Bilde Videoar Filer Lyd Gå gjennom lagring + Inga + Sjølvvald Bruk emoji på systemet Slå av innebygd emoji-støtte i Signal Vidaresend alle samtalar gjennom Signal-tenaren for å unngå å visa IP-adressa di til kontakten din. Dette vil gi dårlegare samtalekvalitet. @@ -1817,6 +1806,7 @@ Ved registrering sender me litt kontaktinformasjon til tenaren. Informasjonen ve Omtalar Varsla meg Få varsel når du blir nemnt i dempa samtalar + Lag eit brukarnamn @@ -2211,5 +2201,6 @@ Du er à jour! QR-kode Del Kopiert til utklippstavla + Denne lenka er ikkje aktiv no diff --git a/app/src/main/res/values-pa-rPK/strings.xml b/app/src/main/res/values-pa-rPK/strings.xml index 086644426..150cf68d0 100644 --- a/app/src/main/res/values-pa-rPK/strings.xml +++ b/app/src/main/res/values-pa-rPK/strings.xml @@ -1303,13 +1303,13 @@ MMSC پاس ورڈ SMS ڈلیوری دیاں اطلاعواں آپنے گھلے ہر اک SMS سنیہے لئی ڈلیوری رپورٹ دی درخاست کرو - گل باتاں اک مخصوص طوالت تائیں لمیاں ہون توں بعد پرانے سنیہاں نوں آپوں آپ مٹا دیو - پرانے سنیہے مٹا دیو + + چیٹس تے میڈیا اسٹوریج گل بات دی لمبائی دی حد - ساریاں گلاں باتاں ہونڑی مختصر کرو - ساریاں گلاں باتاں نوں اسکین کرو تے گل بات دی لمبائی دی حداں نوں مقرر کرو + + لنک کیتے آلے ہلکا گہرا @@ -1333,7 +1333,7 @@ وائی فائی استعمال کردے ویلے رومنگ ویلے میڈیا خودکار ڈاؤن لوڈ - سنیہا مختصر کرنا + اسٹوریج استعمال تصویراں ویڈیوز diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 6ca4df72f..32b41c922 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -1411,13 +1411,13 @@ ਐਮਐਮਐਸਸੀ ਪਾਸਵਰਡ SMS ਡਿਲਿਵਰੀ ਰਿਪੋਰਟ ਤੁਸੀਂ ਭੇਜਣ ਵਾਲੇ ਹਰੇਕ SMS ਸੁਨੇਹੇ ਲਈ ਡਿਲੀਵਰੀ ਰਿਪੋਰਟ ਦੀ ਬੇਨਤੀ ਕਰੋ - ਗੱਲਬਾਤ ਇੱਕ ਖਾਸ ਲੰਬਾਈ ਤੋਂ ਵੱਧ ਗਈ ਤਾਂ ਆਟੋਮੈਟਿਕ ਹੀ ਪੁਰਾਣੇ ਸੁਨੇਹੇ ਹਟਾਓ - ਪੁਰਾਣੇ ਸੁਨੇਹੇ ਮਿਟਾਓ  + + ਗੱਲਬਾਤ ਅਤੇ ਮੀਡੀਆ ਸਟੋਰੇਜ਼ ਗੱਲਬਾਤ ਦੀ ਲੰਬਾਈ ਸੀਮਾ - ਹੁਣ ਸਭ ਗੱਲਬਾਤ ਵੱਢੋ - ਸਭ ਗੱਲਬਾਤ ਸਕੈਨ ਕਰੋ ਅਤੇ ਗੱਲਬਾਤ ਦੀ ਲੰਬਾਈ ਦੀਆਂ ਸੀਮਾਵਾਂ ਨੂੰ ਲਾਗੂ ਕਰੋ + + ਲਿੰਕ ਕੀਤੀਆਂ ਡਿਵਾਈਸਾਂ ਲਾਈਟ ਹਨੇਰਾ @@ -1442,7 +1442,7 @@ ਵਾਈ-ਫਾਈ ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਸਮੇਂ ਰੋਮਿੰਗ ਸਮੇਂ ਮੀਡੀਆ ਆਟੋ-ਡਾਊਨਲੋਡ - ਸੁਨੇਹਾ ਟ੍ਰਰੀਮਿੰਗ + ਸਟੋਰੇਜ਼ ਦੀ ਵਰਤੋਂ ਫ਼ੋਟੋ ਵੀਡੀਓ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9c93cdc70..75ea61378 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -36,9 +36,9 @@ Wyłączyć wiadomości i połączenia Signal? Wyłącz wiadomości i połączenia telefoniczne Signal poprzez wyrejestrowanie z serwera. Będziesz musiał(a) ponownie zarejestrować swój numer telefonu, jeśli będziesz chciał(a) go używać w przyszłości. Błąd połączenia z serwerem! - SMS Włączone + SMS włączone Dotknij, aby zmienić swoją domyślną aplikację SMS - SMS Wyłączone + SMS wyłączone Dotknij, aby ustawić Signal jako domyślną aplikacją SMS włączona Włączone @@ -485,7 +485,7 @@ Oczekujące prośby o przyjęcie Brak próśb o przyjęcie do grupy. - Osoby na tej liście próbują dołączyć do tej grupy przez udostępnialny link. + Osoby na tej liście próbują dołączyć do tej grupy przez link do grupy. Dodałeś(aś) \"%1$s\" Odrzuciłeś(aś) \"%1$s\" @@ -533,7 +533,7 @@ Edytuj informacje o grupie Wybierz kto może edytować nazwę i awatara grupy, oraz czas znikania wiadomości. Wybierz, kto może dodawać lub zapraszać nowych członków. - Udostępnialny link do grupy + Link do grupy Zablokuj grupę Odblokuj grupę Opuść grupę @@ -665,7 +665,7 @@ Linki do grup już wkrótce Uaktualnij Signal, aby korzystać z linków do grup Dołączanie do grupy przez link, nie jest jeszcze obsługiwane w Signal. Ta funkcja znajdzie się w kolejnej aktualizacji. - Używana przez Ciebie wersja Signal nie obsługuje udostępniania linków do grup. Zaktualizuj do najnowszej wersji, aby dołączyć do tej grupy przez link. + Używana przez Ciebie wersja Signal nie obsługuje linków do grup. Zaktualizuj do najnowszej wersji, aby dołączyć do tej grupy przez link. Zaktualizuj Signal Link do grupy jest nieprawidłowy @@ -925,25 +925,25 @@ %1$s zmienił(a), kto może edytować członkostwo w grupie na \"%2$s\". Zmieniono, kto może edytować członkostwo w grupie na \"%1$s\". - Włączyłeś(aś) udostępnialny link do grupy. - Włączyłeś(aś) udostępnialny link do grupy, wymagający zatwierdzenia przez administratora. - Wyłączyłeś(aś) udostępnialny link do grupy. - %1$s włączył(a) udostępnialny link do grupy. - %1$s włączył(a) udostępnialny link do grupy, wymagający zatwierdzenia przez administratora. - %1$s wyłączył(a) udostępnialny link do grupy. - Udostępnialny link do grupy został włączony. - Udostępnialny link do grupy, wymagający zatwierdzenia przez administratora, został włączony. - Udostępnialny link do grupy został wyłączony. + Włączyłeś(aś) link do grupy, niewymagający zatwierdzenia przez administratora. + Włączyłeś(aś) link do grupy, wymagający zatwierdzenia przez administratora. + Wyłączyłeś(aś) link do grupy. + %1$s włączył(a) link do grupy, niewymagający zatwierdzenia przez administratora. + %1$s włączył(a) link do grupy, wymagający zatwierdzenia przez administratora. + %1$s wyłączył(a) link do grupy. + Link do grupy, niewymagający zatwierdzenia przez administratora, został włączony. + Link do grupy, wymagający zatwierdzenia przez administratora, został włączony. + Link do grupy został wyłączony. - Zresetowałeś(aś) udostępnialny link do grupy. - %1$s zresetował(a) udostępnialny link do grupy. - Udostępnialny link do grupy został zresetowany. + Zresetowałeś(aś) link do grupy. + %1$s zresetował(a) link do grupy. + Link do grupy został zresetowany. - Dołączyłeś(aś) do grupy przez udostępnialny link. - %1$s dołączył(a) do grupy przez udostępnialny link. + Dołączyłeś(aś) do grupy przez link. + %1$s dołączył(a) do grupy przez link. Wysłałeś(aś) prośbę o przyjęcie do grupy. - %1$s poprosił(a) o przyjęcie do grupy przez udostępnialny link. + %1$s poprosił(a) o przyjęcie do grupy przez link. %1$s zaakceptował(a) Twoją prośbę o przyjęcie do grupy. %1$s zaakceptował(a) prośbę o przyjęcie do grupy od %2$s. @@ -1531,6 +1531,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Zmiany numeru bezpieczeństwa Wyślij mimo to + Zadzwoń mimo to Następujące osoby mogły ponownie zainstalować aplikację lub zmienić urządzenie. Zweryfikuj wasze numery bezpieczeństwa, aby zapewnić prywatność. Zobacz Wcześniej zweryfikowane @@ -1850,13 +1851,11 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Hasło MMSC Raporty dostarczenia SMS Żądaj raportu dostarczenia każdej wysłanej wiadomości SMS - Automatycznie usuń starsze wiadomości po przekroczeniu określonej długości konwersacji - Usuń stare wiadomości Rozmowy i multimedia Pamięć Limit długości konwersacji - Przytnij wszystkie konwersacje teraz - Przeskanuj wszystkie konwersacje i przytnij to określonej długości + Zachowaj wiadomości + Wyczyść historię wiadomości Połączone urządzenia Jasny Ciemny @@ -1886,17 +1885,34 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Podczas używania Wi-Fi Podczas roamingu Automatyczne pobieranie multimediów - Przycinanie wiadomości + Historia wiadomości Wykorzystana pamięć Zdjęcia Wideo Pliki Dźwięk Przejrzyj pamięć + Usunąć starsze wiadomości? + Wyczyścić historię wiadomości? + To spowoduje trwałe usunięcie z Twojego urządzenia całej historii wiadomości i multimediów, starszych niż %1$s. + To spowoduje trwałe przycięcie wszystkich konwersacji do %1$s najnowszych wiadomości. + To spowoduje trwałe usunięcie z Twojego urządzenia całej historii wiadomości i multimediów. + Czy na pewno chcesz usunąć całą historię wiadomości? + Cała historia wiadomości zostanie trwale usunięta. Tego działania nie da się cofnąć. + Usuń wszystko teraz + Zawsze + 1 rok + 6 miesięcy + 30 dni + Brak + %1$s wiadomości + Własne + Własny limit długości konwersacji Używaj emoji systemu Wyłącz wbudowane wspomaganie emoji Signal Przekazuj połączenia za pomocą serwerów Signal, aby uniknąć ujawnienia adresu IP Twoim kontaktom. Pogorszy to jakość połączenia. Zawsze przekazuj połączenia + Kto może… Dostęp aplikacji Komunikacja Rozmowy @@ -1921,6 +1937,7 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Powiadamiaj Otrzymuj powiadomienia, gdy zostaniesz wspomniany(a) w wyciszonych rozmowach Ustaw nazwę użytkownika + Dostosuj opcję @@ -2235,6 +2252,14 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu. Rejestracja Signal - Kod weryfikacyjny dla systemu Android Nigdy Nieznane + Zobaczyć mój numer telefonu + Znaleźć mnie po numerze telefonu + Wszyscy + Moje kontakty + Nikt + Twój numer będzie widoczny dla wszystkich osób i grup. z którymi wymieniasz wiadomości. + Każdy, kto ma Twój numer telefonu w swoich kontaktach, będzie widzieć Cię, jako użytkownika Signal. Inni będą mogli znaleźć Cię przez wyszukiwanie. + Tylko Twoje kontakty zobaczą Twój numer telefonu w Signal. Blokada ekranu Zablokuj dostęp do aplikacji Signal za pomocą blokady ekranu Android lub za pomocą odcisku palca Blokada ekranu po czasie nieaktywności diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 85c4bd370..8aa61804a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -449,7 +449,6 @@ Solicitações de membros pendentes Não há pedidos de membros para mostrar. - As pessoas desta lista estão tentando participar neste grupo através do link de grupo compartilhável. Adicionado \"%1$s\" Negado \"%1$s\" @@ -491,7 +490,7 @@ Editar informações do grupo Escolha quem pode editar o nome do grupo, avatar, e mensagens efémeras. Escolha quem pode adicionar ou convidar novos membros. - O link de grupo compartilhável + O link de grupo Bloquear grupo Desbloquear o grupo Sair do grupo @@ -611,7 +610,6 @@ Links de grupos em breve Atualize o Signal para usar links do grupo Participar no grupo através de um link ainda não é suportado pelo Signal. Este recurso será lançado na próxima atualização. - A versão do Signal que você está usando não suporta links do grupo compartilháveis. Atualize para a versão mais recente para se unir a este grupo via link. Atualizar o Signal O link de grupo não é válido @@ -847,25 +845,10 @@ %1$s mudou quem pode editar os membros do grupo. Agora é \"%2$s\". Quem pode editar as informações do grupo foi alterado para \"%1$s\". - Você ativou o link de grupo compartilhável. - Você ativou o link de grupo compartilhável com a aprovação de administrador. - Você desativou o link de grupo compartilhável. - %1$s ativou o link de grupo compartilhável. - %1$s ativou o link de grupo compartilhável com a aprovação de administrador. - %1$s desativou o link de grupo compartilhável. - Foi ativado o link de grupo compartilhável. - Foi activado o link de grupo compartilhável com a aprovação de administrador. - Foi desativado o link de grupo compartilhável. - Você redefiniu o link de grupo compartilhável. - %1$s redefiniu o link de grupo compartilhável. - Foi redefinido o link de grupo compartilhável. - Você foi incluído no grupo através do link de grupo compartilhável. - %1$s entrou no grupo através do link de grupo compartilhável. Você enviou um pedido para participar no grupo. - %1$s solicitou participação através do link de grupo compartilhável. %1$s aprovou seu pedido de participação no grupo. %1$s aprovou um pedido de participação no grupo de %2$s. @@ -1750,13 +1733,9 @@ Senha do MMSC Avisos de entrega de SMS Solicite um aviso de entrega para cada SMS enviado - Apagar automaticamente mensagens antigas quando uma conversa exceder um limite especificado - Excluir mensagens antigas Chats e mídia Armazenamento Tamanho máximo de conversa - Limitar o tamanho de todas as conversas agora - Escanear todas as conversas e aplicar os limites de conversa Dispositivos vinculados Claro Escuro @@ -1786,13 +1765,14 @@ Quando usar Wi-Fi Quando em roaming Download automático de mídia - Limitar mensagem Uso do armazenamento Fotos Vídeos Arquivos Áudio Checar armazenamento + Nenhum + Personalizado Usar emoji do sistema Desabilitar o suporte a emoji embutido do Signal Encaminhar todas as chamadas através do servidor Signal para evitar revelar seu endereço IP para seu contato. Habilitar reduzirá a qualidade da chamada. @@ -2119,6 +2099,7 @@ Cadastro Signal - Código de verificação para Android Nunca Desconhecida + Ninguém Bloqueio de tela Proteger acesso ao Signal com tela de bloqueio do Android ou impressão digital. Tempo de espera para bloquear tela diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a86acf4a8..8655c9ae7 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -449,7 +449,6 @@ Pedidos para membro pendentes Sem pedidos para membro. - As pessoas nesta lista estão a tentar entrar para este grupo através do link do grupo partilhável. Adicionado(s) \"%1$s\" Negado(s) \"%1$s\" @@ -491,7 +490,7 @@ Editar informação do grupo Escolha quem pode editar o nome do grupo, avatar e destruição de mensagens. Escolha quem pode adicionar ou convidar novos membros. - Link do grupo partilhável + Link do grupo Bloquear grupo Desbloquear grupo Abandonar grupo @@ -611,7 +610,6 @@ Links para grupo muito em breve Atualize o Signal para utilizar hiperligações de grupo Entrar para um grupo através de um link ainda não é suportado pelo Signal. Este recurso será lançado numa atualização futura. - A versão do Signal que está a utilizar não suporta hiperligações de grupo partilháveis. Atualize para a última versão para juntar este grupo via hiperligação. Atualizar o Signal Link do grupo inválido @@ -847,28 +845,18 @@ %1$s alterou quem pode editar os membros do grupo para \"%2$s\". Quem pode editar os membros do grupo foi alterado para \"%1$s\". - Ativou o link partilhável do grupo - Ativou o link partilhável do grupo com a aprovação do administrador. - Desativou o link partilhável do grupo. - %1$s ativou o link partilhável do grupo. - %1$s ativou o link partilhável de grupo com aprovação do administrador. - %1$s desativou o link partilhável do grupo. - Foi ativado o link partilhável do grupo. - O link partilhável do grupo foi ativado com a aprovação de um administrador. - O link partilhável do grupo foi desativado. + Ativou o link de grupo sem aprovação de administrador. + Ativou o link do grupo com aprovação de administrador. + Desativou o link do grupo. - Redefiniu o link partilhável do grupo. - %1$s redefiniu o link partilhável do grupo. - O link partilhável do grupo foi redefinido. + Redefiniu o link de grupo. - Entrou para o grupo através do link partilhável do grupo. - %1$s entrou para o grupo através do link partilhável do grupo. + Entrou para o grupo através do link do grupo. Enviou um pedido para entrar para o grupo. - %1$s pediu para entrar através do link partilhável do grupo. %1$s aprovou o seu pedido para entrar para o grupo. - %1$s aprovou um pedido de %2$s para entrara para o grupo. + %1$s aprovou um pedido de %2$s para entrar para o grupo. Você aprovou o pedido de %1$s para entrar para o grupo. O seu pedido para entrar para o grupo foi aprovado. Foi aprovado o pedido de %1$s para entrar para o grupo. @@ -1746,13 +1734,9 @@ Mensagem de troca de chaves inválida para esta versão do protocolo. Palavra-passe MMSC Relatórios de entrega de SMS Pedir um aviso de entrega para cada mensagem de SMS enviada - Apagar automaticamente as mensagens mais antigas quando uma conversa exceder um tamanho especifico - Apagar mensagens antigas Conversas e multimédia Armazenamento Tamanho máximo das conversas - Reduzir agora todas as conversas - Pesquisar por todas as conversas e limitá-las ao tamanho máximo das conversas Dispositivos associados Claro Escuro @@ -1782,13 +1766,14 @@ Mensagem de troca de chaves inválida para esta versão do protocolo. Ao utilizar o Wi-Fi Ao utilizar o roaming Descarregar multimédia automaticamente - Redução da mensagem Utilização do armazenamento Fotografias Vídeos Ficheiros Áudio Rever armazenamento + Nenhum(a) + Personalizado Utilizar os emojis do sistema Desativar o suporte de emojis próprios do Signal Passar todas as chamadas pelo servidor do Signal para evitar revelar o seu endereço IP ao destinatário. Ativar esta opção irá reduzir a qualidade da chamada. @@ -2115,6 +2100,7 @@ Mensagem de troca de chaves inválida para esta versão do protocolo. Registo Signal - Código de verificação para Android Nunca Desconhecido + Ninguém Bloqueio de ecrã Bloqueie o acesso ao Signal utilizando o bloqueio de ecrã do Android ou a impressão digital Bloqueio de ecrã devido a inatividade diff --git a/app/src/main/res/values-qu-rEC/strings.xml b/app/src/main/res/values-qu-rEC/strings.xml index 25d3e88e3..5df061cea 100644 --- a/app/src/main/res/values-qu-rEC/strings.xml +++ b/app/src/main/res/values-qu-rEC/strings.xml @@ -1001,12 +1001,12 @@ Shuk ranti pakallayupay chaski chayamurka, shuk ñawpa mana alli protocolo kashk MMSC paskachik killka SMS killkashka chayashkata yachanchik SMS killkashka chayashkata yachankapak, tukuylla SMS kachashkakunapak willachichun mañay. - Yallishka chaskikuna paymanta picharichun, rimarikuna yapata huntakpi - Ñawpa chaskikunata anchuchina + + Rimariy shinallata uyayrikuchikpash Chaykamanlla rimarikuna - Rimarikunata pichana - Tukuy rimarikuna riksishpa chaykamanlla rimana. + + Antahillaykuna tinkishka Punchalla Yanalla @@ -1028,7 +1028,7 @@ Shuk ranti pakallayupay chaski chayamurka, shuk ñawpa mana alli protocolo kashk WiFita hapichishka kashpa Roamingta hapichishka kashpa Paymanta uriyakuchina uyayrikuchita - Chaskimanta pitishka + Uyariy Anta llikapa ñawikukunata rikuchipay Signalpa ñawikukunapa yanapayta anchuchipay diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index c819d3c98..0e61d98a9 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -467,6 +467,8 @@ Cereri de membru în așteptare Nici o cerere de membru de afișat. + \"%1$s\" a fost adăugat + \"%1$s\" a fost refuzat Gata Acestă persoană nu poate fi adăugată la grupurile vechi. @@ -508,6 +510,7 @@ Editare informații despre grup Alegeți cine poate edita numele grupului, avatarul și dispariția mesajelor. Alegeți cine poate adăuga sau invita membri noi. + Link grup Blocați grupul Deblocare grup Părăsiți grupul @@ -627,7 +630,6 @@ Linkurile de grup vin în curând Actualizați Signal pentru a folosi link-uri la grupuri Înscrierea la un grup printr-un link nu este încă suportată de Signal. Această funcționalitate va fi lansată într-o actualizare viitoare. - Versiune de Signal pe care o folosiți nu suportă link-uri partajabile către grup. Actualizați la ultima versine pentru a te alătura acestui grup printr-un link. Actualizați Signal Linkul de grup nu este valid @@ -883,6 +885,7 @@ O cerere de alăturare la grup de la %1$s a fost aprobată. Solicitarea dvs. de a vă alătura grupului a fost respinsă de un administrator. + Ați anulat solicitarea de a vă alătura grupului. Numărul dvs. de siguranță pentru %s s-a schimbat. Ați marcat numărul tău de siguranță cu %s ca și verificat @@ -1766,13 +1769,9 @@ Am primit mesajul conform căruia schimbul de chei a avut loc pentru o versiune Parolă MMSC Confirmări de livrare SMS Cere o confirmare de livrare pentru fiecare SMS trimis - Șterge automat mesajele vechi când conversația depășește mărimea specificată - Șterge mesajele vechi Conversaţii şi media Spaţiu stocare Limita mărime conversație - Scurtează acum toate conversaţiile - Verifică toate conversațiile și aplică limitele de mărime a conversației Dispozitive asociate Luminoasă Întunecată @@ -1803,13 +1802,14 @@ Am primit mesajul conform căruia schimbul de chei a avut loc pentru o versiune Când se utilizează Wi-Fi Când se utilizează roaming-ul Descărcare automată Media - Scurtarea mesajelor Utilizare spațiu stocare Poze Video Fișiere Audio Examinați stocarea + Niciunul + Specific Foloseşte emoji de sistem Dezactivați icoanele emoji oferite de Signal Redirecționați toate apelurile către serverul Signal pentru a evita aflarea adresei IP de către contact tău. Activarea va reduce calitatea apelului. @@ -2144,6 +2144,7 @@ Am primit mesajul conform căruia schimbul de chei a avut loc pentru o versiune Înregistrare Signal - Cod de verificare pentru Android Niciodată Necunoscut + Nimeni Blocare ecran Blocați accesul la Signal prin ecranul de blocare Android sau prin amprentă Expirare timp blocare ecran pentru inactivitate diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 31a295bb1..1b34d1e69 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -485,9 +485,9 @@ Ожидающие запросы на участие Нет запросов на участие. - Люди в этом списке пытаются присоединиться к этой группе по общедоступной ссылке на группу. - Добавлен(-а) «%1$s» - Отклонен(-а) «%1$s» + Люди в этом списке пытаются присоединиться к этой группе по ссылке на группу. + «%1$s» был(-а) добавлен(-а) + «%1$s» был(-а) отклонен(-а) Готово Этот человек не может быть добавлен в старые группы. @@ -533,7 +533,7 @@ Редактировать информацию группы Выберите, кто может редактировать имя, аватар и время исчезновения сообщений группы. Выберите, кто может добавлять или приглашать новых участников. - Общедоступная ссылка на группу + Ссылка на группу Заблокировать группу Разблокировать группу Покинуть группу @@ -626,7 +626,7 @@ Поделиться Сбросить ссылку Запросы на участие - Принять новых участников + Одобрение новых участников Включена Отключена По умолчанию @@ -925,25 +925,25 @@ %1$s изменил(-а), кто может редактировать членство в группе, на «%2$s». Кто может редактировать членство в группе было изменено на «%1$s». - Вы включили общедоступную ссылку на группу. - Вы включили общедоступную ссылку на группу с одобрением администратора. - Вы отключили общедоступную ссылку на группу. - %1$s включил(-а) общедоступную ссылку на группу. - %1$s включил(-а) общедоступную ссылку на группу с одобрением администратора. - %1$s отключил(-а) общедоступную ссылку на группу. - Общедоступная ссылка на группу была включена. - Общедоступная ссылка на группу с одобрением администратора была включена. - Общедоступная ссылка на группу была отключена. + Вы включили ссылку на группу с отключённым одобрением администратора. + Вы включили ссылку на группу с включённым одобрением администратора. + Вы отключили ссылку на группу. + %1$s включил(-а) ссылку на группу с отключённым одобрением администратора. + %1$s включил(-а) ссылку на группу со включённым одобрением администратора. + %1$s отключил(-а) ссылку на группу. + Была включена ссылка на группу с отключённым одобрением администратора. + Была включена ссылка на группу со включённым одобрением администратора. + Ссылка на группу была отключена. - Вы сбросили общедоступную ссылку на группу. - %1$s сбросил(-а) общедоступную ссылку на группу. - Общедоступная ссылка на группу была сброшена. + Вы сбросили ссылку на группу. + %1$s сбросил(-а) ссылку на группу. + Ссылка на группу была сброшена. - Вы присоединились к группе по общедоступной ссылке на группу. - %1$s присоединился(-лась) к группе по общедоступной ссылке на группу. + Вы присоединились к группе по ссылке на группу. + %1$s присоединился(-лась) к группе по ссылке на группу. Вы отправили запрос на присоединение к группе. - %1$s запросил(-а) присоединиться по общедоступной ссылке на группу. + %1$s запросил(-а) присоединиться по ссылке на группу. %1$s принял(-а) ваш запрос на присоединение к группе. %1$s принял(-а) запрос на присоединение к группе от %2$s. @@ -1532,6 +1532,7 @@ Изменения кода безопасности Всё равно отправить + Всё равно позвонить Следующие люди могли переустановить Signal или сменить устройства. Подтвердите ваш код безопасности с ними, чтобы обеспечить конфиденциальность. Просмотреть Ранее проверенный(-ая) @@ -1851,13 +1852,11 @@ Пароль MMSC Отчеты о доставке SMS Запрашивать отчет о доставке для каждого отправленного SMS-сообщения - Автоматически удалять старые сообщения, когда длина разговора превышает заданную - Удалять старые сообщения Чаты и медиа Хранилище Предел длины разговора - Обрезать все разговоры сейчас - Привести длину всех разговоров в соответствие с заданной + Хранить сообщения + Очистить историю сообщений Привязанные устройства Светлая Темная @@ -1887,17 +1886,34 @@ Через Wi-Fi В роуминге Автоматическое скачивание медиа - Обрезка сообщений + История сообщений Использование хранилища Фото Видео Файлы Аудио Просмотреть содержимое + Удалить старые сообщения? + Очистить историю сообщений? + Все сообщения и медиа, которые старее, чем %1$s, будут безвозвратно удалены с вашего устройства. + Все разговоры будут безвозвратно обрезаны до %1$s самых новых сообщений. + Все сообщения и медиа будут безвозвратно удалены с вашего устройства. + Вы действительно хотите удалить всю историю сообщений? + Вся история сообщений будет безвозвратно удалена. Это действие не может быть отменено. + Удалить все сейчас + Бессрочно + 1 год + 6 месяцев + 30 дней + Нет + %1$s сообщений + Пользовательское + Пользовательское ограничение длины разговора Использовать системные эмодзи Отключить встроенные эмодзи Signal Пропускать все звонки через сервер Signal, чтобы не раскрывать ваш IP-адрес собеседнику. Качество звонка ухудшится. Всегда ретранслировать звонки + Кто может… Доступ к приложению Связь Чаты @@ -1922,6 +1938,7 @@ Уведомлять меня Получать уведомления, когда вас упоминают в чатах с отключёнными уведомлениями Настройте имя пользователя + Изменить опцию @@ -2236,6 +2253,14 @@ Регистрация в Signal - код подтверждения для Android Никогда Неизвестно + Видеть мой номер телефона + Найти меня по номеру телефона + Все + Мои контакты + Никто + Ваш номер телефона будет виден всем людям и группам, которым вы отправляете сообщения. + Любой, у кого есть в контактах ваш номер телефона, будет видеть вас как контакта в Signal. Другие смогут найти вас в поиске. + Только ваши контакты будут видеть ваш номер телефона в Signal. Блокировка экрана Блокировка доступа к Signal при помощи блокировки экрана Android или отпечатка пальца Период бездействия для блокировки экрана diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 491bc9cdc..950dde2a0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -213,6 +213,7 @@ Kamera je nedostupná Nemôžem zaznamenať zvuk! Vo vašom zariadení nie je aplikácia schopná otvoriť tento odkaz. + Zrušiť žiadosť Pre posielanie zvukových správ potrebuje Signal prístup k mikrofónu. Signal potrebuje prístup k mikrofónu aby mohol posielať zvukové správy, ale prístup bol natrvalo zakázaný. Prosím v nastaveniach aplikácií zvoľte \"Oprávnenia\", a povoľte \"Mikrofón\". Signal potrebuje prístup k mikrofónu a fotoaparátu aby mohol zavolať %s, ale prístup bol natrvalo zakázaný. Prosím v nastaveniach aplikácií zvoľte \"Oprávnenia\", a povoľte \"Mikrofón\" a \"Fotoaparát\". @@ -479,10 +480,15 @@ Povolené Zakázané Predvolená + Vyžadovať schválenie adminom pri nových členoch, ktorí sa pridajú prostredníctvom odkazu do skupiny. + Ste si istý/á, že chcete zresetovať odkaz do skupiny? Ľudia sa už viac nebudú môcť do skupiny pridať prostredníctvom aktuálneho odkazu. + Ľudia, ktorí si tento kód zoskenujú, sa budú môcť pridať do vašej skupiny. Admini budú ešte stále musieť nových členov schváliť, ak zapnete príslušné nastavenie. + Pridať sa Vyskytla sa chyba siete. + Chcete sa pridať do tejto skupiny a zdieľať vaše meno a foto s jej členmi? Aktualizovať Signal @@ -710,11 +716,18 @@ Zmenili ste, kto môže upravovať členstvo v skupine na \"%1$s\". %1$s zmenil/a, kto môže upravovať členstvo v skupine na \"%2$s\". + Zapli ste odkaz do skupiny so schválením administrátorom neaktívnym. + Zapli ste odkaz do skupiny so schválením administrátorom aktívnym. + Odkaz do skupiny ste vypli. + Zresetovali ste odkaz do skupiny. + Pridali ste sa do skupiny prostredníctvom odkazu do skupiny. + Poslali ste žiadosť o členstvo v skupine. + Zrušili ste vašu žiadosť o členstvo v skupine. Vaše bezpečnostné číslo s %s sa zmenilo. Bezpečnostné číslo s %s ste označili ako overené @@ -1555,13 +1568,9 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. MMSC heslo Správy o doručení SMS Pre každú odoslanú SMS vyžiadať správu o doručení - Automaticky odstrániť staršie správy keď konverzácia presiahne určenú dĺžku - Vymazať staré správy Chat a médiá Úložisko Obmedzenie dĺžky konverzácie - Premazať všetky konverzácie teraz - Skenovať všetky konverzácie a uplatniť limity dĺžky konverzácie Pripojené zariadenia Svetlý Tmavý @@ -1589,13 +1598,14 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. Pri použití Wi-Fi Pri roamingu Automatické preberanie médií - Premazávanie správ Využitie úložiska Fotografie Videá Súbory Zvuk Skontrolovať úložisko + Žiadny + Vlastné Použiť systémové emoji Vypnúť emoji zabudované v Signale Smerovať všetky hovory cez server Signal pre zabránenie odhaleniu vašej IP adresy kontaktu. Zapnutie zníži kvalitu hovoru. @@ -1659,6 +1669,7 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. Skratka pre Nastavenia Hľadať + Pripnuté Chat Fotka kontaktu @@ -1924,6 +1935,8 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. Registrácia v Signali – Overovací kód pre Android Nikdy Neznáma + Nikto + Len vaše kontakty budú vidieť vaše telefónne číslo v službe Signal. Zámok obrazovky Uzamknúť Signal pomocou zámku obrazovky Androidu alebo odtlačkom prstu Časový limit neaktivity pred uzamknutím @@ -1994,6 +2007,7 @@ Bola prijatá správa výmeny kľúčov s neplatnou verziou protokolu. Zastarané skupiny nie je možné prerobiť na nové skupiny, môžete však vytvoriť novú skupinu s tými istými členmi. Na vytvorenie novej skupiny by všetci členovia mali prejsť na najnovšiu verziu aplikácie Signal. + Zdieľať cez Signal Kopírovať QR kód Zdieľať diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 5517cec8a..e05b4db5d 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -485,7 +485,7 @@ Čakajoče prošnje za članstvo Ni prošenj za članstvo. - Uporabniki/ce na tem seznamu se želijo včlaniti v skupino prek deljene povezave. + Uporabniki/ce na tem seznamu bi se radi/e pridružili/le skupini preko povezave do skupine. Dodan/a uporabnik/ca \"%1$s\" Zavrnjen/a uporabnik/ca \"%1$s\" @@ -533,7 +533,7 @@ Uredi podatke o skupini Izberite, kdo lahko ureja ime skupine, njen avatar in čas poteka sporočil. Izberite, kdo lahko dodaja ali povabi nove člane - Deljiva povezava do skupine + Povezava do skupine Blokiraj skupino Odblokiraj skupino Zapusti skupino @@ -665,7 +665,7 @@ Kmalu na voljo: povezave do skupin Za povezave do skupine nadgradite Signal Pridružitev skupini prek povezave v aplikaciji Signal še ni mogoča. Dodana bo v eni prihodnjih posodobitev. - Različica aplikacije Signal, ki jo uporabljate, ne podpira deljenja povezav do skupine. Za priključitev k skupini preko povezav jo morate nadgraditi. + Različica aplikacije Signal, ki jo uporabljate, ne podpira povezav do skupine. Za priključitev k skupini preko povezave jo morate nadgraditi. Posodobite aplikacijo Signal Povezava do skupine ni veljavna @@ -925,25 +925,25 @@ Oseba %1$s je spremenila, kdo lahko ureja članstvo v skupini: \"%2$s\". Sprememba: članstvo v skupini lahko odslej ureja uporabnik/ca \"%1$s\". - Vklopili ste povezavo do skupine. - Vklopili ste povezavo do skupine s skrbniškim privoljenjem. - Izklopili ste povezavo do skupine. - Uporabnika/ca %1$s je vklopil/a povezavo so skupine. - Uporabnik/ca %1$s je vklopil/a povezavo do skupine s skrbniškim privoljenjem. - Uporabnika/ca %1$s je izklopil/a povezavo do skupine. - Povezava do skupine je bila vklopljena. - Povezava do skupine je bila vklopljena s privoljenjem skrbnika. - Povezava do skupine je bila izklopljena. + Vklopili ste povezavo do skupine brez zahtevanega skrbniškega privoljenja. + Vklopili ste povezavo do skupine z zahtevanim skrbniškim privoljenjem. + Izklopili ste povezavo do skupine. + Uporabnik/ca %1$s je vklopil/a povezavo do skupine brez zahtevanega skrbniškega privoljenja. + Uporabnik/ca %1$s je vklopil/a povezavo do skupine z zahtevanim skrbniškim privoljenjem. + Uporabnika/ca %1$s je izklopil/a povezavo do skupine. + Vklopljena je bila povezava do skupine brez zahtevanega skrbniškega privoljenja. + Vklopljena je bila povezava do skupine z zahtevanim skrbniškim privoljenjem. + Povezava do skupine je bila izklopljena. - Ponastavili ste povezavo do skupine. - Uporabnik/ca %1$s je ponastavil/a povezavo do skupine. - Povezava do skupine je bila ponastavljena. + Ponastavili ste povezavo do skupine. + Uporabnik/ca %1$s je ponastavil/a povezavo do skupine. + Povezava do skupine je bila ponastavljena. - Preko deljene povezave ste se pridružili skupini. - Uporabnik/ca %1$s se je pridružil/a skupini preko deljene povezave. + Preko deljene povezave ste se pridružili skupini. + Uporabnik/ca %1$s se je pridružil/a skupini preko deljene povezave. Poslali ste prošnjo za pridružitev skupini. - Uporabnik/ca %1$s je preko deljene povezave zaprosil/a za pridružitev skupini. + Uporabnik/ca %1$s je preko deljene povezave zaprosil/a za pridružitev skupini. Uporabnik/ca %1$s je odobril/a vašo prošnjo za pridružitev skupini. Član/ica %1$s je odobril/a prošnjo uporabnika/ce %2$sza pridružitev skupini. @@ -1530,6 +1530,7 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola. Spremembe varnostnega števila Vseeno pošlji + Vseeno kliči Našteti uporabniki/ce so najbrž ponovno namestili Signal ali zamenjali napravo. Za potrditev istovetnosti ponovno preverite varnostno število z njimi. Preglej Prej potrjeno @@ -1849,13 +1850,11 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola.Geslo MMSC Potrdila o prejemu SMS Za vsako poslano sporočilo SMS zahtevaj potrdilo o prejemu - Samodejen izbris starih sporočil, ko pogovor preseže določeno dolžino - Samodejen izbris Pogovori in večpredstavnost Pomnilnik Omejitev dolžine pogovorov - Obreži vse pogovore zdaj - Uveljavitev omejitve dolžine vseh pogovorov + Obdrži sporočila + Izbriši zgodovino pogovorov Povezane naprave Svetla Temna @@ -1885,17 +1884,34 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola.Ob uporabi omrežja Wi-Fi Med gostovanjem Samodejen prenos priponk - Izbris starih sporočil + Zgodovina pogovorov Poraba pomnilnika Fotografije Video Datoteke Zvok Pregled porabe pomnilnika + Želite izbrisati starejša sporočila? + Želite izbrisati zgodovino sporočil? + S tem boste nepovratno izbrisali celotno zgodovino sporočil in medijskih datotek starejših od %1$sz vaše naprave. + S temo boste nepovratno obrezali vse vaše pogovore na %1$s zadnjih sporočil. + S tem boste nepovratno izbrisali celotno zgodovino sporočil in medijskih datotek z vaše naprave. + Ste prepričani, da želite izbrisati celotno zgodovino sporočil? + Celotna zgodovina vaših sporočil bo za vedno izbrisana. Ta korak je nepovraten. + Izbriši vse zdaj! + kadarkoli + 1 leto + 6 mesecev + 30 dni + Brez + %1$s sporočil + Po meri + Omejitev dolžine pogovorov Sistemski znaki emoji Izklop privzetih znakov emoji aplikacije Signal Vsi klici bodo posredovani preko strežnika Signal. S tem boste klicani strani onemogočili, da bi odkrila vaš naslov IP. Kvaliteta klica bo zaradi tega slabša. Posredovanje klicev + Uporabniki/ce, ki lahko … Dostop Komunikacija Pogovori @@ -1920,6 +1936,7 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola.Obveščaj me Prejemajte obvestila kadar bo vaše ime omenjeno v pogovorih, ki ste jih utišali Določite uporabniško ime + Nastavitve po meri @@ -2234,6 +2251,14 @@ Prejeto sporočilo za izmenjavo ključev za napačno različico protokola.Registracija za storitev Signal - verifikacijska koda za Android Nikoli Neznano + vidijo mojo telefonsko številko + me poiščejo, če poznajo mojo številko + Kdorkoli + Moji stiki + Nikogar + Vaša telefonska številka bo vidna vsem uporabnikom/cam in skupinam, ki jim boste pošiljali sporočila. + Vsak/a uporabnik/ca, ki ima vašo telefonsko številko med svojimi stiki, vas bo lahko kontaktiral/a preko Signala. Drugi/e vas bodo lahko našli/e z iskanjem. + Vašo telefonsko številko bodo lahko na Signalu videli le vaši stiki. Zaklep zaslona Uporaba sistemskega zaklepanja in prstnih odtisov za zaklep aplikacije Časovni interval pred zaklepom aplikacije diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index a0060ee38..867bf652e 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -449,7 +449,6 @@ Kërkesa anëtarësie pezull S’ka kërkesa anëtarësie për t’u shfaqur. - Persona në këtë listë po rreken të bëhen pjesë e këtij grupi përmes lidhjes së ndashme të grupit. U shtua \"%1$s\" U hodh poshtë \"%1$s\" @@ -494,7 +493,7 @@ të përditësojnë Signal-in, ose t’i hiqni para krijimit të grupit. Përpunoni të dhëna grupi Zgjidhni kush mund të përpunojë emër grupi, avatar dhe mesazhe që zhduken. Zgjidhni kush mund të shtojë ose ftojë anëtarë të rinj. - Lidhje grupi e ndashme + Lidhje grupi Blloko grup Zhbllokoje grupin Braktise grupin @@ -605,7 +604,6 @@ të përditësojnë Signal-in, ose t’i hiqni para krijimit të grupit. Që të përdorni lidhje grupi, përditësoni Signal-in - Versioni i Signal-it që po përdorni nuk mbulon lidhje grupi që mund të ndahen me të tjerë. Që të bëheni pjesë e këtij grupi përmes një lidhjeje, përditësojeni me versionin më të ri. Përditësoni Signal-in Të shtohet “%1$s” te grupi? @@ -840,25 +838,10 @@ të përditësojnë Signal-in, ose t’i hiqni para krijimit të grupit. %1$s ndryshoi se cilët mund të përpunojnë anëtarësi grupi te \"%2$s\". Te \"%1$s\" është ndryshuas se kush mund të përpunojë anëtarësi te grupi. - Aktivizuat lidhjen e grupit të ndashme me të tjerë. - Aktivizuat lidhjen e grupit të ndashme me të tjerë, me miratim përgjegjësi. - Çaktivizuat lidhjen e grupit të ndashme me të tjerë. - %1$s aktivizoi lidhjen e grupit të ndashme me të tjerë. - %1$s aktivizoi lidhjen e grupit të ndashme me të tjerët, me miratim përgjegjësi. - %1$s çaktivizoi lidhjen e grupit të ndashme me të tjerë. - Lidhja e ndashme e grupit është aktivizuar. - Lidhja e ndashme e grupit është aktivizuar me miratim përgjegjësi. - Lidhja e grupit e ndashme me të tjerët është çaktivizuar. - Kthyet te parazgjedhjet lidhjen e ndashme të grupit. - %1$s ktheu te parazgjedhjet lidhjen e ndashme të grupit. - Lidhja e ndashme e grupit është kthyer te parazgjedhjet. - U bëtë pjesë e grupit përmes lidhjes së ndashme të grupit - %1$s u bë pjesë e grupit përmes lidhjes së ndashme të grupit. Dërguat një kërkesë të bëheni pjesë e grupit. - %1$s kërkoi të bëhet pjesë e grupit përmes një lidhjes së ndashme të grupit. %1$s miratoi kërkesën tuaj për t’u bërë pjesë e grupit. %1$s miratoi një kërkesë për t’u bërë pjesë e grupit nga %2$s. @@ -1735,13 +1718,9 @@ të përditësojnë Signal-in, ose t’i hiqni para krijimit të grupit. Fjalëkalim MMSC Raporte dërgimi SMS-sh Kërko raport dorëzimi për çdo mesazh SMS që dërgoni. - Fshi vetvetiu mesazhe më të vjetër, sapo një bisedë tejkalon një gjatësi specifike. - Fshiji mesazhet e vjetër Fjalosje dhe media Hapësirë Kufi gjatësie bisedash - Shkurtoji krejt bisedat tash - Skano krejt bisedat dhe zbato detyrimisht kufij gjatësie bisedash. Pajisje të lidhura E çelët E errët @@ -1773,13 +1752,14 @@ aktivizoni Kyçje Regjistrimi, teksa PIN-i është i çaktivizuar. Kur përdoret Wi-Fi Nën roaming Vetëshkarkim mediash - Shkurtim mesazhesh Përdorim hapësire Foto Video Kartela Audio Shqyrtoni hapësirën + Asnjë + Rregullo Përdor emoji të sistemit Çaktivizo mbulimin e brendshëm të Signal-it për emoji-t Kaloji krejt thirrjet përmes shërbyesit Signal për të shmangur zbulimin e adresës tuaj IP nga kontakti juaj. Aktivizimi do të ulë cilësinë e thirrjes. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 245123717..47a15de19 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -1201,13 +1201,13 @@ ММСЦ лозинка Извештаји о испоруци СМС-а Захтевај извештај о испоруци за сваку послату СМС поруку - Аутоматско брисање старих порука када преписка пређе наведену дужину - Бриши старе поруке + + Ћаскања и медији Складиште Ограничење дужине преписке - Скрати све преписке сада - Претражи све преписке и наметни ограничења дужине + + Повезани уређаји Светла Тамна @@ -1230,7 +1230,7 @@ На бежичној У ромингу Ауто-преузимање медија - Скраћивање порука + Звук Користи системски емоџи Искључи уграђене Signal-ове емоџије diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 55211a749..f7d701a2d 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -449,7 +449,6 @@ Väntande medlemsförfrågningar Inga medlemsförfrågningar att visa. - Personer på denna lista försöker gå med i gruppen via den delbara grupplänken. Lade till \"%1$s\" Avvisade \"%1$s\" @@ -491,7 +490,7 @@ Redigera gruppinformation Välj vem som kan redigera gruppnamn, avatar och försvinnande meddelanden. Välj vem som kan lägga till eller bjuda in nya medlemmar. - Delbar grupplänk + Grupplänk Blockera grupp Sluta blockera grupp Lämna grupp @@ -611,7 +610,6 @@ Grupplänkar kommer snart Uppdatera Signal för att använda grupplänkar Att gå med i en grupp via en länk stöds ännu inte av Signal. Denna funktion kommer att släppas i en kommande uppdatering. - Den version av Signal du använder stöder inte delbara grupplänkar. Uppdatera till den senaste versionen för att gå med i denna grupp via länk. Uppdatera Signal Grupplänken är inte giltig @@ -847,25 +845,10 @@ %1$s ändrade vem som kan redigera gruppmedlemskap till \"%2$s\". Vem som kan redigera gruppmedlemskap har ändrats till \"%1$s\". - Du aktiverade den delbara grupplänken. - Du aktiverade den delbara grupplänken med administratörsgodkännande. - Du inaktiverade av den delbara grupplänken. - %1$s aktiverade den delbara grupplänken. - %1$s aktiverade den delbara grupplänken med administratörsgodkännande. - %1$s inaktiverade av den delbara grupplänken. - Den delbara grupplänken har aktiverats. - Den delbara grupplänken har aktiverats med administratörens godkännande. - Den delbara grupplänken har inaktiverats. - Du återställde den delbara grupplänken. - %1$s återställde den delbara grupplänken. - Den delade grupplänken har återställts. - Du gick med i gruppen via den delbara grupplänken. - %1$s gick med i gruppen via den delbara grupplänken. Du skickade en begäran om att gå med i gruppen. - %1$s begärde att gå med via den delbara grupplänken %1$s godkände din begäran om att gå med i gruppen. %1$s godkände en begäran om att gå med i gruppen från %2$s. @@ -1744,13 +1727,9 @@ Tog emot meddelande för nyckelutbyte för ogiltig protokollversion. MMSC Lösenord SMS-leveransrapporter Begär en leveransrapport för varje SMS-meddelande du skickar - Ta bort gamla meddelanden automatiskt när en konversation överskrider en viss längd - Ta bort gamla meddelanden Konversationer och media Lagring Gräns för konversationslängd - Korta ner alla konversationer nu - Skanna alla konversationer och tvinga längdbegränsningar på konversationer Länkade enheter Ljust Mörkt @@ -1780,13 +1759,15 @@ Tog emot meddelande för nyckelutbyte för ogiltig protokollversion. Vid användning av Wi-Fi Vid roaming Automatisk hämtning av media - Trimma meddelanden Lagringsutrymme Foton Videor Filer Ljud Granska lagring + 1 år + Ingen + Skräddarsydd Använd systemets emojier Inaktivera Signals inbyggda emojier Slussa alla samtal via Signal-servern för att undvika att avslöja din IP-adress för din kontakt. Aktivering försämrar samtalskvaliteten. @@ -2113,6 +2094,7 @@ Tog emot meddelande för nyckelutbyte för ogiltig protokollversion. Signal-registrering - Verifieringskod för Android Aldrig Okänd + Ingen Skärmlås Lås åtkomst till Signal med Android-skärmlås eller -fingeravtryck Skärmlåsets tidsgräns för inaktivitet diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 2b3239c8e..9ff445c64 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -1305,13 +1305,13 @@ nambari yako ya simu Nenosiri la MMSC Ripoti ya ujumbe uliowasilishwa Omba ripoti ya uwasilishwaji kwa kila ujumbe wa SMS unayotuma - Futa moja kwa moja ujumbe wa zamani mara mazungumzo yanapozidi muda fulani - Futa ujumbe wa zamani + + Gumzo na Media Hifadhi Upeo wa maongezi - Punguza mazungumzo yote sasa - Pitia mazungumzo yote na uimarishe upeo wa mazungumzo + + Vifaa vilivyounganishwa Mwanga Giza @@ -1335,7 +1335,7 @@ nambari yako ya simu Wakati unatumia WiFi Wakati unarandaranda Media kupakua kiotomatiki - Upunguzaji ujumbe + Matumizi ya hifadhi Picha Video diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index ffc04bcf8..dd48f8b58 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -566,7 +566,6 @@ பிணைய பிழையை எதிர்கொண்டது. குழு இணைப்புகளைப் பயன்படுத்த சிக்னலைப் புதுப்பிக்கவும் - நீங்கள் பயன்படுத்தும் சிக்னலின் பதிப்பு மாற்றத்தக்க குழு இணைப்புகளை ஆதரிக்காது. இணைப்பு வழியாக இந்த குழுவில் சேர சமீபத்திய பதிப்பிற்கு புதுப்பிக்கவும். Signal லைப் புதுப்பிக்கவும் @@ -1672,13 +1671,9 @@ MMSC கடவுச்சொல் SMS விநியோக அறிக்கைகள் நீங்கள் அனுப்பும் அனைத்து SMS-களுக்கும் விநியோக அறிக்கைகளை வேண்டவும் - உரையாடல் ஒரு குறிப்பிட்ட நீளத்தை தாண்டியவுடன் பழைய செய்திகளை தானாக நீக்கு - பழைய செய்திகளை நீக்க அரட்டைகள் மற்றும் ஊடகங்கள் சேமிப்பு உரையாடலின் நீள அளவு - எல்லா உரையாடல்களையும் இப்போது ஒழுங்கமைக்கவும் - எல்லா உரையாடல்களையும் ஸ்கேன் செய்து உரையாடல் நீள வரம்புகளைச் செயல்படுத்தவும் இணைக்கப்பட்ட சாதனங்கள் ஒளி இருட்டு @@ -1708,13 +1703,14 @@ வைஃபை பயன்படுத்தும் போது ரோமிங் செய்யும் போது மீடியா தானாக பதிவிறக்கம் - செய்தி ஒழுங்கமைத்தல் சேமிப்பக பயன்பாடு புகைப்படம் காணொளி கோப்பு ஒலி விமர்சனம் சேமிப்பு + யாரும் + விருப்பத்திர்க்கேர்ப்ப கணினி ஈமோஜியைப் பயன்படுத்தவும் Signal உள்ளமைக்கப்பட்ட ஈமோஜி ஆதரவை முடக்கு உங்கள் ஐபி முகவரியை உங்கள் தொடர்புக்கு வெளிப்படுத்தாமல் இருக்க Signal சேவையகம் மூலம் அனைத்து அழைப்புகளையும் ரிலே செய்யவும். இயக்குவது அழைப்பு தரத்தை குறைக்கும். @@ -2040,6 +2036,7 @@ Signal பதிவு - அண்ட்ராய்டு க்கான சரிபார்ப்புக் குறியீடு ஒருபோதுமில்லை முன் தெரிந்திராத + ஒருவருமில்லை திரை பூட்டு அண்ட்ராய்டு திரை பூட்டு அல்லது கைரேகையுடன் Signal அணுகலைப் பூட்டு திரை பூட்டு செயலற்ற நேரம் முடிந்தது diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index af1224337..cb707f6e4 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -1407,13 +1407,13 @@ ఎమ్మెమ్మెస్సి సాంకేతిక పదము చేరిన ఎస్సెమ్మెస్ నివేదికలు మీరు పంపే ప్రతి SMS కోసం బట్వాడా నివేదికను అభ్యర్థించండి - ఒక సంభాషణ ఒక పేర్కొన్న పొడవు మించితె స్వయంచాలకంగా పాత సందేశాలను తొలగించు - పాత సందేశాలను తొలగించు + + ముచ్చట్లు మరియు ప్రసార మాధ్యమం నిల్వ సంభాషణ విస్తృతికి పరిమితి - ఇప్పుడు అన్ని సంభాషణలు కత్తిరించి సరి చేయుట - అన్ని సంభాషణలు స్కాన్ చేసి తద్వారా సంభాషణ పొడవు పరిమితులు అమలుపరచు + + పరికరాలు అనుసంధానించు లైటు గుప్తమైన @@ -1437,7 +1437,7 @@ వైఫై ఉపయోగించి చేసినప్పుడు రోమింగ్లో ఉన్నప్పుడు ప్రసార మాధ్యమం దానంతట అదే దిగుమతి - సందేశం కత్తిరించి సరి చేయుట + నిల్వ వినియోగం ఫోటోలు వీడియోలు diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index daeca62aa..33567c5fa 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1454,13 +1454,9 @@ รหัสผ่าน MMSC รายงานการส่ง SMS ร้องขอรายงานสำหรับแต่ละข้อความ SMS ที่คุณส่ง - ลบข้อความเก่าโดยอัตโนมัติเมื่อการสนทนายาวเกินกว่าความยาวที่กำหนด - ลบข้อความเก่า การพูดคุยและสื่อ พื้นที่จัดเก็บ ขีดจำกัดความยาวของการสนทนา - ตัดการสนทนาทั้งหมดให้สั้นลงเดี๋ยวนี้ - ไล่ดูการสนทนาทั้งหมดและบังคับใช้ขีดจำกัดความยาวของการสนทนา อุปกรณ์ที่เชื่อมโยงอยู่ สว่าง มืด @@ -1485,13 +1481,13 @@ เมื่อใช้ Wi-Fi เมื่อใช้บริการข้ามเครือข่าย Roaming ดาวน์โหลดสื่ออัตโนมัติ - ตัดข้อความให้สั้นลง การใช้พื้นที่จัดเก็บ รูปภาพ วิดีโอ แฟ้ม เสียง ดูพื้นที่จัดเก็บ + ไม่มี ใช้อีโมจิของระบบ ปิดใช้งานการรองรับอีโมจิของ Signal ส่งต่อสายทั้งหมดผ่านเซิร์ฟเวอร์ Signal เพื่อหลีกเลี่ยงการเปิดเผยที่อยู่ IP ของคนที่คุณติดต่อด้วย การเปิดใช้งานจะลดคุณภาพการโทรลง diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 602bba21a..2f88a7854 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -1376,13 +1376,13 @@ MMSC Password Mga ulat ng paghahatid ng SMS Humingi ng ulat ng paghahatid para sa bawat mensaheng SMS na iyong ipinapadala - Awtomatikong burahin ang mga mas lumang mensahe kapag ang pag-uusap ay lumampas na sa nakatakdang haba - Burahin ang mga lumang mensahe + + Mga chat at media Storage Limitasyon sa haba ng pag-uusap - Paikliin ang lahat ng pag-uusap ngayon - I-scan ang lahat ng pag-uusap at ipatupad ang limitasyon sa haba ng pag-uusap + + Mga naka-link na device Maliwanag Madilim @@ -1407,7 +1407,7 @@ Kapag gumagamit ng WI-FI Kapag roaming Awtomatikong pag-download ng media - Pagpapaikli ng mensahe + Pagkakagamit ng storage Mga larawan Mga video diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index de80264e1..9bd91eaed 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1577,13 +1577,9 @@ Geçersiz protokol sürümünde anahtar değişim iletisi alındı. MMSC Parolası SMS iletim raporları Gönderdiğiniz her SMS için iletim raporu istensin - Bir sohbet belirlenmiş bir uzunluğu aşınca eski iletileri otomatik olarak sil - Eski iletileri sil Sohbet ve içerik Depolama Sohbet uzunluk sınırı - Tüm sohbetleri şimdi kırp - Tüm sohbetleri tara ve sohbet uzunluğu limitlerini uygula Bağlı cihazlar Aydınlık Karanlık @@ -1608,13 +1604,14 @@ Geçersiz protokol sürümünde anahtar değişim iletisi alındı. Kablosuz ağ kullanırken Dolaşımdayken Otomatik içerik indirme - İleti kırpma Depolama kullanımı Fotoğraflar Videolar Dosyalar Ses Depolamayı incele + 1 yıl + Hiçbiri Sistem emojilerini kullan Signal\'in dahili emoji desteğini devre dışı bırak IP adresinizi kişinize göstermekten kaçınmak için tüm aramaları Signal sunucusundan aktarın. Etkinleştirildiğinde arama kalitesi düşecektir. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e64aa8f98..e41226293 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1084,13 +1084,13 @@ Пароль MMSC Звіти про доставку SMS Запитувати звіт про доставку для кожного відісланого вами SMS - Автоматично видаляти старі повідомлення, коли довжина розмови перевищує задану - Видаляти старі повідомлення + + Чати та медіа Сховище Обмеження довжини розмови - Обрізати усі розмови зараз - Переглянути всі розмови і привести їх довжину у відповідність із заданою + + Прив\'язані пристрої Світла Темна @@ -1112,7 +1112,7 @@ При використанні Wi-Fi Під час роумінгу Автозавантаження медіа - Обрізка повідомлень + Аудіо Використовувати системні emoji Вимкнути вбудовану в Signal підтримку emoji diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 758290930..1aaf8f985 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -1300,13 +1300,13 @@ ایم ایم ایس سی پاسورڈ ایس ایم ایس کی ترسیل کی رپورٹ اپنے بھیجنے والے ہر ایس ایم ایس پیغام کے لئے ڈیلیوری رپورٹ کی درخواست کریں - ایک بار جب گفتگو کی طوالت زیادہ ہوجاتی ہے تو پرانے پیغامات کو خود بخود حذف کردیں - پرانے پیغامات حذف کریں + + باتیں اور میڈیا اسٹوریج گفتگو کی لمبائی کی حد - تمام گفتگو کو اب صحیح ترتیب دیں - تمام گفتگو کو اسکین کریں اور گفتگو کی لمبائی کی حدود کو نافذ کریں + + منسلک مشینیں روشنی اندھیرا @@ -1330,7 +1330,7 @@ جب وائی فائی استعمال کریں جب رومنگ ہو رہی ہو میڈیا آٹو ڈاون لوڈ - پیغام صحیح ترتیب دے رہا ہے + اسٹوریج استعمال فوٹوز ویڈیوز diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4a698e7eb..154e58c79 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -469,7 +469,7 @@ Sửa thông tin nhóm Chọn ai có quyền sửa tên nhóm, ảnh nhóm và tin nhắn tự hủy. Chọn ai có quyền thêm hoặc mời thành viên mới - Đường dẫn nhóm có thể chia sẻ + Đường dẫn nhóm Chặn nhóm Bỏ chặn nhóm Rời nhóm @@ -583,7 +583,6 @@ Đường dẫn nhóm sẽ có trong tương lai Cập nhật Signal để dùng liên kết nhóm Tham gia nhóm bằng đường dẫn nhóm hiện chưa được Signal hỗ trợ. Tính năng này sẽ được hoàn thiện trong bản cập nhật tới. - Phiên bản Signal bạn đang dùng không hỗ trợ liên kết nhóm. Vui lòng cập nhật để có thể tham gia nhóm qua liên kết. Cập nhật Signal Đường dẫn nhóm không hợp lệ @@ -1659,13 +1658,9 @@ Nhận thông tin trao đổi mã khóa về phiên bản giao thức không h Mật khẩu MMSC Báo cáo đã gửi SMS Yêu cầu báo cáo đã gửi cho từng tin nhắn SMS gửi đi - Tự động xóa các tin nhắn cũ khi cuộc trò chuyện vượt quá độ dài ấn định - Xóa tin nhắn cũ Trò chuyện và đa phương tiện Lưu trữ Giới hạn độ dài cuộc trò chuyện - Lập tức rút ngắn tất cả các cuộc trò chuyện - Quét tất cả các cuộc trò chuyện và áp dụng giới hạn độ dài trò chuyện Các thiết bị được liên kết Sáng Tối @@ -1695,13 +1690,14 @@ Nhận thông tin trao đổi mã khóa về phiên bản giao thức không h Khi dùng Wi-Fi Khi chuyển vùng quốc tế Tự động tải đa phương tiện - Thu gọn tin nhắn Quản lí bộ nhớ Ảnh Video Tệp Âm thanh Kiểm tra bộ nhớ + Không + Tùy chỉnh Sử dụng biểu tượng cảm xúc hệ thống Tắt hỗ trợ biểu tượng cảm xúc của Signal Chuyển tiếp tất cả cuộc gọi qua máy chủ Signal để tránh lộ địa chỉ IP với đối tác. Bật tính năng này sẽ giảm chất lượng cuộc gọi. @@ -2019,6 +2015,7 @@ Nhận thông tin trao đổi mã khóa về phiên bản giao thức không h Đăng ký Signal - Mã Xác minh cho Android Không bao giờ Không rõ + Không ai Khoá màn hình Khoá truy cập Signal bằng mật khẩu Android hoặc vân tay Thời gian chờ trước khi tự động khoá diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 535e145cd..142b77f6a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -42,6 +42,7 @@ 屏幕锁定:%1$s,注册锁定:%2$s 屏幕锁定 %1$s 主题:%1$s,语言:%2$s + PIN 对于注册锁定是必需的。如需禁用 PIN,请先禁用注册锁定。 PIN 已创建。 隐藏 @@ -1362,13 +1363,13 @@ MMSC 密码 短信送达报告 为每一条发送的短信请求送达报告 - 对话超过指定长度时,自动删除较旧消息 - 删除旧消息 + + 聊天和媒体 存储 对话长度限制 - 马上清理全部对话 - 扫描所有对话,实施对话长度限制 + + 已关联设备 浅色 深色 @@ -1398,7 +1399,7 @@ 当使用 WiFi 时 当漫游时 自动下载媒体 - 清理消息 + 存储使用量 图片 视频 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e7b75667d..33786392d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -431,7 +431,7 @@ 待處理的成員要求 沒有成員要求顯示。 - 此清單中的人員正在嘗試透過可分享的群組連結加入該群組。 + 此清單中的人員正在嘗試透過群組連結加入此群組。 已新增\"%1$s\" 已拒絕 \"%1$s\" @@ -470,7 +470,7 @@ 編輯群組資訊 選擇誰可以編輯群組名稱,頭像和自動銷毀訊息。 選擇誰可以新增或邀請新成員。 - 可分享的群組連結 + 群組連結 封鎖群組 解封鎖群組 離開對話群組 @@ -584,7 +584,7 @@ 群組連結即將推出 更新Signal 以使用群組連結 Signal尚不支援透過連結加入群組。 此功能將在即將進行的更新中發佈。 - 你使用的Signal版本不支援可共享的群組連結。透過連結更新到最新版本以加入該群組。 + 你使用的Signal版本不支援群組連結。 透過連結更新到最新版本以加入該群組。 更新 Signal 群組連結無效 @@ -808,25 +808,25 @@ %1$s變更了誰可以編輯群駔成員資格為\"%2$s\"。 誰可以編輯群成員已變更為\"%1$s\"。 - 你開啟了可分享的群組連結。 - 你已通過管理員批准開啟了可共享的群組連結。 - 你關閉了可共享的群組連結。 - %1$s開啟了共享群組連結。 - %1$s在獲得管理員批准的情況下開啟了可共享的群組連結。 - %1$s關閉了可共享的群組連結。 - 可共享的群組連結已開啟。 - 共享群組連結已在管理員批准下開啟。 - 共享群組連結已關閉。 + 你已打開群組連結,但尚未獲得管理員批准。 + 你已啟用管理員批准的群組連結。 + 你關閉了群組連結。 + %1$s 開啟了群組連結,但未獲得管理員批准。 + %1$s 開啟了具有管理員批准功能的群組連結。 + %1$s 關閉了群組連結。 + 群組連結已開啟,而管理員的批准已關閉。 + 群組連結已在管理員批准下開啟。 + 群組連結已關閉。 - 你重置可共享的群組連結。 - %1$s重置可共享的群組連結。 - 可共享的群組連結已重置。 + 你重置群組連結。 + %1$s 重置了群組連結。 + 群組連結已重置。 - 你透過共享群組連結加入了該群組。 - %1$s透過共享群組連結加入了該群組。 + 你透過群組連結加入了群組。 + %1$s 透過群組連結加入了群組。 你傳送了加入該群組的請求。 - %1$s透過共享群組連結請求加入。 + %1$s 透過群組連結要求加入。 %1$s批准了你加入群組的請求。 %1$s從%2$s批准了加入的請求。 @@ -1398,6 +1398,7 @@ 安全碼變更 仍然傳送 + 無論如何要打電話 以下使用者可能已重新安裝或更換了裝置。與他們一起驗證你的安全碼,以確保隱私性。 檢視 先前已驗證 @@ -1693,13 +1694,11 @@ MMSC 密碼 手機簡訊傳送狀態報告 針對每則手機簡訊都要求回覆傳送報告 - 當對話群組超過一定長度時自動刪除舊的訊息 - 刪除舊的訊息 聊天與媒體 儲存 對話群組長度上限 - 現在開始精簡所有的對話群組 - 這會掃描目前所有的對話群組,並且強制刪除超過長度上限的部分。 + 保留訊息 + 清除訊息歷史記錄 已連結裝置 明亮 深色 @@ -1729,17 +1728,34 @@ 當使用 Wi-Fi 時 當漫遊時 媒體自動下載 - 訊息整理 + 訊息歷史紀錄 已使用的儲存容量 照片 影片 檔案 音訊 檢視儲存空間 + 刪除舊訊息? + 清除訊息歷史記錄? + 這將從裝置中永久刪除所有早於%1$s的訊息歷史記錄和媒體檔案。 + 這將會永久整理所有對話到%1$s最近的訊息。 + 這將從你的裝置中永久刪除所有訊息歷史記錄和媒體檔案。 + 你確定要刪除所有訊息歷史記錄嗎? + 所有訊息歷史記錄將被永久刪除。 此操作無法取消。 + 立即刪除全部 + 永久 + 1年 + 6個月 + 30天 + 靜音 + %1$s則訊息 + 自訂 + 自定義會話長度限制 使用系統表情符號 停用 Signal 內建的表情支援 透過 Signal 伺服器轉發通話,來避免透露你的 IP 位址給你的聯絡人。啟用時,會降低通話品質。 永遠轉發通話 + 誰可以… 應用程式存取 溝通 聊天 @@ -1764,6 +1780,7 @@ 通知我 在靜音聊天中提及你時接收通知 設定使用者名稱 + 自定義選項 @@ -2054,6 +2071,14 @@ Signal 註冊 - Android驗證碼 永不 未知 + 查看我的電話號碼 + 透過電話號碼找到我 + 每個人 + 我的聯絡人 + 無人 + 你的電話號碼將可被與你傳送訊息的所有人員和群組看見。 + 任何在你的聯絡人中擁有你的電話號碼的人都會在 Signal 上將你視為聯絡人。 其他人將可以在搜尋中找到你。 + 只有你的聯絡人會在Signal上看到你的電話號碼。 螢幕鎖定 以 Android 螢幕鎖定或指紋來鎖定 Signal 存取 螢幕鎖定閒置逾時 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 5bde20653..a56c1b815 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -294,6 +294,14 @@ 604800 + + 0 + 5000 + 1000 + 500 + 100 + + #ffffff #ff0000 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 105cc958a..f15d18003 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -240,6 +240,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1669cd0e5..5a3e52e91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,6 +3,7 @@ Signal https://signal.org/install + https://signal.org/donate Yes No @@ -531,7 +532,7 @@ Pending member requests No member requests to show. - People on this list are attempting to join this group via the sharable group link. + People on this list are attempting to join this group via the group link. Added "%1$s" Denied "%1$s" @@ -577,7 +578,7 @@ Edit group info Choose who can edit the group name, avatar, and disappearing messages. Choose who can add or invite new members. - Sharable group link + Group link Block group Unblock group Leave group @@ -710,7 +711,7 @@ Group links coming soon Update Signal to use group links Joining a group via a link is not yet supported by Signal. This feature will be released in an upcoming update. - The version of Signal you’re using does not support sharable group links. Update to the latest version to join this group via link. + The version of Signal you’re using does not support group links. Update to the latest version to join this group via link. Update Signal Group link is not valid @@ -983,28 +984,28 @@ Who can edit group membership has been changed to \"%1$s\". - You turned on the sharable group link. - You turned on the sharable group link with admin approval. - You turned off the sharable group link. - %1$s turned on the sharable group link. - %1$s turned on the sharable group link with admin approval. - %1$s turned off the sharable group link. - The sharable group link has been turned on. - The sharable group link has been turned on with admin approval. - The sharable group link has been turned off. + You turned on the group link with admin approval off. + You turned on the group link with admin approval on. + You turned off the group link. + %1$s turned on the group link with admin approval off. + %1$s turned on the group link with admin approval on. + %1$s turned off the group link. + The group link has been turned on with admin approval off. + The group link has been turned on with admin approval on. + The group link has been turned off. - You reset the sharable group link. - %1$s reset the sharable group link. - The sharable group link has been reset. + You reset the group link. + %1$s reset the group link. + The group link has been reset. - You joined the group via the sharable group link. - %1$s joined the group via the sharable group link. + You joined the group via the group link. + %1$s joined the group via the group link. You sent a request to join the group. - %1$s requested to join via the sharable group link. + %1$s requested to join via the group link. %1$s approved your request to join the group. @@ -1687,6 +1688,7 @@ Safety Number Changes Send anyway + Call anyway The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy. View Previous verified @@ -2048,6 +2050,7 @@ Slow Help Advanced + Donate to Signal Privacy MMS User Agent Manual MMS settings @@ -2058,13 +2061,11 @@ MMSC Password SMS delivery reports Request a delivery report for each SMS message you send - Automatically delete older messages once a conversation exceeds a specified length - Delete old messages Chats and media Storage Conversation length limit - Trim all conversations now - Scan through all conversations and enforce conversation length limits + Keep messages + Clear message history Linked devices Light Dark @@ -2094,17 +2095,34 @@ When using Wi-Fi When roaming Media auto-download - Message trimming + Message history Storage usage Photos Videos Files Audio Review storage + Delete older messages? + Clear message history? + This will permanently delete all message history and media from your device that are older than %1$s. + This will permanently trim all conversations to the %1$s most recent messages. + This will permanently delete all message history and media from your device. + Are you sure you want to delete all message history? + All message history will be permanently removed. This action cannot be undone. + Delete all now + Forever + 1 year + 6 months + 30 days + None + %1$s messages + Custom + Custom conversation length limit Use system emoji Disable Signal\'s built-in emoji support Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality. Always relay calls + Who can… App access Communication Chats @@ -2130,6 +2148,8 @@ Receive notifications when you’re mentioned in muted chats Setup a username + Customize option + Internal Preferences Groups V2 @@ -2504,6 +2524,14 @@ Signal Registration - Verification Code for Android Never Unknown + See my phone number + Find me by phone number + Everyone + My contacts + Nobody + Your phone number will be visible to all people and groups you message. + Anyone who has your phone number in their contacts will see you as a contact on Signal. Others will be able to find you in search. + Only your contacts will see your phone number on Signal. Screen lock Lock Signal access with Android screen lock or fingerprint Screen lock inactivity timeout diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 5f3e55dd3..726a938c2 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -400,6 +400,7 @@ @drawable/ic_advanced_24 @drawable/ic_safety_number_outline_24 @drawable/ic_help_outline_24 + @drawable/ic_heart_outline_24 @drawable/message_request_button_background_light @color/core_grey_90 @color/core_grey_60 @@ -731,6 +732,7 @@ @drawable/ic_advanced_24 @drawable/ic_safety_number_solid_24 @drawable/ic_help_solid_24 + @drawable/ic_heart_solid_24 @drawable/message_request_button_background_dark @color/core_grey_05 @color/core_grey_25 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 052e35cfe..66f4e0983 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -46,4 +46,9 @@ android:title="@string/preferences__advanced" android:icon="?attr/advanced_icon"/> + + diff --git a/app/src/main/res/xml/preferences_app_protection.xml b/app/src/main/res/xml/preferences_app_protection.xml index 13bc8b0c8..730c14d93 100644 --- a/app/src/main/res/xml/preferences_app_protection.xml +++ b/app/src/main/res/xml/preferences_app_protection.xml @@ -1,6 +1,21 @@ + + + + + + + + + - + @@ -9,26 +10,22 @@ - - - - + android:title="@string/preferences_chats__message_history"> + android:key="settings.keep_messages_duration" + android:title="@string/preferences__keep_messages" + tools:summary="@string/preferences_storage__forever" /> + + + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java index a05020807..25efd4611 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java @@ -143,7 +143,7 @@ public final class GroupsV2UpdateMessageProducerTest { .addMember(you) .build(); - assertThat(describeChange(change), is(singletonList("You joined the group via the sharable group link."))); + assertThat(describeChange(change), is(singletonList("You joined the group via the group link."))); } @Test @@ -152,7 +152,7 @@ public final class GroupsV2UpdateMessageProducerTest { .addMember(bob) .build(); - assertThat(describeChange(change), is(singletonList("Bob joined the group via the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Bob joined the group via the group link."))); } @Test @@ -209,7 +209,7 @@ public final class GroupsV2UpdateMessageProducerTest { .addMember(you) .build(); - assertThat(describeChange(change), is(Arrays.asList("You joined the group via the sharable group link.", "You added Alice."))); + assertThat(describeChange(change), is(Arrays.asList("You joined the group via the group link.", "You added Alice."))); } // Member removals @@ -846,7 +846,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ANY) .build(); - assertThat(describeChange(change), is(singletonList("You turned on the sharable group link."))); + assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval off."))); } @Test @@ -855,7 +855,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) .build(); - assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval."))); + assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval on."))); } @Test @@ -864,7 +864,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) .build(); - assertThat(describeChange(change), is(singletonList("You turned off the sharable group link."))); + assertThat(describeChange(change), is(singletonList("You turned off the group link."))); } @Test @@ -873,7 +873,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ANY) .build(); - assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Alice turned on the group link with admin approval off."))); } @Test @@ -882,7 +882,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) .build(); - assertThat(describeChange(change), is(singletonList("Bob turned on the sharable group link with admin approval."))); + assertThat(describeChange(change), is(singletonList("Bob turned on the group link with admin approval on."))); } @Test @@ -891,7 +891,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) .build(); - assertThat(describeChange(change), is(singletonList("Alice turned off the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Alice turned off the group link."))); } @Test @@ -900,7 +900,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ANY) .build(); - assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on."))); + assertThat(describeChange(change), is(singletonList("The group link has been turned on with admin approval off."))); } @Test @@ -909,7 +909,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR) .build(); - assertThat(describeChange(change), is(singletonList("The sharable group link has been turned on with admin approval."))); + assertThat(describeChange(change), is(singletonList("The group link has been turned on with admin approval on."))); } @Test @@ -918,7 +918,7 @@ public final class GroupsV2UpdateMessageProducerTest { .inviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE) .build(); - assertThat(describeChange(change), is(singletonList("The sharable group link has been turned off."))); + assertThat(describeChange(change), is(singletonList("The group link has been turned off."))); } // Group link reset @@ -929,7 +929,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(singletonList("You reset the sharable group link."))); + assertThat(describeChange(change), is(singletonList("You reset the group link."))); } @Test @@ -938,7 +938,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(singletonList("Alice reset the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Alice reset the group link."))); } @Test @@ -947,7 +947,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(singletonList("The sharable group link has been reset."))); + assertThat(describeChange(change), is(singletonList("The group link has been reset."))); } /** @@ -961,7 +961,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(singletonList("Alice turned on the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Alice turned on the group link with admin approval off."))); } /** @@ -975,7 +975,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(singletonList("You turned on the sharable group link with admin approval."))); + assertThat(describeChange(change), is(singletonList("You turned on the group link with admin approval on."))); } @Test @@ -985,7 +985,7 @@ public final class GroupsV2UpdateMessageProducerTest { .resetGroupLink() .build(); - assertThat(describeChange(change), is(Arrays.asList("You turned off the sharable group link.", "You reset the sharable group link."))); + assertThat(describeChange(change), is(Arrays.asList("You turned off the group link.", "You reset the group link."))); } // Group link request @@ -1005,7 +1005,7 @@ public final class GroupsV2UpdateMessageProducerTest { .requestJoin() .build(); - assertThat(describeChange(change), is(singletonList("Bob requested to join via the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Bob requested to join via the group link."))); } @Test @@ -1014,7 +1014,7 @@ public final class GroupsV2UpdateMessageProducerTest { .requestJoin(alice) .build(); - assertThat(describeChange(change), is(singletonList("Alice requested to join via the sharable group link."))); + assertThat(describeChange(change), is(singletonList("Alice requested to join via the group link."))); } @Test @@ -1196,12 +1196,36 @@ public final class GroupsV2UpdateMessageProducerTest { // Group state without a change record + @Test + public void you_created_a_group_change_not_found() { + DecryptedGroup group = newGroupBy(you, 0) + .build(); + + assertThat(describeNewGroup(group), is("You joined the group.")); + } + @Test public void you_created_a_group() { DecryptedGroup group = newGroupBy(you, 0) .build(); - assertThat(describeNewGroup(group), is("You created the group.")); + DecryptedGroupChange change = changeBy(you) + .addMember(alice) + .addMember(you) + .addMember(bob) + .title("New title") + .build(); + + assertThat(describeNewGroup(group, change), is("You created the group.")); + } + + @Test + public void alice_created_a_group_change_not_found() { + DecryptedGroup group = newGroupBy(alice, 0) + .member(you) + .build(); + + assertThat(describeNewGroup(group), is("You joined the group.")); } @Test @@ -1210,7 +1234,14 @@ public final class GroupsV2UpdateMessageProducerTest { .member(you) .build(); - assertThat(describeNewGroup(group), is("Alice added you to the group.")); + DecryptedGroupChange change = changeBy(alice) + .addMember(you) + .addMember(alice) + .addMember(bob) + .title("New title") + .build(); + + assertThat(describeNewGroup(group, change), is("Alice added you to the group.")); } @Test @@ -1247,8 +1278,12 @@ public final class GroupsV2UpdateMessageProducerTest { } private @NonNull String describeNewGroup(@NonNull DecryptedGroup group) { + return describeNewGroup(group, DecryptedGroupChange.getDefaultInstance()); + } + + private @NonNull String describeNewGroup(@NonNull DecryptedGroup group, @NonNull DecryptedGroupChange groupChange) { MainThreadUtil.setMainThread(false); - return producer.describeNewGroup(group).getString(); + return producer.describeNewGroup(group, groupChange).getString(); } private static GroupStateBuilder newGroupBy(UUID foundingMember, int revision) { diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index c2e61950e..94be9748d 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -432,8 +432,8 @@ dependencyVerification { ['org.signal:argon2:13.1', '0f686ccff0d4842bfcc74d92e8dc780a5f159b9376e37a1189fabbcdac458bef'], - ['org.signal:ringrtc-android:2.5.0', - '83cb3d26c5ab8e96539ca0583b9b79240e853db4baaea6d6cda8e09e598ae19d'], + ['org.signal:ringrtc-android:2.5.1', + 'bbb357f7a6286c3fc641e84394e2903db72a0cbeb10393afed4e689c587ecdf9'], ['org.signal:signal-metadata-java:0.1.2', '6aaeb6a33bf3161a3e6ac9db7678277f7a4cf5a2c96b84342e4007ee49bab1bd'], diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 3cc0ec4e4..6f62e5b7d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -9,7 +9,6 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; -import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.whispersystems.libsignal.IdentityKey; @@ -80,13 +79,10 @@ import java.security.KeyStore; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; -import java.sql.Time; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -148,8 +144,8 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getSenderCertificate(); } - public byte[] getSenderCertificateLegacy() throws IOException { - return this.pushServiceSocket.getSenderCertificateLegacy(); + public byte[] getSenderCertificateForPhoneNumberPrivacy() throws IOException { + return this.pushServiceSocket.getUuidOnlySenderCertificate(); } /** @@ -248,7 +244,8 @@ public class SignalServiceAccountManager { public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - SignalServiceProfile.Capabilities capabilities) + SignalServiceProfile.Capabilities capabilities, + boolean discoverableByPhoneNumber) throws IOException { return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey, @@ -257,7 +254,8 @@ public class SignalServiceAccountManager { pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, - capabilities); + capabilities, + discoverableByPhoneNumber); } /** @@ -276,13 +274,15 @@ public class SignalServiceAccountManager { public void setAccountAttributes(String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - SignalServiceProfile.Capabilities capabilities) + SignalServiceProfile.Capabilities capabilities, + boolean discoverableByPhoneNumber) throws IOException { this.pushServiceSocket.setAccountAttributes(signalingKey, signalProtocolRegistrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, - capabilities); + capabilities, + discoverableByPhoneNumber); } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index c325bf306..810efd57e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -218,11 +218,11 @@ public class SignalServiceMessageReceiver { StickerProtos.Pack pack = StickerProtos.Pack.parseFrom(outputStream.toByteArray()); List stickers = new ArrayList<>(pack.getStickersCount()); - SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji()) + SignalServiceStickerManifest.StickerInfo cover = pack.hasCover() ? new SignalServiceStickerManifest.StickerInfo(pack.getCover().getId(), pack.getCover().getEmoji(), pack.getCover().getContentType()) : null; for (StickerProtos.Pack.Sticker sticker : pack.getStickersList()) { - stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji())); + stickers.add(new SignalServiceStickerManifest.StickerInfo(sticker.getId(), sticker.getEmoji(), sticker.getContentType())); } return new SignalServiceStickerManifest(pack.getTitle(), pack.getAuthor(), cover, stickers); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index b52362459..73e5024cc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -586,6 +586,7 @@ public class SignalServiceMessageSender { quoteBuilder.setAuthorUuid(message.getQuote().get().getAuthor().getUuid().get().toString()); } + // TODO [Alan] PhoneNumberPrivacy: Do not set this number if (message.getQuote().get().getAuthor().getNumber().isPresent()) { quoteBuilder.setAuthorE164(message.getQuote().get().getAuthor().getNumber().get()); } @@ -660,6 +661,7 @@ public class SignalServiceMessageSender { stickerBuilder.setPackId(ByteString.copyFrom(message.getSticker().get().getPackId())); stickerBuilder.setPackKey(ByteString.copyFrom(message.getSticker().get().getPackKey())); stickerBuilder.setStickerId(message.getSticker().get().getStickerId()); + stickerBuilder.setEmoji(message.getSticker().get().getEmoji()); if (message.getSticker().get().getAttachment().isStream()) { stickerBuilder.setData(createAttachmentPointer(message.getSticker().get().getAttachment().asStream())); @@ -681,6 +683,7 @@ public class SignalServiceMessageSender { .setRemove(message.getReaction().get().isRemove()) .setTargetSentTimestamp(message.getReaction().get().getTargetSentTimestamp()); + // TODO [Alan] PhoneNumberPrivacy: Do not set this number if (message.getReaction().get().getTargetAuthor().getNumber().isPresent()) { reactionBuilder.setTargetAuthorE164(message.getReaction().get().getTargetAuthor().getNumber().get()); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index 2ae80f0eb..d4228b2a1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -160,16 +160,6 @@ public final class DecryptedGroupUtil { return Optional.absent(); } - public static Optional firstMember(Collection members) { - Iterator iterator = members.iterator(); - - if (iterator.hasNext()) { - return Optional.of(iterator.next()); - } else { - return Optional.absent(); - } - } - public static Optional findPendingByUuid(Collection members, UUID uuid) { ByteString uuidBytes = UuidUtil.toByteString(uuid); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index aa36878a1..31c69f098 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -736,9 +736,10 @@ public final class SignalServiceContent { SignalServiceProtos.DataMessage.Sticker sticker = content.getSticker(); return new SignalServiceDataMessage.Sticker(sticker.getPackId().toByteArray(), - sticker.getPackKey().toByteArray(), - sticker.getStickerId(), - createAttachmentPointer(sticker.getData())); + sticker.getPackKey().toByteArray(), + sticker.getStickerId(), + sticker.getEmoji(), + createAttachmentPointer(sticker.getData())); } private static SignalServiceDataMessage.Reaction createReaction(SignalServiceProtos.DataMessage content) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index 874ec226e..3458a5391 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -449,12 +449,14 @@ public class SignalServiceDataMessage { private final byte[] packId; private final byte[] packKey; private final int stickerId; + private final String emoji; private final SignalServiceAttachment attachment; - public Sticker(byte[] packId, byte[] packKey, int stickerId, SignalServiceAttachment attachment) { + public Sticker(byte[] packId, byte[] packKey, int stickerId, String emoji, SignalServiceAttachment attachment) { this.packId = packId; this.packKey = packKey; this.stickerId = stickerId; + this.emoji = emoji; this.attachment = attachment; } @@ -470,6 +472,10 @@ public class SignalServiceDataMessage { return stickerId; } + public String getEmoji() { + return emoji; + } + public SignalServiceAttachment getAttachment() { return attachment; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStickerManifest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStickerManifest.java index 56e462787..4625862f8 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStickerManifest.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStickerManifest.java @@ -39,10 +39,12 @@ public class SignalServiceStickerManifest { public static final class StickerInfo { private final int id; private final String emoji; + private final String contentType; - public StickerInfo(int id, String emoji) { - this.id = id; - this.emoji = emoji; + public StickerInfo(int id, String emoji, String contentType) { + this.id = id; + this.emoji = emoji; + this.contentType = contentType; } public int getId() { @@ -52,5 +54,9 @@ public class SignalServiceStickerManifest { public String getEmoji() { return emoji; } + + public String getContentType() { + return contentType; + } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java index 316f395f9..488e05dfe 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java @@ -80,6 +80,14 @@ public final class SignalAccountRecord implements SignalRecord { return proto.getLinkPreviews(); } + public AccountRecord.PhoneNumberSharingMode getPhoneNumberSharingMode() { + return proto.getPhoneNumberSharingMode(); + } + + public boolean isPhoneNumberUnlisted() { + return proto.getUnlistedPhoneNumber(); + } + AccountRecord toProto() { return proto; } @@ -159,6 +167,16 @@ public final class SignalAccountRecord implements SignalRecord { return this; } + public Builder setPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode mode) { + builder.setPhoneNumberSharingMode(mode); + return this; + } + + public Builder setUnlistedPhoneNumber(boolean unlisted) { + builder.setUnlistedPhoneNumber(unlisted); + return this; + } + public SignalAccountRecord build() { AccountRecord proto = builder.build(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java index 9d9cfcc17..5e7eb4956 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AccountAttributes.java @@ -39,6 +39,9 @@ public class AccountAttributes { @JsonProperty private boolean unrestrictedUnidentifiedAccess; + @JsonProperty + private boolean discoverableByPhoneNumber; + @JsonProperty private SignalServiceProfile.Capabilities capabilities; @@ -49,7 +52,8 @@ public class AccountAttributes { String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - SignalServiceProfile.Capabilities capabilities) + SignalServiceProfile.Capabilities capabilities, + boolean discoverableByPhoneNumber) { this.signalingKey = signalingKey; this.registrationId = registrationId; @@ -61,6 +65,7 @@ public class AccountAttributes { this.unidentifiedAccessKey = unidentifiedAccessKey; this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; this.capabilities = capabilities; + this.discoverableByPhoneNumber = discoverableByPhoneNumber; } public AccountAttributes() {} @@ -101,6 +106,10 @@ public class AccountAttributes { return unrestrictedUnidentifiedAccess; } + public boolean isDiscoverableByPhoneNumber() { + return discoverableByPhoneNumber; + } + public SignalServiceProfile.Capabilities getCapabilities() { return capabilities; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index e2674bfd7..11439ad2f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -181,8 +181,8 @@ public class PushServiceSocket { private static final String PROFILE_PATH = "/v1/profile/%s"; private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s"; - private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery"; - private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true"; + private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true"; + private static final String SENDER_CERTIFICATE_NO_E164_PATH = "/v1/certificate/delivery?includeUuid=true&includeE164=false"; private static final String KBS_AUTH_PATH = "/v1/backup/auth"; @@ -292,10 +292,11 @@ public class PushServiceSocket { public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - SignalServiceProfile.Capabilities capabilities) + SignalServiceProfile.Capabilities capabilities, + boolean discoverableByPhoneNumber) throws IOException { - AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities); + AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, discoverableByPhoneNumber); String requestBody = JsonUtil.toJson(signalingKeyEntity); String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody); @@ -305,7 +306,8 @@ public class PushServiceSocket { public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin, String registrationLock, byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - SignalServiceProfile.Capabilities capabilities) + SignalServiceProfile.Capabilities capabilities, + boolean discoverableByPhoneNumber) throws IOException { if (registrationLock != null && pin != null) { @@ -313,7 +315,8 @@ public class PushServiceSocket { } AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, - unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities); + unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, + discoverableByPhoneNumber); makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); } @@ -363,13 +366,13 @@ public class PushServiceSocket { makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null); } - public byte[] getSenderCertificateLegacy() throws IOException { - String responseText = makeServiceRequest(SENDER_CERTIFICATE_LEGACY_PATH, "GET", null); + public byte[] getSenderCertificate() throws IOException { + String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); } - public byte[] getSenderCertificate() throws IOException { - String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null); + public byte[] getUuidOnlySenderCertificate() throws IOException { + String responseText = makeServiceRequest(SENDER_CERTIFICATE_NO_E164_PATH, "GET", null); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 06bbf608a..4ad684da0 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -215,6 +215,7 @@ message DataMessage { optional bytes packKey = 2; optional uint32 stickerId = 3; optional AttachmentPointer data = 4; + optional string emoji = 5; } message Reaction { diff --git a/libsignal/service/src/main/proto/SignalStorage.proto b/libsignal/service/src/main/proto/SignalStorage.proto index 40e81a2f7..33ab327e5 100644 --- a/libsignal/service/src/main/proto/SignalStorage.proto +++ b/libsignal/service/src/main/proto/SignalStorage.proto @@ -97,15 +97,24 @@ message GroupV2Record { } message AccountRecord { - bytes profileKey = 1; - string givenName = 2; - string familyName = 3; - string avatarUrlPath = 4; - bool noteToSelfArchived = 5; - bool readReceipts = 6; - bool sealedSenderIndicators = 7; - bool typingIndicators = 8; - bool proxiedLinkPreviews = 9; + + enum PhoneNumberSharingMode { + EVERYBODY = 0; + CONTACTS_ONLY = 1; + NOBODY = 2; + } + + bytes profileKey = 1; + string givenName = 2; + string familyName = 3; + string avatarUrlPath = 4; + bool noteToSelfArchived = 5; + bool readReceipts = 6; + bool sealedSenderIndicators = 7; + bool typingIndicators = 8; + bool proxiedLinkPreviews = 9; // 10 is reserved for unread - bool linkPreviews = 11; + bool linkPreviews = 11; + PhoneNumberSharingMode phoneNumberSharingMode = 12; + bool unlistedPhoneNumber = 13; } diff --git a/libsignal/service/src/main/proto/StickerResources.proto b/libsignal/service/src/main/proto/StickerResources.proto index 409360883..03e5ccbf1 100644 --- a/libsignal/service/src/main/proto/StickerResources.proto +++ b/libsignal/service/src/main/proto/StickerResources.proto @@ -12,8 +12,9 @@ option java_outer_classname = "StickerProtos"; message Pack { message Sticker { - optional uint32 id = 1; - optional string emoji = 2; + optional uint32 id = 1; + optional string emoji = 2; + optional string contentType = 3; } optional string title = 1;