aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/eu/siacs/conversations/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu/siacs/conversations/utils')
-rw-r--r--src/main/java/eu/siacs/conversations/utils/CryptoHelper.java129
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java179
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java46
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java117
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExifHelper.java161
-rw-r--r--src/main/java/eu/siacs/conversations/utils/GeoHelper.java77
-rw-r--r--src/main/java/eu/siacs/conversations/utils/MimeUtils.java487
-rw-r--r--src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PRNGFixes.java327
-rw-r--r--src/main/java/eu/siacs/conversations/utils/PhoneHelper.java97
-rw-r--r--src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java34
-rw-r--r--src/main/java/eu/siacs/conversations/utils/UIHelper.java308
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmlHelper.java12
-rw-r--r--src/main/java/eu/siacs/conversations/utils/Xmlns.java9
-rw-r--r--src/main/java/eu/siacs/conversations/utils/XmppUri.java85
15 files changed, 2077 insertions, 0 deletions
diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
new file mode 100644
index 00000000..2dec203d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java
@@ -0,0 +1,129 @@
+package eu.siacs.conversations.utils;
+
+import java.security.SecureRandom;
+import java.text.Normalizer;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+
+public final class CryptoHelper {
+ public static final String FILETRANSFER = "?FILETRANSFERv1:";
+ private final static char[] hexArray = "0123456789abcdef".toCharArray();
+ private final static char[] vowels = "aeiou".toCharArray();
+ private final static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray();
+ final public static byte[] ONE = new byte[] { 0, 0, 0, 1 };
+
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ public static byte[] hexToBytes(String hexString) {
+ int len = hexString.length();
+ byte[] array = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
+ .digit(hexString.charAt(i + 1), 16));
+ }
+ return array;
+ }
+
+ public static String hexToString(final String hexString) {
+ return new String(hexToBytes(hexString));
+ }
+
+ public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
+ byte[] result = new byte[a.length + b.length];
+ System.arraycopy(a, 0, result, 0, a.length);
+ System.arraycopy(b, 0, result, a.length, b.length);
+ return result;
+ }
+
+ public static String randomMucName(SecureRandom random) {
+ return randomWord(3, random) + "." + randomWord(7, random);
+ }
+
+ private static String randomWord(int lenght, SecureRandom random) {
+ StringBuilder builder = new StringBuilder(lenght);
+ for (int i = 0; i < lenght; ++i) {
+ if (i % 2 == 0) {
+ builder.append(consonants[random.nextInt(consonants.length)]);
+ } else {
+ builder.append(vowels[random.nextInt(vowels.length)]);
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Escapes usernames or passwords for SASL.
+ */
+ public static String saslEscape(final String s) {
+ final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1));
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case ',':
+ sb.append("=2C");
+ break;
+ case '=':
+ sb.append("=3D");
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String saslPrep(final String s) {
+ return Normalizer.normalize(s, Normalizer.Form.NFKC);
+ }
+
+ public static String prettifyFingerprint(String fingerprint) {
+ if (fingerprint==null) {
+ return "";
+ } else if (fingerprint.length() < 40) {
+ return fingerprint;
+ }
+ StringBuilder builder = new StringBuilder(fingerprint);
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ return builder.toString();
+ }
+
+ public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) {
+ final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS));
+ final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites);
+ cipherSuites.retainAll(platformCiphers);
+ cipherSuites.addAll(platformCiphers);
+ filterWeakCipherSuites(cipherSuites);
+ return cipherSuites.toArray(new String[cipherSuites.size()]);
+ }
+
+ private static void filterWeakCipherSuites(final Collection<String> cipherSuites) {
+ final Iterator<String> it = cipherSuites.iterator();
+ while (it.hasNext()) {
+ String cipherName = it.next();
+ // remove all ciphers with no or very weak encryption or no authentication
+ for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) {
+ if (cipherName.contains(weakCipherPattern)) {
+ it.remove();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
new file mode 100644
index 00000000..d5271903
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,179 @@
+package eu.siacs.conversations.utils;
+
+import de.measite.minidns.Client;
+import de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.Record.TYPE;
+import de.measite.minidns.Record.CLASS;
+import de.measite.minidns.record.SRV;
+import de.measite.minidns.record.A;
+import de.measite.minidns.record.AAAA;
+import de.measite.minidns.record.Data;
+import de.measite.minidns.util.NameUtil;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Random;
+import java.util.TreeMap;
+import java.util.regex.Pattern;
+
+import android.os.Bundle;
+import de.thedevstack.conversationsplus.utils.Logging;
+
+public class DNSHelper {
+
+ public static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
+ public static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
+ public static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
+ public static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
+ public static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
+
+ protected static Client client = new Client();
+
+ public static Bundle getSRVRecord(final Jid jid) throws IOException {
+ final String host = jid.getDomainpart();
+ String dns[] = client.findDNS();
+
+ if (dns != null) {
+ for (String dnsserver : dns) {
+ InetAddress ip = InetAddress.getByName(dnsserver);
+ Bundle b = queryDNS(host, ip);
+ if (b.containsKey("values")) {
+ return b;
+ }
+ }
+ }
+ return queryDNS(host, InetAddress.getByName("8.8.8.8"));
+ }
+
+ public static Bundle queryDNS(String host, InetAddress dnsServer) {
+ Bundle bundle = new Bundle();
+ try {
+ String qname = "_xmpp-client._tcp." + host;
+ Logging.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host);
+ DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress());
+
+ TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<>();
+ TreeMap<String, ArrayList<String>> ips4 = new TreeMap<>();
+ TreeMap<String, ArrayList<String>> ips6 = new TreeMap<>();
+
+ for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) {
+ for (Record rr : rrset) {
+ Data d = rr.getPayload();
+ if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
+ SRV srv = (SRV) d;
+ if (!priorities.containsKey(srv.getPriority())) {
+ priorities.put(srv.getPriority(),new ArrayList<SRV>());
+ }
+ priorities.get(srv.getPriority()).add(srv);
+ }
+ if (d instanceof A) {
+ A a = (A) d;
+ if (!ips4.containsKey(rr.getName())) {
+ ips4.put(rr.getName(), new ArrayList<String>());
+ }
+ ips4.get(rr.getName()).add(a.toString());
+ }
+ if (d instanceof AAAA) {
+ AAAA aaaa = (AAAA) d;
+ if (!ips6.containsKey(rr.getName())) {
+ ips6.put(rr.getName(), new ArrayList<String>());
+ }
+ ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
+ }
+ }
+ }
+
+ ArrayList<SRV> result = new ArrayList<>();
+ for (ArrayList<SRV> s : priorities.values()) {
+ result.addAll(s);
+ }
+
+ ArrayList<Bundle> values = new ArrayList<>();
+ if (result.size() == 0) {
+ DNSMessage response;
+ response = client.query(host, TYPE.A, CLASS.IN, dnsServer.getHostAddress());
+ for(int i = 0; i < response.getAnswers().length; ++i) {
+ values.add(createNamePortBundle(host,5222,response.getAnswers()[i].getPayload()));
+ }
+ response = client.query(host, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
+ for(int i = 0; i < response.getAnswers().length; ++i) {
+ values.add(createNamePortBundle(host,5222,response.getAnswers()[i].getPayload()));
+ }
+ values.add(createNamePortBundle(host,5222));
+ bundle.putParcelableArrayList("values", values);
+ return bundle;
+ }
+ for (SRV srv : result) {
+ if (ips6.containsKey(srv.getName())) {
+ values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6));
+ } else {
+ DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
+ for(int i = 0; i < response.getAnswers().length; ++i) {
+ values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload()));
+ }
+ }
+ if (ips4.containsKey(srv.getName())) {
+ values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4));
+ } else {
+ DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress());
+ for(int i = 0; i < response.getAnswers().length; ++i) {
+ values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload()));
+ }
+ }
+ values.add(createNamePortBundle(srv.getName(), srv.getPort()));
+ }
+ bundle.putParcelableArrayList("values", values);
+ } catch (SocketTimeoutException e) {
+ bundle.putString("error", "timeout");
+ } catch (Exception e) {
+ e.printStackTrace();
+ bundle.putString("error", "unhandled");
+ }
+ return bundle;
+ }
+
+ private static Bundle createNamePortBundle(String name, int port) {
+ Bundle namePort = new Bundle();
+ namePort.putString("name", name);
+ namePort.putInt("port", port);
+ return namePort;
+ }
+
+ private static Bundle createNamePortBundle(String name, int port, TreeMap<String, ArrayList<String>> ips) {
+ Bundle namePort = new Bundle();
+ namePort.putString("name", name);
+ namePort.putInt("port", port);
+ if (ips!=null) {
+ ArrayList<String> ip = ips.get(name);
+ Collections.shuffle(ip, new Random());
+ namePort.putString("ip", ip.get(0));
+ }
+ return namePort;
+ }
+
+ private static Bundle createNamePortBundle(String name, int port, Data data) {
+ Bundle namePort = new Bundle();
+ namePort.putString("name", name);
+ namePort.putInt("port", port);
+ if (data instanceof A) {
+ namePort.putString("ip", data.toString());
+ } else if (data instanceof AAAA) {
+ namePort.putString("ip","["+data.toString()+"]");
+ }
+ return namePort;
+ }
+
+ public static boolean isIp(final String server) {
+ return PATTERN_IPV4.matcher(server).matches()
+ || PATTERN_IPV6.matcher(server).matches()
+ || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
+ || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
+ || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
new file mode 100644
index 00000000..0ad57fe2
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java
@@ -0,0 +1,46 @@
+package eu.siacs.conversations.utils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+
+import android.content.Context;
+
+public class ExceptionHandler implements UncaughtExceptionHandler {
+
+ private UncaughtExceptionHandler defaultHandler;
+ private Context context;
+
+ public ExceptionHandler(Context context) {
+ this.context = context;
+ this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ Writer result = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(result);
+ ex.printStackTrace(printWriter);
+ String stacktrace = result.toString();
+ printWriter.close();
+ try {
+ OutputStream os = context.openFileOutput("stacktrace.txt",
+ Context.MODE_PRIVATE);
+ os.write(stacktrace.getBytes());
+ os.flush();
+ os.close();
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ this.defaultHandler.uncaughtException(thread, ex);
+ }
+
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
new file mode 100644
index 00000000..20b374c8
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java
@@ -0,0 +1,117 @@
+package eu.siacs.conversations.utils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.DialogInterface.OnClickListener;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.preference.PreferenceManager;
+import android.text.format.DateUtils;
+import de.thedevstack.conversationsplus.utils.Logging;
+
+public class ExceptionHelper {
+ public static void init(Context context) {
+ if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(
+ context));
+ }
+ }
+
+ public static void checkForCrash(Context context,
+ final XmppConnectionService service) {
+ try {
+ boolean neverSend = ConversationsPlusPreferences.neverSend();
+ if (neverSend) {
+ return;
+ }
+ List<Account> accounts = service.getAccounts();
+ Account account = null;
+ for (int i = 0; i < accounts.size(); ++i) {
+ if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) {
+ account = accounts.get(i);
+ break;
+ }
+ }
+ if (account == null) {
+ return;
+ }
+ final Account finalAccount = account;
+ FileInputStream file = context.openFileInput("stacktrace.txt");
+ InputStreamReader inputStreamReader = new InputStreamReader(file);
+ BufferedReader stacktrace = new BufferedReader(inputStreamReader);
+ final StringBuilder report = new StringBuilder();
+ PackageManager pm = context.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
+ report.append("Version: " + packageInfo.versionName + '\n');
+ report.append("Last Update: "
+ + DateUtils.formatDateTime(context,
+ packageInfo.lastUpdateTime,
+ DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_SHOW_DATE) + '\n');
+ } catch (NameNotFoundException e) {
+ }
+ String line;
+ while ((line = stacktrace.readLine()) != null) {
+ report.append(line);
+ report.append('\n');
+ }
+ file.close();
+ context.deleteFile("stacktrace.txt");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(context.getString(R.string.crash_report_title));
+ builder.setMessage(context.getText(R.string.crash_report_message));
+ builder.setPositiveButton(context.getText(R.string.send_now),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+
+ Logging.d(Config.LOGTAG, "using account="
+ + finalAccount.getJid().toBareJid()
+ + " to send in stack trace");
+ Conversation conversation = null;
+ try {
+ conversation = service.findOrCreateConversation(finalAccount,
+ Jid.fromString(context.getString(R.string.cplus_bugreport_jabberid)), false);
+ } catch (final InvalidJidException ignored) {
+ }
+ Message message = new Message(conversation, report
+ .toString(), Message.ENCRYPTION_NONE);
+ service.sendMessage(message);
+ }
+ });
+ builder.setNegativeButton(context.getText(R.string.send_never),
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ConversationsPlusPreferences.applyNeverSend(true);
+ }
+ });
+ builder.create().show();
+ } catch (final IOException ignored) {
+ }
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
new file mode 100644
index 00000000..9e987cc5
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package eu.siacs.conversations.utils;
+
+import de.thedevstack.conversationsplus.utils.Logging;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ExifHelper {
+ private static final String TAG = "CameraExif";
+
+ public static int getOrientation(InputStream is) {
+ if (is == null) {
+ return 0;
+ }
+
+ byte[] buf = new byte[8];
+ int length = 0;
+
+ // ISO/IEC 10918-1:1993(E)
+ while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
+ int marker = buf[1] & 0xFF;
+
+ // Check if the marker is a padding.
+ if (marker == 0xFF) {
+ continue;
+ }
+
+ // Check if the marker is SOI or TEM.
+ if (marker == 0xD8 || marker == 0x01) {
+ continue;
+ }
+ // Check if the marker is EOI or SOS.
+ if (marker == 0xD9 || marker == 0xDA) {
+ return 0;
+ }
+
+ // Get the length and check if it is reasonable.
+ if (!read(is, buf, 2)) {
+ return 0;
+ }
+ length = pack(buf, 0, 2, false);
+ if (length < 2) {
+ Logging.e(TAG, "Invalid length");
+ return 0;
+ }
+ length -= 2;
+
+ // Break if the marker is EXIF in APP1.
+ if (marker == 0xE1 && length >= 6) {
+ if (!read(is, buf, 6)) return 0;
+ length -= 6;
+ if (pack(buf, 0, 4, false) == 0x45786966 &&
+ pack(buf, 4, 2, false) == 0) {
+ break;
+ }
+ }
+
+ // Skip other markers.
+ try {
+ is.skip(length);
+ } catch (IOException ex) {
+ return 0;
+ }
+ length = 0;
+ }
+
+ // JEITA CP-3451 Exif Version 2.2
+ if (length > 8) {
+ int offset = 0;
+ byte[] jpeg = new byte[length];
+ if (!read(is, jpeg, length)) {
+ return 0;
+ }
+
+ // Identify the byte order.
+ int tag = pack(jpeg, offset, 4, false);
+ if (tag != 0x49492A00 && tag != 0x4D4D002A) {
+ Logging.e(TAG, "Invalid byte order");
+ return 0;
+ }
+ boolean littleEndian = (tag == 0x49492A00);
+
+ // Get the offset and check if it is reasonable.
+ int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
+ if (count < 10 || count > length) {
+ Logging.e(TAG, "Invalid offset");
+ return 0;
+ }
+ offset += count;
+ length -= count;
+
+ // Get the count and go through all the elements.
+ count = pack(jpeg, offset - 2, 2, littleEndian);
+ while (count-- > 0 && length >= 12) {
+ // Get the tag and check if it is orientation.
+ tag = pack(jpeg, offset, 2, littleEndian);
+ if (tag == 0x0112) {
+ // We do not really care about type and count, do we?
+ int orientation = pack(jpeg, offset + 8, 2, littleEndian);
+ switch (orientation) {
+ case 1:
+ return 0;
+ case 3:
+ return 180;
+ case 6:
+ return 90;
+ case 8:
+ return 270;
+ }
+ Logging.i(TAG, "Unsupported orientation");
+ return 0;
+ }
+ offset += 12;
+ length -= 12;
+ }
+ }
+
+ Logging.i(TAG, "Orientation not found");
+ return 0;
+ }
+
+ private static int pack(byte[] bytes, int offset, int length,
+ boolean littleEndian) {
+ int step = 1;
+ if (littleEndian) {
+ offset += length - 1;
+ step = -1;
+ }
+
+ int value = 0;
+ while (length-- > 0) {
+ value = (value << 8) | (bytes[offset] & 0xFF);
+ offset += step;
+ }
+ return value;
+ }
+
+ private static boolean read(InputStream is, byte[] buf, int length) {
+ try {
+ return is.read(buf, 0, length) == length;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
new file mode 100644
index 00000000..74f91a98
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java
@@ -0,0 +1,77 @@
+package eu.siacs.conversations.utils;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+
+public class GeoHelper {
+ private static Pattern GEO_URI = Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE);
+
+ public static boolean isGeoUri(String body) {
+ return body != null && GEO_URI.matcher(body).matches();
+ }
+
+ public static ArrayList<Intent> createGeoIntentsFromMessage(Message message) {
+ final ArrayList<Intent> intents = new ArrayList<>();
+ Matcher matcher = GEO_URI.matcher(message.getBody());
+ if (!matcher.matches()) {
+ return intents;
+ }
+ double latitude;
+ double longitude;
+ try {
+ latitude = Double.parseDouble(matcher.group(1));
+ if (latitude > 90.0 || latitude < -90.0) {
+ return intents;
+ }
+ longitude = Double.parseDouble(matcher.group(2));
+ if (longitude > 180.0 || longitude < -180.0) {
+ return intents;
+ }
+ } catch (NumberFormatException nfe) {
+ return intents;
+ }
+ final Conversation conversation = message.getConversation();
+ String label;
+ if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) {
+ try {
+ label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")";
+ } catch (UnsupportedEncodingException e) {
+ label = "";
+ }
+ } else {
+ label = "";
+ }
+
+ Intent locationPluginIntent = new Intent("eu.siacs.conversations.location.show");
+ locationPluginIntent.putExtra("latitude",latitude);
+ locationPluginIntent.putExtra("longitude",longitude);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ locationPluginIntent.putExtra("name",conversation.getName());
+ locationPluginIntent.putExtra("jid",message.getCounterpart().toString());
+ }
+ else {
+ locationPluginIntent.putExtra("jid",conversation.getAccount().getJid().toString());
+ }
+ }
+ intents.add(locationPluginIntent);
+
+ Intent geoIntent = new Intent(Intent.ACTION_VIEW);
+ geoIntent.setData(Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude) + "?q=" + String.valueOf(latitude) + "," + String.valueOf(longitude) + label));
+ intents.add(geoIntent);
+
+ Intent httpIntent = new Intent(Intent.ACTION_VIEW);
+ httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+String.valueOf(latitude) + "," + String.valueOf(longitude) +label));
+ intents.add(httpIntent);
+ return intents;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
new file mode 100644
index 00000000..a9e89d1b
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package eu.siacs.conversations.utils;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+/**
+ * Utilities for dealing with MIME types.
+ * Used to implement java.net.URLConnection and android.webkit.MimeTypeMap.
+ */
+public final class MimeUtils {
+ private static final Map<String, String> mimeTypeToExtensionMap = new HashMap<String, String>();
+ private static final Map<String, String> extensionToMimeTypeMap = new HashMap<String, String>();
+ static {
+ // The following table is based on /etc/mime.types data minus
+ // chemical/* MIME types and MIME types that don't map to any
+ // file extensions. We also exclude top-level domain names to
+ // deal with cases like:
+ //
+ // mail.google.com/a/google.com
+ //
+ // and "active" MIME types (due to potential security issues).
+ // Note that this list is _not_ in alphabetical order and must not be sorted.
+ // The "most popular" extension must come first, so that it's the one returned
+ // by guessExtensionFromMimeType.
+ add("application/andrew-inset", "ez");
+ add("application/dsptype", "tsp");
+ add("application/hta", "hta");
+ add("application/mac-binhex40", "hqx");
+ add("application/mathematica", "nb");
+ add("application/msaccess", "mdb");
+ add("application/oda", "oda");
+ add("application/ogg", "ogg");
+ add("application/ogg", "oga");
+ add("application/pdf", "pdf");
+ add("application/pgp-keys", "key");
+ add("application/pgp-signature", "pgp");
+ add("application/pics-rules", "prf");
+ add("application/pkix-cert", "cer");
+ add("application/rar", "rar");
+ add("application/rdf+xml", "rdf");
+ add("application/rss+xml", "rss");
+ add("application/zip", "zip");
+ add("application/vnd.android.package-archive", "apk");
+ add("application/vnd.cinderella", "cdy");
+ add("application/vnd.ms-pki.stl", "stl");
+ add("application/vnd.oasis.opendocument.database", "odb");
+ add("application/vnd.oasis.opendocument.formula", "odf");
+ add("application/vnd.oasis.opendocument.graphics", "odg");
+ add("application/vnd.oasis.opendocument.graphics-template", "otg");
+ add("application/vnd.oasis.opendocument.image", "odi");
+ add("application/vnd.oasis.opendocument.spreadsheet", "ods");
+ add("application/vnd.oasis.opendocument.spreadsheet-template", "ots");
+ add("application/vnd.oasis.opendocument.text", "odt");
+ add("application/vnd.oasis.opendocument.text-master", "odm");
+ add("application/vnd.oasis.opendocument.text-template", "ott");
+ add("application/vnd.oasis.opendocument.text-web", "oth");
+ add("application/vnd.google-earth.kml+xml", "kml");
+ add("application/vnd.google-earth.kmz", "kmz");
+ add("application/msword", "doc");
+ add("application/msword", "dot");
+ add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
+ add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx");
+ add("application/vnd.ms-excel", "xls");
+ add("application/vnd.ms-excel", "xlt");
+ add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
+ add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx");
+ add("application/vnd.ms-powerpoint", "ppt");
+ add("application/vnd.ms-powerpoint", "pot");
+ add("application/vnd.ms-powerpoint", "pps");
+ add("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx");
+ add("application/vnd.openxmlformats-officedocument.presentationml.template", "potx");
+ add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx");
+ add("application/vnd.rim.cod", "cod");
+ add("application/vnd.smaf", "mmf");
+ add("application/vnd.stardivision.calc", "sdc");
+ add("application/vnd.stardivision.draw", "sda");
+ add("application/vnd.stardivision.impress", "sdd");
+ add("application/vnd.stardivision.impress", "sdp");
+ add("application/vnd.stardivision.math", "smf");
+ add("application/vnd.stardivision.writer", "sdw");
+ add("application/vnd.stardivision.writer", "vor");
+ add("application/vnd.stardivision.writer-global", "sgl");
+ add("application/vnd.sun.xml.calc", "sxc");
+ add("application/vnd.sun.xml.calc.template", "stc");
+ add("application/vnd.sun.xml.draw", "sxd");
+ add("application/vnd.sun.xml.draw.template", "std");
+ add("application/vnd.sun.xml.impress", "sxi");
+ add("application/vnd.sun.xml.impress.template", "sti");
+ add("application/vnd.sun.xml.math", "sxm");
+ add("application/vnd.sun.xml.writer", "sxw");
+ add("application/vnd.sun.xml.writer.global", "sxg");
+ add("application/vnd.sun.xml.writer.template", "stw");
+ add("application/vnd.visio", "vsd");
+ add("application/x-abiword", "abw");
+ add("application/x-apple-diskimage", "dmg");
+ add("application/x-bcpio", "bcpio");
+ add("application/x-bittorrent", "torrent");
+ add("application/x-cdf", "cdf");
+ add("application/x-cdlink", "vcd");
+ add("application/x-chess-pgn", "pgn");
+ add("application/x-cpio", "cpio");
+ add("application/x-debian-package", "deb");
+ add("application/x-debian-package", "udeb");
+ add("application/x-director", "dcr");
+ add("application/x-director", "dir");
+ add("application/x-director", "dxr");
+ add("application/x-dms", "dms");
+ add("application/x-doom", "wad");
+ add("application/x-dvi", "dvi");
+ add("application/x-font", "pfa");
+ add("application/x-font", "pfb");
+ add("application/x-font", "gsf");
+ add("application/x-font", "pcf");
+ add("application/x-font", "pcf.Z");
+ add("application/x-freemind", "mm");
+ // application/futuresplash isn't IANA, so application/x-futuresplash should come first.
+ add("application/x-futuresplash", "spl");
+ add("application/futuresplash", "spl");
+ add("application/x-gnumeric", "gnumeric");
+ add("application/x-go-sgf", "sgf");
+ add("application/x-graphing-calculator", "gcf");
+ add("application/x-gtar", "tgz");
+ add("application/x-gtar", "gtar");
+ add("application/x-gtar", "taz");
+ add("application/x-hdf", "hdf");
+ add("application/x-ica", "ica");
+ add("application/x-internet-signup", "ins");
+ add("application/x-internet-signup", "isp");
+ add("application/x-iphone", "iii");
+ add("application/x-iso9660-image", "iso");
+ add("application/x-jmol", "jmz");
+ add("application/x-kchart", "chrt");
+ add("application/x-killustrator", "kil");
+ add("application/x-koan", "skp");
+ add("application/x-koan", "skd");
+ add("application/x-koan", "skt");
+ add("application/x-koan", "skm");
+ add("application/x-kpresenter", "kpr");
+ add("application/x-kpresenter", "kpt");
+ add("application/x-kspread", "ksp");
+ add("application/x-kword", "kwd");
+ add("application/x-kword", "kwt");
+ add("application/x-latex", "latex");
+ add("application/x-lha", "lha");
+ add("application/x-lzh", "lzh");
+ add("application/x-lzx", "lzx");
+ add("application/x-maker", "frm");
+ add("application/x-maker", "maker");
+ add("application/x-maker", "frame");
+ add("application/x-maker", "fb");
+ add("application/x-maker", "book");
+ add("application/x-maker", "fbdoc");
+ add("application/x-mif", "mif");
+ add("application/x-ms-wmd", "wmd");
+ add("application/x-ms-wmz", "wmz");
+ add("application/x-msi", "msi");
+ add("application/x-ns-proxy-autoconfig", "pac");
+ add("application/x-nwc", "nwc");
+ add("application/x-object", "o");
+ add("application/x-oz-application", "oza");
+ add("application/x-pem-file", "pem");
+ add("application/x-pkcs12", "p12");
+ add("application/x-pkcs12", "pfx");
+ add("application/x-pkcs7-certreqresp", "p7r");
+ add("application/x-pkcs7-crl", "crl");
+ add("application/x-quicktimeplayer", "qtl");
+ add("application/x-shar", "shar");
+ add("application/x-shockwave-flash", "swf");
+ add("application/x-stuffit", "sit");
+ add("application/x-sv4cpio", "sv4cpio");
+ add("application/x-sv4crc", "sv4crc");
+ add("application/x-tar", "tar");
+ add("application/x-texinfo", "texinfo");
+ add("application/x-texinfo", "texi");
+ add("application/x-troff", "t");
+ add("application/x-troff", "roff");
+ add("application/x-troff-man", "man");
+ add("application/x-ustar", "ustar");
+ add("application/x-wais-source", "src");
+ add("application/x-wingz", "wz");
+ add("application/x-webarchive", "webarchive");
+ add("application/x-webarchive-xml", "webarchivexml");
+ add("application/x-x509-ca-cert", "crt");
+ add("application/x-x509-user-cert", "crt");
+ add("application/x-x509-server-cert", "crt");
+ add("application/x-xcf", "xcf");
+ add("application/x-xfig", "fig");
+ add("application/xhtml+xml", "xhtml");
+ add("audio/3gpp", "3gpp");
+ add("audio/aac", "aac");
+ add("audio/aac-adts", "aac");
+ add("audio/amr", "amr");
+ add("audio/amr-wb", "awb");
+ add("audio/basic", "snd");
+ add("audio/flac", "flac");
+ add("application/x-flac", "flac");
+ add("audio/imelody", "imy");
+ add("audio/midi", "mid");
+ add("audio/midi", "midi");
+ add("audio/midi", "ota");
+ add("audio/midi", "kar");
+ add("audio/midi", "rtttl");
+ add("audio/midi", "xmf");
+ add("audio/mobile-xmf", "mxmf");
+ // add ".mp3" first so it will be the default for guessExtensionFromMimeType
+ add("audio/mpeg", "mp3");
+ add("audio/mpeg", "mpga");
+ add("audio/mpeg", "mpega");
+ add("audio/mpeg", "mp2");
+ add("audio/mpeg", "m4a");
+ add("audio/mpegurl", "m3u");
+ add("audio/prs.sid", "sid");
+ add("audio/x-aiff", "aif");
+ add("audio/x-aiff", "aiff");
+ add("audio/x-aiff", "aifc");
+ add("audio/x-gsm", "gsm");
+ add("audio/x-matroska", "mka");
+ add("audio/x-mpegurl", "m3u");
+ add("audio/x-ms-wma", "wma");
+ add("audio/x-ms-wax", "wax");
+ add("audio/x-pn-realaudio", "ra");
+ add("audio/x-pn-realaudio", "rm");
+ add("audio/x-pn-realaudio", "ram");
+ add("audio/x-realaudio", "ra");
+ add("audio/x-scpls", "pls");
+ add("audio/x-sd2", "sd2");
+ add("audio/x-wav", "wav");
+ // image/bmp isn't IANA, so image/x-ms-bmp should come first.
+ add("image/x-ms-bmp", "bmp");
+ add("image/bmp", "bmp");
+ add("image/gif", "gif");
+ // image/ico isn't IANA, so image/x-icon should come first.
+ add("image/x-icon", "ico");
+ add("image/ico", "cur");
+ add("image/ico", "ico");
+ add("image/ief", "ief");
+ // add ".jpg" first so it will be the default for guessExtensionFromMimeType
+ add("image/jpeg", "jpg");
+ add("image/jpeg", "jpeg");
+ add("image/jpeg", "jpe");
+ add("image/pcx", "pcx");
+ add("image/png", "png");
+ add("image/svg+xml", "svg");
+ add("image/svg+xml", "svgz");
+ add("image/tiff", "tiff");
+ add("image/tiff", "tif");
+ add("image/vnd.djvu", "djvu");
+ add("image/vnd.djvu", "djv");
+ add("image/vnd.wap.wbmp", "wbmp");
+ add("image/webp", "webp");
+ add("image/x-cmu-raster", "ras");
+ add("image/x-coreldraw", "cdr");
+ add("image/x-coreldrawpattern", "pat");
+ add("image/x-coreldrawtemplate", "cdt");
+ add("image/x-corelphotopaint", "cpt");
+ add("image/x-jg", "art");
+ add("image/x-jng", "jng");
+ add("image/x-photoshop", "psd");
+ add("image/x-portable-anymap", "pnm");
+ add("image/x-portable-bitmap", "pbm");
+ add("image/x-portable-graymap", "pgm");
+ add("image/x-portable-pixmap", "ppm");
+ add("image/x-rgb", "rgb");
+ add("image/x-xbitmap", "xbm");
+ add("image/x-xpixmap", "xpm");
+ add("image/x-xwindowdump", "xwd");
+ add("model/iges", "igs");
+ add("model/iges", "iges");
+ add("model/mesh", "msh");
+ add("model/mesh", "mesh");
+ add("model/mesh", "silo");
+ add("text/calendar", "ics");
+ add("text/calendar", "icz");
+ add("text/comma-separated-values", "csv");
+ add("text/css", "css");
+ add("text/html", "htm");
+ add("text/html", "html");
+ add("text/h323", "323");
+ add("text/iuls", "uls");
+ add("text/mathml", "mml");
+ // add ".txt" first so it will be the default for guessExtensionFromMimeType
+ add("text/plain", "txt");
+ add("text/plain", "asc");
+ add("text/plain", "text");
+ add("text/plain", "diff");
+ add("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint
+ add("text/richtext", "rtx");
+ add("text/rtf", "rtf");
+ add("text/text", "phps");
+ add("text/tab-separated-values", "tsv");
+ add("text/xml", "xml");
+ add("text/x-bibtex", "bib");
+ add("text/x-boo", "boo");
+ add("text/x-c++hdr", "hpp");
+ add("text/x-c++hdr", "h++");
+ add("text/x-c++hdr", "hxx");
+ add("text/x-c++hdr", "hh");
+ add("text/x-c++src", "cpp");
+ add("text/x-c++src", "c++");
+ add("text/x-c++src", "cc");
+ add("text/x-c++src", "cxx");
+ add("text/x-chdr", "h");
+ add("text/x-component", "htc");
+ add("text/x-csh", "csh");
+ add("text/x-csrc", "c");
+ add("text/x-dsrc", "d");
+ add("text/x-haskell", "hs");
+ add("text/x-java", "java");
+ add("text/x-literate-haskell", "lhs");
+ add("text/x-moc", "moc");
+ add("text/x-pascal", "p");
+ add("text/x-pascal", "pas");
+ add("text/x-pcs-gcd", "gcd");
+ add("text/x-setext", "etx");
+ add("text/x-tcl", "tcl");
+ add("text/x-tex", "tex");
+ add("text/x-tex", "ltx");
+ add("text/x-tex", "sty");
+ add("text/x-tex", "cls");
+ add("text/x-vcalendar", "vcs");
+ add("text/x-vcard", "vcf");
+ add("video/3gpp", "3gpp");
+ add("video/3gpp", "3gp");
+ add("video/3gpp2", "3gpp2");
+ add("video/3gpp2", "3g2");
+ add("video/avi", "avi");
+ add("video/dl", "dl");
+ add("video/dv", "dif");
+ add("video/dv", "dv");
+ add("video/fli", "fli");
+ add("video/m4v", "m4v");
+ add("video/mp2ts", "ts");
+ add("video/mpeg", "mpeg");
+ add("video/mpeg", "mpg");
+ add("video/mpeg", "mpe");
+ add("video/mp4", "mp4");
+ add("video/mpeg", "VOB");
+ add("video/quicktime", "qt");
+ add("video/quicktime", "mov");
+ add("video/vnd.mpegurl", "mxu");
+ add("video/webm", "webm");
+ add("video/x-la-asf", "lsf");
+ add("video/x-la-asf", "lsx");
+ add("video/x-matroska", "mkv");
+ add("video/x-mng", "mng");
+ add("video/x-ms-asf", "asf");
+ add("video/x-ms-asf", "asx");
+ add("video/x-ms-wm", "wm");
+ add("video/x-ms-wmv", "wmv");
+ add("video/x-ms-wmx", "wmx");
+ add("video/x-ms-wvx", "wvx");
+ add("video/x-sgi-movie", "movie");
+ add("video/x-webex", "wrf");
+ add("x-conference/x-cooltalk", "ice");
+ add("x-epoc/x-sisx-app", "sisx");
+ applyOverrides();
+ }
+ private static void add(String mimeType, String extension) {
+ // If we have an existing x -> y mapping, we do not want to
+ // override it with another mapping x -> y2.
+ // If a mime type maps to several extensions
+ // the first extension added is considered the most popular
+ // so we do not want to overwrite it later.
+ if (!mimeTypeToExtensionMap.containsKey(mimeType)) {
+ mimeTypeToExtensionMap.put(mimeType, extension);
+ }
+ if (!extensionToMimeTypeMap.containsKey(extension)) {
+ extensionToMimeTypeMap.put(extension, mimeType);
+ }
+ }
+ private static InputStream getContentTypesPropertiesStream() {
+ // User override?
+ String userTable = System.getProperty("content.types.user.table");
+ if (userTable != null) {
+ File f = new File(userTable);
+ if (f.exists()) {
+ try {
+ return new FileInputStream(f);
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ // Standard location?
+ File f = new File(System.getProperty("java.home"), "lib" + File.separator + "content-types.properties");
+ if (f.exists()) {
+ try {
+ return new FileInputStream(f);
+ } catch (IOException ignored) {
+ }
+ }
+ return null;
+ }
+ /**
+ * This isn't what the RI does. The RI doesn't have hard-coded defaults, so supplying your
+ * own "content.types.user.table" means you don't get any of the built-ins, and the built-ins
+ * come from "$JAVA_HOME/lib/content-types.properties".
+ */
+ private static void applyOverrides() {
+ // Get the appropriate InputStream to read overrides from, if any.
+ InputStream stream = getContentTypesPropertiesStream();
+ if (stream == null) {
+ return;
+ }
+ try {
+ try {
+ // Read the properties file...
+ Properties overrides = new Properties();
+ overrides.load(stream);
+ // And translate its mapping to ours...
+ for (Map.Entry<Object, Object> entry : overrides.entrySet()) {
+ String extension = (String) entry.getKey();
+ String mimeType = (String) entry.getValue();
+ add(mimeType, extension);
+ }
+ } finally {
+ stream.close();
+ }
+ } catch (IOException ignored) {
+ }
+ }
+ private MimeUtils() {
+ }
+ /**
+ * Returns true if the given MIME type has an entry in the map.
+ * @param mimeType A MIME type (i.e. text/plain)
+ * @return True iff there is a mimeType entry in the map.
+ */
+ public static boolean hasMimeType(String mimeType) {
+ if (mimeType == null || mimeType.isEmpty()) {
+ return false;
+ }
+ return mimeTypeToExtensionMap.containsKey(mimeType);
+ }
+ /**
+ * Returns the MIME type for the given extension.
+ * @param extension A file extension without the leading '.'
+ * @return The MIME type for the given extension or null iff there is none.
+ */
+ public static String guessMimeTypeFromExtension(String extension) {
+ if (extension == null || extension.isEmpty()) {
+ return null;
+ }
+ return extensionToMimeTypeMap.get(extension);
+ }
+ /**
+ * Returns true if the given extension has a registered MIME type.
+ * @param extension A file extension without the leading '.'
+ * @return True iff there is an extension entry in the map.
+ */
+ public static boolean hasExtension(String extension) {
+ if (extension == null || extension.isEmpty()) {
+ return false;
+ }
+ return extensionToMimeTypeMap.containsKey(extension);
+ }
+ /**
+ * Returns the registered extension for the given MIME type. Note that some
+ * MIME types map to multiple extensions. This call will return the most
+ * common extension for the given MIME type.
+ * @param mimeType A MIME type (i.e. text/plain)
+ * @return The extension for the given MIME type or null iff there is none.
+ */
+ public static String guessExtensionFromMimeType(String mimeType) {
+ if (mimeType == null || mimeType.isEmpty()) {
+ return null;
+ }
+ return mimeTypeToExtensionMap.get(mimeType);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
new file mode 100644
index 00000000..9a689768
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+import java.util.List;
+
+import android.os.Bundle;
+
+public interface OnPhoneContactsLoadedListener {
+ public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
new file mode 100644
index 00000000..741afb35
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java
@@ -0,0 +1,327 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Build;
+import android.os.Process;
+import de.thedevstack.conversationsplus.utils.Logging;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {
+ }
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException
+ * if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class
+ .forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException
+ * if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom() throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders = Security
+ .getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class
+ .equals(secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider()
+ .getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through all
+ * requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG", 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG (
+ * {@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Logging.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read from "
+ + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(new FileInputStream(
+ URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
new file mode 100644
index 00000000..e0556af3
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -0,0 +1,97 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+public class PhoneHelper {
+
+ public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {
+ final String[] PROJECTION = new String[] { ContactsContract.Data._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_URI,
+ ContactsContract.Data.LOOKUP_KEY,
+ ContactsContract.CommonDataKinds.Im.DATA };
+
+ final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ + "\")";
+
+ CursorLoader mCursorLoader = new CursorLoader(context,
+ ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
+ null);
+ mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+ while (cursor.moveToNext()) {
+ Bundle contact = new Bundle();
+ contact.putInt("phoneid", cursor.getInt(cursor
+ .getColumnIndex(ContactsContract.Data._ID)));
+ contact.putString(
+ "displayname",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
+ contact.putString("photouri", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.PHOTO_URI)));
+ contact.putString("lookup", cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
+
+ contact.putString(
+ "jid",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
+ phoneContacts.add(contact);
+ }
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ cursor.close();
+ }
+ });
+ try {
+ mCursorLoader.startLoading();
+ } catch (RejectedExecutionException e) {
+ if (listener != null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ }
+ }
+
+ public static Uri getSefliUri(Context context) {
+ String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_URI };
+ Cursor mProfileCursor = context.getContentResolver().query(
+ Profile.CONTENT_URI, mProjection, null, null, null);
+
+ if (mProfileCursor == null || mProfileCursor.getCount() == 0) {
+ return null;
+ } else {
+ mProfileCursor.moveToFirst();
+ String uri = mProfileCursor.getString(1);
+ mProfileCursor.close();
+ if (uri == null) {
+ return null;
+ } else {
+ return Uri.parse(uri);
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java
new file mode 100644
index 00000000..bfb4668d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.utils;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class SerialSingleThreadExecutor implements Executor {
+
+ final Executor executor = Executors.newSingleThreadExecutor();
+ final Queue<Runnable> tasks = new ArrayDeque();
+ Runnable active;
+
+ public synchronized void execute(final Runnable r) {
+ tasks.offer(new Runnable() {
+ public void run() {
+ try {
+ r.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (active == null) {
+ scheduleNext();
+ }
+ }
+
+ protected synchronized void scheduleNext() {
+ if ((active = tasks.poll()) != null) {
+ executor.execute(active);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
new file mode 100644
index 00000000..7562c446
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -0,0 +1,308 @@
+package eu.siacs.conversations.utils;
+
+import java.math.BigDecimal;
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.net.URLConnection;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.Spannable.Factory;
+import android.text.style.ImageSpan;
+import android.text.Spannable;
+import android.util.Pair;
+
+public class UIHelper {
+
+ private static String BLACK_HEART_SUIT = "\u2665";
+ private static String HEAVY_BLACK_HEART_SUIT = "\u2764";
+ private static String WHITE_HEART_SUIT = "\u2661";
+
+ public static final ArrayList<String> HEARTS = new ArrayList<>(Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT));
+
+ private static final ArrayList<String> LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList(
+ "where are you", //en
+ "where are you now", //en
+ "where are you right now", //en
+ "whats your 20", //en
+ "what is your 20", //en
+ "what's your 20", //en
+ "whats your twenty", //en
+ "what is your twenty", //en
+ "what's your twenty", //en
+ "wo bist du", //de
+ "wo bist du jetzt", //de
+ "wo bist du gerade", //de
+ "wo seid ihr", //de
+ "wo seid ihr jetzt", //de
+ "wo seid ihr gerade", //de
+ "dónde estás", //es
+ "donde estas" //es
+ ));
+
+ private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
+ private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE;
+
+ public static String readableTimeDifference(Context context, long time) {
+ return readableTimeDifference(context, time, false);
+ }
+
+ public static String readableTimeDifferenceFull(Context context, long time) {
+ return readableTimeDifference(context, time, true);
+ }
+
+ private static String readableTimeDifference(Context context, long time,
+ boolean fullDate) {
+ if (time == 0) {
+ return context.getString(R.string.just_now);
+ }
+ Date date = new Date(time);
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.just_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.minute_ago);
+ } else if (difference < 60 * 15) {
+ return context.getString(R.string.minutes_ago,
+ Math.round(difference / 60.0));
+ } else if (today(date)) {
+ java.text.DateFormat df = DateFormat.getTimeFormat(context);
+ return df.format(date);
+ } else {
+ if (fullDate) {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ FULL_DATE_FLAGS);
+ } else {
+ return DateUtils.formatDateTime(context, date.getTime(),
+ SHORT_DATE_FLAGS);
+ }
+ }
+ }
+
+ private static boolean today(Date date) {
+ return sameDay(date,new Date(System.currentTimeMillis()));
+ }
+
+ public static boolean sameDay(long timestamp1, long timestamp2) {
+ return sameDay(new Date(timestamp1),new Date(timestamp2));
+ }
+
+ private static boolean sameDay(Date a, Date b) {
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal1.setTime(a);
+ cal2.setTime(b);
+ return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
+ && cal1.get(Calendar.DAY_OF_YEAR) == cal2
+ .get(Calendar.DAY_OF_YEAR);
+ }
+
+ public static String lastseen(Context context, long time) {
+ if (time == 0) {
+ return context.getString(R.string.never_seen);
+ }
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return context.getString(R.string.last_seen_now);
+ } else if (difference < 60 * 2) {
+ return context.getString(R.string.last_seen_min);
+ } else if (difference < 60 * 60) {
+ return context.getString(R.string.last_seen_mins,
+ Math.round(difference / 60.0));
+ } else if (difference < 60 * 60 * 2) {
+ return context.getString(R.string.last_seen_hour);
+ } else if (difference < 60 * 60 * 24) {
+ return context.getString(R.string.last_seen_hours,
+ Math.round(difference / (60.0 * 60.0)));
+ } else if (difference < 60 * 60 * 48) {
+ return context.getString(R.string.last_seen_day);
+ } else {
+ return context.getString(R.string.last_seen_days,
+ Math.round(difference / (60.0 * 60.0 * 24.0)));
+ }
+ }
+
+ public static int getColorForName(String name) {
+ if (name.isEmpty()) {
+ return 0xFF202020;
+ }
+ int colors[] = {0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
+ 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
+ 0xFF795548, 0xFF607d8b};
+ return colors[(int) ((name.hashCode() & 0xffffffffl) % colors.length)];
+ }
+
+ public static Pair<String,Boolean> getMessagePreview(final Context context, final Message message) {
+ final Transferable d = message.getTransferable();
+ if (d != null ) {
+ switch (d.getStatus()) {
+ case Transferable.STATUS_CHECKING:
+ return new Pair<>(context.getString(R.string.checking_x,
+ getFileDescriptionString(context,message)),true);
+ case Transferable.STATUS_DOWNLOADING:
+ return new Pair<>(context.getString(R.string.receiving_x_file,
+ getFileDescriptionString(context,message),
+ d.getProgress()),true);
+ case Transferable.STATUS_OFFER:
+ case Transferable.STATUS_OFFER_CHECK_FILESIZE:
+ return new Pair<>(context.getString(R.string.x_file_offered_for_download,
+ getFileDescriptionString(context,message)),true);
+ case Transferable.STATUS_DELETED:
+ return new Pair<>(context.getString(R.string.file_deleted),true);
+ case Transferable.STATUS_FAILED:
+ return new Pair<>(context.getString(R.string.file_transmission_failed),true);
+ case Transferable.STATUS_UPLOADING:
+ if (message.getStatus() == Message.STATUS_OFFERED) {
+ return new Pair<>(context.getString(R.string.offering_x_file,
+ getFileDescriptionString(context, message)), true);
+ } else {
+ return new Pair<>(context.getString(R.string.sending_x_file,
+ getFileDescriptionString(context, message)), true);
+ }
+ default:
+ return new Pair<>("",false);
+ }
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ return new Pair<>(context.getString(R.string.encrypted_message_received),true);
+ } else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ return new Pair<>(context.getString(R.string.received_x_file,
+ getFileDescriptionString(context, message)), true);
+ } else {
+ return new Pair<>(getFileDescriptionString(context,message),true);
+ }
+ } else {
+ if (message.getBody().startsWith(Message.ME_COMMAND)) {
+ return new Pair<>(message.getBody().replaceAll("^" + Message.ME_COMMAND,
+ UIHelper.getMessageDisplayName(message) + " "), false);
+ } else if (GeoHelper.isGeoUri(message.getBody())) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ return new Pair<>(context.getString(R.string.received_location),true);
+ } else {
+ return new Pair<>(context.getString(R.string.location), true);
+ }
+ } else{
+ return new Pair<>(message.getBody(), false);
+ }
+ }
+ }
+
+ public static String getFileDescriptionString(final Context context, final Message message) {
+ if (message.getType() == Message.TYPE_IMAGE) {
+ return context.getString(R.string.image);
+ }
+ final String mime = message.getMimeType();
+ if (mime == null) {
+ return context.getString(R.string.file);
+ } else if (mime.startsWith("audio/")) {
+ return context.getString(R.string.audio);
+ } else if(mime.startsWith("video/")) {
+ return context.getString(R.string.video);
+ } else if (mime.startsWith("image/")) {
+ return context.getString(R.string.image);
+ } else if (mime.contains("pdf")) {
+ return context.getString(R.string.pdf_document) ;
+ } else if (mime.contains("application/vnd.android.package-archive")) {
+ return context.getString(R.string.apk) ;
+ } else if (mime.contains("vcard")) {
+ return context.getString(R.string.vcard) ;
+ } else {
+ return mime;
+ }
+ }
+
+ public static String getMessageDisplayName(final Message message) {
+ if (message.getStatus() == Message.STATUS_RECEIVED) {
+ final Contact contact = message.getContact();
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (contact != null) {
+ return contact.getDisplayName();
+ } else {
+ return getDisplayedMucCounterpart(message.getCounterpart());
+ }
+ } else {
+ return contact != null ? contact.getDisplayName() : "";
+ }
+ } else {
+ if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ return getDisplayedMucCounterpart(message.getConversation().getJid());
+ } else {
+ final Jid jid = message.getConversation().getAccount().getJid();
+ return jid.hasLocalpart() ? jid.getLocalpart() : jid.toDomainJid().toString();
+ }
+ }
+ }
+
+ public static String getStatusColor(int status) {
+ switch (status) {
+ case Presences.ONLINE:
+ case Presences.CHAT:
+ return "#259B23";
+ case Presences.AWAY:
+ case Presences.XA:
+ return "#FF9800";
+ case Presences.DND:
+ return "#E51C23";
+ }
+ return "#CCCCCC";
+ }
+
+ private static String getDisplayedMucCounterpart(final Jid counterpart) {
+ if (counterpart==null) {
+ return "";
+ } else if (!counterpart.isBareJid()) {
+ return counterpart.getResourcepart();
+ } else {
+ return counterpart.toString();
+ }
+ }
+
+ public static boolean receivedLocationQuestion(Message message) {
+ if (message == null
+ || message.getStatus() != Message.STATUS_RECEIVED
+ || message.getType() != Message.TYPE_TEXT) {
+ return false;
+ }
+ String body = message.getBody() == null ? null : message.getBody().toLowerCase(Locale.getDefault());
+ body = body.replace("?","").replace("¿","");
+ return LOCATION_QUESTIONS.contains(body);
+ }
+
+ public static String getHumanReadableFileSize(long filesize) {
+ if (0 > filesize) {
+ return "?";
+ }
+ double size = Double.valueOf(filesize);
+ String[] sizes = {" bytes", " Kb", " Mb", " Gb", " Tb"};
+ int i = 0;
+ while (1023 < size) {
+ size /= 1024d;
+ ++i;
+ }
+ BigDecimal readableSize = new BigDecimal(size);
+ readableSize = readableSize.setScale(2, BigDecimal.ROUND_HALF_UP);
+ return readableSize.doubleValue() + sizes[i];
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
new file mode 100644
index 00000000..4dee07cf
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.utils;
+
+public class XmlHelper {
+ public static String encodeEntities(String content) {
+ content = content.replace("&", "&amp;");
+ content = content.replace("<", "&lt;");
+ content = content.replace(">", "&gt;");
+ content = content.replace("\"", "&quot;");
+ content = content.replace("'", "&apos;");
+ return content;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/Xmlns.java b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
new file mode 100644
index 00000000..868566d9
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/Xmlns.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+public final class Xmlns {
+ public static final String BLOCKING = "urn:xmpp:blocking";
+ public static final String ROSTER = "jabber:iq:roster";
+ public static final String REGISTER = "jabber:iq:register";
+ public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
+ public static final String HTTP_UPLOAD = "eu:siacs:conversations:http:upload";
+}
diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
new file mode 100644
index 00000000..92c0241e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java
@@ -0,0 +1,85 @@
+package eu.siacs.conversations.utils;
+
+import android.net.Uri;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class XmppUri {
+
+ protected String jid;
+ protected boolean muc;
+ protected String fingerprint;
+
+ public XmppUri(String uri) {
+ try {
+ parse(Uri.parse(uri));
+ } catch (IllegalArgumentException e) {
+ try {
+ jid = Jid.fromString(uri).toBareJid().toString();
+ } catch (InvalidJidException e2) {
+ jid = null;
+ }
+ }
+ }
+
+ public XmppUri(Uri uri) {
+ parse(uri);
+ }
+
+ protected void parse(Uri uri) {
+ String scheme = uri.getScheme();
+ if ("xmpp".equalsIgnoreCase(scheme)) {
+ // sample: xmpp:jid@foo.com
+ muc = "join".equalsIgnoreCase(uri.getQuery());
+ if (uri.getAuthority() != null) {
+ jid = uri.getAuthority();
+ } else {
+ jid = uri.getSchemeSpecificPart().split("\\?")[0];
+ }
+ fingerprint = parseFingerprint(uri.getQuery());
+ } else if ("imto".equalsIgnoreCase(scheme)) {
+ // sample: imto://xmpp/jid@foo.com
+ try {
+ jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1];
+ } catch (final UnsupportedEncodingException ignored) {
+ jid = null;
+ }
+ } else {
+ try {
+ jid = Jid.fromString(uri.toString()).toBareJid().toString();
+ } catch (final InvalidJidException ignored) {
+ jid = null;
+ }
+ }
+ }
+
+ protected String parseFingerprint(String query) {
+ if (query == null) {
+ return null;
+ } else {
+ final String NEEDLE = "otr-fingerprint=";
+ int index = query.indexOf(NEEDLE);
+ if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
+ return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public Jid getJid() {
+ try {
+ return this.jid == null ? null :Jid.fromString(this.jid.toLowerCase());
+ } catch (InvalidJidException e) {
+ return null;
+ }
+ }
+
+ public String getFingerprint() {
+ return this.fingerprint;
+ }
+}