aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/thedevstack/conversationsplus
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus')
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java105
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java159
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java383
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java65
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/enums/UserDecision.java10
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java13
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java16
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/exceptions/RemoteFileNotFoundException.java20
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java22
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java203
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java60
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java46
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java57
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/adapter/PresencesArrayAdapter.java62
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/AbstractAlertDialog.java125
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java177
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java71
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/LogCatOutputCopyOnClickListener.java41
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java210
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShareWithResizePictureUserDecisionListener.java48
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShowResourcesListDialogListener.java47
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java12
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/preferences/LogInformationPreference.java31
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java203
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ClipboardUtil.java48
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java77
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java372
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java125
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java63
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/UiUpdateHelper.java52
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/XmppConnectionServiceAccessor.java32
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/XmppSendUtil.java34
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java91
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketGenerator.java124
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketParser.java29
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java15
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java33
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java32
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java27
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacketGenerator.java33
40 files changed, 3373 insertions, 0 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java
new file mode 100644
index 00000000..4b11bb4a
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java
@@ -0,0 +1,105 @@
+package de.thedevstack.conversationsplus;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.preference.PreferenceManager;
+
+import java.io.File;
+
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+
+/**
+ * This class is used to provide static access to the applicationcontext.
+ */
+public class ConversationsPlusApplication extends Application {
+ /**
+ * Application instance for static access
+ */
+ private static ConversationsPlusApplication instance;
+
+ private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
+ private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
+
+ /**
+ * Initializes the application and saves its instance.
+ */
+ public void onCreate(){
+ super.onCreate();
+ ConversationsPlusApplication.instance = this;
+ ConversationsPlusPreferences.init(PreferenceManager.getDefaultSharedPreferences(getAppContext()));
+ ImageUtil.initBitmapCache();
+ FileBackend.init();
+ }
+
+ /**
+ * Returns the instance of the application
+ * @return this application instance
+ */
+ public static ConversationsPlusApplication getInstance() {
+ return ConversationsPlusApplication.instance;
+ }
+
+ public static void executeFileAdding(Runnable r) {
+ getInstance().mFileAddingExecutor.execute(r);
+ }
+
+ public static void executeDatabaseOperation(Runnable r) {
+ getInstance().mDatabaseExecutor.execute(r);
+ }
+
+ /**
+ * Returns the application's context.
+ * @return Context the application's context
+ */
+ public static Context getAppContext() {
+ return ConversationsPlusApplication.instance.getApplicationContext();
+ }
+
+ /**
+ * Returns the application's private data directory.
+ * @return File the application's private data dir
+ */
+ public static File getPrivateFilesDir() {
+ return ConversationsPlusApplication.instance.getFilesDir();
+ }
+
+ /**
+ * Returns the version of the application.
+ * @see android.content.pm.PackageInfo#versionName
+ * @return a string representation of the version stored in packageInfo
+ */
+ public static String getVersion() {
+ final String packageName = ConversationsPlusApplication.getAppContext().getPackageName();
+ if (packageName != null) {
+ try {
+ return ConversationsPlusApplication.getAppContext().getPackageManager().getPackageInfo(packageName, 0).versionName;
+ } catch (final PackageManager.NameNotFoundException e) {
+ return "unknown";
+ }
+ } else {
+ return "unknown";
+ }
+ }
+
+ /**
+ * Returns the application's name.
+ * @return the name as it is defined in R.string.app_name
+ */
+ public static String getName() {
+ return ConversationsPlusApplication.getAppContext().getString(R.string.app_name);
+ }
+
+ /**
+ * Returns the name and the version of this application.
+ * @see #getName() and #getVersion
+ * @return a concatination of name and version with a whitespace in between
+ */
+ public static String getNameAndVersion() {
+ return getName() + " " + getVersion();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java
new file mode 100644
index 00000000..e0434e8b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java
@@ -0,0 +1,159 @@
+package de.thedevstack.conversationsplus;
+
+import eu.siacs.conversations.R;
+
+/**
+ * Helper class for accessing colors.
+ */
+public final class ConversationsPlusColors {
+ /**
+ * Returns the primary background color.
+ * @return the primary background color
+ */
+ public static int primaryBackground() {
+ return byId(R.color.primaryBackground);
+ }
+
+ /**
+ * Returns the secondary background color.
+ * @return the secondary background color
+ */
+ public static int secondaryBackground() {
+ return byId(R.color.secondaryBackground);
+ }
+
+ /**
+ * Returns the primary text color.
+ * @return the primary text color
+ */
+ public static int primaryText() {
+ return byId(R.color.primaryText);
+ }
+
+ /**
+ * Returns the secondary text color.
+ * @return the secondary text color
+ */
+ public static int secondaryText() {
+ return byId(R.color.secondaryText);
+ }
+
+ /**
+ * Returns the tertiary text color.
+ * @return the tertiary text color
+ */
+ public static int tertiaryText() {
+ return byId(R.color.tertiaryText);
+ }
+
+ /**
+ * Returns the primary text color on dark background.
+ * @return the primary text color on dark background
+ */
+ public static int primaryTextOnDark() {
+ return byId(R.color.primaryTextOnDark);
+ }
+
+ /**
+ * Returns the secondary text color on dark background.
+ * @return the secondary text color on dark background
+ */
+ public static int secondaryTextOnDark() {
+ return byId(R.color.secondaryTextOnDark);
+ }
+
+ /**
+ * Returns the online color.
+ * @return the online color
+ */
+ public static int online() {
+ return byId(R.color.online);
+ }
+
+ /**
+ * Returns the color for the presence status 'chat'.
+ * @return the color for the presence status 'chat'
+ */
+ public static int chat() {
+ return byId(R.color.chat);
+ }
+
+ /**
+ * Returns the color for the presence status 'away'.
+ * @return the color for the presence status 'away'
+ */
+ public static int away() {
+ return byId(R.color.away);
+ }
+
+ /**
+ * Returns the color for the presence status 'dnd'.
+ * @return the color for the presence status 'dnd'
+ */
+ public static int dnd() {
+ return byId(R.color.dnd);
+ }
+
+ /**
+ * Returns the color for the presence status 'xa'.
+ * @return the color for the presence status 'xa'
+ */
+ public static int xa() {
+ return byId(R.color.xa);
+ }
+
+ /**
+ * Returns the color for the presence status 'offline'.
+ * @return the color for the presence status 'offline'
+ */
+ public static int offline() {
+ return byId(R.color.offline);
+ }
+
+ /**
+ * Returns the error color.
+ * @return the error color
+ */
+ public static int error() {
+ return byId(R.color.error);
+ }
+
+ /**
+ * Returns the warning color.
+ * @return the warning color
+ */
+ public static int warning() {
+ return byId(R.color.warning);
+ }
+
+ /**
+ * Returns the notification color.
+ * @return the notification color
+ */
+ public static int notification() {
+ return byId(R.color.notification);
+ }
+
+ /**
+ * Returns the accent color.
+ * @return the accent color
+ */
+ public static int accent() {
+ return byId(R.color.accent);
+ }
+
+ /**
+ * Returns the color identified by id.
+ * Delegates to android.content.res.Resources.getColor(int)
+ * @param id the id of the color
+ * @see {@link android.content.res.Resources#getColor(int)}
+ * @return the color identified by id
+ */
+ private static int byId(int id) {
+ return ConversationsPlusApplication.getAppContext().getResources().getColor(id);
+ }
+
+ private ConversationsPlusColors() {
+ // avoid instantiation - helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java
new file mode 100644
index 00000000..70eec7a2
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java
@@ -0,0 +1,383 @@
+package de.thedevstack.conversationsplus;
+
+import android.content.SharedPreferences;
+
+import java.util.Set;
+
+import de.thedevstack.conversationsplus.enums.UserDecision;
+import de.tzur.conversations.Settings;
+
+/**
+ * Utility Class to access shared preferences of Conversations+.
+ */
+public class ConversationsPlusPreferences extends Settings {
+ private static ConversationsPlusPreferences instance;
+ private final SharedPreferences sharedPreferences;
+
+ public static boolean omemoEnabled() {
+ return getBoolean("omemo_enabled", false);
+ }
+
+ public static String imgTransferFolder() {
+ return getString("img_transfer_folder", getString("app_name", "Conversations+"));
+ }
+
+ public static String fileTransferFolder() {
+ return getString("file_transfer_folder", getString("app_name", "Conversations+"));
+ }
+
+ public static UserDecision resizePicture() {
+ return getEnumFromStringPref("resize_picture", UserDecision.ASK);
+ }
+
+ public static void applyResizePicture(UserDecision decision) {
+ applyString("resize_picture", decision.name());
+ }
+
+ /**
+ * Whether automatic downloads should only be done when connected to Wifi or not.
+ * @return
+ */
+ public static boolean autoDownloadFileWLAN() {
+ return getBoolean("auto_download_file_wlan", true);
+ }
+ /**
+ * Whether image-links should be downloaded or not.
+ * @return
+ */
+ public static boolean autoDownloadFileLink() {
+ return getBoolean("auto_download_file_link", true);
+ }
+
+ public static boolean showDynamicTags() {
+ return getBoolean("show_dynamic_tags", false);
+ }
+
+ /**
+ * Whether to send report to developer or not.
+ * @return
+ */
+ public static boolean neverSend() {
+ return getBoolean("never_send", false);
+ }
+
+ public static void applyNeverSend(boolean neverSend) {
+ applyBoolean("never_send", neverSend);
+ }
+
+ /**
+ * The name used for the resource part of the accounts' JID.
+ * @return the resource name, <i>mobile</i> as default value
+ */
+ public static String resource() {
+ return getString("resource", "mobile");
+ }
+
+ /**
+ * Whether to enable legacy SSL support.
+ * @return <code>true</code>if legacy support for SSL is enabled, <i>false</i> as default value
+ */
+ public static boolean enableLegacySSL() {
+ return getBoolean("enable_legacy_ssl", false);
+ }
+
+ public static boolean showBatteryOptimization() {
+ return getBoolean("show_battery_optimization", true);
+ }
+
+ public static void commitShowBatteryOptimization(boolean showBatteryOptimization) {
+ commitBoolean("show_battery_optimization", showBatteryOptimization);
+ }
+
+ public static boolean advancedMucMode() {
+ return getBoolean("advanced_muc_mode", false);
+ }
+
+ public static void commitAdvancedMucMode(boolean advancedMucMode) {
+ commitBoolean("advanced_muc_mode", advancedMucMode);
+ }
+
+ /**
+ * Whether to show extended connection options or not
+ * @return
+ */
+ public static boolean showConnectionOptions() {
+ return getBoolean("show_connection_options", false);
+ }
+
+ /**
+ * Whether to respect auto join on bookmarks or not.
+ * @return
+ */
+ public static boolean autojoin() {
+ return getBoolean("autojoin", true);
+ }
+ public static boolean xaOnSilentMode() {
+ return getBoolean("xa_on_silent_mode", false);
+ }
+
+ public static boolean treatVibrateAsSilent() {
+ return getBoolean("treat_vibrate_as_silent", false);
+ }
+
+ public static boolean awayWhenScreenOff() {
+ return getBoolean("away_when_screen_off", false);
+ }
+
+ public static boolean useSubject() {
+ return getBoolean("use_subject", true);
+ }
+
+ public static boolean displayEnterKey() {
+ return getBoolean("display_enter_key", false);
+ }
+
+ public static boolean useLargerFont() {
+ return getBoolean("use_larger_font", false);
+ }
+
+ public static boolean hideOffline() {
+ return getBoolean("hide_offline", false);
+ }
+
+ public static void commitHideOffline(boolean hideOffline) {
+ commitBoolean("hide_offline", hideOffline);
+ }
+
+ public static String recentlyUsedQuickAction() {
+ return getString("recently_used_quick_action", "text");
+ }
+
+ public static void applyRecentlyUsedQuickAction(String recentlyUsedQuickAction) {
+ applyString("recently_used_quick_action", recentlyUsedQuickAction);
+ }
+
+ public static String quickAction() {
+ return getString("quick_action", "recent");
+ }
+
+ public static boolean sendButtonStatus() {
+ return getBoolean("send_button_status", false);
+ }
+
+ public static boolean enterIsSend() {
+ return getBoolean("enter_is_send", false);
+ }
+
+ public static long autoAcceptFileSize() {
+ return getLongFromStringPref("auto_accept_file_size", 524288);
+ }
+
+ public static boolean vibrateOnNotification() {
+ return getBoolean("vibrate_on_notification", true);
+ }
+
+ public static String notificationRingtone() {
+ return getString("notification_ringtone", null);
+ }
+
+ public static boolean showNotification() {
+ return getBoolean("show_notification", true);
+ }
+
+ public static long quietHoursEnd() {
+ return getLong("quiet_hours_end", 0);
+ }
+
+ public static long quietHoursStart() {
+ return getLong("quiet_hours_start", 0);
+ }
+
+ public static boolean enableQuietHours() {
+ return getBoolean("enable_quiet_hours", false);
+ }
+
+ public static boolean dontTrustSystemCAs() {
+ return getBoolean("dont_trust_system_cas", false);
+ }
+
+ public static boolean grantNewContacts() {
+ return getBoolean("grant_new_contacts", true);
+ }
+
+ public static boolean keepForegroundService() {
+ return getBoolean("keep_foreground_service", false);
+ }
+
+ public static void commitKeepForegroundService(boolean keepForegroundService) {
+ commitBoolean("keep_foreground_service", keepForegroundService);
+ }
+
+ public static boolean forceEncryption() {
+ return getBoolean("force_encryption", false);
+ }
+
+ public static boolean dontSaveEncrypted() {
+ return getBoolean("dont_save_encrypted", false);
+ }
+
+ /**
+ * Whether the chat states should be send or not.
+ * @return
+ */
+ public static boolean chatStates() {
+ return getBoolean("chat_states", false);
+ }
+
+ /**
+ * Whether the receipient notification should be requested from the counterpart or not.
+ * <br>Default value is <code>false</code>
+ * @return <code>true</code> if the receipt should be requested, <code>false</code> otherwise
+ */
+ public static boolean indicateReceived() {
+ return getBoolean("indicate_received", false);
+ }
+
+ public static boolean allowMessageCorrection() {
+ return getBoolean("allow_message_correction", true);
+ }
+
+ public static boolean returnToPrevious() {
+ return getBoolean("return_to_previous", false);
+ }
+
+ public static boolean led() {
+ return getBoolean("led", true);
+ }
+
+ private ConversationsPlusPreferences(SharedPreferences sharedPreferences) {
+ this.sharedPreferences = sharedPreferences;
+ }
+
+ public synchronized static void init(SharedPreferences sharedPreferences) {
+ if (null == instance) {
+ instance = new ConversationsPlusPreferences(sharedPreferences);
+ initSettingsClassWithPreferences(sharedPreferences);
+ }
+ }
+
+ private static SharedPreferences getSharedPreferences() {
+ return instance.sharedPreferences;
+ }
+
+ private static SharedPreferences.Editor getSharedPreferencesEditor() {
+ return getSharedPreferences().edit();
+ }
+
+ private static String getString(String key, String defValue) {
+ return getSharedPreferences().getString(key, defValue);
+ }
+
+ private static float getFloat(String key, float defValue) {
+ return getSharedPreferences().getFloat(key, defValue);
+ }
+
+ private static float getFloatFromStringPref(String key, float defValue) {
+ try {
+ return Float.parseFloat(getString(key, String.valueOf(defValue)));
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private static int getInt(String key, int defValue) {
+ return getSharedPreferences().getInt(key, defValue);
+ }
+
+ private static int getIntFromStringPref(String key, int defValue) {
+ try {
+ return Integer.parseInt(getString(key, String.valueOf(defValue)));
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ private static Set<String> getStringSet(String key, Set<String> defValues) {
+ return getSharedPreferences().getStringSet(key, defValues);
+ }
+
+ private static boolean contains(String key) {
+ return getSharedPreferences().contains(key);
+ }
+
+ private static long getLong(String key, long defValue) {
+ return getSharedPreferences().getLong(key, defValue);
+ }
+
+ private static long getLongFromStringPref(String key, long defValue) {
+ try {
+ return Long.parseLong(getString(key, String.valueOf(defValue)));
+ } catch (NumberFormatException e) {
+ return defValue;
+ }
+ }
+
+ protected static <T extends Enum<T>> T getEnumFromStringPref(String key, T defaultValue) {
+ String enumValueAsString = getString(key, defaultValue.name());
+ return (T) Enum.valueOf(defaultValue.getClass(), enumValueAsString);
+ }
+
+ private static boolean getBoolean(String key, boolean defValue) {
+ return getSharedPreferences().getBoolean(key, defValue);
+ }
+
+ private static void commitBoolean(String key, boolean value) {
+ putBoolean(key, value).commit();
+ }
+
+ private static void applyBoolean(String key, boolean value) {
+ putBoolean(key, value).apply();
+ }
+
+ private static SharedPreferences.Editor putBoolean(String key, boolean value) {
+ return getSharedPreferencesEditor().putBoolean(key, value);
+ }
+
+ private static void commitString(String key, String value) {
+ putString(key, value).commit();
+ }
+
+ private static void applyString(String key, String value) {
+ putString(key, value).apply();
+ }
+
+ private static SharedPreferences.Editor putString(String key, String value) {
+ return getSharedPreferencesEditor().putString(key, value);
+ }
+
+ private static void commitInt(String key, int value) {
+ putInt(key, value).commit();
+ }
+
+ private static void applyInt(String key, int value) {
+ putInt(key, value).apply();
+ }
+
+ private static SharedPreferences.Editor putInt(String key, int value) {
+ return getSharedPreferencesEditor().putInt(key, value);
+ }
+
+ private static void commitLong(String key, long value) {
+ putLong(key, value).commit();
+ }
+
+ private static void applyLong(String key, long value) {
+ putLong(key, value).apply();
+ }
+
+ private static SharedPreferences.Editor putLong(String key, long value) {
+ return getSharedPreferencesEditor().putLong(key, value);
+ }
+
+ private static void commitFloat(String key, float value) {
+ putFloat(key, value).commit();
+ }
+
+ private static void applyLong(String key, float value) {
+ putFloat(key, value).apply();
+ }
+
+ private static SharedPreferences.Editor putFloat(String key, float value) {
+ return getSharedPreferencesEditor().putFloat(key, value);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java b/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java
new file mode 100644
index 00000000..1e0eebc7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java
@@ -0,0 +1,65 @@
+package de.thedevstack.conversationsplus.dto;
+
+/**
+ * An SRV record as it is currently used in Conversations Plus.
+ * The weight of the SRV record is skipped.
+ */
+public class SrvRecord implements Comparable<SrvRecord> {
+ private int priority;
+ private String name;
+ private int port;
+ private boolean useTls = false;
+
+ public SrvRecord(int priority, String name, int port) {
+ this.priority = priority;
+ this.name = name;
+ this.port = port;
+ }
+
+ public SrvRecord(int priority, String name, int port, boolean useTls) {
+ this.priority = priority;
+ this.name = name;
+ this.port = port;
+ this.useTls = useTls;
+ }
+
+ /**
+ * Compares this record to the specified record to determine their relative
+ * order.
+ *
+ * @param another the object to compare to this instance.
+ * @return a negative integer if the priority of this record is lower than the priority of {@code another};
+ * a positive integer if the priority of this record is higher than
+ * {@code another}; 0 if the priority of this record is equal to the priority of
+ * {@code another}.
+ */
+ @Override
+ public int compareTo(SrvRecord another) {
+ return this.getPriority() < another.getPriority() ? -1 : (this.getPriority() == another.getPriority() ? 0 : 1);
+ }
+
+ @Override
+ public String toString() {
+ return "SrvRecord{" +
+ "priority=" + priority +
+ ", name='" + name + '\'' +
+ ", port=" + port +
+ '}';
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public boolean isUseTls() {
+ return useTls;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/UserDecision.java b/src/main/java/de/thedevstack/conversationsplus/enums/UserDecision.java
new file mode 100644
index 00000000..ccb658d5
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/enums/UserDecision.java
@@ -0,0 +1,10 @@
+package de.thedevstack.conversationsplus.enums;
+
+/**
+ * Created by tzur on 30.10.2015.
+ */
+public enum UserDecision {
+ ASK,
+ ALWAYS,
+ NEVER;
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java
new file mode 100644
index 00000000..858b4563
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java
@@ -0,0 +1,13 @@
+package de.thedevstack.conversationsplus.exceptions;
+
+public class FileCopyException extends UiException {
+ private static final long serialVersionUID = -1010013599132881427L;
+
+ public FileCopyException(int resId) {
+ super(resId);
+ }
+
+ public FileCopyException(int resId, Throwable e) {
+ super(resId, e);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java
new file mode 100644
index 00000000..b5786990
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java
@@ -0,0 +1,16 @@
+package de.thedevstack.conversationsplus.exceptions;
+
+/**
+ * Created by tzur on 15.12.2015.
+ */
+public class ImageResizeException extends UiException {
+ private static final long serialVersionUID = -1010013599112881427L;
+
+ public ImageResizeException(int resId) {
+ super(resId);
+ }
+
+ public ImageResizeException(int resId, Throwable e) {
+ super(resId, e);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/RemoteFileNotFoundException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/RemoteFileNotFoundException.java
new file mode 100644
index 00000000..41a548cb
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/RemoteFileNotFoundException.java
@@ -0,0 +1,20 @@
+package de.thedevstack.conversationsplus.exceptions;
+
+import java.io.IOException;
+
+/**
+ * Created by lookshe on 15.03.16.
+ *
+ * Exception class if HTTP status code 404 occured
+ */
+public class RemoteFileNotFoundException extends IOException {
+ private static final long serialVersionUID = -1010013599132881427L;
+
+ public RemoteFileNotFoundException() {
+ super();
+ }
+
+ public RemoteFileNotFoundException(Throwable e) {
+ super(e);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java
new file mode 100644
index 00000000..b05c5025
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java
@@ -0,0 +1,22 @@
+package de.thedevstack.conversationsplus.exceptions;
+
+/**
+ * Exception to be shown in UI.
+ */
+public class UiException extends Exception {
+ private static final long serialVersionUID = -1010015239132881427L;
+ private int resId;
+
+ public UiException(int resId) {
+ this.resId = resId;
+ }
+
+ public UiException(int resId, Throwable e) {
+ super(e);
+ this.resId = resId;
+ }
+
+ public int getResId() {
+ return resId;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java b/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java
new file mode 100644
index 00000000..7e3fdab0
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java
@@ -0,0 +1,203 @@
+package de.thedevstack.conversationsplus.persistance;
+
+import android.database.Cursor;
+
+/**
+ * Created by steckbrief on 15.04.2016.
+ */
+public abstract class CursorHelper {
+
+ static double getDouble(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return Double.MIN_VALUE;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return Double.MIN_VALUE;
+ }
+ return getDouble(cursor, columnIndex);
+ }
+
+ static String getString(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return null;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return null;
+ }
+ return getString(cursor, columnIndex);
+ }
+
+ static float getFloat(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return Float.MIN_VALUE;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return Float.MIN_VALUE;
+ }
+ return getFloat(cursor, columnIndex);
+ }
+
+ static int getInt(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return Integer.MIN_VALUE;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return Integer.MIN_VALUE;
+ }
+ return getInt(cursor, columnIndex);
+ }
+
+ static long getLong(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return Long.MIN_VALUE;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return Long.MIN_VALUE;
+ }
+ return getLong(cursor, columnIndex);
+ }
+
+ static int getShort(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return Short.MIN_VALUE;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return Short.MIN_VALUE;
+ }
+ return getShort(cursor, columnIndex);
+ }
+
+ static byte[] getBlob(Cursor cursor, String columnName) {
+ if (null == cursor) {
+ return null;
+ }
+ int columnIndex = getColumnIndex(cursor, columnName);
+ if (columnIndex < 0) {
+ return null;
+ }
+ return getBlob(cursor, columnIndex);
+ }
+
+ /**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ * If you expect the column to exist use {@link Cursor#getColumnIndexOrThrow(String)} instead, which
+ * will make the error more clear.
+ *
+ * @param columnName the name of the target column.
+ * @return the zero-based column index for the given column name, or -1 if
+ * the column name does not exist.
+ * @see Cursor#getColumnIndexOrThrow(String)
+ */
+ static int getColumnIndex(Cursor cursor, String columnName) {
+ return cursor.getColumnIndex(columnName);
+ }
+
+ /**
+ * Returns the value of the requested column as a byte array.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null or the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a byte array.
+ */
+ static byte[] getBlob(Cursor cursor, int columnIndex) {
+ return cursor.getBlob(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as a String.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null or the column type is not a string type is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a String.
+ */
+ static String getString(Cursor cursor, int columnIndex) {
+ return cursor.getString(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as a short.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [<code>Short.MIN_VALUE</code>,
+ * <code>Short.MAX_VALUE</code>] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a short.
+ */
+ static short getShort(Cursor cursor, int columnIndex) {
+ return cursor.getShort(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as an int.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [<code>Integer.MIN_VALUE</code>,
+ * <code>Integer.MAX_VALUE</code>] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as an int.
+ */
+ static int getInt(Cursor cursor, int columnIndex) {
+ return cursor.getInt(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as a long.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [<code>Long.MIN_VALUE</code>,
+ * <code>Long.MAX_VALUE</code>] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a long.
+ */
+ static long getLong(Cursor cursor, int columnIndex) {
+ return cursor.getLong(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as a float.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null, the column type is not a floating-point type, or the
+ * floating-point value is not representable as a <code>float</code> value is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a float.
+ */
+ static float getFloat(Cursor cursor, int columnIndex) {
+ return cursor.getFloat(columnIndex);
+ }
+
+ /**
+ * Returns the value of the requested column as a double.
+ *
+ * <p>The result and whether this method throws an exception when the
+ * column value is null, the column type is not a floating-point type, or the
+ * floating-point value is not representable as a <code>double</code> value is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a double.
+ */
+ static double getDouble(Cursor cursor, int columnIndex) {
+ return cursor.getDouble(columnIndex);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java
new file mode 100644
index 00000000..ba5f4a2c
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java
@@ -0,0 +1,60 @@
+package de.thedevstack.conversationsplus.persistance;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import de.thedevstack.android.logcat.Logging;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+/**
+ * Created by steckbrief on 15.04.2016.
+ */
+public class MessageDatabaseAccess {
+ public static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters";
+ public static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload";
+ public static final String COLUMN_NAME_MSG_PARAMS_MSGUUID = "message_uuid";
+ public static final String COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION = "treatasdownloadable_decision";
+
+ public static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V0 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " ("
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', "
+ + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)";
+
+ public static ContentValues getAdditionalParametersContentValues(Message message) {
+ ContentValues additionalParameters = new ContentValues();
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0);
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name());
+
+ return additionalParameters;
+ }
+
+ public static void populateMessageParametersFromCursor(Cursor cursor, Message message) {
+ boolean isHttpUploaded = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD) == 1;
+ message.setHttpUploaded(isHttpUploaded);
+ String downloadable = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION);
+ Message.Decision treatAsDownloadable = Message.Decision.NOT_DECIDED;
+ try {
+ treatAsDownloadable = Message.Decision.valueOf(downloadable);
+ } catch (IllegalArgumentException e) {
+ // Should only happen if the database is corrupted, but to be on the save side catch it here
+ Logging.e("db.msg", "Unknown Decision for treatAsDownloadable found: '" + downloadable + "'");
+ }
+ message.setTreatAsDownloadable(treatAsDownloadable);
+ }
+
+ public static void populateMessageParameters(SQLiteDatabase db, Message message) {
+ Cursor paramsCursor = db.query(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS,
+ null, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?",
+ new String[] {message.getUuid()},
+ null, null, null, null);
+ paramsCursor.moveToNext();
+ MessageDatabaseAccess.populateMessageParametersFromCursor(paramsCursor, message);
+ paramsCursor.close();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java b/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java
new file mode 100644
index 00000000..a313c8b1
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java
@@ -0,0 +1,46 @@
+package de.thedevstack.conversationsplus.persistance.observers;
+
+import android.os.FileObserver;
+
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+
+/**
+ * Observer to mark messages containing files which are deleted.
+ */
+public class FileDeletionObserver extends FileObserver {
+ public FileDeletionObserver(String path) {
+ super(path, FileObserver.DELETE);
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (null != path) {
+ markFileDeleted(path.split("\\.")[0]);
+ }
+ }
+
+ private void markFileDeleted(String uuid) {
+ if (null != XmppConnectionServiceAccessor.xmppConnectionService) {
+ for (Conversation conversation : XmppConnectionServiceAccessor.xmppConnectionService.getConversations()) {
+ Message message = conversation.findMessageWithFileAndUuid(uuid);
+ if (message != null) {
+ message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ final int s = message.getStatus();
+ if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
+ MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
+ } else {
+ UiUpdateHelper.updateConversationUi();
+ }
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java
new file mode 100644
index 00000000..a1f99a48
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java
@@ -0,0 +1,57 @@
+package de.thedevstack.conversationsplus.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ListView;
+
+import de.thedevstack.android.logcat.adapters.LogCatArrayAdapter;
+import de.thedevstack.android.logcat.tasks.ReadLogCatAsyncTask;
+import de.thedevstack.conversationsplus.ui.listeners.LogCatOutputCopyOnClickListener;
+import de.thedevstack.conversationsplus.utils.ClipboardUtil;
+
+import eu.siacs.conversations.R;
+
+/**
+ * Activity to display the logcat output.
+ */
+public class LogCatOutputActivity extends Activity {
+ /**
+ * List adapter containing the logcat entries.
+ */
+ private LogCatArrayAdapter logCatArrayAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_logcatoutput);
+ ListView lv = (ListView)findViewById(R.id.actLogInfoOutput);
+ this.logCatArrayAdapter = new LogCatArrayAdapter(this, R.layout.list_item_logcatoutput);
+ lv.setAdapter(this.logCatArrayAdapter);
+ new ReadLogCatAsyncTask(this.logCatArrayAdapter).execute();
+ Button copyButton = (Button) findViewById(R.id.actLogOutputCopyButton);
+ copyButton.setOnClickListener(new LogCatOutputCopyOnClickListener(this.logCatArrayAdapter));
+ registerForContextMenu(lv);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, 123456789, 0, R.string.cplus_copy_item);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ if (123456789 == item.getItemId()) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+ String itemText = this.logCatArrayAdapter.getItems().get(info.position);
+ ClipboardUtil.copyToClipboard(itemText);
+ return true;
+ }
+ return super.onContextItemSelected(item);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/PresencesArrayAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/PresencesArrayAdapter.java
new file mode 100644
index 00000000..d1f1e835
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/PresencesArrayAdapter.java
@@ -0,0 +1,62 @@
+package de.thedevstack.conversationsplus.ui.adapter;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.utils.UIHelper;
+
+/**
+ * Created by tzur on 27.09.2015.
+ */
+public class PresencesArrayAdapter extends ArrayAdapter<Presence> {
+ private final Context context;
+ private final Presence[] values;
+
+ public PresencesArrayAdapter(Context context, Presences presences) {
+ super(context, R.layout.dialog_resources_status);
+ this.context = context;
+ this.values = getPresenceArray(presences);
+ addAll(this.values);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rowView = inflater.inflate(R.layout.dialog_resources_status, parent, false);
+ TextView textView = (TextView) rowView.findViewById(R.id.dlg_res_stat_resource_name);
+ textView.setText(this.values[position].resource);
+ textView.setTextColor(UIHelper.getStatusColor(this.values[position].status));
+
+ return rowView;
+ }
+
+ private static Presence[] getPresenceArray(Presences presences) {
+ ArrayList<Presence> presenceArrayList = new ArrayList<>();
+ if (null != presences && null != presences.getPresences() && !presences.getPresences().isEmpty()) {
+ for (Map.Entry<String, eu.siacs.conversations.entities.Presence> entry : presences.getPresences().entrySet()) {
+ Presence p = new Presence();
+ p.resource = entry.getKey();
+ p.status = entry.getValue().getStatus();
+ presenceArrayList.add(p);
+ }
+ presenceArrayList.trimToSize();
+ }
+ return presenceArrayList.toArray(new Presence[0]);
+ }
+}
+
+class Presence {
+ String resource;
+ eu.siacs.conversations.entities.Presence.Status status;
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/AbstractAlertDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/AbstractAlertDialog.java
new file mode 100644
index 00000000..2f394fb3
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/AbstractAlertDialog.java
@@ -0,0 +1,125 @@
+package de.thedevstack.conversationsplus.ui.dialogs;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+
+import eu.siacs.conversations.R;
+
+/**
+ * Created by tzur on 29.09.2015.
+ */
+public class AbstractAlertDialog {
+ protected AlertDialog.Builder builder;
+
+ public AbstractAlertDialog(Context context, String title) {
+ this.builder = new AlertDialog.Builder(context);
+ this.builder.setTitle(title);
+ this.builder.setPositiveButton(R.string.cplus_ok, null);
+ }
+
+ public AbstractAlertDialog(Context context, int titleTextId) {
+ this(context, context.getString(titleTextId));
+ }
+
+ public void show() {
+ this.builder.show();
+ }
+
+ public Context getContext() {
+ return builder.getContext();
+ }
+
+ public AlertDialog.Builder setTitle(int titleId) {
+ return builder.setTitle(titleId);
+ }
+
+ public AlertDialog.Builder setTitle(CharSequence title) {
+ return builder.setTitle(title);
+ }
+
+ public AlertDialog.Builder setIcon(int iconId) {
+ return builder.setIcon(iconId);
+ }
+
+ public AlertDialog.Builder setIcon(Drawable icon) {
+ return builder.setIcon(icon);
+ }
+
+ public AlertDialog.Builder setMessage(CharSequence message) {
+ return builder.setMessage(message);
+ }
+
+ public AlertDialog.Builder setMessage(int messageId) {
+ return builder.setMessage(messageId);
+ }
+
+ public AlertDialog.Builder setIconAttribute(int attrId) {
+ return builder.setIconAttribute(attrId);
+ }
+
+ public AlertDialog.Builder setPositiveButton(int textId, DialogInterface.OnClickListener listener) {
+ return builder.setPositiveButton(textId, listener);
+ }
+
+ public AlertDialog.Builder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) {
+ return builder.setPositiveButton(text, listener);
+ }
+
+ public AlertDialog.Builder setNegativeButton(int textId, DialogInterface.OnClickListener listener) {
+ return builder.setNegativeButton(textId, listener);
+ }
+
+ public AlertDialog.Builder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) {
+ return builder.setNegativeButton(text, listener);
+ }
+
+ public AlertDialog.Builder setNeutralButton(int textId, DialogInterface.OnClickListener listener) {
+ return builder.setNeutralButton(textId, listener);
+ }
+
+ public AlertDialog.Builder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) {
+ return builder.setNeutralButton(text, listener);
+ }
+
+ public AlertDialog.Builder setCancelable(boolean cancelable) {
+ return builder.setCancelable(cancelable);
+ }
+
+ public AlertDialog.Builder setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) {
+ return builder.setOnCancelListener(onCancelListener);
+ }
+
+ public AlertDialog.Builder setOnDismissListener(DialogInterface.OnDismissListener onDismissListener) {
+ return builder.setOnDismissListener(onDismissListener);
+ }
+
+ public AlertDialog.Builder setOnKeyListener(DialogInterface.OnKeyListener onKeyListener) {
+ return builder.setOnKeyListener(onKeyListener);
+ }
+
+ public AlertDialog.Builder setAdapter(ListAdapter adapter, DialogInterface.OnClickListener listener) {
+ return builder.setAdapter(adapter, listener);
+ }
+
+ public AlertDialog.Builder setCursor(Cursor cursor, DialogInterface.OnClickListener listener, String labelColumn) {
+ return builder.setCursor(cursor, listener, labelColumn);
+ }
+
+ public AlertDialog.Builder setView(View view) {
+ return builder.setView(view);
+ }
+
+ public AlertDialog.Builder setView(int layoutResId) {
+ return builder.setView(layoutResId);
+ }
+
+ public AlertDialog.Builder setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
+ return builder.setOnItemSelectedListener(listener);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java
new file mode 100644
index 00000000..4f6cffb4
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java
@@ -0,0 +1,177 @@
+package de.thedevstack.conversationsplus.ui.dialogs;
+
+import android.app.Activity;
+import android.text.format.DateFormat;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.Date;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.utils.UIHelper;
+
+/**
+ * Fills the contents to the message details dialog.
+ * The view definition is done in R.layout.dialog_message_details.
+ */
+public class MessageDetailsDialog extends AbstractAlertDialog {
+
+ /**
+ * Initializes the Message Details Dialog.
+ * @param context the context of this alert dialog (the parent activity).
+ * @param message the message to be displayed
+ */
+ public MessageDetailsDialog(Activity context, Message message) {
+ super(context, R.string.dlg_msg_details_title);
+ this.createView(context, message);
+ }
+
+ /**
+ * Creates the view for the message details alert dialog.
+ * @param context the context of this alert dialog (the parent activity).
+ * @param message the message to be displayed
+ */
+ protected void createView(Activity context, Message message) {
+ int viewId = R.layout.dialog_message_details;
+ View view = context.getLayoutInflater().inflate(viewId, null);
+
+ displayMessageSentTime(view, message);
+ displaySenderAndReceiver(view, message);
+ displayMessageTypeInfo(view, message);
+ displayMessageStatusInfo(view, message);
+ displayFileInfo(view, message);
+
+ this.setView(view);
+ }
+
+ /**
+ * Publishes file information, if message contains an image to view.
+ * @param view the dialog view
+ * @param message the message to display in dialog
+ */
+ protected void displayFileInfo(View view, Message message) {
+ if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) {
+ Logging.d("messagedetailsfile", "File is stored in path: " + message.getRelativeFilePath());
+ view.findViewById(R.id.dlgMsgDetFileTable).setVisibility(View.VISIBLE);
+ if (null != message.getFileParams()) {
+ Message.FileParams params = message.getFileParams();
+ TextViewUtil.setText(view, R.id.dlgMsgDetFileSize, UIHelper.getHumanReadableFileSize(params.size));
+ }
+ TextViewUtil.setText(view, R.id.dlgMsgDetFileMimeType, message.getMimeType());
+
+ TextViewUtil.setText(view, R.id.dlgMsgDetFileHttpUploaded, message.isHttpUploaded() ? R.string.cplus_yes : R.string.cplus_no);
+ }
+ }
+
+ /**
+ * Displays message status info to view.
+ * @param view the dialog view
+ * @param message the message to display in dialog
+ */
+ protected void displayMessageStatusInfo(View view, Message message) {
+ TextView msgStatusTextView = (TextView) view.findViewById(R.id.dlgMsgDetMsgStatus);
+ int msgStatusResId;
+ switch (message.getStatus()) {
+ case Message.STATUS_WAITING:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_waiting;
+ break;
+ case Message.STATUS_UNSEND:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_unsend;
+ break;
+ case Message.STATUS_OFFERED:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_offered;
+ break;
+ case Message.STATUS_SEND_FAILED:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_failed;
+ msgStatusTextView.setTextColor(ConversationsPlusColors.error());
+ break;
+ case Message.STATUS_RECEIVED:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_received;
+ break;
+ case Message.STATUS_SEND:
+ case Message.STATUS_SEND_DISPLAYED:
+ case Message.STATUS_SEND_RECEIVED:
+ default:
+ msgStatusResId = R.string.dlg_msg_details_msg_status_sent;
+ }
+ msgStatusTextView.setText(msgStatusResId);
+ }
+
+ /**
+ * Publishes message type information to view.
+ * @param view the dialog view
+ * @param message the message to display in dialog
+ */
+ protected void displayMessageTypeInfo(View view, Message message) {
+ int msgTypeResId;
+ switch (message.getType()) {
+ case Message.TYPE_PRIVATE:
+ msgTypeResId = R.string.dlg_msg_details_msg_type_private;
+ break;
+ case Message.TYPE_FILE:
+ msgTypeResId = R.string.dlg_msg_details_msg_type_file;
+ break;
+ case Message.TYPE_IMAGE:
+ msgTypeResId = R.string.dlg_msg_details_msg_type_image;
+ break;
+ case Message.TYPE_STATUS:
+ msgTypeResId = R.string.dlg_msg_details_msg_type_status;
+ break;
+ case Message.TYPE_TEXT:
+ default:
+ msgTypeResId = R.string.dlg_msg_details_msg_type_text;
+ }
+ TextViewUtil.setText(view, R.id.dlgMsgDetMsgType, msgTypeResId);
+ }
+
+ /**
+ * Publishes information about sending and receiving parties to view.
+ * @param view the dialog view
+ * @param message the message to display in dialog
+ */
+ protected void displaySenderAndReceiver(View view, Message message) {
+ Conversation conversation = message.getConversation();
+ // Get own resource name -> What about msg written on other client?
+ String me = conversation.getAccount().getJid().getResourcepart();
+ // Get resource name of chat partner, if available
+ String other = (null == message.getCounterpart() || message.getCounterpart().isBareJid()) ? "" : message.getCounterpart().getResourcepart();
+ Logging.d("MesageDialog", "Me: " + me + ", other: " + other);
+ TextView sender = (TextView) view.findViewById(R.id.dlgMsgDetSender);
+ TextView receipient = (TextView) view.findViewById(R.id.dlgMsgDetReceipient);
+
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ // Change label of sending and receiving party to MUC terminology
+ TextViewUtil.setText(view, R.id.dlgMsgDetLblSender, R.string.dlg_msg_details_sender_nick);
+ TextViewUtil.setText(view, R.id.dlgMsgDetLblReceipient, R.string.dlg_msg_details_receipient_nick);
+
+ // Get own nick for MUC
+ me = conversation.getMucOptions().getActualNick();
+ }
+ if (Message.STATUS_RECEIVED == message.getStatus()) {
+ // Sender was chat partner, if the status is for my account received
+ sender.setText(other);
+ // Set receipient to myself in case of normal chat or private message in MUC
+ if (conversation.getMode() == Conversation.MODE_SINGLE || Message.TYPE_PRIVATE == message.getType()) {
+ receipient.setText(me);
+ }
+ } else {
+ sender.setText(me);
+ receipient.setText(other);
+ }
+ }
+
+ /**
+ * Publishes information about message sent time to view.
+ * @param view the dialog view
+ * @param message the message to display in dialog
+ */
+ protected void displayMessageSentTime(View view, Message message) {
+ TextViewUtil.setText(view, R.id.dlgMsgDetTimeSent, DateFormat.format("dd.MM.yyyy kk:mm:ss", new Date(message.getTimeSent())));
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java
new file mode 100644
index 00000000..c29832a5
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java
@@ -0,0 +1,71 @@
+package de.thedevstack.conversationsplus.ui.dialogs;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import de.thedevstack.conversationsplus.enums.UserDecision;
+import de.thedevstack.conversationsplus.ui.listeners.UserDecisionListener;
+import eu.siacs.conversations.R;
+
+/**
+ * A dialog to give the user the choice to decide whether to do something or not.
+ * The user also has the choice to save his answer for the future.
+ * A UserDecisionListener is used to provide the functionality to be performed by clicking on yes, or no.
+ */
+public class UserDecisionDialog extends AbstractAlertDialog {
+ protected final UserDecisionListener listener;
+ protected final CheckBox rememberCheckBox;
+
+ public UserDecisionDialog(Activity context, int questionResourceId, UserDecisionListener userDecisionListener) {
+ super(context, questionResourceId);
+ this.listener = userDecisionListener;
+
+ int viewId = R.layout.dialog_userdecision;
+ View view = context.getLayoutInflater().inflate(viewId, null);
+
+ this.rememberCheckBox = (CheckBox) view.findViewById(R.id.dlgUserDecRemember);
+
+ this.setPositiveButton(R.string.cplus_yes, new PositiveOnClickListener());
+ this.setNegativeButton(R.string.cplus_no, new NegativeOnClickListener());
+ this.setView(view);
+ }
+
+ public void decide(UserDecision baseDecision) {
+ switch (baseDecision) {
+ case ALWAYS:
+ this.listener.onYes();
+ break;
+ case NEVER:
+ this.listener.onNo();
+ break;
+ case ASK:
+ this.show();
+ break;
+ }
+ }
+
+ class PositiveOnClickListener implements DialogInterface.OnClickListener {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ listener.onYes();
+ if (rememberCheckBox.isChecked()) {
+ listener.onRemember(UserDecision.ALWAYS);
+ }
+ }
+ }
+
+ class NegativeOnClickListener implements DialogInterface.OnClickListener {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ listener.onNo();
+ if (rememberCheckBox.isChecked()) {
+ listener.onRemember(UserDecision.NEVER);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/LogCatOutputCopyOnClickListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/LogCatOutputCopyOnClickListener.java
new file mode 100644
index 00000000..f71c67db
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/LogCatOutputCopyOnClickListener.java
@@ -0,0 +1,41 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import android.view.View;
+
+import java.util.List;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.android.logcat.adapters.LogCatArrayAdapter;
+import de.thedevstack.conversationsplus.utils.ClipboardUtil;
+
+/**
+ * OnClickListener to copy logcat entries from LogCatArrayAdapter to clipboard.
+ */
+public class LogCatOutputCopyOnClickListener implements View.OnClickListener {
+ private final LogCatArrayAdapter logCatOutputAdapter;
+
+ public LogCatOutputCopyOnClickListener(LogCatArrayAdapter logCatOutputAdapter) {
+ this.logCatOutputAdapter = logCatOutputAdapter;
+ }
+
+ /**
+ * Copies the entries of LogCatArrayAdapter separated by a new line to the clipboard.
+ *
+ * @param v The view that was clicked.
+ */
+ @Override
+ public void onClick(View v) {
+ Logging.d("copylogcat", "Start Copying log cat");
+ List<String> items = this.logCatOutputAdapter.getItems();
+ String textToCopy = null;
+ if (null != items && !items.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ for (String item : items) {
+ sb.append(item);
+ sb.append("\n");
+ }
+ textToCopy = sb.toString();
+ }
+ ClipboardUtil.copyToClipboard("c+logcat", textToCopy);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java
new file mode 100644
index 00000000..0cfee1d8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java
@@ -0,0 +1,210 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import android.app.PendingIntent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.widget.Toast;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.enums.UserDecision;
+import de.thedevstack.conversationsplus.exceptions.UiException;
+import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.utils.FileUtils;
+
+/**
+ * Listener to let the user decide whether to resize a picture before sending or not.
+ */
+public class ResizePictureUserDecisionListener implements UserDecisionListener {
+ protected Uri uri;
+ protected final Conversation conversation;
+ protected final UiCallback<Message> callback;
+ protected final XmppConnectionService xmppConnectionService;
+ protected final Toast prepareFileToast;
+ protected final XmppActivity activity;
+
+ public ResizePictureUserDecisionListener(XmppActivity activity, Conversation conversation, XmppConnectionService xmppConnectionService) {
+ this.xmppConnectionService = xmppConnectionService;
+ this.conversation = conversation;
+ this.activity = activity;
+ this.prepareFileToast = Toast.makeText(ConversationsPlusApplication.getAppContext(), ConversationsPlusApplication.getInstance().getText(R.string.preparing_image), Toast.LENGTH_LONG);
+ this.callback = new UiCallback<Message>() {
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ hidePrepareFileToast();
+ }
+
+ @Override
+ public void success(Message message) {
+ ResizePictureUserDecisionListener.this.xmppConnectionService.sendMessage(message);
+ }
+
+ @Override
+ public void error(int error, Message message) {
+ hidePrepareFileToast();
+ //TODO Find another way to display an error dialog
+ ResizePictureUserDecisionListener.this.activity.displayErrorDialog(error);
+ }
+
+ protected void hidePrepareFileToast() {
+ ResizePictureUserDecisionListener.this.activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ResizePictureUserDecisionListener.this.prepareFileToast.cancel();
+ }
+ });
+ }
+ };
+ }
+
+ public ResizePictureUserDecisionListener(XmppActivity activity, Conversation conversation, Uri uri, XmppConnectionService xmppConnectionService) {
+ this(activity, conversation, xmppConnectionService);
+ this.uri = uri;
+ }
+
+ public void setUri(Uri uri) {
+ this.uri = uri;
+ }
+
+ protected void showPrepareFileToast() {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ prepareFileToast.show();
+ }
+ });
+ }
+
+ @Override
+ public void onYes() {
+ this.showPrepareFileToast();
+ final Message message;
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "", conversation.getNextEncryption());
+ }
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_IMAGE);
+ ConversationsPlusApplication.executeFileAdding(new OnYesRunnable(message, uri));
+ }
+
+ @Override
+ public void onNo() {
+ this.showPrepareFileToast();
+ final Message message;
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "", conversation.getNextEncryption());
+ }
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_IMAGE);
+ ConversationsPlusApplication.executeFileAdding(new OnNoRunnable(message, uri));
+ }
+
+ @Override
+ public void onRemember(UserDecision decision) {
+ ConversationsPlusPreferences.applyResizePicture(decision);
+ }
+
+ private abstract class OnClickRunnable implements Runnable {
+
+ protected final Message message;
+ protected final Uri uri;
+
+ public OnClickRunnable(Message message, Uri uri) {
+ this.message = message;
+ this.uri = uri;
+ }
+ }
+
+ private class OnNoRunnable extends OnClickRunnable {
+
+ public OnNoRunnable(Message message, Uri uri) {
+ super(message, uri);
+ }
+
+ @Override
+ public void run() {
+ InputStream is = null;
+ try {
+ is = StreamUtil.openInputStreamFromContentResolver(uri);
+ long imageSize = is.available();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, options);
+ int imageHeight = options.outHeight;
+ int imageWidth = options.outWidth;
+ String filePath = FileUtils.getPath(uri);
+ MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight);
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ PgpEngine.getInstance().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileNotFoundException e) {
+ Logging.e("picturesending", "File not found to send not resized. " + e.getMessage());
+ callback.error(R.string.error_file_not_found, message);
+ } catch (IOException e) {
+ Logging.e("picturesending", "Error while sending not resized picture. " + e.getMessage());
+ callback.error(R.string.error_io_exception, message);
+ } finally {
+ if (null != is) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ Logging.w("picturesending", "Error while closing stream for sending not resized picture. " + e.getMessage());
+ }
+ }
+ }
+ }
+ }
+
+ private class OnYesRunnable extends OnClickRunnable {
+
+ public OnYesRunnable(Message message, Uri uri) {
+ super(message, uri);
+ }
+
+ @Override
+ public void run() {
+ try {
+ Bitmap resizedAndRotatedImage = ImageUtil.resizeAndRotateImage(uri);
+ DownloadableFile file = FileBackend.compressImageAndCopyToPrivateStorage(message, resizedAndRotatedImage);
+ String filePath = file.getAbsolutePath();
+ long imageSize = file.getSize();
+ int imageWidth = resizedAndRotatedImage.getWidth();
+ int imageHeight = resizedAndRotatedImage.getHeight();
+ MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight);
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ PgpEngine.getInstance().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (final UiException e) {
+ Logging.e("pictureresizesending", "Error while sending resized picture. " + e.getMessage());
+ callback.error(e.getResId(), message);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShareWithResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShareWithResizePictureUserDecisionListener.java
new file mode 100644
index 00000000..7455cf97
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShareWithResizePictureUserDecisionListener.java
@@ -0,0 +1,48 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import android.net.Uri;
+
+import java.util.List;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.ui.XmppActivity;
+
+/**
+ * Created by tzur on 03.11.2015.
+ */
+public class ShareWithResizePictureUserDecisionListener extends ResizePictureUserDecisionListener {
+ protected final List<Uri> uris;
+
+ public ShareWithResizePictureUserDecisionListener(XmppActivity activity, Conversation conversation, XmppConnectionService xmppConnectionService, List<Uri> uris) {
+ super(activity, conversation, xmppConnectionService);
+ this.uris = uris;
+ }
+
+ @Override
+ public void onYes() {
+ if (null != this.uris && !this.uris.isEmpty()) {
+ for (Uri uri : this.uris) {
+ this.setUri(uri);
+ super.onYes();
+ }
+ }
+ this.finishSharing();
+ }
+
+ @Override
+ public void onNo() {
+ if (null != this.uris && !this.uris.isEmpty()) {
+ for (Uri uri : this.uris) {
+ this.setUri(uri);
+ super.onNo();
+ }
+ }
+ this.finishSharing();
+ }
+
+ protected void finishSharing() {
+ this.activity.switchToConversation(conversation, null, true);
+ this.activity.finish();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShowResourcesListDialogListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShowResourcesListDialogListener.java
new file mode 100644
index 00000000..1c16095c
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ShowResourcesListDialogListener.java
@@ -0,0 +1,47 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import android.content.Context;
+import android.view.View;
+
+import de.thedevstack.conversationsplus.ui.adapter.PresencesArrayAdapter;
+import de.thedevstack.conversationsplus.ui.dialogs.AbstractAlertDialog;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+
+/**
+ * This listener shows the dialog with the resources of a contact.
+ * The resources are shown with the color of their current online mode.
+ * This listener implements OnClickListener and OnLongClickListener.
+ */
+public class ShowResourcesListDialogListener extends AbstractAlertDialog implements View.OnClickListener, View.OnLongClickListener {
+ private Contact contact;
+
+ public ShowResourcesListDialogListener(Context context, Contact contact) {
+ super(context, getTitle(context, contact));
+ this.contact = contact;
+ this.init();
+ }
+
+ private static final String getTitle(Context context, Contact contact) {
+ if (null != contact && null != contact.getJid() && null != contact.getJid().toBareJid()) {
+ int presenceCount = null != contact.getPresences() ? contact.getPresences().size() : 0;
+ return context.getString(R.string.dlg_resources_title, contact.getJid().toBareJid().toString(), presenceCount);
+ }
+ return null != contact ? contact.toString() : "";
+ }
+
+ protected void init() {
+ this.builder.setAdapter(new PresencesArrayAdapter(getContext(), this.contact.getPresences()), null);
+ }
+
+ @Override
+ public void onClick(View v) {
+ this.show();
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ this.show();
+ return true;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java
new file mode 100644
index 00000000..fbee6290
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java
@@ -0,0 +1,12 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import de.thedevstack.conversationsplus.enums.UserDecision;
+
+/**
+ * Created by tzur on 31.10.2015.
+ */
+public interface UserDecisionListener {
+ void onYes();
+ void onNo();
+ void onRemember(UserDecision decision);
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/preferences/LogInformationPreference.java b/src/main/java/de/thedevstack/conversationsplus/ui/preferences/LogInformationPreference.java
new file mode 100644
index 00000000..5dcfc607
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/preferences/LogInformationPreference.java
@@ -0,0 +1,31 @@
+package de.thedevstack.conversationsplus.ui.preferences;
+
+import android.content.Context;
+import android.content.Intent;
+import android.preference.Preference;
+import android.util.AttributeSet;
+
+import de.thedevstack.conversationsplus.ui.LogCatOutputActivity;
+
+/**
+ * Created by tzur on 07.10.2015.
+ */
+public class LogInformationPreference extends Preference {
+ public LogInformationPreference(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public LogInformationPreference(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+ public LogInformationPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+ final Intent intent = new Intent(getContext(), LogCatOutputActivity.class);
+ getContext().startActivity(intent);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java
new file mode 100644
index 00000000..b6ffd570
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java
@@ -0,0 +1,203 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+
+/**
+ * This util provides access to saved avatars, creating avatars.
+ */
+public final class AvatarUtil {
+
+ /**
+ * Get the PEP Avatar.
+ * TODO: Why PEP Avatar?
+ * @param image the uri to the avatar's image
+ * @param size the image width/height to resize to
+ * @param format the format for the avatar
+ * @return the avatar
+ */
+ public static Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
+ try {
+ Avatar avatar = new Avatar();
+ Bitmap bm = ImageUtil.cropCenterSquare(image, size);
+ if (bm == null) {
+ return null;
+ }
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
+ mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(
+ mBase64OutputSttream, digest);
+ if (!bm.compress(format, 75, mDigestOutputStream)) {
+ return null;
+ }
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ return avatar;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } catch (IOException e) {
+ return null;
+ }
+
+ }
+
+ public static Avatar getStoredPepAvatar(String hash) {
+ if (hash == null) {
+ return null;
+ }
+ Avatar avatar = new Avatar();
+ File file = new File(getAvatarPath(hash));
+ FileInputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ is = new FileInputStream(file);
+ ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
+ byte[] buffer = new byte[4096];
+ int length;
+ while ((length = is.read(buffer)) > 0) {
+ os.write(buffer, 0, length);
+ }
+ os.flush();
+ os.close();
+ avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ avatar.image = new String(mByteArrayOutputStream.toByteArray());
+ avatar.height = options.outHeight;
+ avatar.width = options.outWidth;
+ return avatar;
+ } catch (IOException e) {
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ } finally {
+ StreamUtil.close(is);
+ }
+ }
+
+ /**
+ * Returns whether the avatar is cached or not.
+ * @param avatar the avatar to check the existance
+ * @return <code>true</code> if the file of the avatar exists, <code>false</code> otherwise
+ */
+ public static boolean isAvatarCached(Avatar avatar) {
+ File file = new File(getAvatarPath(avatar.getFilename()));
+ return file.exists();
+ }
+
+ /**
+ * Saves an avatar to the file system.
+ * All exceptions are silently ignored.
+ * TODO: Move real saving operation to FileBackend
+ * @param avatar the avatar to save
+ * @return <code>true</code> if the avatar was saved successfully, <code>false</code> otherwise.
+ */
+ public static boolean save(Avatar avatar) {
+ File file;
+ if (isAvatarCached(avatar)) {
+ file = new File(getAvatarPath(avatar.getFilename()));
+ } else {
+ String filename = getAvatarPath(avatar.getFilename());
+ file = new File(filename + ".tmp");
+ file.getParentFile().mkdirs();
+ OutputStream os = null;
+ try {
+ file.createNewFile();
+ os = new FileOutputStream(file);
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.reset();
+ DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
+ mDigestOutputStream.write(avatar.getImageAsBytes());
+ mDigestOutputStream.flush();
+ mDigestOutputStream.close();
+ String sha1sum = CryptoHelper.bytesToHex(digest.digest());
+ if (sha1sum.equals(avatar.sha1sum)) {
+ file.renameTo(new File(filename));
+ } else {
+ Logging.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner);
+ file.delete();
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ } catch (NoSuchAlgorithmException e) {
+ return false;
+ } finally {
+ StreamUtil.close(os);
+ }
+ }
+ avatar.size = file.length();
+ return true;
+ }
+
+ /**
+ * Returns the avatar for an uri.
+ * @param avatar the avatar's uri
+ * @param size the height/width the avatar should have
+ * @return the bitmap of the uri
+ */
+ public static Bitmap getAvatar(String avatar, int size) {
+ if (avatar == null) {
+ return null;
+ }
+ Bitmap bm = ImageUtil.cropCenter(getAvatarUri(avatar), size, size);
+ if (bm == null) {
+ return null;
+ }
+ return bm;
+ }
+
+ /**
+ * Returns the path to an avatar
+ * @param avatar the name of the avatar.
+ * @return the path as string
+ */
+ public static String getAvatarPath(String avatar) {
+ return ConversationsPlusApplication.getInstance().getFilesDir().getAbsolutePath()+ "/avatars/" + avatar;
+ }
+
+ /**
+ * Returns the path to an avatar as an uri.
+ * @param avatar the name of the avatar
+ * @return the path as uri
+ */
+ public static Uri getAvatarUri(String avatar) {
+ return Uri.parse("file:" + getAvatarPath(avatar));
+ }
+
+ /**
+ * Avoid instantiation it's an helper class.
+ */
+ private AvatarUtil() {
+ // Static helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ClipboardUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ClipboardUtil.java
new file mode 100644
index 00000000..4d6220e0
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ClipboardUtil.java
@@ -0,0 +1,48 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.widget.Toast;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.R;
+
+/**
+ * Util class to work with the Clipboard.
+ */
+public final class ClipboardUtil {
+ private static final String CLIPBOARD_LABEL = "c+clipboard";
+
+ /**
+ * Copies a text to the clipboard.
+ * @param clipboardLabel the label to show to a user to allow identifying the text in clipboard.
+ * @param text the text to copy
+ */
+ public static void copyToClipboard(String clipboardLabel, String text) {
+ Context context = ConversationsPlusApplication.getAppContext();
+ if (null != text && !text.isEmpty()) {
+ String label = (null == clipboardLabel) ? CLIPBOARD_LABEL : clipboardLabel;
+ ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText(label, text);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(context, R.string.cplus_copied_to_clipboard, Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(context, R.string.cplus_not_copied_to_clipboard_empty, Toast.LENGTH_LONG).show();
+ }
+
+ }
+
+ /**
+ * Copies a text to the clipboard.
+ * @param text the text to copy
+ */
+ public static void copyToClipboard(String text) {
+ copyToClipboard(CLIPBOARD_LABEL, text);
+ }
+
+ private ClipboardUtil() {
+ // helper class - avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java
new file mode 100644
index 00000000..958e9de8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java
@@ -0,0 +1,77 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.net.Uri;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.exceptions.FileCopyException;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.UiCallback;
+import eu.siacs.conversations.utils.FileUtils;
+
+/**
+ * Utility class to work with conversations.
+ */
+public class ConversationUtil {
+
+ public static void attachLocationToConversation(final Conversation conversation,
+ final Uri uri,
+ final UiCallback<Message> callback) {
+ int encryption = conversation.getNextEncryption();
+ if (encryption == Message.ENCRYPTION_PGP) {
+ encryption = Message.ENCRYPTION_DECRYPTED;
+ }
+ Message message = new Message(conversation, uri.toString(), encryption);
+ if (conversation.getNextCounterpart() != null) {
+ message.setCounterpart(conversation.getNextCounterpart());
+ }
+ if (encryption == Message.ENCRYPTION_DECRYPTED) {
+ PgpEngine.getInstance().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ }
+
+ public static void attachFileToConversation(final Conversation conversation,
+ final Uri uri,
+ final UiCallback<Message> callback) {
+ final Message message;
+ if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
+ message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
+ } else {
+ message = new Message(conversation, "", conversation.getNextEncryption());
+ }
+ message.setCounterpart(conversation.getNextCounterpart());
+ message.setType(Message.TYPE_FILE);
+ String path = FileUtils.getPath(uri);
+ if (path != null) {
+ message.setRelativeFilePath(path);
+ MessageUtil.updateFileParams(message);
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ PgpEngine.getInstance().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } else {
+ ConversationsPlusApplication.executeFileAdding(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ FileBackend.copyFileToPrivateStorage(message, uri);
+ MessageUtil.updateFileParams(message);
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ PgpEngine.getInstance().encrypt(message, callback);
+ } else {
+ callback.success(message);
+ }
+ } catch (FileCopyException e) {
+ callback.error(e.getResId(), message);
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java
new file mode 100644
index 00000000..b28e6f1c
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java
@@ -0,0 +1,372 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.util.LruCache;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.exceptions.ImageResizeException;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.utils.ExifHelper;
+
+/**
+ * This util provides
+ */
+public final class ImageUtil {
+
+ private static int IMAGE_SIZE = 1920;
+ private static LruCache<String, Bitmap> BITMAP_CACHE;
+
+ /**
+ * Returns a bitmap from the cache.
+ * @see LruCache#get(Object) for details
+ * @param key the key of the bitmap to get
+ * @return the bitmap
+ */
+ public static Bitmap getBitmapFromCache(String key) {
+ return BITMAP_CACHE.get(key);
+ }
+
+ /**
+ * Adds a bitmap with the given key to the cache.
+ * @see LruCache#put(Object, Object) for details
+ * @param key the key to identify this bitmap
+ * @param bitmap the bitmap to cache
+ */
+ public static void addBitmapToCache(String key, Bitmap bitmap) {
+ BITMAP_CACHE.put(key, bitmap);
+ }
+
+ /**
+ * Removes the bitmap with given key from the cache.
+ * @param key the key of the bitmap to remove
+ */
+ public static void removeBitmapFromCache(String key) {
+ BITMAP_CACHE.remove(key);
+ }
+
+ /**
+ * Clears the cache.
+ * @see LruCache#evictAll() for more details.
+ */
+ public static void evictBitmapCache() {
+ BITMAP_CACHE.evictAll();
+ }
+
+ /**
+ * Initializes the bitmap cache.
+ * This has to be executed once on application start.
+ * @see LruCache#LruCache(int) for details
+ */
+ public static void initBitmapCache() {
+ Logging.i("Conversations+ImageUtil", "Initializing BitmapCache");
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 8;
+ BITMAP_CACHE = new LruCache<String, Bitmap>(cacheSize) {
+ @Override
+ protected int sizeOf(final String key, final Bitmap bitmap) {
+ return bitmap.getByteCount() / 1024;
+ }
+ };
+ }
+
+ /**
+ * Resizes a given bitmap and return a new and resized bitmap.
+ * The bitmap is only resized if either the width or the height of the original bitmap is smaller than the given size.
+ * @param originalBitmap the bitmap to resize
+ * @param size the size to scale to
+ * @return new and resized bitmap or the original bitmap if width and height are smaller than size
+ */
+ public static Bitmap resize(Bitmap originalBitmap, int size) {
+ int w = originalBitmap.getWidth();
+ int h = originalBitmap.getHeight();
+ if (Math.max(w, h) > size) {
+ int scalledW;
+ int scalledH;
+ if (w <= h) {
+ scalledW = (int) (w / ((double) h / size));
+ scalledH = size;
+ } else {
+ scalledW = size;
+ scalledH = (int) (h / ((double) w / size));
+ }
+ return Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true);
+ } else {
+ return originalBitmap;
+ }
+ }
+
+ /**
+ * Resizes and rotates an image given by uri and returns the bitmap.
+ * @param image the uri of the image to be resized and rotated
+ * @return resized and rotated bitmap
+ * @throws ImageResizeException
+ */
+ public static Bitmap resizeAndRotateImage(Uri image) throws ImageResizeException {
+ return ImageUtil.resizeAndRotateImage(image, 0);
+ }
+
+ /**
+ * Resizes and rotates an image given by uri and returns the bitmap.
+ * @param image the uri of the image to be resized and rotated
+ * @return resized and rotated bitmap
+ * @throws ImageResizeException
+ */
+ private static Bitmap resizeAndRotateImage(Uri image, int sampleSize) throws ImageResizeException {
+ InputStream imageInputStream = null;
+ try {
+ imageInputStream = StreamUtil.openInputStreamFromContentResolver(image);
+ Bitmap originalBitmap;
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ int inSampleSize = (int) Math.pow(2, sampleSize);
+ Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize);
+ options.inSampleSize = inSampleSize;
+ originalBitmap = BitmapFactory.decodeStream(imageInputStream, null, options);
+ imageInputStream.close();
+ if (originalBitmap == null) {
+ throw new ImageResizeException(R.string.error_not_an_image_file);
+ }
+ Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE);
+ int rotation = ImageUtil.getRotation(image);
+ if (rotation > 0) {
+ scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation);
+ }
+
+ return scaledBitmap;
+ } catch (FileNotFoundException e) {
+ throw new ImageResizeException(R.string.error_file_not_found);
+ } catch (IOException e) {
+ throw new ImageResizeException(R.string.error_io_exception);
+ } catch (OutOfMemoryError e) {
+ ++sampleSize;
+ if (sampleSize <= 3) {
+ return resizeAndRotateImage(image, sampleSize);
+ } else {
+ throw new ImageResizeException(R.string.error_out_of_memory);
+ }
+ } finally {
+ StreamUtil.close(imageInputStream);
+ }
+ }
+
+ /**
+ * Returns the rotation from the exif information of an image identified with the given uri.
+ * The orientation is retrieved by parsing the stream of the image.
+ * FileNotFoundException is silently ignored.
+ * @param image the uri of the image to get the rotation
+ * @return the rotation value for the image, <code>0</code> if the file cannot be found.
+ */
+ public static int getRotation(Uri image) {
+ InputStream is = null;
+ try {
+ is = StreamUtil.openInputStreamFromContentResolver(image);
+ return ExifHelper.getOrientation(is);
+ } catch (FileNotFoundException e) {
+ return 0;
+ } finally {
+ StreamUtil.close(is);
+ }
+ }
+
+ /**
+ * Returns a thumbnail for a bitmap in a message.
+ * @param message the message to get the thumbnail for
+ * @param size the size to resize the original image to
+ * @param cacheOnly whether only cached images should be returned or not
+ * @return the resized thumbail
+ * @throws FileNotFoundException if the original image does not exist anymore or an IOException occurs.
+ */
+ public static Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
+ throws FileNotFoundException {
+ Bitmap thumbnail = ImageUtil.getBitmapFromCache(message.getUuid());
+ if ((thumbnail == null) && (!cacheOnly)) {
+ File file = FileBackend.getFile(message);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(file, size);
+ Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
+ if (fullsize == null) {
+ throw new FileNotFoundException();
+ }
+ thumbnail = resize(fullsize, size);
+ thumbnail = rotate(thumbnail, file.getAbsolutePath());
+
+ ImageUtil.addBitmapToCache(message.getUuid(), thumbnail);
+ }
+ return thumbnail;
+ }
+
+ /**
+ * Rotates an bitmap. Only the values 90°, 180° and 270° are considered to rotate the image.
+ * The orientation information is read using the ExifInterface.
+ * @param original the original bitmap
+ * @param srcPath the path to the original bitmap (used to read the exif information)
+ * @return rotated bitmap, or original bitmap if criteria are not met
+ * @throws IOException
+ */
+ public static Bitmap rotate(Bitmap original, String srcPath) {
+ try {
+ ExifInterface exif = new ExifInterface(srcPath);
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
+ int rotation = 0;
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ rotation = 90;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ rotation = 180;
+ break;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ rotation = 270;
+ break;
+ }
+ if (rotation > 0) {
+ return rotate(original, rotation);
+ }
+ } catch (IOException e) {
+ Logging.w("filebackend", "Error while rotating image, returning original (" + e.getMessage() + ")");
+ }
+ return original;
+ }
+
+ /**
+ * Rotates a bitmap with given degrees.
+ * @param bitmap the bitmap to be rotated
+ * @param degree the degrees to rotate the bitmap
+ * @return a newly created bitmap
+ */
+ public static Bitmap rotate(Bitmap bitmap, int degree) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ Matrix mtx = new Matrix();
+ mtx.postRotate(degree);
+ return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
+ }
+
+
+ public static Bitmap cropCenterSquare(Uri image, int size) {
+ if (image == null) {
+ return null;
+ }
+ InputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image, size);
+ is = StreamUtil.openInputStreamFromContentResolver(image);
+ Bitmap input = BitmapFactory.decodeStream(is, null, options);
+ if (input == null) {
+ return null;
+ } else {
+ int rotation = getRotation(image);
+ if (rotation > 0) {
+ input = rotate(input, rotation);
+ }
+ return cropCenterSquare(input, size);
+ }
+ } catch (FileNotFoundException e) {
+ return null;
+ } finally {
+ StreamUtil.close(is);
+ }
+ }
+
+ public static Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
+ if (image == null) {
+ return null;
+ }
+ InputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth));
+ is = StreamUtil.openInputStreamFromContentResolver(image);
+ Bitmap source = BitmapFactory.decodeStream(is, null, options);
+ if (source == null) {
+ return null;
+ }
+ int sourceWidth = source.getWidth();
+ int sourceHeight = source.getHeight();
+ float xScale = (float) newWidth / sourceWidth;
+ float yScale = (float) newHeight / sourceHeight;
+ float scale = Math.max(xScale, yScale);
+ float scaledWidth = scale * sourceWidth;
+ float scaledHeight = scale * sourceHeight;
+ float left = (newWidth - scaledWidth) / 2;
+ float top = (newHeight - scaledHeight) / 2;
+
+ RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
+ Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(dest);
+ canvas.drawBitmap(source, null, targetRect, null);
+ return dest;
+ } catch (FileNotFoundException e) {
+ return null;
+ } finally {
+ StreamUtil.close(is);
+ }
+ }
+
+ public static Bitmap cropCenterSquare(Bitmap input, int size) {
+ int w = input.getWidth();
+ int h = input.getHeight();
+
+ float scale = Math.max((float) size / h, (float) size / w);
+
+ float outWidth = scale * w;
+ float outHeight = scale * h;
+ float left = (size - outWidth) / 2;
+ float top = (size - outHeight) / 2;
+ RectF target = new RectF(left, top, left + outWidth, top + outHeight);
+
+ Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+ canvas.drawBitmap(input, null, target, null);
+ return output;
+ }
+
+ public static int calcSampleSize(Uri image, int size) throws FileNotFoundException {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options);
+ return calcSampleSize(options, size);
+ }
+
+ public static int calcSampleSize(File image, int size) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(image.getAbsolutePath(), options);
+ return calcSampleSize(options, size);
+ }
+
+ public static int calcSampleSize(BitmapFactory.Options options, int size) {
+ int height = options.outHeight;
+ int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > size || width > size) {
+ int halfHeight = height / 2;
+ int halfWidth = width / 2;
+
+ while ((halfHeight / inSampleSize) > size
+ && (halfWidth / inSampleSize) > size) {
+ inSampleSize *= 2;
+ }
+ }
+ return inSampleSize;
+ }
+
+ private ImageUtil() {
+ // Static helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java
new file mode 100644
index 00000000..ca24bd1d
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java
@@ -0,0 +1,125 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.graphics.BitmapFactory;
+
+import java.net.URL;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.FileBackend;
+
+/**
+ * Utility class to work with messages.
+ */
+public final class MessageUtil {
+
+
+ public static boolean markMessage(Conversation conversation, String uuid, int status) {
+ if (uuid == null) {
+ return false;
+ } else {
+ Message message = conversation.findSentMessageWithUuid(uuid);
+ if (message != null) {
+ markMessage(message, status);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ public static void markMessage(Message message, int status) {
+ if (status == Message.STATUS_SEND_FAILED
+ && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
+ .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
+ return;
+ }
+ message.setStatus(status);
+ DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateMessage(message);
+ UiUpdateHelper.updateConversationUi();
+ }
+
+ public static boolean wasHighlightedOrPrivate(final Message message) {
+ final String nick = message.getConversation().getMucOptions().getActualNick();
+ final Pattern highlight = generateNickHighlightPattern(nick);
+ if (message.getBody() == null || nick == null) {
+ return false;
+ }
+ final Matcher m = highlight.matcher(message.getBody());
+ return (m.find() || message.getType() == Message.TYPE_PRIVATE);
+ }
+
+ private static Pattern generateNickHighlightPattern(final String nick) {
+ // We expect a word boundary, i.e. space or start of string, followed by
+ // the
+ // nick (matched in case-insensitive manner), followed by optional
+ // punctuation (for example "bob: i disagree" or "how are you alice?"),
+ // followed by another word boundary.
+ return Pattern.compile("\\b" + Pattern.quote(nick) + "\\p{Punct}?\\b",
+ Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+ }
+
+ public static void updateMessageWithImageDetails(Message message, String filePath, long size, int imageWidth, int imageHeight) {
+ message.setRelativeFilePath(filePath);
+ MessageUtil.updateMessageBodyWithImageParams(message, size, imageWidth, imageHeight);
+ }
+
+ public static void updateFileParams(Message message) {
+ updateFileParams(message, null);
+ }
+
+ public static void updateFileParams(Message message, URL url) {
+ DownloadableFile file = FileBackend.getFile(message);
+ int imageWidth = -1;
+ int imageHeight = -1;
+ if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ imageHeight = options.outHeight;
+ imageWidth = options.outWidth;
+ }
+
+ MessageUtil.updateMessageBodyWithFileParams(message, url, file.getSize(), imageWidth, imageHeight);
+ }
+
+ private static void updateMessageBodyWithFileParams(Message message, URL url, long fileSize, int imageWidth, int imageHeight) {
+ message.setBody(MessageUtil.getMessageBodyWithImageParams(url, fileSize, imageWidth, imageHeight));
+ }
+
+ private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) {
+ MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight);
+ }
+
+ private static void updateMessageBodyWithImageParams(Message message, URL url, long size, int imageWidth, int imageHeight) {
+ message.setBody(MessageUtil.getMessageBodyWithImageParams(url, size, imageWidth, imageHeight));
+ }
+
+ private static String getMessageBodyWithImageParams(URL url, long size, int imageWidth, int imageHeight) {
+ StringBuilder sb = new StringBuilder();
+ if (null != url) {
+ sb.append(url.toString());
+ sb.append('|');
+ }
+ sb.append(size);
+ if (-1 < imageWidth) {
+ sb.append('|');
+ sb.append(imageWidth);
+ }
+ if (-1 < imageHeight) {
+ sb.append('|');
+ sb.append(imageHeight);
+ }
+ return sb.toString();
+ }
+
+ private MessageUtil() {
+ // Static helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java
new file mode 100644
index 00000000..729bdf11
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java
@@ -0,0 +1,63 @@
+package de.thedevstack.conversationsplus.utils;
+
+import android.net.Uri;
+
+import java.io.Closeable;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+/**
+ * Util to handle streams.
+ */
+public final class StreamUtil {
+
+ /**
+ * Opens an InputStream from Uri using the ContentResolver from application.
+ * @see android.content.ContentResolver#openInputStream(Uri)
+ * @param uri the uri to open
+ * @return the InputStream for given uri
+ * @throws FileNotFoundException if the provided URI could not be opened.
+ */
+ public static InputStream openInputStreamFromContentResolver(Uri uri) throws FileNotFoundException {
+ return ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri);
+ }
+
+ /**
+ * Closes a stream.
+ * IOException is silently ignored.
+ * @param stream the stream to close
+ */
+ public static void close(Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * Closes a socket.
+ * IOException is silently ignored.
+ * @param socket the socket to close
+ */
+ public static void close(Socket socket) {
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * Avoid instantiation of util class.
+ */
+ private StreamUtil() {
+ // Static helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/UiUpdateHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/UiUpdateHelper.java
new file mode 100644
index 00000000..84ce200a
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/UiUpdateHelper.java
@@ -0,0 +1,52 @@
+package de.thedevstack.conversationsplus.utils;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+/**
+ * Helper class to avoid passing the xmppConnectionService to everywhere just to update the UI.
+ * TODO: Make even this helper class work without XmppConnectionService
+ */
+public class UiUpdateHelper {
+ private static XmppConnectionService xmppConnectionService;
+
+ public static void initXmppConnectionService(XmppConnectionService xmppConnectionService) {
+ if (null == UiUpdateHelper.xmppConnectionService) {
+ UiUpdateHelper.xmppConnectionService = xmppConnectionService;
+ } else {
+ Logging.e("UiUpdateHelper", "XMPP Connection Service already instantiated.");
+ }
+ }
+
+ public static void updateConversationUi() {
+ if (null != UiUpdateHelper.xmppConnectionService) {
+ UiUpdateHelper.xmppConnectionService.updateConversationUi();
+ } else {
+ Logging.e("UiUpdateHelper", "XMPP Connection Service not initialized. Conversation Ui not updated.");
+ }
+ }
+
+ public static void updateAccountUi() {
+ if (null != UiUpdateHelper.xmppConnectionService) {
+ UiUpdateHelper.xmppConnectionService.updateAccountUi();
+ } else {
+ Logging.e("UiUpdateHelper", "XMPP Connection Service not initialized. Account Ui not updated.");
+ }
+ }
+
+ public static void updateRosterUi() {
+ if (null != UiUpdateHelper.xmppConnectionService) {
+ UiUpdateHelper.xmppConnectionService.updateRosterUi();
+ } else {
+ Logging.e("UiUpdateHelper", "XMPP Connection Service not initialized. Roster Ui not updated.");
+ }
+ }
+
+ public static void updateMucRosterUi() {
+ if (null != UiUpdateHelper.xmppConnectionService) {
+ UiUpdateHelper.xmppConnectionService.updateMucRosterUi();
+ } else {
+ Logging.e("UiUpdateHelper", "XMPP Connection Service not initialized. MUC Roster Ui not updated.");
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/XmppConnectionServiceAccessor.java b/src/main/java/de/thedevstack/conversationsplus/utils/XmppConnectionServiceAccessor.java
new file mode 100644
index 00000000..20cd7361
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/XmppConnectionServiceAccessor.java
@@ -0,0 +1,32 @@
+package de.thedevstack.conversationsplus.utils;
+
+import de.thedevstack.android.logcat.Logging;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+
+/**
+ * Accessor utility to access XmppConnectionService without having to pass the XmppConnectionService every time.
+ */
+public final class XmppConnectionServiceAccessor {
+ public static XmppConnectionService xmppConnectionService;
+
+ /**
+ * Initializes the XmppConnectionService.
+ * This method needs to be called once in XmppConnectionService#onCreate.
+ * @param xmppConnectionService
+ */
+ public static void initXmppConnectionService(XmppConnectionService xmppConnectionService) {
+ if (null == XmppConnectionServiceAccessor.xmppConnectionService) {
+ XmppConnectionServiceAccessor.xmppConnectionService = xmppConnectionService;
+ } else {
+ Logging.e("XmppConnectionServiceAccessor", "XMPP Connection Service already instantiated.");
+ }
+ }
+
+ /**
+ * Avoid instantiation
+ */
+ private XmppConnectionServiceAccessor() {
+ // avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/XmppSendUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/XmppSendUtil.java
new file mode 100644
index 00000000..d4a555f2
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/XmppSendUtil.java
@@ -0,0 +1,34 @@
+package de.thedevstack.conversationsplus.utils;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
+/**
+ * Created by tzur on 09.01.2016.
+ */
+public class XmppSendUtil {
+ public static void sendIqPacket(Account account, IqPacket packet, OnIqPacketReceived callback) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendIqPacket(packet, callback);
+ }
+ }
+
+ public static void sendPresencePacket(Account account, PresencePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendPresencePacket(packet);
+ }
+ }
+
+ public static void sendMessagePacket(Account account, MessagePacket packet) {
+ XmppConnection connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.sendMessagePacket(packet);
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java
new file mode 100644
index 00000000..a775dad6
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java
@@ -0,0 +1,91 @@
+package de.thedevstack.conversationsplus.utils.ui;
+
+import android.support.annotation.StringRes;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Created by steckbrief on 29.03.2016.
+ */
+public final class TextViewUtil {
+
+ public static void setText(View parentView, int textViewId, CharSequence text) {
+ TextView tv = (TextView) parentView.findViewById(textViewId);
+ if (null != tv) {
+ tv.setText(text);
+ }
+ }
+
+ public static void setText(View parentView, int textViewId, int textResId) {
+ TextView tv = (TextView) parentView.findViewById(textViewId);
+ if (null != tv) {
+ tv.setText(textResId);
+ }
+ }
+
+ public static void enable(TextView tv) {
+ setColorEnabledAndTextResId(tv, null, true, null);
+ }
+
+ public static void enable(TextView tv, String text) {
+ setColorEnabledAndText(tv, null, true, text);
+ }
+
+ public static void enable(TextView tv, Integer color) {
+ setColorEnabledAndTextResId(tv, color, true, null);
+ }
+
+ public static void enable(TextView tv, Integer color, @StringRes Integer resid) {
+ setColorEnabledAndTextResId(tv, color, true, resid);
+ }
+
+ public static void disable(TextView tv) {
+ setColorEnabledAndTextResId(tv, null, false, null);
+ }
+
+ public static void disable(TextView tv, String text) {
+ setColorEnabledAndText(tv, null, false, text);
+ }
+
+ public static void disable(TextView tv, Integer color) {
+ setColorEnabledAndTextResId(tv, color, false, null);
+ }
+
+ public static void disable(TextView tv, Integer color, @StringRes Integer resid) {
+ setColorEnabledAndTextResId(tv, color, false, resid);
+ }
+
+ public static void setColor(TextView tv, Integer color) {
+ setColorEnabledAndTextResId(tv, color, null, null);
+ }
+
+ public static void setColorEnabledAndTextResId(TextView tv, Integer color, Boolean enabled, @StringRes Integer resid) {
+ if (null != color) {
+ tv.setTextColor(color);
+ }
+
+ if (enabled != null) {
+ tv.setEnabled(enabled);
+ }
+ if (resid != null) {
+ tv.setText(resid);
+ }
+ }
+
+ public static void setColorEnabledAndText(TextView tv, Integer color, Boolean enabled, String text) {
+ if (null != color) {
+ tv.setTextColor(color);
+ }
+
+ if (enabled != null) {
+ tv.setEnabled(enabled);
+ }
+ if (text != null) {
+ tv.setText(text);
+ }
+ }
+
+ private TextViewUtil() {
+ // avoid instantiation - helper class
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketGenerator.java
new file mode 100644
index 00000000..46e2e642
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketGenerator.java
@@ -0,0 +1,124 @@
+package de.thedevstack.conversationsplus.xmpp.avatar;
+
+import de.thedevstack.conversationsplus.xmpp.pubsub.PubSubPacketGenerator;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Generates the IQ Packets for handling Avatars
+ * as defined in XEP-0084.
+ * @see {@link http://xmpp.org/extensions/xep-0084.html}
+ */
+public final class AvatarPacketGenerator {
+ public static final String NAMESPACE_AVATAR_DATA = "urn:xmpp:avatar:data";
+ public static final String NAMESPACE_AVATAR_METADATA = "urn:xmpp:avatar:metadata";
+
+ /**
+ * Generates an IqPacket for publishing avatar data.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq type='set'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <publish node='urn:xmpp:avatar:data'>
+ * <item id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'>
+ * <data xmlns='urn:xmpp:avatar:data'>
+ * qANQR1DBwU4DX7jmYZnncm...
+ * </data>
+ * </item>
+ * </publish>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @param avatar the avatar to publish
+ * @return the IqPacket
+ */
+ public static IqPacket generatePublishAvatarPacket(Avatar avatar) {
+ final Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ final Element data = item.addChild("data", NAMESPACE_AVATAR_DATA);
+ data.setContent(avatar.image);
+ return PubSubPacketGenerator.generatePubSubPublishPacket(NAMESPACE_AVATAR_DATA, item);
+ }
+
+ /**
+ * Generates an IqPacket to retrieve avatar data.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq type='get'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <items node='urn:xmpp:avatar:data'>
+ * <item id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'/>
+ * </items>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @param avatar the avatar to retrieve
+ * @return the IqPacket
+ */
+ public static IqPacket generateRetrieveAvatarPacket(Avatar avatar) {
+ final Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ final IqPacket packet = PubSubPacketGenerator.generatePubSubRetrievePacket(NAMESPACE_AVATAR_DATA, item);
+ packet.setTo(avatar.owner);
+ return packet;
+ }
+
+ /**
+ * Generates an IqPacket to publish metadata for an avatar.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq type='set'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <publish node='urn:xmpp:avatar:metadata'>
+ * <item id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'>
+ * <metadata xmlns='urn:xmpp:avatar:metadata'>
+ * <info bytes='12345'
+ * id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'
+ * height='64'
+ * type='image/png'
+ * width='64'/>
+ * </metadata>
+ * </item>
+ * </publish>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @param avatar the avatar to publish the metadata
+ * @return the IqPacket
+ */
+ public static IqPacket generatePublishAvatarMetadataPacket(Avatar avatar) {
+ final Element item = new Element("item");
+ item.setAttribute("id", avatar.sha1sum);
+ final Element metadata = item.addChild("metadata", NAMESPACE_AVATAR_METADATA);
+ final Element info = metadata.addChild("info");
+ info.setAttribute("bytes", avatar.size);
+ info.setAttribute("id", avatar.sha1sum);
+ info.setAttribute("height", avatar.height);
+ info.setAttribute("width", avatar.height);
+ info.setAttribute("type", avatar.type);
+ return PubSubPacketGenerator.generatePubSubPublishPacket(NAMESPACE_AVATAR_METADATA, item);
+ }
+
+ /**
+ * Generates an IqPacket to retrieve metadata of an avatar.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * @param to the Jid to deliver the metadata to
+ * @return the IqPacket
+ */
+ public static IqPacket generateRetrieveAvatarMetadataPacket(Jid to) {
+ final IqPacket packet = PubSubPacketGenerator.generatePubSubRetrievePacket(NAMESPACE_AVATAR_METADATA, null);
+ if (to != null) {
+ packet.setTo(to);
+ }
+ return packet;
+ }
+
+ /**
+ * Helper class - private constructor to avoid instantiation
+ */
+ private AvatarPacketGenerator() {
+ // avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketParser.java
new file mode 100644
index 00000000..48045a3c
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketParser.java
@@ -0,0 +1,29 @@
+package de.thedevstack.conversationsplus.xmpp.avatar;
+
+import de.thedevstack.conversationsplus.xmpp.pubsub.PubSubPacketParser;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Parses the IQ Packets for handling Avatars
+ * as defined in XEP-0084.
+ * @see {@link http://xmpp.org/extensions/xep-0084.html}
+ */
+public class AvatarPacketParser {
+ /**
+ * Extracts the base64 encoded avatar data from an IqPacket.
+ * @param packet the IqPacket to be parsed.
+ * @return base64 encoded avatar data
+ */
+ public static String parseAvatarData(IqPacket packet) {
+ Element items = PubSubPacketParser.findItems(packet);
+ String base64Avatar = null;
+ if (null != items) {
+ Element item = items.findChild("item");
+ if (null != item) {
+ base64Avatar = item.findChildContent("data", AvatarPacketGenerator.NAMESPACE_AVATAR_DATA);
+ }
+ }
+ return base64Avatar;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java
new file mode 100644
index 00000000..7868a2f5
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java
@@ -0,0 +1,15 @@
+package de.thedevstack.conversationsplus.xmpp.httpuploadim;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 17.04.2016.
+ */
+public class HttpUploadHint extends Element {
+ public static final String NAMESPACE = "urn:xmpp:hints";
+ public static final String ELEMENT_NAME = "httpupload";
+
+ public HttpUploadHint() {
+ super(ELEMENT_NAME, NAMESPACE);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java
new file mode 100644
index 00000000..961277cb
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java
@@ -0,0 +1,33 @@
+package de.thedevstack.conversationsplus.xmpp.pubsub;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by tzur on 15.01.2016.
+ */
+public class PubSubPacket extends IqPacket {
+ public static final String NAMESPACE = "http://jabber.org/protocol/pubsub";
+ public static final String ELEMENT_NAME = "pubsub";
+ private Element pubSubElement;
+
+ public PubSubPacket(IqPacket.TYPE type) {
+ super(type);
+ this.pubSubElement = super.addChild(PubSubPacket.ELEMENT_NAME, PubSubPacket.NAMESPACE);
+ }
+
+ @Override
+ public Element addChild(Element child) {
+ return this.pubSubElement.addChild(child);
+ }
+
+ @Override
+ public Element addChild(String name) {
+ return this.pubSubElement.addChild(name);
+ }
+
+ @Override
+ public Element addChild(String name, String xmlns) {
+ return this.pubSubElement.addChild(name, xmlns);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java
new file mode 100644
index 00000000..398ec032
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java
@@ -0,0 +1,32 @@
+package de.thedevstack.conversationsplus.xmpp.pubsub;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by tzur on 15.01.2016.
+ */
+public final class PubSubPacketGenerator {
+
+ public static PubSubPacket generatePubSubPublishPacket(String nodeName, Element item) {
+ final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.SET);
+ final Element publish = pubsub.addChild("publish");
+ publish.setAttribute("node", nodeName);
+ publish.addChild(item);
+ return pubsub;
+ }
+
+ public static PubSubPacket generatePubSubRetrievePacket(String nodeName, Element item) {
+ final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.GET);
+ final Element items = pubsub.addChild("items");
+ items.setAttribute("node", nodeName);
+ if (item != null) {
+ items.addChild(item);
+ }
+ return pubsub;
+ }
+
+ private PubSubPacketGenerator() {
+ // Avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java
new file mode 100644
index 00000000..394fb5b2
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java
@@ -0,0 +1,27 @@
+package de.thedevstack.conversationsplus.xmpp.pubsub;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by tzur on 15.01.2016.
+ */
+public class PubSubPacketParser {
+ public static Element findPubSubPacket(IqPacket packet){
+ if (null == packet) {
+ return null;
+ }
+ return packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
+ }
+
+ public static Element findItemsFromPubSubElement(Element pubSubPacket) {
+ if (null == pubSubPacket) {
+ return null;
+ }
+ return pubSubPacket.findChild("items");
+ }
+
+ public static Element findItems(IqPacket packet) {
+ return PubSubPacketParser.findItemsFromPubSubElement(PubSubPacketParser.findPubSubPacket(packet));
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacketGenerator.java
new file mode 100644
index 00000000..bdf0f4b0
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/IqPacketGenerator.java
@@ -0,0 +1,33 @@
+package de.thedevstack.conversationsplus.xmpp.stanzas;
+
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by tzur on 15.01.2016.
+ */
+public final class IqPacketGenerator {
+
+ private static IqPacket generateIqPacket(IqPacket.TYPE type) {
+ return new IqPacket(type);
+ }
+
+ public static IqPacket generateIqSetPacket() {
+ return generateIqPacket(IqPacket.TYPE.SET);
+ }
+
+ public static IqPacket generateIqGetPacket() {
+ return generateIqPacket(IqPacket.TYPE.GET);
+ }
+
+ public static IqPacket generateIqResultPacket() {
+ return generateIqPacket(IqPacket.TYPE.RESULT);
+ }
+
+ public static IqPacket generateIqErrorPacket() {
+ return generateIqPacket(IqPacket.TYPE.ERROR);
+ }
+
+ private IqPacketGenerator() {
+ // avoid Instantiation
+ }
+}