mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-19 20:08:27 +00:00
Converted three classes to kotlin (#1552)
This commit is contained in:
parent
e2a40ddabc
commit
d23a0b8ceb
@ -1,64 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.v2.Util;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompositeEmojiPageModel implements EmojiPageModel {
|
||||
@AttrRes private final int iconAttr;
|
||||
@NonNull private final List<EmojiPageModel> models;
|
||||
|
||||
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
|
||||
this.iconAttr = iconAttr;
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return Util.hasItems(models) ? models.get(0).getKey() : "";
|
||||
}
|
||||
|
||||
public int getIconAttr() {
|
||||
return iconAttr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
List<String> emojis = new LinkedList<>();
|
||||
for (EmojiPageModel model : models) {
|
||||
emojis.addAll(model.getEmoji());
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
List<Emoji> emojis = new LinkedList<>();
|
||||
for (EmojiPageModel model : models) {
|
||||
emojis.addAll(model.getDisplayEmoji());
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Uri getSpriteUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.components.emoji
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AttrRes
|
||||
import java.util.LinkedList
|
||||
|
||||
class CompositeEmojiPageModel(
|
||||
@field:AttrRes @param:AttrRes private val iconAttr: Int,
|
||||
private val models: List<EmojiPageModel>
|
||||
) : EmojiPageModel {
|
||||
|
||||
override fun getKey(): String {
|
||||
return if (models.isEmpty()) "" else models[0].key
|
||||
}
|
||||
|
||||
override fun getIconAttr(): Int { return iconAttr }
|
||||
|
||||
override fun getEmoji(): List<String> {
|
||||
val emojis: MutableList<String> = LinkedList()
|
||||
for (model in models) {
|
||||
emojis.addAll(model.emoji)
|
||||
}
|
||||
return emojis
|
||||
}
|
||||
|
||||
override fun getDisplayEmoji(): List<Emoji> {
|
||||
val emojis: MutableList<Emoji> = LinkedList()
|
||||
for (model in models) {
|
||||
emojis.addAll(model.displayEmoji)
|
||||
}
|
||||
return emojis
|
||||
}
|
||||
|
||||
override fun hasSpriteMap(): Boolean { return false }
|
||||
|
||||
override fun getSpriteUri(): Uri? { return null }
|
||||
|
||||
override fun isDynamic(): Boolean { return false }
|
||||
}
|
@ -1,381 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation.v2;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.CharacterSets;
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue;
|
||||
|
||||
import org.session.libsignal.utilities.Log;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class Util {
|
||||
private static final String TAG = Log.tag(Util.class);
|
||||
|
||||
private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90);
|
||||
|
||||
public static <T> List<T> asList(T... elements) {
|
||||
List<T> result = new LinkedList<>();
|
||||
Collections.addAll(result, elements);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String join(String[] list, String delimiter) {
|
||||
return join(Arrays.asList(list), delimiter);
|
||||
}
|
||||
|
||||
public static <T> String join(Collection<T> list, String delimiter) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
int i = 0;
|
||||
|
||||
for (T item : list) {
|
||||
result.append(item);
|
||||
|
||||
if (++i < list.size())
|
||||
result.append(delimiter);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String join(long[] list, String delimeter) {
|
||||
List<Long> boxed = new ArrayList<>(list.length);
|
||||
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
boxed.add(list[i]);
|
||||
}
|
||||
|
||||
return join(boxed, delimeter);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static @NonNull <E> List<E> join(@NonNull List<E>... lists) {
|
||||
int totalSize = Stream.of(lists).reduce(0, (sum, list) -> sum + list.size());
|
||||
List<E> joined = new ArrayList<>(totalSize);
|
||||
|
||||
for (List<E> list : lists) {
|
||||
joined.addAll(list);
|
||||
}
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
public static String join(List<Long> list, String delimeter) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int j = 0; j < list.size(); j++) {
|
||||
if (j != 0) sb.append(delimeter);
|
||||
sb.append(list.get(j));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String rightPad(String value, int length) {
|
||||
if (value.length() >= length) {
|
||||
return value;
|
||||
}
|
||||
|
||||
StringBuilder out = new StringBuilder(value);
|
||||
while (out.length() < length) {
|
||||
out.append(" ");
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public static boolean isEmpty(EncodedStringValue[] value) {
|
||||
return value == null || value.length == 0;
|
||||
}
|
||||
|
||||
public static boolean isEmpty(ComposeText value) {
|
||||
return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed());
|
||||
}
|
||||
|
||||
public static boolean isEmpty(Collection<?> collection) {
|
||||
return collection == null || collection.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isEmpty(@Nullable CharSequence charSequence) {
|
||||
return charSequence == null || charSequence.length() == 0;
|
||||
}
|
||||
|
||||
public static boolean hasItems(@Nullable Collection<?> collection) {
|
||||
return collection != null && !collection.isEmpty();
|
||||
}
|
||||
|
||||
public static <K, V> V getOrDefault(@NonNull Map<K, V> map, K key, V defaultValue) {
|
||||
return map.containsKey(key) ? map.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
public static String getFirstNonEmpty(String... values) {
|
||||
for (String value : values) {
|
||||
if (!Util.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static @NonNull String emptyIfNull(@Nullable String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence emptyIfNull(@Nullable CharSequence value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static CharSequence getBoldedString(String value) {
|
||||
SpannableString spanned = new SpannableString(value);
|
||||
spanned.setSpan(new StyleSpan(Typeface.BOLD), 0,
|
||||
spanned.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return spanned;
|
||||
}
|
||||
|
||||
public static @NonNull String toIsoString(byte[] bytes) {
|
||||
try {
|
||||
return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("ISO_8859_1 must be supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] toIsoBytes(String isoString) {
|
||||
try {
|
||||
return isoString.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("ISO_8859_1 must be supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] toUtf8Bytes(String utf8String) {
|
||||
try {
|
||||
return utf8String.getBytes(CharacterSets.MIMENAME_UTF_8);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError("UTF_8 must be supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void wait(Object lock, long timeout) {
|
||||
try {
|
||||
lock.wait(timeout);
|
||||
} catch (InterruptedException ie) {
|
||||
throw new AssertionError(ie);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> split(String source, String delimiter) {
|
||||
List<String> results = new LinkedList<>();
|
||||
|
||||
if (TextUtils.isEmpty(source)) {
|
||||
return results;
|
||||
}
|
||||
|
||||
String[] elements = source.split(delimiter);
|
||||
Collections.addAll(results, elements);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
|
||||
byte[][] parts = new byte[2][];
|
||||
|
||||
parts[0] = new byte[firstLength];
|
||||
System.arraycopy(input, 0, parts[0], 0, firstLength);
|
||||
|
||||
parts[1] = new byte[secondLength];
|
||||
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public static byte[] combine(byte[]... elements) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
for (byte[] element : elements) {
|
||||
baos.write(element);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] trim(byte[] input, int length) {
|
||||
byte[] result = new byte[length];
|
||||
System.arraycopy(input, 0, result, 0, result.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] getSecretBytes(int size) {
|
||||
return getSecretBytes(new SecureRandom(), size);
|
||||
}
|
||||
|
||||
public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size) {
|
||||
byte[] secret = new byte[size];
|
||||
secureRandom.nextBytes(secret);
|
||||
return secret;
|
||||
}
|
||||
|
||||
public static <T> T getRandomElement(T[] elements) {
|
||||
return elements[new SecureRandom().nextInt(elements.length)];
|
||||
}
|
||||
|
||||
public static <T> T getRandomElement(List<T> elements) {
|
||||
return elements.get(new SecureRandom().nextInt(elements.size()));
|
||||
}
|
||||
|
||||
public static boolean equals(@Nullable Object a, @Nullable Object b) {
|
||||
return a == b || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
public static int hashCode(@Nullable Object... objects) {
|
||||
return Arrays.hashCode(objects);
|
||||
}
|
||||
|
||||
public static @Nullable Uri uri(@Nullable String uri) {
|
||||
if (uri == null) return null;
|
||||
else return Uri.parse(uri);
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
public static boolean isLowMemory(Context context) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
|
||||
return (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice()) ||
|
||||
activityManager.getLargeMemoryClass() <= 64;
|
||||
}
|
||||
|
||||
public static int clamp(int value, int min, int max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static long clamp(long value, long min, long max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static float clamp(float value, float min, float max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns half of the difference between the given length, and the length when scaled by the
|
||||
* given scale.
|
||||
*/
|
||||
public static float halfOffsetFromScale(int length, float scale) {
|
||||
float scaledLength = length * scale;
|
||||
return (length - scaledLength) / 2;
|
||||
}
|
||||
|
||||
public static @Nullable String readTextFromClipboard(@NonNull Context context) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
if (clipboardManager.hasPrimaryClip() && clipboardManager.getPrimaryClip().getItemCount() > 0) {
|
||||
return clipboardManager.getPrimaryClip().getItemAt(0).getText().toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) {
|
||||
writeTextToClipboard(context, context.getString(R.string.app_name), text);
|
||||
}
|
||||
|
||||
public static void writeTextToClipboard(@NonNull Context context, @NonNull String label, @NonNull String text) {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(label, text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
|
||||
public static int toIntExact(long value) {
|
||||
if ((int)value != value) {
|
||||
throw new ArithmeticException("integer overflow");
|
||||
}
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
public static boolean isEquals(@Nullable Long first, long second) {
|
||||
return first != null && first == second;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> List<T> concatenatedList(Collection <T>... items) {
|
||||
final List<T> concat = new ArrayList<>(Stream.of(items).reduce(0, (sum, list) -> sum + list.size()));
|
||||
|
||||
for (Collection<T> list : items) {
|
||||
concat.addAll(list);
|
||||
}
|
||||
|
||||
return concat;
|
||||
}
|
||||
|
||||
public static boolean isLong(String value) {
|
||||
try {
|
||||
Long.parseLong(value);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseInt(String integer, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(integer);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.ActivityManager
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import android.view.View
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.mms.pdu_alt.CharacterSets
|
||||
import com.google.android.mms.pdu_alt.EncodedStringValue
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.SecureRandom
|
||||
import java.util.Arrays
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.components.ComposeText
|
||||
|
||||
object Util {
|
||||
private val TAG: String = Log.tag(Util::class.java)
|
||||
|
||||
private val BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90)
|
||||
|
||||
fun <T> asList(vararg elements: T): List<T> {
|
||||
val result = mutableListOf<T>() // LinkedList()
|
||||
Collections.addAll(result, *elements)
|
||||
return result
|
||||
}
|
||||
|
||||
fun join(list: Array<String?>, delimiter: String?): String {
|
||||
return join(listOf(*list), delimiter)
|
||||
}
|
||||
|
||||
fun <T> join(list: Collection<T>, delimiter: String?): String {
|
||||
val result = StringBuilder()
|
||||
var i = 0
|
||||
|
||||
for (item in list) {
|
||||
result.append(item)
|
||||
if (++i < list.size) result.append(delimiter)
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun join(list: LongArray, delimeter: String?): String {
|
||||
val boxed: MutableList<Long> = ArrayList(list.size)
|
||||
|
||||
for (i in list.indices) {
|
||||
boxed.add(list[i])
|
||||
}
|
||||
|
||||
return join(boxed, delimeter)
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
fun <E> join(vararg lists: List<E>): List<E> {
|
||||
val totalSize = Stream.of(*lists).reduce(0) { sum: Int, list: List<E> -> sum + list.size }
|
||||
val joined: MutableList<E> = ArrayList(totalSize)
|
||||
|
||||
for (list in lists) {
|
||||
joined.addAll(list)
|
||||
}
|
||||
|
||||
return joined
|
||||
}
|
||||
|
||||
fun join(list: List<Long>, delimeter: String?): String {
|
||||
val sb = StringBuilder()
|
||||
|
||||
for (j in list.indices) {
|
||||
if (j != 0) sb.append(delimeter)
|
||||
sb.append(list[j])
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun rightPad(value: String, length: Int): String {
|
||||
if (value.length >= length) {
|
||||
return value
|
||||
}
|
||||
|
||||
val out = StringBuilder(value)
|
||||
while (out.length < length) {
|
||||
out.append(" ")
|
||||
}
|
||||
|
||||
return out.toString()
|
||||
}
|
||||
|
||||
fun isEmpty(value: Array<EncodedStringValue?>?): Boolean {
|
||||
return value == null || value.size == 0
|
||||
}
|
||||
|
||||
fun isEmpty(value: ComposeText?): Boolean {
|
||||
return value == null || value.text == null || TextUtils.isEmpty(value.textTrimmed)
|
||||
}
|
||||
|
||||
fun isEmpty(collection: Collection<*>?): Boolean {
|
||||
return collection == null || collection.isEmpty()
|
||||
}
|
||||
|
||||
fun isEmpty(charSequence: CharSequence?): Boolean {
|
||||
return charSequence == null || charSequence.length == 0
|
||||
}
|
||||
|
||||
fun hasItems(collection: Collection<*>?): Boolean {
|
||||
return collection != null && !collection.isEmpty()
|
||||
}
|
||||
|
||||
fun <K, V> getOrDefault(map: Map<K, V>, key: K, defaultValue: V): V? {
|
||||
return if (map.containsKey(key)) map[key] else defaultValue
|
||||
}
|
||||
|
||||
fun getFirstNonEmpty(vararg values: String?): String {
|
||||
for (value in values) {
|
||||
if (!value.isNullOrEmpty()) { return value }
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun emptyIfNull(value: String?): String {
|
||||
return value ?: ""
|
||||
}
|
||||
|
||||
fun emptyIfNull(value: CharSequence?): CharSequence {
|
||||
return value ?: ""
|
||||
}
|
||||
|
||||
fun getBoldedString(value: String?): CharSequence {
|
||||
val spanned = SpannableString(value)
|
||||
spanned.setSpan(
|
||||
StyleSpan(Typeface.BOLD), 0,
|
||||
spanned.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
|
||||
return spanned
|
||||
}
|
||||
|
||||
fun toIsoString(bytes: ByteArray?): String {
|
||||
try {
|
||||
return String(bytes!!, charset(CharacterSets.MIMENAME_ISO_8859_1))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw AssertionError("ISO_8859_1 must be supported!")
|
||||
}
|
||||
}
|
||||
|
||||
fun toIsoBytes(isoString: String): ByteArray {
|
||||
try {
|
||||
return isoString.toByteArray(charset(CharacterSets.MIMENAME_ISO_8859_1))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw AssertionError("ISO_8859_1 must be supported!")
|
||||
}
|
||||
}
|
||||
|
||||
fun toUtf8Bytes(utf8String: String): ByteArray {
|
||||
try {
|
||||
return utf8String.toByteArray(charset(CharacterSets.MIMENAME_UTF_8))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
throw AssertionError("UTF_8 must be supported!")
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(lock: Any, timeout: Long) {
|
||||
try {
|
||||
(lock as Object).wait(timeout)
|
||||
} catch (ie: InterruptedException) {
|
||||
throw AssertionError(ie)
|
||||
}
|
||||
}
|
||||
|
||||
fun split(source: String, delimiter: String): List<String> {
|
||||
val results = mutableListOf<String>()
|
||||
|
||||
if (TextUtils.isEmpty(source)) {
|
||||
return results
|
||||
}
|
||||
|
||||
val elements =
|
||||
source.split(delimiter.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
Collections.addAll(results, *elements)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
fun split(input: ByteArray?, firstLength: Int, secondLength: Int): Array<ByteArray?> {
|
||||
val parts = arrayOfNulls<ByteArray>(2)
|
||||
|
||||
parts[0] = ByteArray(firstLength)
|
||||
System.arraycopy(input, 0, parts[0], 0, firstLength)
|
||||
|
||||
parts[1] = ByteArray(secondLength)
|
||||
System.arraycopy(input, firstLength, parts[1], 0, secondLength)
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
fun combine(vararg elements: ByteArray?): ByteArray {
|
||||
try {
|
||||
val baos = ByteArrayOutputStream()
|
||||
|
||||
for (element in elements) {
|
||||
baos.write(element)
|
||||
}
|
||||
|
||||
return baos.toByteArray()
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun trim(input: ByteArray?, length: Int): ByteArray {
|
||||
val result = ByteArray(length)
|
||||
System.arraycopy(input, 0, result, 0, result.size)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun getSecretBytes(size: Int): ByteArray {
|
||||
return getSecretBytes(SecureRandom(), size)
|
||||
}
|
||||
|
||||
fun getSecretBytes(secureRandom: SecureRandom, size: Int): ByteArray {
|
||||
val secret = ByteArray(size)
|
||||
secureRandom.nextBytes(secret)
|
||||
return secret
|
||||
}
|
||||
|
||||
fun <T> getRandomElement(elements: Array<T>): T {
|
||||
return elements[SecureRandom().nextInt(elements.size)]
|
||||
}
|
||||
|
||||
fun <T> getRandomElement(elements: List<T>): T {
|
||||
return elements[SecureRandom().nextInt(elements.size)]
|
||||
}
|
||||
|
||||
fun equals(a: Any?, b: Any?): Boolean {
|
||||
return a === b || (a != null && a == b)
|
||||
}
|
||||
|
||||
fun hashCode(vararg objects: Any?): Int {
|
||||
return objects.contentHashCode()
|
||||
}
|
||||
|
||||
fun uri(uri: String?): Uri? {
|
||||
return if (uri == null) null
|
||||
else Uri.parse(uri)
|
||||
}
|
||||
|
||||
@TargetApi(VERSION_CODES.KITKAT)
|
||||
fun isLowMemory(context: Context): Boolean {
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
|
||||
return (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice) ||
|
||||
activityManager.largeMemoryClass <= 64
|
||||
}
|
||||
|
||||
fun clamp(value: Int, min: Int, max: Int): Int {
|
||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toInt()
|
||||
}
|
||||
|
||||
fun clamp(value: Long, min: Long, max: Long): Long {
|
||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toLong()
|
||||
}
|
||||
|
||||
fun clamp(value: Float, min: Float, max: Float): Float {
|
||||
return min(max(value.toDouble(), min.toDouble()), max.toDouble()).toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns half of the difference between the given length, and the length when scaled by the
|
||||
* given scale.
|
||||
*/
|
||||
fun halfOffsetFromScale(length: Int, scale: Float): Float {
|
||||
val scaledLength = length * scale
|
||||
return (length - scaledLength) / 2
|
||||
}
|
||||
|
||||
fun readTextFromClipboard(context: Context): String? {
|
||||
run {
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
return if (clipboardManager.hasPrimaryClip() && clipboardManager.primaryClip!!.itemCount > 0) {
|
||||
clipboardManager.primaryClip!!.getItemAt(0).text.toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writeTextToClipboard(context: Context, text: String) {
|
||||
writeTextToClipboard(context, context.getString(R.string.app_name), text)
|
||||
}
|
||||
|
||||
fun writeTextToClipboard(context: Context, label: String, text: String) {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(label, text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
fun toIntExact(value: Long): Int {
|
||||
if (value.toInt().toLong() != value) {
|
||||
throw ArithmeticException("integer overflow")
|
||||
}
|
||||
return value.toInt()
|
||||
}
|
||||
|
||||
fun isEquals(first: Long?, second: Long): Boolean {
|
||||
return first != null && first == second
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
fun <T> concatenatedList(vararg items: Collection<T>): List<T> {
|
||||
val concat: MutableList<T> = ArrayList(
|
||||
Stream.of(*items).reduce(0) { sum: Int, list: Collection<T> -> sum + list.size })
|
||||
|
||||
for (list in items) {
|
||||
concat.addAll(list)
|
||||
}
|
||||
|
||||
return concat
|
||||
}
|
||||
|
||||
fun isLong(value: String): Boolean {
|
||||
try {
|
||||
value.toLong()
|
||||
return true
|
||||
} catch (e: NumberFormatException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun parseInt(integer: String, defaultValue: Int): Int {
|
||||
return try {
|
||||
integer.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Method to determine if we're currently in a left-to-right or right-to-left language like Arabic
|
||||
fun usingRightToLeftLanguage(context: Context): Boolean {
|
||||
val config = context.resources.configuration
|
||||
return config.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
}
|
||||
|
||||
// Method to determine if we're currently in a left-to-right or right-to-left language like Arabic
|
||||
fun usingLeftToRightLanguage(context: Context): Boolean {
|
||||
val config = context.resources.configuration
|
||||
return config.layoutDirection == View.LAYOUT_DIRECTION_LTR
|
||||
}
|
||||
}
|
@ -1,209 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import static org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.squareup.phrase.Phrase;
|
||||
import java.security.SecureRandom;
|
||||
import network.loki.messenger.R;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment;
|
||||
import org.session.libsession.utilities.Util;
|
||||
import org.session.libsignal.utilities.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public abstract class Slide {
|
||||
|
||||
protected final Attachment attachment;
|
||||
protected final Context context;
|
||||
|
||||
public Slide(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
this.context = context;
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return attachment.getContentType();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return attachment.getDataUri();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getThumbnailUri() {
|
||||
return attachment.getThumbnailUri();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getBody() {
|
||||
String attachmentString = context.getString(R.string.attachment);
|
||||
|
||||
if (MediaUtil.isAudio(attachment)) {
|
||||
// A missing file name is the legacy way to determine if an audio attachment is
|
||||
// a voice note vs. other arbitrary audio attachments.
|
||||
if (attachment.isVoiceNote() || attachment.getFileName() == null ||
|
||||
attachment.getFileName().isEmpty()) {
|
||||
attachmentString = context.getString(R.string.attachment_type_voice_message);
|
||||
return Optional.fromNullable("🎤 " + attachmentString);
|
||||
}
|
||||
}
|
||||
String txt = Phrase.from(context, R.string.attachmentsNotification)
|
||||
.put(EMOJI_KEY, emojiForMimeType())
|
||||
.format().toString();
|
||||
return Optional.fromNullable(txt);
|
||||
}
|
||||
|
||||
private String emojiForMimeType() {
|
||||
if (MediaUtil.isImage(attachment)) {
|
||||
return "📷";
|
||||
} else if (MediaUtil.isVideo(attachment)) {
|
||||
return "🎥";
|
||||
} else if (MediaUtil.isAudio(attachment)) {
|
||||
return "🎧";
|
||||
} else if (MediaUtil.isFile(attachment)) {
|
||||
return "📎";
|
||||
} else {
|
||||
return "🎡"; // `isGif`
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getCaption() {
|
||||
return Optional.fromNullable(attachment.getCaption());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Optional<String> getFileName() {
|
||||
return Optional.fromNullable(attachment.getFileName());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFastPreflightId() {
|
||||
return attachment.getFastPreflightId();
|
||||
}
|
||||
|
||||
public long getFileSize() {
|
||||
return attachment.getSize();
|
||||
}
|
||||
|
||||
public boolean hasImage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasVideo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasAudio() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasDocument() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull String getContentDescription() { return ""; }
|
||||
|
||||
public @NonNull Attachment asAttachment() {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
public boolean isInProgress() {
|
||||
return attachment.isInProgress();
|
||||
}
|
||||
|
||||
public boolean isPendingDownload() {
|
||||
return getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED ||
|
||||
getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING;
|
||||
}
|
||||
|
||||
public int getTransferState() {
|
||||
return attachment.getTransferState();
|
||||
}
|
||||
|
||||
public @DrawableRes int getPlaceholderRes(Theme theme) {
|
||||
throw new AssertionError("getPlaceholderRes() called for non-drawable slide");
|
||||
}
|
||||
|
||||
public boolean hasPlaceholder() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasPlayOverlay() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
@Nullable String caption,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
{
|
||||
String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime);
|
||||
String fastPreflightId = String.valueOf(new SecureRandom().nextLong());
|
||||
return new UriAttachment(uri,
|
||||
hasThumbnail ? uri : null,
|
||||
resolvedType,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
fileName,
|
||||
fastPreflightId,
|
||||
voiceNote,
|
||||
quote,
|
||||
caption);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
if (!(other instanceof Slide)) return false;
|
||||
|
||||
Slide that = (Slide)other;
|
||||
|
||||
return Util.equals(this.getContentType(), that.getContentType()) &&
|
||||
this.hasAudio() == that.hasAudio() &&
|
||||
this.hasImage() == that.hasImage() &&
|
||||
this.hasVideo() == that.hasVideo() &&
|
||||
this.getTransferState() == that.getTransferState() &&
|
||||
Util.equals(this.getUri(), that.getUri()) &&
|
||||
Util.equals(this.getThumbnailUri(), that.getThumbnailUri());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Util.hashCode(getContentType(), hasAudio(), hasImage(),
|
||||
hasVideo(), getUri(), getThumbnailUri(), getTransferState());
|
||||
}
|
||||
}
|
180
app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt
Normal file
180
app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt
Normal file
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http:></http:>//www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mms
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.squareup.phrase.Phrase
|
||||
import java.security.SecureRandom
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.UriAttachment
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
||||
import org.session.libsession.utilities.Util.equals
|
||||
import org.session.libsession.utilities.Util.hashCode
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import org.thoughtcrime.securesms.conversation.v2.Util
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
abstract class Slide(@JvmField protected val context: Context, protected val attachment: Attachment) {
|
||||
val contentType: String
|
||||
get() = attachment.contentType
|
||||
|
||||
val uri: Uri?
|
||||
get() = attachment.dataUri
|
||||
|
||||
open val thumbnailUri: Uri?
|
||||
get() = attachment.thumbnailUri
|
||||
|
||||
val body: Optional<String>
|
||||
get() {
|
||||
if (MediaUtil.isAudio(attachment)) {
|
||||
// A missing file name is the legacy way to determine if an audio attachment is
|
||||
// a voice note vs. other arbitrary audio attachments.
|
||||
if (attachment.isVoiceNote || attachment.fileName.isNullOrEmpty()) {
|
||||
val baseString = context.getString(R.string.attachment_type_voice_message)
|
||||
val languageIsLTR = Util.usingLeftToRightLanguage(context)
|
||||
val attachmentString = if (languageIsLTR) {
|
||||
"🎙 $baseString"
|
||||
} else {
|
||||
"$baseString 🎙"
|
||||
}
|
||||
return Optional.fromNullable(attachmentString)
|
||||
}
|
||||
}
|
||||
val txt = Phrase.from(context, R.string.attachmentsNotification)
|
||||
.put(EMOJI_KEY, emojiForMimeType())
|
||||
.format().toString()
|
||||
return Optional.fromNullable(txt)
|
||||
}
|
||||
|
||||
private fun emojiForMimeType(): String {
|
||||
return if (MediaUtil.isGif(attachment)) {
|
||||
"🎡"
|
||||
} else if (MediaUtil.isImage(attachment)) {
|
||||
"📷"
|
||||
} else if (MediaUtil.isVideo(attachment)) {
|
||||
"🎥"
|
||||
} else if (MediaUtil.isAudio(attachment)) {
|
||||
"🎧"
|
||||
} else if (MediaUtil.isFile(attachment)) {
|
||||
"📎"
|
||||
} else {
|
||||
// We don't provide emojis for other mime-types such as VCARD
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
val caption: Optional<String?>
|
||||
get() = Optional.fromNullable(attachment.caption)
|
||||
|
||||
val fileName: Optional<String?>
|
||||
get() = Optional.fromNullable(attachment.fileName)
|
||||
|
||||
val fastPreflightId: String?
|
||||
get() = attachment.fastPreflightId
|
||||
|
||||
val fileSize: Long
|
||||
get() = attachment.size
|
||||
|
||||
open fun hasImage(): Boolean { return false }
|
||||
|
||||
open fun hasVideo(): Boolean { return false }
|
||||
|
||||
open fun hasAudio(): Boolean { return false }
|
||||
|
||||
open fun hasDocument(): Boolean { return false }
|
||||
|
||||
open val contentDescription: String
|
||||
get() = ""
|
||||
|
||||
fun asAttachment(): Attachment { return attachment }
|
||||
|
||||
val isInProgress: Boolean
|
||||
get() = attachment.isInProgress
|
||||
|
||||
val isPendingDownload: Boolean
|
||||
get() = transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED ||
|
||||
transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_PENDING
|
||||
|
||||
val transferState: Int
|
||||
get() = attachment.transferState
|
||||
|
||||
@DrawableRes
|
||||
open fun getPlaceholderRes(theme: Resources.Theme?): Int {
|
||||
throw AssertionError("getPlaceholderRes() called for non-drawable slide")
|
||||
}
|
||||
|
||||
open fun hasPlaceholder(): Boolean { return false }
|
||||
|
||||
open fun hasPlayOverlay(): Boolean { return false }
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null) return false
|
||||
if (other !is Slide) return false
|
||||
|
||||
return (equals(this.contentType, other.contentType) &&
|
||||
hasAudio() == other.hasAudio() &&
|
||||
hasImage() == other.hasImage() &&
|
||||
hasVideo() == other.hasVideo()) &&
|
||||
this.transferState == other.transferState &&
|
||||
equals(this.uri, other.uri) &&
|
||||
equals(this.thumbnailUri, other.thumbnailUri)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return hashCode(contentType, hasAudio(), hasImage(), hasVideo(), uri, thumbnailUri, transferState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
protected fun constructAttachmentFromUri(
|
||||
context: Context,
|
||||
uri: Uri,
|
||||
defaultMime: String,
|
||||
size: Long,
|
||||
width: Int,
|
||||
height: Int,
|
||||
hasThumbnail: Boolean,
|
||||
fileName: String?,
|
||||
caption: String?,
|
||||
voiceNote: Boolean,
|
||||
quote: Boolean
|
||||
): Attachment {
|
||||
val resolvedType =
|
||||
Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime)
|
||||
val fastPreflightId = SecureRandom().nextLong().toString()
|
||||
return UriAttachment(
|
||||
uri,
|
||||
if (hasThumbnail) uri else null,
|
||||
resolvedType!!,
|
||||
AttachmentTransferProgress.TRANSFER_PROGRESS_STARTED,
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
fileName,
|
||||
fastPreflightId,
|
||||
voiceNote,
|
||||
quote,
|
||||
caption
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user