diff options
Diffstat (limited to 'src/main/java')
109 files changed, 6316 insertions, 3757 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..d902e8d4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -0,0 +1,104 @@ +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.createNoMedia(); + } + + /** + * 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..dda4208c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java @@ -0,0 +1,332 @@ +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 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); + } + + 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/ui/LogCatOutputActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java new file mode 100644 index 00000000..52891a91 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.ui; + +import android.app.Activity; +import android.os.Bundle; +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.android.logcat.ui.LogCatOutputCopyOnClickListener; +import eu.siacs.conversations.R; + +/** + * Created by tzur on 07.10.2015. + */ +public class LogCatOutputActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_logcatoutput); + ListView lv = (ListView)findViewById(R.id.actLogInfoOutput); + LogCatArrayAdapter logCatOutputAdapter = new LogCatArrayAdapter(this, R.layout.list_item_logcatoutput); + lv.setAdapter(logCatOutputAdapter); + new ReadLogCatAsyncTask(logCatOutputAdapter).execute(); + Button copyButton = (Button) findViewById(R.id.actLogOutputCopyButton); + copyButton.setOnClickListener(new LogCatOutputCopyOnClickListener(this, logCatOutputAdapter, R.string.cplus_copied_to_clipboard, R.string.cplus_not_copied_to_clipboard_empty)); + } +} 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..e5a478e8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java @@ -0,0 +1,180 @@ +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 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(); + TextView tvFilesize = (TextView) view.findViewById(R.id.dlgMsgDetFileSize); + tvFilesize.setText(UIHelper.getHumanReadableFileSize(params.size)); + } + TextView mimetype = (TextView) view.findViewById(R.id.dlgMsgDetFileMimeType); + mimetype.setText(message.getMimeType()); + } + } + + /** + * 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) { + TextView msgTypeTextView = (TextView) view.findViewById(R.id.dlgMsgDetMsgType); + 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; + } + msgTypeTextView.setText(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 + TextView senderLabel = (TextView) view.findViewById(R.id.dlgMsgDetLblSender); + senderLabel.setText(R.string.dlg_msg_details_sender_nick); + TextView receipientLabel = (TextView) view.findViewById(R.id.dlgMsgDetLblReceipient); + receipientLabel.setText(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) { + TextView timeSent = (TextView) view.findViewById(R.id.dlgMsgDetTimeSent); + timeSent.setText(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/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java new file mode 100644 index 00000000..e3dab516 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java @@ -0,0 +1,209 @@ +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.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; + +/** + * Created by tzur on 31.10.2015. + */ +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) { + xmppConnectionService.getPgpEngine().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) { + xmppConnectionService.getPgpEngine().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..63dc320e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java @@ -0,0 +1,163 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Base64; +import android.util.Base64OutputStream; + +import java.io.ByteArrayOutputStream; +import java.io.File; +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; + } + } + + /** + * 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/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..f8310206 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -0,0 +1,95 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.BitmapFactory; + +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.persistance.FileBackend; + +/** + * Utility class to work with messages. + */ +public final class MessageUtil { + + 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/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..bb08014b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java @@ -0,0 +1,75 @@ +package de.thedevstack.conversationsplus.utils.ui; + +import android.support.annotation.StringRes; +import android.widget.TextView; + +/** + * Created by steckbrief on 29.03.2016. + */ +public final class TextViewUtil { + 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/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 + } +} diff --git a/src/main/java/de/tzur/conversations/Settings.java b/src/main/java/de/tzur/conversations/Settings.java new file mode 100644 index 00000000..5d64f084 --- /dev/null +++ b/src/main/java/de/tzur/conversations/Settings.java @@ -0,0 +1,76 @@ +package de.tzur.conversations; + +import android.content.SharedPreferences; + +import de.thedevstack.android.logcat.Logging; + +/** + * This class is used to provide access to settings which have to be accessed frequently. + * Every setting in this class has to be updated using @see SettingsActivity#onSharedPreferenceChanged. + */ +public abstract class Settings { + + /** + * Initializes the settings provided via this static class. + * @param preferences the shared preferences of the app. + */ + public static void initSettingsClassWithPreferences(SharedPreferences preferences) { + Logging.d("SETTING", "Initializing settings"); + String[] preferenceNames = { "parse_emoticons", "send_button_status", "led_notification_color", "auto_download_file_wlan", "auto_download_file_link", "confirm_messages_list" }; + for (String name : preferenceNames) { + Settings.synchronizeSettingsClassWithPreferences(preferences, name); + } + } + + /** + * Synchronizes the setting value in this class on settings update in SettingsActivity. + * @param preferences the shared preferences of the app. + * @param name the name of the setting to synchronize. + */ + public static void synchronizeSettingsClassWithPreferences(SharedPreferences preferences, String name) { + Logging.d("SETTING", "Synchronizing settings"); + switch (name) { + case "parse_emoticons": + Settings.PARSE_EMOTICONS = preferences.getBoolean(name, Settings.PARSE_EMOTICONS); + break; + case "send_button_status": + Settings.SHOW_ONLINE_STATUS = preferences.getBoolean(name, Settings.SHOW_ONLINE_STATUS); + break; + case "led_notify_color": + Settings.LED_COLOR = preferences.getInt(name, Settings.LED_COLOR); + break; + case "confirm_messages_list": + int iPref = Settings.CONFIRM_MESSAGE_RECEIVED && Settings.CONFIRM_MESSAGE_READ ? 2 : Settings.CONFIRM_MESSAGE_RECEIVED ? 1 : 0; + try { + iPref = Integer.valueOf(preferences.getString(name, new Integer(iPref).toString())); + } catch (NumberFormatException e) { + // ignored, fallback-value set above + } + Settings.CONFIRM_MESSAGE_RECEIVED = iPref >= 1; + Settings.CONFIRM_MESSAGE_READ = iPref >= 2; + break; + } + } + + /** + * Boolean if emoticons should be parsed to emoticons or not. + */ + public static boolean PARSE_EMOTICONS = true; + /** + * Boolean if online status should be shown or not. + */ + public static boolean SHOW_ONLINE_STATUS = true; + /** + * LED Color + */ + public static int LED_COLOR = 0xffffffff; + /** + * Boolean if confirm received messages + */ + public static boolean CONFIRM_MESSAGE_RECEIVED = true; + /** + * Boolean if confirm read message + */ + public static boolean CONFIRM_MESSAGE_READ = true; + +} diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index e39cd654..25f08f30 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -2,6 +2,7 @@ package eu.siacs.conversations; import android.graphics.Bitmap; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.xmpp.chatstate.ChatState; public final class Config { @@ -27,7 +28,7 @@ public final class Config { } public static boolean supportOmemo() { - return (ENCRYPTION_MASK & OMEMO) != 0; + return ConversationsPlusPreferences.omemoEnabled(); } public static boolean multipleEncryptionChoices() { @@ -45,7 +46,6 @@ public final class Config { public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true - public static final boolean FORCE_ORBOT = false; // always use TOR public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; public static final boolean SHOW_CONNECTED_ACCOUNTS = false; //show number of connected accounts in foreground notification @@ -65,7 +65,7 @@ public final class Config { public static final boolean CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND = false; public static final int AVATAR_SIZE = 192; - public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.PNG; public static final int IMAGE_SIZE = 1920; public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG; @@ -74,9 +74,12 @@ public final class Config { public static final int MESSAGE_MERGE_WINDOW = 20; + public static final boolean UTF8_EMOTICONS = false; + public static final int PAGE_SIZE = 50; public static final int MAX_NUM_PAGES = 3; + public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; public static final int REFRESH_UI_INTERVAL = 500; public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java index 1804704e..4ddf51fb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.crypto; -import android.util.Log; - import net.java.otr4j.OtrEngineHost; import net.java.otr4j.OtrException; import net.java.otr4j.OtrPolicy; @@ -26,6 +24,8 @@ import java.security.spec.DSAPrivateKeySpec; import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -110,7 +110,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { mXmppConnectionService.updateConversationUi(); } } catch (InvalidJidException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": smp in invalid session "+id.toString()); } } @@ -151,7 +151,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { this.saveKey(); mXmppConnectionService.databaseBackend.updateAccount(account); } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "error generating key pair " + e.getMessage()); } } @@ -185,7 +185,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { Jid jid = Jid.fromSessionID(session); Conversation conversation = mXmppConnectionService.find(account,jid); if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (mXmppConnectionService.sendChatStates()) { + if (ConversationsPlusPreferences.chatStates()) { packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); } } @@ -217,7 +217,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { @Override public void showError(SessionID arg0, String arg1) throws OtrException { - Log.d(Config.LOGTAG,"show error"); + Logging.d(Config.LOGTAG,"show error"); } @Override @@ -252,7 +252,8 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { @Override public void unreadableMessageReceived(SessionID session) throws OtrException { - Log.d(Config.LOGTAG,"unreadable message received"); + Logging.d(Config.LOGTAG,"unreadable message received"); + // Hier update des contents fuer FS#96 sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message"); } @@ -266,8 +267,8 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { .generateOtrError(jid, id, errorText); packet.setFrom(account.getJid()); mXmppConnectionService.sendMessagePacket(account,packet); - Log.d(Config.LOGTAG,packet.toString()); - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString() + Logging.d(Config.LOGTAG,packet.toString()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString() +": unreadable OTR message in "+conversation.getName()); } } catch (InvalidJidException e) { @@ -282,7 +283,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { @Override public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); + Logging.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); try { final Jid jid = Jid.fromSessionID(id); Conversation conversation = this.mXmppConnectionService.find(this.account,jid); diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index ed67dc65..5afbe5c4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -2,15 +2,15 @@ package eu.siacs.conversations.crypto; import android.app.PendingIntent; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.UiCallback; - import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.UiCallback; + public class PgpDecryptionService { private final XmppConnectionService xmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 7ca5bea0..56ca26da 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -16,6 +16,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -59,7 +62,8 @@ public class PgpEngine { final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER - && manager.getAutoAcceptFileSize() > 0) { + && ConversationsPlusPreferences.autoDownloadFileLink() + && ConversationsPlusPreferences.autoAcceptFileSize() > 0) { manager.createNewDownloadConnection(message); } mXmppConnectionService.updateMessage(message); @@ -83,10 +87,8 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { try { - final DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - final DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); + final DownloadableFile inputFile = FileBackend.getFile(message, false); + final DownloadableFile outputFile = FileBackend.getFile(message, true); outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); @@ -100,12 +102,12 @@ public class PgpEngine { OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: URL url = message.getFileParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message,url); + MessageUtil.updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); inputFile.delete(); - mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile); + FileBackend.updateMediaScanner(outputFile, mXmppConnectionService); callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: @@ -164,7 +166,7 @@ public class PgpEngine { String[] lines = os.toString().split("\n"); for (int i = 2; i < lines.length - 1; ++i) { if (!lines[i].contains("Version")) { - encryptedMessageBody.append(lines[i].trim()); + encryptedMessageBody.append(lines[i]); } } message.setEncryptedBody(encryptedMessageBody @@ -188,10 +190,8 @@ public class PgpEngine { }); } else { try { - DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); + DownloadableFile inputFile = FileBackend.getFile(message, true); + DownloadableFile outputFile = FileBackend.getFile(message, false); outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); final InputStream is = new FileInputStream(inputFile); @@ -209,7 +209,7 @@ public class PgpEngine { } catch (IOException ignored) { //ignored } - FileBackend.close(os); + StreamUtil.close(os); callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: @@ -246,7 +246,7 @@ public class PgpEngine { pgpSig.append("-----BEGIN PGP SIGNATURE-----"); pgpSig.append('\n'); pgpSig.append('\n'); - pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append(signature.replace("\n", "")); pgpSig.append('\n'); pgpSig.append("-----END PGP SIGNATURE-----"); Intent params = new Intent(); @@ -326,7 +326,7 @@ public class PgpEngine { sig = false; } else { if (!line.contains("Version")) { - signatureBuilder.append(line.trim()); + signatureBuilder.append(line); } } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index cc5c2491..e490ac64 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,211 +1,39 @@ package eu.siacs.conversations.crypto.axolotl; -import android.os.Bundle; -import android.security.KeyChain; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; -import android.util.Pair; -import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.IdentityKey; -import org.whispersystems.libaxolotl.IdentityKeyPair; -import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.InvalidKeyIdException; -import org.whispersystems.libaxolotl.SessionBuilder; -import org.whispersystems.libaxolotl.UntrustedIdentityException; -import org.whispersystems.libaxolotl.ecc.ECPublicKey; -import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; -import org.whispersystems.libaxolotl.util.KeyHelper; -import java.security.PrivateKey; -import java.security.Security; -import java.security.Signature; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Random; import java.util.Set; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { +/** + * Created by tzur on 02.03.2016. + */ +public interface AxolotlService extends OnAdvancedStreamFeaturesLoaded { - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; - public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; + String LOGPREFIX = "AxolotlService"; - public static final String LOGPREFIX = "AxolotlService"; + String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + String PEP_VERIFICATION = PEP_PREFIX + ".verification"; - public static final int NUM_KEYS_TO_PUBLISH = 100; - public static final int publishTriesThreshold = 3; + int NUM_KEYS_TO_PUBLISH = 100; - private final Account account; - private final XmppConnectionService mXmppConnectionService; - private final SQLiteAxolotlStore axolotlStore; - private final SessionMap sessions; - private final Map<Jid, Set<Integer>> deviceIds; - private final Map<String, XmppAxolotlMessage> messageCache; - private final FetchStatusMap fetchStatusMap; - private final SerialSingleThreadExecutor executor; - private int numPublishTriesOnEmptyPep = 0; - private boolean pepBroken = false; - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) { - publishBundlesIfNeeded(true, false); - } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization"); - } - } - - public boolean fetchMapHasErrors(List<Jid> jids) { - for(Jid jid : jids) { - if (deviceIds.get(jid) != null) { - for (Integer foreignId : this.deviceIds.get(jid)) { - AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); - if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { - return true; - } - } - } - } - return false; - } - - private static class AxolotlAddressMap<T> { - protected Map<String, Map<Integer, T>> map; - protected final Object MAP_LOCK = new Object(); - - public AxolotlAddressMap() { - this.map = new HashMap<>(); - } - - public void put(AxolotlAddress address, T value) { - synchronized (MAP_LOCK) { - Map<Integer, T> devices = map.get(address.getName()); - if (devices == null) { - devices = new HashMap<>(); - map.put(address.getName(), devices); - } - devices.put(address.getDeviceId(), value); - } - } - - public T get(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map<Integer, T> devices = map.get(address.getName()); - if (devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map<Integer, T> getAll(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map<Integer, T> devices = map.get(address.getName()); - if (devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map<Integer, T> devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - public void clear() { - map.clear(); - } - - } - - private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> { - private final XmppConnectionService xmppConnectionService; - private final Account account; - - public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { - super(); - this.xmppConnectionService = service; - this.account = account; - this.fillMap(store); - } - - private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { - for (Integer deviceId : deviceIds) { - AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); - IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); - if(Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); - if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.fromString(bareJid); - Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final InvalidJidException ignored) { - //ignored - } - } - } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); - } - } - - private void fillMap(SQLiteAxolotlStore store) { - List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString()); - putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store); - for (Contact contact : account.getRoster().getContacts()) { - Jid bareJid = contact.getJid().toBareJid(); - String address = bareJid.toString(); - deviceIds = store.getSubDeviceSessions(address); - putDevicesForJid(address, deviceIds, store); - } - - } - - @Override - public void put(AxolotlAddress address, XmppAxolotlSession value) { - super.put(address, value); - value.setNotFresh(); - xmppConnectionService.syncRosterToDisk(account); - } - - public void put(XmppAxolotlSession session) { - this.put(session.getRemoteAddress(), session); - } - } - - public enum FetchStatus { + enum FetchStatus { PENDING, SUCCESS, SUCCESS_VERIFIED, @@ -213,836 +41,78 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { ERROR } - private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> { + String getOwnFingerprint(); - } + Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust); - public static String getLogprefix(Account account) { - return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; - } - public AxolotlService(Account account, XmppConnectionService connectionService) { - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } - this.mXmppConnectionService = connectionService; - this.account = account; - this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.deviceIds = new HashMap<>(); - this.messageCache = new HashMap<>(); - this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); - this.fetchStatusMap = new FetchStatusMap(); - this.executor = new SerialSingleThreadExecutor(); - } + Set<String> getFingerprintsForOwnSessions(); - public String getOwnFingerprint() { - return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); - } + Set<String> getFingerprintsForContact(Contact contact); - public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { - return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust); - } + boolean isPepBroken(); - public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) { - return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust); - } + void regenerateKeys(boolean wipeOther); - public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) { - Set<IdentityKey> keys = new HashSet<>(); - for(Jid jid : jids) { - keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust)); - } - return keys; - } + int getOwnDeviceId(); - public long getNumTrustedKeys(Jid jid) { - return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()); - } - - public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) { - for(Jid jid : jids) { - if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) { - return true; - } - } - return false; - } - - private AxolotlAddress getAddressForJid(Jid jid) { - return new AxolotlAddress(jid.toString(), 0); - } - - private Set<XmppAxolotlSession> findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); - return new HashSet<>(this.sessions.getAll(ownAddress).values()); - } - - private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - return new HashSet<>(this.sessions.getAll(contactAddress).values()); - } - - private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) { - HashSet<XmppAxolotlSession> sessions = new HashSet<>(); - for(Jid jid : conversation.getAcceptedCryptoTargets()) { - sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values()); - } - return sessions; - } - - public Set<String> getFingerprintsForOwnSessions() { - Set<String> fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findOwnSessions()) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } - - public Set<String> getFingerprintsForContact(final Contact contact) { - Set<String> fingerprints = new HashSet<>(); - for (XmppAxolotlSession session : findSessionsForContact(contact)) { - fingerprints.add(session.getFingerprint()); - } - return fingerprints; - } + Set<Integer> getOwnDeviceIds(); - private boolean hasAny(Jid jid) { - return sessions.hasAny(getAddressForJid(jid)); - } + void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds); - public boolean isPepBroken() { - return this.pepBroken; - } + void wipeOtherPepDevices(); - public void regenerateKeys(boolean wipeOther) { - axolotlStore.regenerate(); - sessions.clear(); - fetchStatusMap.clear(); - publishBundlesIfNeeded(true, wipeOther); - } + void purgeKey(String fingerprint); - public int getOwnDeviceId() { - return axolotlStore.getLocalRegistrationId(); - } - - public Set<Integer> getOwnDeviceIds() { - return this.deviceIds.get(account.getJid().toBareJid()); - } + void publishOwnDeviceIdIfNeeded(); - private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds, - final XmppAxolotlSession.Trust from, - final XmppAxolotlSession.Trust to) { - for (Integer deviceId : deviceIds) { - AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null - && session.getTrust() == from) { - session.setTrust(to); - } - } - } + void publishOwnDeviceId(Set<Integer> deviceIds); - public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { - if (jid.toBareJid().equals(account.getJid().toBareJid())) { - if (!deviceIds.isEmpty()) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status."); - pepBroken = false; - numPublishTriesOnEmptyPep = 0; - } - if (deviceIds.contains(getOwnDeviceId())) { - deviceIds.remove(getOwnDeviceId()); - } else { - publishOwnDeviceId(deviceIds); - } - for (Integer deviceId : deviceIds) { - AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId); - if (sessions.get(ownDeviceAddress) == null) { - buildSessionFromPEP(ownDeviceAddress); - } - } - } - Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString())); - expiredDevices.removeAll(deviceIds); - setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED, - XmppAxolotlSession.Trust.INACTIVE_TRUSTED); - setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509, - XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509); - setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED, - XmppAxolotlSession.Trust.INACTIVE_UNDECIDED); - setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED, - XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED); - Set<Integer> newDevices = new HashSet<>(deviceIds); - setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED, - XmppAxolotlSession.Trust.TRUSTED); - setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509, - XmppAxolotlSession.Trust.TRUSTED_X509); - setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED, - XmppAxolotlSession.Trust.UNDECIDED); - setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED, - XmppAxolotlSession.Trust.UNTRUSTED); - this.deviceIds.put(jid, deviceIds); - mXmppConnectionService.keyStatusUpdated(null); - } + void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord, + Set<PreKeyRecord> preKeyRecords, + boolean announceAfter, + boolean wipe); - public void wipeOtherPepDevices() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); - return; - } - Set<Integer> deviceIds = new HashSet<>(); - deviceIds.add(getOwnDeviceId()); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - } - }); - } + void publishBundlesIfNeeded(boolean announce, boolean wipe); - public void purgeKey(final String fingerprint) { - axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED); - } + XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint); - public void publishOwnDeviceIdIfNeeded() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); - } else { - Element item = mXmppConnectionService.getIqParser().getItem(packet); - Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if (!deviceIds.contains(getOwnDeviceId())) { - publishOwnDeviceId(deviceIds); - } - } - } - }); - } + X509Certificate getFingerprintCertificate(String fingerprint); - public void publishOwnDeviceId(Set<Integer> deviceIds) { - Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds); - if (!deviceIdsCopy.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist."); - if (deviceIdsCopy.isEmpty()) { - if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); - pepBroken = true; - return; - } else { - numPublishTriesOnEmptyPep++; - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); - } - } else { - numPublishTriesOnEmptyPep = 0; - } - deviceIdsCopy.add(getOwnDeviceId()); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); - } - } - }); - } - } + void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust); - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set<PreKeyRecord> preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - try { - IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); - X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG()); - verifier.update(axolotlPublicKey.serialize()); - byte[] signature = verifier.sign(); - IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } + Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation); - public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + boolean createSessionsIfNeeded(Conversation conversation); - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; //ignore timeout. do nothing - } - - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.findChild("error"); - if (error == null || !error.hasChild("item-not-found")) { - pepBroken = true; - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); - return; - } - } - - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); - } - try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - changed = true; - } - - // Validate signedPreKeyRecord + ID - SignedPreKeyRecord signedPreKeyRecord; - int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); - try { - signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if (flush - || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - - // Validate PreKeys - Set<PreKeyRecord> preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); - } - } catch (InvalidKeyIdException ignored) { - } - } - } - int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); - if (newKeys > 0) { - List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId() + 1, newKeys); - preKeyRecords.addAll(newRecords); - for (PreKeyRecord record : newRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); - } - - - if (changed) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - }); - } - - private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set<PreKeyRecord> preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error")); - } - } - }); - } - - public boolean isConversationAxolotlCapable(Conversation conversation) { - final List<Jid> jids = getCryptoTargets(conversation); - for(Jid jid : jids) { - if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) { - return false; - } - } - return jids.size() > 0; - } - - public List<Jid> getCryptoTargets(Conversation conversation) { - final List<Jid> jids; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - jids = Arrays.asList(conversation.getJid().toBareJid()); - } else { - jids = conversation.getMucOptions().getMembers(); - jids.remove(account.getJid().toBareJid()); - } - return jids; - } - - public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { - return axolotlStore.getFingerprintTrust(fingerprint); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return axolotlStore.getFingerprintCertificate(fingerprint); - } - - public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { - axolotlStore.setFingerprintTrust(fingerprint, trust); - } - - private void verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); - final AxolotlAddress address = session.getRemoteAddress(); - final IdentityKey identityKey = session.getIdentityKey(); - try { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { - try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); - setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.fromString(address.getName()); - Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final InvalidJidException ignored) { - //ignored - } - finishBuildingSessionsFromPEP(address); - return; - } catch (Exception e) { - Log.d(Config.LOGTAG,"could not verify certificate"); - } - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG,"no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - }); - } catch (InvalidJidException e) { - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - } - - private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); - if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) - && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { - FetchStatus report = null; - if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED) - | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) { - report = FetchStatus.SUCCESS_VERIFIED; - } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR) - || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { - report = FetchStatus.ERROR; - } - mXmppConnectionService.keyStatusUpdated(report); - } - } - - private void buildSessionFromPEP(final AxolotlAddress address) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString()); - if (address.getDeviceId() == getOwnDeviceId()) { - throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); - } - - try { - IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( - Jid.fromString(address.getName()), address.getDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - return; - } - Random random = new Random(); - final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (preKey == null) { - //should never happen - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - return; - } - - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - try { - SessionBuilder builder = new SessionBuilder(axolotlStore, address); - builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); - sessions.put(address, session); - if (Config.X509_VERIFICATION) { - verifySessionWithPEP(session); - } else { - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - } - } else { - fetchStatusMap.put(address, FetchStatus.ERROR); - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); - finishBuildingSessionsFromPEP(address); - } - } - }); - } catch (InvalidJidException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); - } - } - - public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) { - Set<AxolotlAddress> addresses = new HashSet<>(); - for(Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); - if (deviceIds.get(jid) != null) { - for (Integer foreignId : this.deviceIds.get(jid)) { - AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); - } - } - } - } - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); - } - } - if (deviceIds.get(account.getJid().toBareJid()) != null) { - for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) { - AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken"); - } - } - } - } - } - - return addresses; - } - - public boolean createSessionsIfNeeded(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); - boolean newSessions = false; - Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); - for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); - FetchStatus status = fetchStatusMap.get(address); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(address); - newSessions = true; - } else if (status == FetchStatus.PENDING) { - newSessions = true; - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); - } - } - - return newSessions; - } - - public boolean trustedSessionVerified(final Conversation conversation) { - Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation); - sessions.addAll(findOwnSessions()); - boolean verified = false; - for(XmppAxolotlSession session : sessions) { - if (session.getTrust().trusted()) { - if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) { - verified = true; - } else { - return false; - } - } - } - return verified; - } - - public boolean hasPendingKeyFetches(Account account, List<Jid> jids) { - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); - if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) { - return true; - } - for(Jid jid : jids) { - AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0); - if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { - return true; - } - } - return false; - } - - @Nullable - private XmppAxolotlMessage buildHeader(Conversation conversation) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( - account.getJid().toBareJid(), getOwnDeviceId()); - - Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation); - Set<XmppAxolotlSession> ownSessions = findOwnSessions(); - if (remoteSessions.isEmpty()) { - return null; - } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements..."); - for (XmppAxolotlSession session : remoteSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); - axolotlMessage.addDevice(session); - } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements..."); - for (XmppAxolotlSession session : ownSessions) { - Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString()); - axolotlMessage.addDevice(session); - } - - return axolotlMessage; - } + boolean trustedSessionVerified(Conversation conversation); @Nullable - public XmppAxolotlMessage encrypt(Message message) { - XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation()); - - if (axolotlMessage != null) { - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } - try { - axolotlMessage.encrypt(content); - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); - return null; - } - } - - return axolotlMessage; - } - - public void preparePayloadMessage(final Message message, final boolean delay) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = encrypt(message); - if (axolotlMessage == null) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - //mXmppConnectionService.updateConversationUi(); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay); - } - } - }); - } + XmppAxolotlMessage encrypt(Message message); - public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = buildHeader(conversation); - onMessageCreatedCallback.run(axolotlMessage); - } - }); - } + void preparePayloadMessage(Message message, boolean delay); - public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { - XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); - if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); - messageCache.remove(message.getUuid()); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); - } - return axolotlMessage; - } + XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message); - private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - return (identityKey != null) - ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) - : null; - } + XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message); - private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { - AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), - message.getSenderDeviceId()); - XmppAxolotlSession session = sessions.get(senderAddress); - if (session == null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); - session = recreateUncachedSession(senderAddress); - if (session == null) { - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); - } - } - return session; - } + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message); - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + boolean fetchMapHasErrors(List<Jid> jids); - XmppAxolotlSession session = getReceivingSession(message); - try { - plaintextMessage = message.decrypt(session, getOwnDeviceId()); - Integer preKeyId = session.getPreKeyId(); - if (preKeyId != null) { - publishBundlesIfNeeded(false, false); - session.resetPreKeyId(); - } - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); - } + Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid); - if (session.isFresh() && plaintextMessage != null) { - putFreshSession(session); - } + Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids); - return plaintextMessage; - } + long getNumTrustedKeys(Jid jid); - public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { - XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + boolean anyTargetHasNoTrustedKeys(List<Jid> jids); - XmppAxolotlSession session = getReceivingSession(message); - keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + boolean isConversationAxolotlCapable(Conversation conversation); - if (session.isFresh() && keyTransportMessage != null) { - putFreshSession(session); - } + List<Jid> getCryptoTargets(Conversation conversation); - return keyTransportMessage; - } + boolean hasPendingKeyFetches(Account account, List<Jid> jids); - private void putFreshSession(XmppAxolotlSession session) { - Log.d(Config.LOGTAG,"put fresh session"); - sessions.put(session); - if (Config.X509_VERIFICATION) { - if (session.getIdentityKey() != null) { - verifySessionWithPEP(session); - } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); - } - } - } + void prepareKeyTransportMessage(Conversation conversation, OnMessageCreatedCallback onMessageCreatedCallback); } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java new file mode 100644 index 00000000..73974652 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java @@ -0,0 +1,1041 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.os.Bundle; +import android.security.KeyChain; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.util.Pair; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.UntrustedIdentityException; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class AxolotlServiceImpl implements AxolotlService { + + public static final int publishTriesThreshold = 3; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final Map<Jid, Set<Integer>> deviceIds; + private final Map<String, XmppAxolotlMessage> messageCache; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; + private int numPublishTriesOnEmptyPep = 0; + private boolean pepBroken = false; + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) { + publishBundlesIfNeeded(true, false); + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization"); + } + } + + @Override + public boolean fetchMapHasErrors(List<Jid> jids) { + for(Jid jid : jids) { + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); + if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + return true; + } + } + } + } + return false; + } + + private static class AxolotlAddressMap<T> { + protected Map<String, Map<Integer, T>> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map<Integer, T> getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + if (devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map<Integer, T> devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + public void clear() { + map.clear(); + } + + } + + private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> { + private final XmppConnectionService xmppConnectionService; + private final Account account; + + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + super(); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); + } + + private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) { + for (Integer deviceId : deviceIds) { + AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); + IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + if(Config.X509_VERIFICATION) { + X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); + if (certificate != null) { + Bundle information = CryptoHelper.extractCertificateInformation(certificate); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(bareJid); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } + } + } + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + } + } + + private void fillMap(SQLiteAxolotlStore store) { + List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString()); + putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store); + for (Contact contact : account.getRoster().getContacts()) { + Jid bareJid = contact.getJid().toBareJid(); + String address = bareJid.toString(); + deviceIds = store.getSubDeviceSessions(address); + putDevicesForJid(address, deviceIds, store); + } + + } + + @Override + public void put(AxolotlAddress address, XmppAxolotlSession value) { + super.put(address, value); + value.setNotFresh(); + xmppConnectionService.syncRosterToDisk(account); + } + + public void put(XmppAxolotlSession session) { + this.put(session.getRemoteAddress(), session); + } + } + + private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> { + + } + + public static String getLogprefix(Account account) { + return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): "; + } + + public AxolotlServiceImpl(Account account, XmppConnectionService connectionService) { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor(); + } + + public String getOwnFingerprint() { + return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", ""); + } + + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) { + return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toString(), trust); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) { + Set<IdentityKey> keys = new HashSet<>(); + for(Jid jid : jids) { + keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), trust)); + } + return keys; + } + + @Override + public long getNumTrustedKeys(Jid jid) { + return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()); + } + + @Override + public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) { + for(Jid jid : jids) { + if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toString()) == 0) { + return true; + } + } + return false; + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set<XmppAxolotlSession> findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); + return new HashSet<>(this.sessions.getAll(ownAddress).values()); + } + + private Set<XmppAxolotlSession> findSessionsForContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return new HashSet<>(this.sessions.getAll(contactAddress).values()); + } + + private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) { + HashSet<XmppAxolotlSession> sessions = new HashSet<>(); + for(Jid jid : conversation.getAcceptedCryptoTargets()) { + sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values()); + } + return sessions; + } + + public Set<String> getFingerprintsForOwnSessions() { + Set<String> fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findOwnSessions()) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + public Set<String> getFingerprintsForContact(final Contact contact) { + Set<String> fingerprints = new HashSet<>(); + for (XmppAxolotlSession session : findSessionsForContact(contact)) { + fingerprints.add(session.getFingerprint()); + } + return fingerprints; + } + + private boolean hasAny(Jid jid) { + return sessions.hasAny(getAddressForJid(jid)); + } + + public boolean isPepBroken() { + return this.pepBroken; + } + + public void regenerateKeys(boolean wipeOther) { + axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); + publishBundlesIfNeeded(true, wipeOther); + } + + public int getOwnDeviceId() { + return axolotlStore.getLocalRegistrationId(); + } + + public Set<Integer> getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().toBareJid()); + } + + private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds, + final XmppAxolotlSession.Trust from, + final XmppAxolotlSession.Trust to) { + for (Integer deviceId : deviceIds) { + AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null + && session.getTrust() == from) { + session.setTrust(to); + } + } + } + + public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) { + if (jid.toBareJid().equals(account.getJid().toBareJid())) { + if (!deviceIds.isEmpty()) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status."); + pepBroken = false; + numPublishTriesOnEmptyPep = 0; + } + if (deviceIds.contains(getOwnDeviceId())) { + deviceIds.remove(getOwnDeviceId()); + } else { + publishOwnDeviceId(deviceIds); + } + for (Integer deviceId : deviceIds) { + AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + buildSessionFromPEP(ownDeviceAddress); + } + } + } + Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString())); + expiredDevices.removeAll(deviceIds); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED, + XmppAxolotlSession.Trust.INACTIVE_TRUSTED); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509, + XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED, + XmppAxolotlSession.Trust.INACTIVE_UNDECIDED); + setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED, + XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED); + Set<Integer> newDevices = new HashSet<>(deviceIds); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED, + XmppAxolotlSession.Trust.TRUSTED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509, + XmppAxolotlSession.Trust.TRUSTED_X509); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED, + XmppAxolotlSession.Trust.UNDECIDED); + setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED, + XmppAxolotlSession.Trust.UNTRUSTED); + this.deviceIds.put(jid, deviceIds); + mXmppConnectionService.keyStatusUpdated(null); + } + + public void wipeOtherPepDevices() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + return; + } + Set<Integer> deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Wiping all other devices from Pep:" + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + + public void purgeKey(final String fingerprint) { + axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED); + } + + public void publishOwnDeviceIdIfNeeded() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); + } else { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (!deviceIds.contains(getOwnDeviceId())) { + publishOwnDeviceId(deviceIds); + } + } + } + }); + } + + public void publishOwnDeviceId(Set<Integer> deviceIds) { + Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds); + if (!deviceIdsCopy.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist."); + if (deviceIdsCopy.isEmpty()) { + if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); + pepBroken = true; + return; + } else { + numPublishTriesOnEmptyPep++; + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); + } + } else { + numPublishTriesOnEmptyPep = 0; + } + deviceIdsCopy.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); + } + } + }); + } + } + + public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, + final Set<PreKeyRecord> preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + try { + IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); + PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); + X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG()); + verifier.update(axolotlPublicKey.serialize()); + byte[] signature = verifier.sign(); + IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": publish verification for device "+getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; //ignore timeout. do nothing + } + + if (packet.getType() == IqPacket.TYPE.ERROR) { + Element error = packet.findChild("error"); + if (error == null || !error.hasChild("item-not-found")) { + pepBroken = true; + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); + return; + } + } + + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + + // Validate PreKeys + Set<PreKeyRecord> preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); + } + + + if (changed) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } else { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); + if (wipe) { + wipeOtherPepDevices(); + } else if (announce) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + }); + } + + private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, + Set<PreKeyRecord> preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error")); + } + } + }); + } + + @Override + public boolean isConversationAxolotlCapable(Conversation conversation) { + final List<Jid> jids = getCryptoTargets(conversation); + for(Jid jid : jids) { + if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) { + return false; + } + } + return jids.size() > 0; + } + + @Override + public List<Jid> getCryptoTargets(Conversation conversation) { + final List<Jid> jids; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + jids = Arrays.asList(conversation.getJid().toBareJid()); + } else { + jids = conversation.getMucOptions().getMembers(); + jids.remove(account.getJid().toBareJid()); + } + return jids; + } + + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintTrust(fingerprint); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + axolotlStore.setFingerprintTrust(fingerprint, trust); + } + + private void verifySessionWithPEP(final XmppAxolotlSession session) { + Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + final AxolotlAddress address = session.getRemoteAddress(); + final IdentityKey identityKey = session.getIdentityKey(); + try { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Pair<X509Certificate[],byte[]> verification = mXmppConnectionService.getIqParser().verification(packet); + if (verification != null) { + try { + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); + setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(address.getName()); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } + finishBuildingSessionsFromPEP(address); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG,"could not verify certificate"); + } + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); + } + } else { + Log.d(Config.LOGTAG,"no verification found"); + } + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + }); + } catch (InvalidJidException e) { + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } + + private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) { + FetchStatus report = null; + if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED) + | fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) { + report = FetchStatus.SUCCESS_VERIFIED; + } else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR) + || fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) { + report = FetchStatus.ERROR; + } + mXmppConnectionService.keyStatusUpdated(report); + } + } + + private void buildSessionFromPEP(final AxolotlAddress address) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building new sesstion for " + address.toString()); + if (address.getDeviceId() == getOwnDeviceId()) { + throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); + } + + try { + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( + Jid.fromString(address.getName()), address.getDeviceId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + } else if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + return; + } + + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); + sessions.put(address, session); + if (Config.X509_VERIFICATION) { + verifySessionWithPEP(session); + } else { + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + } + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); + finishBuildingSessionsFromPEP(address); + } + } + }); + } catch (InvalidJidException e) { + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Got address with invalid jid: " + address.getName()); + } + } + + public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) { + Set<AxolotlAddress> addresses = new HashSet<>(); + for(Jid jid : getCryptoTargets(conversation)) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Finding devices without session for " + jid); + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + jid + ":" + foreignId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + } + } + } + } + } else { + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Have no target devices in PEP!"); + } + } + if (deviceIds.get(account.getJid().toBareJid()) != null) { + for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) { + AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken"); + } + } + } + } + } + + return addresses; + } + + public boolean createSessionsIfNeeded(final Conversation conversation) { + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation); + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if (status == null || status == FetchStatus.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + } + } + + return newSessions; + } + + public boolean trustedSessionVerified(final Conversation conversation) { + Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation); + sessions.addAll(findOwnSessions()); + boolean verified = false; + for(XmppAxolotlSession session : sessions) { + if (session.getTrust().trusted()) { + if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) { + verified = true; + } else { + return false; + } + } + } + return verified; + } + + @Override + public boolean hasPendingKeyFetches(Account account, List<Jid> jids) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0); + if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) { + return true; + } + for(Jid jid : jids) { + AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toString(), 0); + if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + return true; + } + } + return false; + } + + @Nullable + private XmppAxolotlMessage buildHeader(Conversation conversation) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage( + account.getJid().toBareJid(), getOwnDeviceId()); + + Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation); + Set<XmppAxolotlSession> ownSessions = findOwnSessions(); + if (remoteSessions.isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl foreign keyElements..."); + for (XmppAxolotlSession session : remoteSessions) { + Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Building axolotl own keyElements..."); + for (XmppAxolotlSession session : ownSessions) { + Log.v(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + session.getRemoteAddress().toString()); + axolotlMessage.addDevice(session); + } + + return axolotlMessage; + } + + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation()); + + if (axolotlMessage != null) { + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } + try { + axolotlMessage.encrypt(content); + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + return null; + } + } + + return axolotlMessage; + } + + public void preparePayloadMessage(final Message message, final boolean delay) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); + } + + @Override + public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = buildHeader(conversation); + onMessageCreatedCallback.run(axolotlMessage); + } + }); + } + + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); + if (axolotlMessage != null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Cache miss: " + message.getUuid()); + } + return axolotlMessage; + } + + private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + return (identityKey != null) + ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) + : null; + } + + private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { + AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), + message.getSenderDeviceId()); + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); + session = recreateUncachedSession(senderAddress); + if (session == null) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } + } + return session; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + try { + plaintextMessage = message.decrypt(session, getOwnDeviceId()); + Integer preKeyId = session.getPreKeyId(); + if (preKeyId != null) { + publishBundlesIfNeeded(false, false); + session.resetPreKeyId(); + } + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); + } + + if (session.isFresh() && plaintextMessage != null) { + putFreshSession(session); + } + + return plaintextMessage; + } + + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + + XmppAxolotlSession session = getReceivingSession(message); + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + + if (session.isFresh() && keyTransportMessage != null) { + putFreshSession(session); + } + + return keyTransportMessage; + } + + private void putFreshSession(XmppAxolotlSession session) { + Log.d(Config.LOGTAG,"put fresh session"); + sessions.put(session); + if (Config.X509_VERIFICATION) { + if (session.getIdentityKey() != null) { + verifySessionWithPEP(session); + } else { + Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java new file mode 100644 index 00000000..b0228d34 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceStub.java @@ -0,0 +1,207 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.xmpp.jid.Jid; + +/** + * Axolotl Service Stub implementation to avoid axolotl usage. + */ +public class AxolotlServiceStub implements AxolotlService { + + @Override + public String getOwnFingerprint() { + return null; + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) { + return Collections.emptySet(); + } + + @Override + public Set<String> getFingerprintsForOwnSessions() { + return Collections.emptySet(); + } + + @Override + public Set<String> getFingerprintsForContact(Contact contact) { + return Collections.emptySet(); + } + + @Override + public boolean isPepBroken() { + return true; + } + + @Override + public void regenerateKeys(boolean wipeOther) { + + } + + @Override + public int getOwnDeviceId() { + return 0; + } + + @Override + public Set<Integer> getOwnDeviceIds() { + return Collections.emptySet(); + } + + @Override + public void registerDevices(Jid jid, @NonNull Set<Integer> deviceIds) { + + } + + @Override + public void wipeOtherPepDevices() { + + } + + @Override + public void purgeKey(String fingerprint) { + + } + + @Override + public void publishOwnDeviceIdIfNeeded() { + + } + + @Override + public void publishOwnDeviceId(Set<Integer> deviceIds) { + + } + + @Override + public void publishDeviceVerificationAndBundle(SignedPreKeyRecord signedPreKeyRecord, Set<PreKeyRecord> preKeyRecords, boolean announceAfter, boolean wipe) { + + } + + @Override + public void publishBundlesIfNeeded(boolean announce, boolean wipe) { + + } + + @Override + public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) { + return XmppAxolotlSession.Trust.TRUSTED; + } + + @Override + public X509Certificate getFingerprintCertificate(String fingerprint) { + return null; + } + + @Override + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { + + } + + @Override + public Set<AxolotlAddress> findDevicesWithoutSession(Conversation conversation) { + return Collections.emptySet(); + } + + @Override + public boolean createSessionsIfNeeded(Conversation conversation) { + return false; + } + + @Override + public boolean trustedSessionVerified(Conversation conversation) { + return false; + } + + @Nullable + @Override + public XmppAxolotlMessage encrypt(Message message) { + return null; + } + + @Override + public void preparePayloadMessage(Message message, boolean delay) { + + } + + @Override + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + return null; + } + + @Override + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) { + return null; + } + + @Override + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) { + return null; + } + + @Override + public boolean fetchMapHasErrors(List<Jid> jids) { + return false; + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) { + return Collections.emptySet(); + } + + @Override + public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) { + return Collections.emptySet(); + } + + @Override + public long getNumTrustedKeys(Jid jid) { + return 0; + } + + @Override + public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) { + return false; + } + + @Override + public boolean isConversationAxolotlCapable(Conversation conversation) { + return false; + } + + @Override + public List<Jid> getCryptoTargets(Conversation conversation) { + return Collections.emptyList(); + } + + @Override + public boolean hasPendingKeyFetches(Account account, List<Jid> jids) { + return false; + } + + @Override + public void prepareKeyTransportMessage(Conversation conversation, OnMessageCreatedCallback onMessageCreatedCallback) { + + } + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java index 3c8cb3c1..c634d877 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -77,7 +77,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { this.localRegistrationId = loadRegistrationId(); this.currentPreKeyId = loadCurrentPreKeyId(); for (SignedPreKeyRecord record : loadSignedPreKeys()) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId()); } } @@ -95,7 +95,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { if (ownKey != null) { return ownKey; } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); ownKey = generateIdentityKeyPair(); mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); } @@ -112,13 +112,13 @@ public class SQLiteAxolotlStore implements AxolotlStore { if (!regenerate && regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); reg_id = generateRegistrationId(); boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to write new key to the database!"); } } return reg_id; @@ -130,7 +130,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { if (regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); reg_id = 0; } return reg_id; @@ -344,7 +344,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Failed to write new prekey id to the database!"); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java index b713eb5f..93ed32a2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -168,10 +168,10 @@ public class XmppAxolotlSession { try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); IdentityKey msgIdentityKey = message.getIdentityKey(); if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); + Log.e(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); } else { this.identityKey = msgIdentityKey; plaintext = cipher.decrypt(message); @@ -180,14 +180,14 @@ public class XmppAxolotlSession { } } } catch (InvalidMessageException | InvalidVersionException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received"); + Log.i(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "WhisperMessage received"); WhisperMessage message = new WhisperMessage(encryptedKey); plaintext = cipher.decrypt(message); } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); } } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + Log.w(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); } if (plaintext != null) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index df92898c..8fd91cf4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; + import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 2356ffb9..4c4a1916 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -5,7 +5,6 @@ import android.database.Cursor; import android.os.SystemClock; import android.util.Pair; -import eu.siacs.conversations.crypto.PgpDecryptionService; import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; @@ -20,10 +19,14 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OtrService; +import eu.siacs.conversations.crypto.PgpDecryptionService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl; +import eu.siacs.conversations.crypto.axolotl.AxolotlServiceStub; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -169,6 +172,10 @@ public class Account extends AbstractEntity { private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>(); + public Account() { + this.uuid = "0"; + } + public Account(final Jid jid, final String password) { this(java.util.UUID.randomUUID().toString(), jid, password, 0, null, "", null, null, null, 5222); @@ -255,10 +262,6 @@ public class Account extends AbstractEntity { return this.hostname == null ? "" : this.hostname; } - public boolean isOnion() { - return getServer().toString().toLowerCase().endsWith(".onion"); - } - public void setPort(int port) { this.port = port; } @@ -356,10 +359,15 @@ public class Account extends AbstractEntity { public void initAccountServices(final XmppConnectionService context) { this.mOtrService = new OtrService(context, this); - this.axolotlService = new AxolotlService(this, context); - if (xmppConnection != null) { - xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } + if (ConversationsPlusPreferences.omemoEnabled()) { + this.axolotlService = new AxolotlServiceImpl(this, context); + if (xmppConnection != null) { + xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } + } else { + this.axolotlService = new AxolotlServiceStub(); + } + this.pgpDecryptionService = new PgpDecryptionService(context); } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 088dfd8a..fa30443d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.entities; +import android.graphics.Color; + import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -87,6 +89,11 @@ public class Bookmark extends Element implements ListItem { return tags; } + @Override + public int getStatusColor() { + return Color.parseColor("#259B23"); + } + public String getNick() { return this.findChildContent("nick"); } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 691fc3e4..9b4be366 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.graphics.Color; import org.json.JSONArray; import org.json.JSONException; @@ -167,11 +168,16 @@ public class Contact implements ListItem, Blockable { return tags; } + @Override + public int getStatusColor() { + return UIHelper.getStatusColor(getMostAvailableStatus()); + } + public boolean match(String needle) { if (needle == null || needle.isEmpty()) { return true; } - needle = needle.toLowerCase(Locale.US).trim(); + needle = needle.toLowerCase(Locale.US); String[] parts = needle.split("\\s+"); if (parts.length > 1) { for(int i = 0; i < parts.length; ++i) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 0252ea74..62f976b3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -21,6 +21,8 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.MessageUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.xmpp.chatstate.ChatState; @@ -259,16 +261,12 @@ public class Conversation extends AbstractEntity implements Blockable { return null; } + // TODO Check if this is really necessary public void populateWithMessages(final List<Message> messages) { synchronized (this.messages) { messages.clear(); messages.addAll(this.messages); } - for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) { - if (iterator.next().wasMergedIntoPrevious()) { - iterator.remove(); - } - } } @Override @@ -308,14 +306,6 @@ public class Conversation extends AbstractEntity implements Blockable { return this.mFirstMamReference; } - public void setLastClearHistory(long time) { - setAttribute("last_clear_history",String.valueOf(time)); - } - - public long getLastClearHistory() { - return getLongAttribute("last_clear_history", 0); - } - public List<Jid> getAcceptedCryptoTargets() { if (mode == MODE_SINGLE) { return Arrays.asList(getJid().toBareJid()); @@ -769,7 +759,7 @@ public class Conversation extends AbstractEntity implements Blockable { if (message.hasFileOnRemoteHost()) { otherBody = message.getFileParams().url.toString(); } else { - otherBody = message.body; + otherBody = message.getBody(); } if (otherBody != null && otherBody.equals(body)) { return message; @@ -781,10 +771,6 @@ public class Conversation extends AbstractEntity implements Blockable { } public long getLastMessageTransmitted() { - long last_clear = getLastClearHistory(); - if (last_clear != 0) { - return last_clear; - } synchronized (this.messages) { for(int i = this.messages.size() - 1; i >= 0; --i) { Message message = this.messages.get(i); @@ -941,13 +927,19 @@ public class Conversation extends AbstractEntity implements Blockable { } public int unreadCount() { + if (getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) { + return 0; + } synchronized (this.messages) { int count = 0; for(int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).isRead()) { + Message message = this.messages.get(i); + if (message.isRead()) { return count; } - ++count; + if (alwaysNotify() || MessageUtil.wasHighlightedOrPrivate(message)) { + ++count; + } } return count; } diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java index 22aedd4b..56804fbf 100644 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -11,6 +11,8 @@ public interface ListItem extends Comparable<ListItem> { Jid getJid(); + public int getStatusColor(); + List<Tag> getTags(); final class Tag { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index f6a45533..7a5be8f8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -5,13 +5,10 @@ import android.database.Cursor; import java.net.MalformedURLException; import java.net.URL; -import java.util.Arrays; -import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -66,7 +63,7 @@ public class Message extends AbstractEntity { protected String conversationUuid; protected Jid counterpart; protected Jid trueCounterpart; - protected String body; + private String body; protected String encryptedBody; protected long timeSent; protected int encryption; @@ -84,6 +81,7 @@ public class Message extends AbstractEntity { private Message mNextMessage = null; private Message mPreviousMessage = null; private String axolotlFingerprint = null; + private Decision mTreatAsDownloadAble = Decision.NOT_DECIDED; private Message() { @@ -189,14 +187,6 @@ public class Message extends AbstractEntity { return message; } - public static Message createLoadMoreMessage(Conversation conversation) { - final Message message = new Message(); - message.setType(Message.TYPE_STATUS); - message.setConversation(conversation); - message.setBody("LOAD_MORE"); - return message; - } - @Override public ContentValues getContentValues() { ContentValues values = new ContentValues(); @@ -353,10 +343,6 @@ public class Message extends AbstractEntity { this.carbon = carbon; } - public void setEdited(String edited) { - this.edited = edited; - } - public boolean edited() { return this.edited != null; } @@ -365,10 +351,6 @@ public class Message extends AbstractEntity { this.trueCounterpart = trueCounterpart; } - public Jid getTrueCounterpart() { - return this.trueCounterpart; - } - public Transferable getTransferable() { return this.transferable; } @@ -378,30 +360,40 @@ public class Message extends AbstractEntity { } public boolean equals(Message message) { - if (this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()); - } else if (this.body == null || this.counterpart == null) { + if (this.getServerMsgId() != null && message.getServerMsgId() != null) { + return this.getServerMsgId().equals(message.getServerMsgId()); + } else if (this.getBody() == null || this.getCounterpart() == null + || message.getBody() == null || message.getCounterpart() == null) { return false; } else { String body, otherBody; if (this.hasFileOnRemoteHost()) { - body = getFileParams().url.toString(); - otherBody = message.body == null ? null : message.body.trim(); + body = this.getFileParams().url.toString(); } else { - body = this.body; - otherBody = message.body; + body = this.getBody(); } - if (message.getRemoteMsgId() != null) { - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) - && this.counterpart.equals(message.getCounterpart()) + if (message.hasFileOnRemoteHost()) { + otherBody = message.getFileParams().url.toString(); + } else { + otherBody = message.getBody(); + } + + if (message.getRemoteMsgId() != null && this.getRemoteMsgId() != null) { + return (message.getRemoteMsgId().equals(this.getRemoteMsgId()) + || message.getRemoteMsgId().equals(this.getUuid()) + || message.getUuid().equals(this.getRemoteMsgId())) + && this.getCounterpart().equals(message.getCounterpart()) && (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ; } else { - return this.remoteMsgId == null - && this.counterpart.equals(message.getCounterpart()) - && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; + // existing (send) message with no remoteMsgId and MAM message with remoteMsgId + return ((this.getRemoteMsgId() == null && message.getRemoteMsgId() != null) + || (this.getRemoteMsgId() != null && message.getRemoteMsgId() == null) + // both null is also acceptable + || (this.getRemoteMsgId() == null && message.getRemoteMsgId() == null)) + && this.getCounterpart().equals(message.getCounterpart()) + && body.equals(otherBody); } } } @@ -434,97 +426,16 @@ public class Message extends AbstractEntity { } } - public boolean isLastCorrectableMessage() { - Message next = next(); - while(next != null) { - if (next.isCorrectable()) { - return false; - } - next = next.next(); - } - return isCorrectable(); - } - - private boolean isCorrectable() { - return getStatus() != STATUS_RECEIVED && !isCarbon(); - } - - public boolean mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - this.edited() == message.edited() && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - !GeoHelper.isGeoUri(message.getBody()) && - !GeoHelper.isGeoUri(this.body) && - message.treatAsDownloadable() == Decision.NEVER && - this.treatAsDownloadable() == Decision.NEVER && - !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) && - !this.bodyIsHeart() && - !message.bodyIsHeart() && - this.isTrusted() == message.isTrusted() - ); - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) - ); - } - - public String getMergedBody() { - StringBuilder body = new StringBuilder(this.body.trim()); - Message current = this; - while(current.mergeable(current.next())) { - current = current.next(); - body.append(MERGE_SEPARATOR); - body.append(current.getBody().trim()); - } - return body.toString(); - } - public boolean hasMeCommand() { - return getMergedBody().startsWith(ME_COMMAND); + return getBody().startsWith(ME_COMMAND); } - public int getMergedStatus() { - int status = this.status; - Message current = this; - while(current.mergeable(current.next())) { - current = current.next(); - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while(current.mergeable(current.next())) { - current = current.next(); - time = current.timeSent; + public String getBodyReplacedMeCommand(String replaceString) { + try { + return getBody().replaceAll("^" + Message.ME_COMMAND, replaceString + " "); + } catch (ArrayIndexOutOfBoundsException e) { + return getBody(); } - return time; - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); } public boolean trusted() { @@ -568,28 +479,33 @@ public class Message extends AbstractEntity { MUST, SHOULD, NEVER, + NOT_DECIDED, } - private static String extractRelevantExtension(URL url) { + private String extractRelevantExtension(URL url) { + if (url == null) { + return null; + } String path = url.getPath(); return extractRelevantExtension(path); } - private static String extractRelevantExtension(String path) { + private String extractRelevantExtension(String path) { if (path == null || path.isEmpty()) { return null; } String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); - int dotPosition = filename.lastIndexOf("."); - if (dotPosition != -1) { - String extension = filename.substring(dotPosition + 1); + final String lastPart = FileUtils.getLastExtension(filename); + + if (!lastPart.isEmpty()) { // we want the real file extension, not the crypto one - if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { - return extractRelevantExtension(filename.substring(0,dotPosition)); + final String secondToLastPart = FileUtils.getSecondToLastExtension(filename); + if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + return secondToLastPart; } else { - return extension; + return lastPart; } } return null; @@ -605,53 +521,74 @@ public class Message extends AbstractEntity { } } else { try { - return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim()))); + return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(this.getBody()))); } catch (MalformedURLException e) { return null; } } } + /** + * in case of later found error with decision, set it to a value which does not affect anything, hopefully + */ + public void setNoDownloadable() { + mTreatAsDownloadAble = Decision.NEVER; + } + public Decision treatAsDownloadable() { - if (body.trim().contains(" ")) { - return Decision.NEVER; + // only test this ones, body will not change + if (mTreatAsDownloadAble != Decision.NOT_DECIDED) { + return mTreatAsDownloadAble; + } + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://example.com/image.jpg" text that will not be shown /abc.png" + * or more than one image link in one message. + */ + if (getBody().contains(" ")) { + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } try { URL url = new URL(body); if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { - return Decision.NEVER; + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } else if (oob) { - return Decision.MUST; + mTreatAsDownloadAble = Decision.MUST; + return mTreatAsDownloadAble; } String extension = extractRelevantExtension(url); if (extension == null) { - return Decision.NEVER; + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } String ref = url.getRef(); boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}"); if (encrypted) { if (MimeUtils.guessMimeTypeFromExtension(extension) != null) { - return Decision.MUST; + mTreatAsDownloadAble = Decision.MUST; + return mTreatAsDownloadAble; } else { - return Decision.NEVER; + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension) || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) { - return Decision.SHOULD; + mTreatAsDownloadAble = Decision.SHOULD; + return mTreatAsDownloadAble; } else { - return Decision.NEVER; + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } } catch (MalformedURLException e) { - return Decision.NEVER; + mTreatAsDownloadAble = Decision.NEVER; + return mTreatAsDownloadAble; } } - public boolean bodyIsHeart() { - return body != null && UIHelper.HEARTS.contains(body.trim()); - } - public FileParams getFileParams() { FileParams params = getLegacyFileParams(); if (params != null) { @@ -661,10 +598,10 @@ public class Message extends AbstractEntity { if (this.transferable != null) { params.size = this.transferable.getFileSize(); } - if (body == null) { + if (this.getBody() == null) { return params; } - String parts[] = body.split("\\|"); + String parts[] = this.getBody().split("\\|"); switch (parts.length) { case 1: try { @@ -723,10 +660,10 @@ public class Message extends AbstractEntity { public FileParams getLegacyFileParams() { FileParams params = new FileParams(); - if (body == null) { + if (this.getBody() == null) { return params; } - String parts[] = body.split(","); + String parts[] = this.getBody().split(","); if (parts.length == 3) { try { params.size = Long.parseLong(parts[0]); diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index c4fc30ab..a8f60e39 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -160,7 +160,7 @@ public class ServiceDiscoveryResult { } public String getVer() { - return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim(); + return new String(Base64.encode(this.ver, Base64.DEFAULT)); } public ServiceDiscoveryResult(Cursor cursor) throws JSONException { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 05fa0b82..649f767d 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -12,11 +12,9 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.tzur.conversations.Settings; import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xmpp.jid.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; public abstract class AbstractGenerator { private final String[] FEATURES = { @@ -38,35 +36,13 @@ public abstract class AbstractGenerator { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; - private final String[] MESSAGE_CORRECTION_FEATURES = { - "urn:xmpp:message-correct:0" - }; - private String mVersion = null; - protected final String IDENTITY_NAME = "Conversations"; protected final String IDENTITY_TYPE = "phone"; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - protected XmppConnectionService mXmppConnectionService; - - protected AbstractGenerator(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - protected String getIdentityVersion() { - if (mVersion == null) { - this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); - } - return this.mVersion; - } - - public String getIdentityName() { - return IDENTITY_NAME + " " + getIdentityVersion(); - } - public String getCapHash() { StringBuilder s = new StringBuilder(); - s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<"); + s.append("client/" + IDENTITY_TYPE + "//" + ConversationsPlusApplication.getNameAndVersion() + "<"); MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); @@ -78,7 +54,7 @@ public abstract class AbstractGenerator { s.append(feature + "<"); } byte[] sha1 = md.digest(s.toString().getBytes()); - return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); + return new String(Base64.encode(sha1, Base64.DEFAULT)); } public static String getTimestamp(long time) { @@ -89,12 +65,9 @@ public abstract class AbstractGenerator { public List<String> getFeatures() { ArrayList<String> features = new ArrayList<>(); features.addAll(Arrays.asList(FEATURES)); - if (mXmppConnectionService.confirmMessages()) { + if (Settings.CONFIRM_MESSAGE_RECEIVED) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } - if (mXmppConnectionService.allowMessageCorrection()) { - features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); - } Collections.sort(features); return features; } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index b7911ef7..eff9d9c0 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -15,13 +15,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.forms.Data; @@ -31,10 +31,6 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class IqGenerator extends AbstractGenerator { - public IqGenerator(final XmppConnectionService service) { - super(service); - } - public IqPacket discoResponse(final IqPacket request) { final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); packet.setId(request.getId()); @@ -45,7 +41,7 @@ public class IqGenerator extends AbstractGenerator { final Element identity = query.addChild("identity"); identity.setAttribute("category", "client"); identity.setAttribute("type", IDENTITY_TYPE); - identity.setAttribute("name", getIdentityName()); + identity.setAttribute("name", ConversationsPlusApplication.getNameAndVersion()); for (final String feature : getFeatures()) { query.addChild("feature").setAttribute("var", feature); } @@ -55,8 +51,8 @@ public class IqGenerator extends AbstractGenerator { public IqPacket versionResponse(final IqPacket request) { final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); Element query = packet.query("jabber:iq:version"); - query.addChild("name").setContent(IDENTITY_NAME); - query.addChild("version").setContent(getIdentityVersion()); + query.addChild("name").setContent(ConversationsPlusApplication.getName()); + query.addChild("version").setContent(ConversationsPlusApplication.getVersion()); return packet; } @@ -88,51 +84,13 @@ public class IqGenerator extends AbstractGenerator { return publish("http://jabber.org/protocol/nick", item); } - public IqPacket publishAvatar(Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", "urn:xmpp:avatar:data"); - data.setContent(avatar.image); - return publish("urn:xmpp:avatar:data", item); - } - - public IqPacket publishAvatarMetadata(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element metadata = item - .addChild("metadata", "urn:xmpp: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 publish("urn:xmpp:avatar:metadata", item); - } - - public IqPacket retrievePepAvatar(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); - packet.setTo(avatar.owner); - return packet; - } - - public IqPacket retrieveVcardAvatar(final Avatar avatar) { + public static IqPacket retrieveVcardAvatar(final Avatar avatar) { final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(avatar.owner); packet.addChild("vCard", "vcard-temp"); return packet; } - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); - if (to != null) { - packet.setTo(to); - } - return packet; - } - public IqPacket retrieveDeviceIds(final Jid to) { final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if(to != null) { diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 0e7a8ce6..2d7b66b5 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -9,20 +9,17 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { - public MessageGenerator(XmppConnectionService service) { - super(service); - } private MessagePacket preparePacket(Message message) { Conversation conversation = message.getConversation(); @@ -32,13 +29,13 @@ public class MessageGenerator extends AbstractGenerator { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); packet.addChild("markable", "urn:xmpp:chat-markers:0"); - if (this.mXmppConnectionService.indicateReceived()) { + if (ConversationsPlusPreferences.indicateReceived()) { packet.addChild("request", "urn:xmpp:receipts"); } } else if (message.getType() == Message.TYPE_PRIVATE) { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); - if (this.mXmppConnectionService.indicateReceived()) { + if (ConversationsPlusPreferences.indicateReceived()) { packet.addChild("request", "urn:xmpp:receipts"); } } else { @@ -72,6 +69,15 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public static void addXhtmlImImage(MessagePacket packet, Message.FileParams params) { + Element html = packet.addChild("html", "http://jabber.org/protocol/xhtml-im"); + Element body = html.addChild("body", "http://www.w3.org/1999/xhtml"); + Element img = body.addChild("img"); + img.setAttribute("src", params.url.toString()); + img.setAttribute("height", params.height); + img.setAttribute("width", params.width); + } + public static void addMessageHints(MessagePacket packet) { packet.addChild("private", "urn:xmpp:carbons:2"); packet.addChild("no-copy", "urn:xmpp:hints"); @@ -107,6 +113,9 @@ public class MessageGenerator extends AbstractGenerator { Message.FileParams fileParams = message.getFileParams(); content = fileParams.url.toString(); packet.addChild("x","jabber:x:oob").addChild("url").setContent(content); + if (fileParams.width > 0 && fileParams.height > 0) { + addXhtmlImImage(packet,fileParams); + } } else { content = message.getBody(); } @@ -142,9 +151,8 @@ public class MessageGenerator extends AbstractGenerator { packet.setType(MessagePacket.TYPE_CHAT); packet.setTo(to); packet.setFrom(account.getJid()); - Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0"); + Element received = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); received.setAttribute("id", id); - packet.addChild("store", "urn:xmpp:hints"); return packet; } diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 093a8963..9ac7d318 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -3,16 +3,11 @@ package eu.siacs.conversations.generator; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceGenerator extends AbstractGenerator { - public PresenceGenerator(XmppConnectionService service) { - super(service); - } - private PresencePacket subscription(String type, Contact contact) { PresencePacket packet = new PresencePacket(); packet.setAttribute("type", type); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index a8b31a7a..f105646f 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -1,7 +1,5 @@ package eu.siacs.conversations.http; -import android.os.Build; - import org.apache.http.conn.ssl.StrictHostnameVerifier; import java.io.IOException; diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index d23cb71a..66687c3a 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -15,6 +15,12 @@ import java.util.concurrent.CancellationException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; @@ -25,6 +31,7 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.FileUtils; public class HttpDownloadConnection implements Transferable { @@ -37,13 +44,11 @@ public class HttpDownloadConnection implements Transferable { private int mStatus = Transferable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; private int mProgress = 0; - private boolean mUseTor = false; - private boolean canceled = false; + private boolean canceled = false; public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; this.mXmppConnectionService = manager.getXmppConnectionService(); - this.mUseTor = mXmppConnectionService.useTorToConnect(); } @Override @@ -73,23 +78,23 @@ public class HttpDownloadConnection implements Transferable { } else { mUrl = new URL(message.getBody()); } - String[] parts = mUrl.getPath().toLowerCase().split("\\."); - String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null; - String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; - if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { + final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/')).toLowerCase(); + final String lastPart = FileUtils.getLastExtension(sUrlFilename); + + if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { this.message.setEncryption(Message.ENCRYPTION_PGP); } else if (message.getEncryption() != Message.ENCRYPTION_OTR && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; - if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { - extension = secondToLast; + if (!lastPart.isEmpty() && VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + extension = FileUtils.getSecondToLastExtension(sUrlFilename); } else { extension = lastPart; } message.setRelativeFilePath(message.getUuid() + "." + extension); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); String reference = mUrl.getRef(); if (reference != null && reference.length() == 96) { this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); @@ -123,7 +128,7 @@ public class HttpDownloadConnection implements Transferable { } private void finish() { - mXmppConnectionService.getFileBackend().updateMediaScanner(file); + FileBackend.updateMediaScanner(file, mXmppConnectionService); message.setTransferable(null); mHttpConnectionManager.finishConnection(this); if (message.getEncryption() == Message.ENCRYPTION_PGP) { @@ -169,6 +174,10 @@ public class HttpDownloadConnection implements Transferable { HttpDownloadConnection.this.acceptedAutomatically = false; HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); return; + } catch (RemoteFileNotFoundException e) { + message.setNoDownloadable(); + cancel(); + return; } catch (IOException e) { Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); if (interactive) { @@ -178,7 +187,10 @@ public class HttpDownloadConnection implements Transferable { return; } file.setExpectedSize(size); - if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + if (mHttpConnectionManager.hasStoragePermission() + && size != -1 + && size <= ConversationsPlusPreferences.autoAcceptFileSize() + && mXmppConnectionService.isDownloadAllowedInConnection()) { HttpDownloadConnection.this.acceptedAutomatically = true; new Thread(new FileDownloader(interactive)).start(); } else { @@ -189,34 +201,37 @@ public class HttpDownloadConnection implements Transferable { } private long retrieveFileSize() throws IOException { - try { - Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); + try { + Logging.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); changeStatus(STATUS_CHECKING); - HttpURLConnection connection; - if (mUseTor) { - connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); connection.setRequestMethod("HEAD"); - Log.d(Config.LOGTAG,"url: "+connection.getURL().toString()); - Log.d(Config.LOGTAG,"connection: "+connection.toString()); - connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName()); + Logging.d(Config.LOGTAG, "url: " + connection.getURL().toString()); + Logging.d(Config.LOGTAG, "connection: " + connection.toString()); + connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); + // https://code.google.com/p/android/issues/detail?id=24672 + connection.setRequestProperty("Accept-Encoding", ""); if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); } connection.connect(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { + Logging.d(Config.LOGTAG, "remote file not found"); + throw new RemoteFileNotFoundException(); + } String contentLength = connection.getHeaderField("Content-Length"); connection.disconnect(); if (contentLength == null) { - throw new IOException(); + return -1; } return Long.parseLong(contentLength, 10); - } catch (IOException e) { + } catch (RemoteFileNotFoundException e) { throw e; - } catch (NumberFormatException e) { - throw new IOException(); - } + } catch (IOException e) { + return -1; + } catch (NumberFormatException e) { + return -1; + } } } @@ -248,71 +263,66 @@ public class HttpDownloadConnection implements Transferable { } } - private void download() throws Exception { - InputStream is = null; - PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid()); - try { - wakeLock.acquire(); - HttpURLConnection connection; - if (mUseTor) { - connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mUrl.openConnection(); - } - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName()); - final boolean tryResume = file.exists() && file.getKey() == null; - if (tryResume) { - Log.d(Config.LOGTAG,"http download trying resume"); - long size = file.getSize(); - connection.setRequestProperty("Range", "bytes="+size+"-"); - } - connection.connect(); - is = new BufferedInputStream(connection.getInputStream()); - boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges")); - long transmitted = 0; - long expected = file.getExpectedSize(); - if (tryResume && serverResumed) { - Log.d(Config.LOGTAG,"server resumed"); - transmitted = file.getSize(); - updateProgress((int) ((((double) transmitted) / expected) * 100)); - os = AbstractConnectionManager.createAppendedOutputStream(file); - } else { - file.getParentFile().mkdirs(); - file.createNewFile(); - os = AbstractConnectionManager.createOutputStream(file, true); - } - int count = -1; - byte[] buffer = new byte[1024]; - while ((count = is.read(buffer)) != -1) { - transmitted += count; - os.write(buffer, 0, count); - updateProgress((int) ((((double) transmitted) / expected) * 100)); - if (canceled) { - throw new CancellationException(); - } - } - } catch (CancellationException | IOException e) { - throw e; - } finally { - if (os != null) { - try { - os.flush(); - } catch (final IOException ignored) { - - } - } - FileBackend.close(os); - FileBackend.close(is); - wakeLock.release(); - } + private void download() throws SSLHandshakeException, IOException { + InputStream is = null; + PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid()); + try { + wakeLock.acquire(); + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); + final boolean tryResume = file.exists() && file.getKey() == null; + if (tryResume) { + Logging.d(Config.LOGTAG, "http download trying resume"); + long size = file.getSize(); + connection.setRequestProperty("Range", "bytes="+size+"-"); + } + connection.connect(); + is = new BufferedInputStream(connection.getInputStream()); + boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges")); + long transmitted = 0; + long expected = file.getExpectedSize(); + if (tryResume && serverResumed) { + Logging.d(Config.LOGTAG, "server resumed"); + transmitted = file.getSize(); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + os = AbstractConnectionManager.createAppendedOutputStream(file); + } else { + file.getParentFile().mkdirs(); + file.createNewFile(); + os = AbstractConnectionManager.createOutputStream(file, true); + } + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + transmitted += count; + os.write(buffer, 0, count); + updateProgress((int) ((((double) transmitted) / expected) * 100)); + if (canceled) { + throw new CancellationException(); + } + } + } catch (CancellationException | IOException e) { + throw e; + } finally { + if (os != null) { + try { + os.flush(); + } catch (final IOException ignored) { + + } + } + StreamUtil.close(os); + StreamUtil.close(is); + wakeLock.release(); + } } private void updateImageBounds() { message.setType(Message.TYPE_FILE); - mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl); + MessageUtil.updateFileParams(message, mUrl); mXmppConnectionService.updateMessage(message); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index e337509b..e236cdc0 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.http; import android.app.PendingIntent; import android.os.PowerManager; -import android.util.Log; import android.util.Pair; import java.io.FileNotFoundException; @@ -12,9 +11,14 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.util.Scanner; import javax.net.ssl.HttpsURLConnection; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; @@ -44,7 +48,6 @@ public class HttpUploadConnection implements Transferable { private String mime; private URL mGetUrl; private URL mPutUrl; - private boolean mUseTor = false; private byte[] key = null; @@ -55,7 +58,6 @@ public class HttpUploadConnection implements Transferable { public HttpUploadConnection(HttpConnectionManager httpConnectionManager) { this.mHttpConnectionManager = httpConnectionManager; this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); - this.mUseTor = mXmppConnectionService.useTorToConnect(); } @Override @@ -90,13 +92,13 @@ public class HttpUploadConnection implements Transferable { mHttpConnectionManager.finishUploadConnection(this); message.setTransferable(null); mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - FileBackend.close(mFileInputStream); + StreamUtil.close(mFileInputStream); } public void init(Message message, boolean delay) { this.message = message; this.account = message.getConversation().getAccount(); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); this.mime = this.file.getMimeType(); this.delayed = delay; if (Config.ENCRYPT_ON_HTTP_UPLOADED @@ -153,23 +155,21 @@ public class HttpUploadConnection implements Transferable { private void upload() { OutputStream os = null; + InputStream errorStream = null; HttpURLConnection connection = null; PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid()); try { wakeLock.acquire(); - Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); - if (mUseTor) { - connection = (HttpURLConnection) mPutUrl.openConnection(mHttpConnectionManager.getProxy()); - } else { - connection = (HttpURLConnection) mPutUrl.openConnection(); - } - if (connection instanceof HttpsURLConnection) { + Logging.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); + connection = (HttpURLConnection) mPutUrl.openConnection(); + + if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true); } connection.setRequestMethod("PUT"); connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime); - connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName()); + connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); connection.setDoOutput(true); connection.connect(); os = connection.getOutputStream(); @@ -186,12 +186,12 @@ public class HttpUploadConnection implements Transferable { mFileInputStream.close(); int code = connection.getResponseCode(); if (code == 200 || code == 201) { - Log.d(Config.LOGTAG, "finished uploading file"); + Logging.d(Config.LOGTAG, "finished uploading file"); if (key != null) { mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); } - mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl); - mXmppConnectionService.getFileBackend().updateMediaScanner(file); + MessageUtil.updateFileParams(message, mGetUrl); + FileBackend.updateMediaScanner(file, mXmppConnectionService); message.setTransferable(null); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { @@ -215,15 +215,17 @@ public class HttpUploadConnection implements Transferable { mXmppConnectionService.resendMessage(message, delayed); } } else { + errorStream = connection.getErrorStream(); + Logging.e("httpupload", "file upload failed: http code (" + code + ") " + new Scanner(errorStream).useDelimiter("\\A").next()); fail(); } } catch (IOException e) { - e.printStackTrace(); - Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); + errorStream = connection.getErrorStream(); + Logging.e("httpupload", "http response: " + new Scanner(errorStream).useDelimiter("\\A").next() + ", exception message: " + e.getMessage()); fail(); } finally { - FileBackend.close(mFileInputStream); - FileBackend.close(os); + StreamUtil.close(os); + StreamUtil.close(errorStream); if (connection != null) { connection.disconnect(); } diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 7d399073..ad368f11 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.parser; - import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -21,6 +20,15 @@ public abstract class AbstractParser { this.mXmppConnectionService = service; } + /** + * Gets the timestamp from the 'delay' element. + * Refer to XEP-0203: Delayed Delivery for details. @link{http://xmpp.org/extensions/xep-0203.html} + * @param element the element to find the child element 'delay' in. + * @return the time in milli seconds of the attribute 'stamp' of the + * element 'delay'. In case there is no 'delay' element or no 'stamp' + * attribute or the current time is less than the value of the 'stamp' + * attribute the current time is returned. + */ public static Long getTimestamp(Element element, Long defaultValue) { Element delay = element.findChild("delay","urn:xmpp:delay"); if (delay != null) { @@ -40,12 +48,27 @@ public abstract class AbstractParser { return getTimestamp(packet,System.currentTimeMillis()); } + /** + * Parses the timestamp according to XEP-0082: XMPP Date and Time Profiles. + * @link{http://xmpp.org/extensions/xep-0082.html} + * + * @param timestamp the timestamp to parse + * @return Date + * @throws ParseException + */ public static Date parseTimestamp(String timestamp) throws ParseException { - timestamp = timestamp.replace("Z", "+0000"); - SimpleDateFormat dateFormat; - timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length()); - dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US); - return dateFormat.parse(timestamp); + /*try { + Logging.d("TIMESTAMP", timestamp); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(timestamp).toGregorianCalendar().getTime(); + } catch (DatatypeConfigurationException e) { + Logging.d("TIMESTAMP", e.getMessage()); + return new Date(); + }*/ + timestamp = timestamp.replace("Z", "+0000"); + SimpleDateFormat dateFormat; + timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length()); + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US); + return dateFormat.parse(timestamp); } protected void updateLastseen(final AbstractStanza packet, final Account account, final boolean presenceOverwrite) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 5903d511..c03ed42f 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -22,10 +22,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; @@ -69,7 +71,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { contact.parseSubscriptionFromElement(item); } } - mXmppConnectionService.getAvatarService().clear(contact); + AvatarService.getInstance().clear(contact); } } mXmppConnectionService.updateConversationUi(); @@ -155,7 +157,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT); } catch (Throwable e) { - Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature"); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature"); return null; } } @@ -169,7 +171,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); } catch (Throwable e) { - Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); } return identityKey; } @@ -209,7 +211,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public Pair<X509Certificate[],byte[]> verification(final IqPacket packet) { Element item = getItem(packet); - Element verification = item != null ? item.findChild("verification",AxolotlService.PEP_PREFIX) : null; + Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null; Element chain = verification != null ? verification.findChild("chain") : null; Element signature = verification != null ? verification.findChild("signature") : null; if (chain != null && signature != null) { @@ -280,7 +282,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) && packet.fromServer(account)) { // Block list or block push. - Log.d(Config.LOGTAG, "Received blocklist update from server"); + Logging.d(Config.LOGTAG, "Received blocklist update from server"); final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING); final Element block = packet.findChild("block", Xmlns.BLOCKING); final Collection<Element> items = blocklist != null ? blocklist.getChildren() : @@ -308,7 +310,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); } else if (packet.hasChild("unblock", Xmlns.BLOCKING) && packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { - Log.d(Config.LOGTAG, "Received unblock update from server"); + Logging.d(Config.LOGTAG, "Received unblock update from server"); final Collection<Element> items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren(); if (items.size() == 0) { // No children to unblock == unblock all diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 21bdbdf6..3354c2d7 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -3,16 +3,23 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; +import de.tzur.conversations.Settings; + import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import java.net.URL; import java.util.ArrayList; import java.util.Set; -import java.util.UUID; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.AvatarUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.OtrService; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; @@ -21,6 +28,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; @@ -45,6 +53,7 @@ public class MessageParser extends AbstractParser implements if (from.toBareJid().equals(account.getJid().toBareJid())) { conversation.setOutgoingChatState(state); if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) { + Logging.d("markRead", "MessageParser.extractChatState (" + conversation.getName() + ")"); mXmppConnectionService.markRead(conversation); account.activateGracePeriod(); } @@ -115,7 +124,7 @@ public class MessageParser extends AbstractParser implements if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); finishedMessage.setFingerprint(plaintextMessage.getFingerprint()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); + Logging.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); } return finishedMessage; @@ -179,23 +188,23 @@ public class MessageParser extends AbstractParser implements Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { avatar.owner = from.toBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (account.getJid().toBareJid().equals(from)) { if (account.setAvatar(avatar.getFilename())) { mXmppConnectionService.databaseBackend.updateAccount(account); } - mXmppConnectionService.getAvatarService().clear(account); + AvatarService.getInstance().clear(account); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } else { Contact contact = account.getRoster().getContact(from); contact.setAvatar(avatar); - mXmppConnectionService.getAvatarService().clear(contact); + AvatarService.getInstance().clear(contact); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateRosterUi(); } } else { - mXmppConnectionService.fetchAvatar(account, avatar); + AvatarService.getInstance().fetchAvatar(account, avatar); } } } else if ("http://jabber.org/protocol/nick".equals(node)) { @@ -204,12 +213,12 @@ public class MessageParser extends AbstractParser implements if (nick != null && nick.getContent() != null) { Contact contact = account.getRoster().getContact(from); contact.setPresenceName(nick.getContent()); - mXmppConnectionService.getAvatarService().clear(account); + AvatarService.getInstance().clear(account); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } - } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); + } else if (ConversationsPlusPreferences.omemoEnabled() && AxolotlService.PEP_DEVICE_LIST.equals(node)) { + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); @@ -270,7 +279,7 @@ public class MessageParser extends AbstractParser implements serverMsgId = result.getAttribute("id"); query.incrementMessageCount(); } else if (query != null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); return; } else if (original.fromServer(account)) { Pair<MessagePacket, Long> f; @@ -294,10 +303,8 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); final Element oob = packet.findChild("x", "jabber:x:oob"); final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url")); - final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; @@ -357,7 +364,7 @@ public class MessageParser extends AbstractParser implements return; } } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed)); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed)); message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } } else if (pgpEncrypted != null && Config.supportOpenPgp()) { @@ -405,42 +412,6 @@ public class MessageParser extends AbstractParser implements updateLastseen(timestamp, account, packet.getFrom(), true); } - if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) { - Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, - counterpart, - message.getStatus() == Message.STATUS_RECEIVED, - message.isCarbon()); - if (replacedMessage != null) { - final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null - || replacedMessage.getFingerprint().equals(message.getFingerprint()); - final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null - && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart()); - if (fingerprintsMatch && (trueCountersMatch || conversation.getMode() == Conversation.MODE_SINGLE)) { - Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'"); - final String uuid = replacedMessage.getUuid(); - replacedMessage.setUuid(UUID.randomUUID().toString()); - replacedMessage.setBody(message.getBody()); - replacedMessage.setEdited(replacedMessage.getRemoteMsgId()); - replacedMessage.setRemoteMsgId(remoteMsgId); - replacedMessage.setEncryption(message.getEncryption()); - if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) { - replacedMessage.markUnread(); - } - mXmppConnectionService.updateMessage(replacedMessage, uuid); - mXmppConnectionService.getNotificationService().updateNotification(false); - if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { - sendMessageReceipts(account, packet); - } - if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) { - conversation.getAccount().getPgpDecryptionService().add(replacedMessage); - } - return; - } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received message correction but verification didn't check out"); - } - } - } - boolean checkForDuplicates = query != null || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) || message.getType() == Message.TYPE_PRIVATE; @@ -461,12 +432,16 @@ public class MessageParser extends AbstractParser implements if (query == null || query.getWith() == null) { //either no mam or catchup if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { + Logging.d("markRead", "MessageParser.onMessagePacketReceived1 (" + conversation.getName() + ")"); mXmppConnectionService.markRead(conversation); if (query == null) { account.activateGracePeriod(); } } else { - message.markUnread(); + // only not mam messages should be marked as unread + if (query == null) { + message.markUnread(); + } } } @@ -474,7 +449,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); } - if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { + if (Settings.CONFIRM_MESSAGE_READ && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { sendMessageReceipts(account, packet); } @@ -485,17 +460,23 @@ public class MessageParser extends AbstractParser implements conversation.endOtrIfNeeded(); } - if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { mXmppConnectionService.databaseBackend.createMessage(message); } - final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); - if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) { - manager.createNewDownloadConnection(message); - } else if (!message.isRead()) { + if (message.trusted() + && message.treatAsDownloadable() != Message.Decision.NEVER + && ConversationsPlusPreferences.autoAcceptFileSize() > 0 + && ConversationsPlusPreferences.autoDownloadFileLink()) { + this.mXmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message); + } else { if (query == null) { mXmppConnectionService.getNotificationService().push(message); } else if (query.getWith() == null) { // mam catchup - mXmppConnectionService.getNotificationService().pushFromBacklog(message); + /* + Like suggested in https://bugs.thedevstack.de/task/156 user should be notified + in some other way of loaded messages. + */ + // mXmppConnectionService.getNotificationService().pushFromBacklog(message); } } } else if (!packet.hasChild("body")){ //no body @@ -540,6 +521,7 @@ public class MessageParser extends AbstractParser implements if (packet.fromAccount(account)) { Conversation conversation = mXmppConnectionService.find(account,counterpart.toBareJid()); if (conversation != null) { + Logging.d("markRead", "MessageParser.onMessagePacketReceived2 (" + conversation.getName() + ")"); mXmppConnectionService.markRead(conversation); } } else { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index c2782d23..76da5a31 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -5,6 +5,9 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.utils.AvatarUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; @@ -14,8 +17,10 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -42,12 +47,13 @@ public class PresenceParser extends AbstractParser implements processConferencePresence(packet, mucOptions); final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5); if (!tileUserAfter.equals(tileUserBefore)) { - mXmppConnectionService.getAvatarService().clear(mucOptions); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": update tiles for " + conversation.getName()); + AvatarService.getInstance().clear(conversation); } if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) { - mXmppConnectionService.updateConversationUi(); + UiUpdateHelper.updateConversationUi(); } else if (mucOptions.online()) { - mXmppConnectionService.updateMucRosterUi(); + UiUpdateHelper.updateMucRosterUi(); } } } @@ -97,12 +103,12 @@ public class PresenceParser extends AbstractParser implements } if (avatar != null) { avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (user.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(user); + AvatarService.getInstance().clear(user); } } else { - mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar); + AvatarService.getInstance().fetchAvatar(mucOptions.getAccount(), avatar); } } } @@ -129,7 +135,7 @@ public class PresenceParser extends AbstractParser implements } else if (!from.isBareJid()){ MucOptions.User user = mucOptions.deleteUser(from.getResourcepart()); if (user != null) { - mXmppConnectionService.getAvatarService().clear(user); + AvatarService.getInstance().clear(user); } } } else if (type.equals("error")) { @@ -182,14 +188,14 @@ public class PresenceParser extends AbstractParser implements Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); if (avatar != null && !contact.isSelf()) { avatar.owner = from.toBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (contact.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); + AvatarService.getInstance().clear(contact); + UiUpdateHelper.updateConversationUi(); + UiUpdateHelper.updateRosterUi(); } } else { - mXmppConnectionService.fetchAvatar(account, avatar); + AvatarService.getInstance().fetchAvatar(account, avatar); } } int sizeBefore = contact.getPresences().size(); @@ -240,7 +246,7 @@ public class PresenceParser extends AbstractParser implements } } } - mXmppConnectionService.updateRosterUi(); + UiUpdateHelper.updateRosterUi(); } @Override diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 04c53238..e8acd400 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -33,8 +33,9 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.json.JSONException; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl; import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; @@ -245,7 +246,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) ).toString(); } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " + Logging.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + ": " + ignored + ". Skipping..."); continue; @@ -270,7 +271,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.getString(cursor.getColumnIndex(Contact.JID)) ).toString(); } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Contact JID " + Logging.e(Config.LOGTAG, "Failed to migrate Contact JID " + cursor.getString(cursor.getColumnIndex(Contact.JID)) + ": " + ignored + ". Skipping..."); continue; @@ -299,7 +300,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { "mobile" ).getDomainpart(); } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " + Logging.e(Config.LOGTAG, "Failed to migrate Account SERVER " + cursor.getString(cursor.getColumnIndex(Account.SERVER)) + ": " + ignored + ". Skipping..."); continue; @@ -365,7 +366,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } - if (oldVersion < 22 && newVersion >= 22) { + if (oldVersion < 22 && oldVersion >= 15 && newVersion >= 22) { db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); } @@ -956,7 +957,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } } cursor.close(); @@ -981,7 +982,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } } cursor.close(); @@ -1111,7 +1112,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void recreateAxolotlDb(SQLiteDatabase db) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + Log.d(Config.LOGTAG, AxolotlServiceImpl.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); @@ -1124,7 +1125,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void wipeAxolotlDb(Account account) { String accountName = account.getUuid(); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); SQLiteDatabase db = this.getWritableDatabase(); String[] deleteArgs = { accountName diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 21ba4509..c0d09c07 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1,59 +1,37 @@ package eu.siacs.conversations.persistance; -import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.RectF; import android.net.Uri; import android.os.Environment; -import android.provider.OpenableColumns; -import android.util.Base64; -import android.util.Base64OutputStream; import android.util.Log; import android.webkit.MimeTypeMap; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.Socket; -import java.net.URL; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.List; import java.util.Locale; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.ExifHelper; -import eu.siacs.conversations.utils.FileUtils; -import eu.siacs.conversations.xmpp.pep.Avatar; public class FileBackend { - private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); + private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - private XmppConnectionService mXmppConnectionService; - - public FileBackend(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private void createNoMedia() { + public static void createNoMedia() { final File nomedia = new File(getConversationsFileDirectory()+".nomedia"); if (!nomedia.exists()) { try { @@ -64,31 +42,29 @@ public class FileBackend { } } - public void updateMediaScanner(File file) { + public static void updateMediaScanner(File file, XmppConnectionService xmppConnectionService) { if (file.getAbsolutePath().startsWith(getConversationsImageDirectory())) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); - mXmppConnectionService.sendBroadcast(intent); - } else { - createNoMedia(); + xmppConnectionService.sendBroadcast(intent); } } - public boolean deleteFile(Message message) { + public static boolean deleteFile(Message message, XmppConnectionService xmppConnectionService) { File file = getFile(message); if (file.delete()) { - updateMediaScanner(file); + updateMediaScanner(file, xmppConnectionService); return true; } else { return false; } } - public DownloadableFile getFile(Message message) { + public static DownloadableFile getFile(Message message) { return getFile(message, true); } - public DownloadableFile getFile(Message message, boolean decrypted) { + public static DownloadableFile getFile(Message message, boolean decrypted) { final boolean encrypted = !decrypted && (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); @@ -114,110 +90,30 @@ public class FileBackend { } } - private static long getFileSize(Context context, Uri uri) { - Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); - } else { - return -1; - } - } - - public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) { - if (max <= 0) { - return true; //exception to be compatible with HTTP Upload < v0.2 - } - for(Uri uri : uris) { - if (FileBackend.getFileSize(context, uri) > max) { - return false; - } - } - return true; - } - public static String getConversationsFileDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; + return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator; } public static String getConversationsImageDirectory() { - return Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath() - + "/Conversations/"; - } - - public 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)); - } - Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); - if (originalBitmap != null && !originalBitmap.isRecycled()) { - originalBitmap.recycle(); - } - return result; - } else { - return originalBitmap; - } + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator; } - public static Bitmap rotate(Bitmap bitmap, int degree) { - if (degree == 0) { - return bitmap; - } - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - Matrix mtx = new Matrix(); - mtx.postRotate(degree); - Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - } - return result; - } - - public boolean useImageAsIs(Uri uri) { - String path = getOriginalPath(uri); - if (path == null) { - return false; - } - File file = new File(path); - long size = file.length(); - if (size == 0 || size >= Config.IMAGE_MAX_SIZE ) { - return false; - } - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - try { - BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options); - if (options == null || options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { - return false; - } - return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); - } catch (FileNotFoundException e) { - return false; - } - } + public static String getPrivateFileDirectoryPath() { + return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath(); + } - public String getOriginalPath(Uri uri) { - return FileUtils.getPath(mXmppConnectionService,uri); - } + private static String getPrivateImageDirectoryPath() { + return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator; + } - public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { + public static void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { file.getParentFile().mkdirs(); OutputStream os = null; InputStream is = null; try { file.createNewFile(); os = new FileOutputStream(file); - is = mXmppConnectionService.getContentResolver().openInputStream(uri); + is = StreamUtil.openInputStreamFromContentResolver(uri); byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) > 0) { @@ -230,420 +126,69 @@ public class FileBackend { e.printStackTrace(); throw new FileCopyException(R.string.error_io_exception); } finally { - close(os); - close(is); + StreamUtil.close(os); + StreamUtil.close(is); } - Log.d(Config.LOGTAG, "output file name " + file.getAbsolutePath()); + Logging.d(Config.LOGTAG, "output file name " + file); } - public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + public static void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); - String mime = mXmppConnectionService.getContentResolver().getType(uri); + String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); message.setRelativeFilePath(message.getUuid() + "." + extension); - copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); - } - - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { - file.getParentFile().mkdirs(); - InputStream is = null; - OutputStream os = null; - try { - file.createNewFile(); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - Bitmap originalBitmap; - BitmapFactory.Options options = new BitmapFactory.Options(); - int inSampleSize = (int) Math.pow(2, sampleSize); - Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); - options.inSampleSize = inSampleSize; - originalBitmap = BitmapFactory.decodeStream(is, null, options); - is.close(); - if (originalBitmap == null) { - throw new FileCopyException(R.string.error_not_an_image_file); - } - Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); - int rotation = getRotation(image); - scaledBitmap = rotate(scaledBitmap, rotation); - boolean targetSizeReached = false; - int quality = Config.IMAGE_QUALITY; - while(!targetSizeReached) { - os = new FileOutputStream(file); - boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); - if (!success) { - throw new FileCopyException(R.string.error_compressing_image); - } - os.flush(); - targetSizeReached = file.length() <= Config.IMAGE_MAX_SIZE || quality <= 50; - quality -= 5; - } - scaledBitmap.recycle(); - return; - } catch (FileNotFoundException e) { - throw new FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - e.printStackTrace(); - throw new FileCopyException(R.string.error_io_exception); - } catch (SecurityException e) { - throw new FileCopyException(R.string.error_security_exception_during_image_copy); - } catch (OutOfMemoryError e) { - ++sampleSize; - if (sampleSize <= 3) { - copyImageToPrivateStorage(file, image, sampleSize); - } else { - throw new FileCopyException(R.string.error_out_of_memory); - } - } catch (NullPointerException e) { - throw new FileCopyException(R.string.error_io_exception); - } finally { - close(os); - close(is); - } - } - - public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { - copyImageToPrivateStorage(file, image, 0); - } - - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { - switch(Config.IMAGE_FORMAT) { - case JPEG: - message.setRelativeFilePath(message.getUuid()+".jpg"); - break; - case PNG: - message.setRelativeFilePath(message.getUuid()+".png"); - break; - case WEBP: - message.setRelativeFilePath(message.getUuid()+".webp"); - break; - } - copyImageToPrivateStorage(getFile(message), image); - updateFileParams(message); - } - - private int getRotation(File file) { - return getRotation(Uri.parse("file://"+file.getAbsolutePath())); - } - - private int getRotation(Uri image) { - InputStream is = null; - try { - is = mXmppConnectionService.getContentResolver().openInputStream(image); - return ExifHelper.getOrientation(is); - } catch (FileNotFoundException e) { - return 0; - } finally { - close(is); - } - } - - public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) - throws FileNotFoundException { - Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(message.getUuid()); - if ((thumbnail == null) && (!cacheOnly)) { - File file = 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, getRotation(file)); - this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),thumbnail); - } - return thumbnail; - } - - public Uri getTakePhotoUri() { + copyFileToPrivateStorage(getFile(message), uri); + } + + public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException { + message.setRelativeFilePath(FileBackend.getPrivateImageDirectoryPath() + message.getUuid() + ".jpg"); + DownloadableFile file = getFile(message); + file.getParentFile().mkdirs(); + OutputStream os = null; + try { + file.createNewFile(); + os = new FileOutputStream(file); + + boolean success = scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 75, os); + if (!success) { + throw new FileCopyException(R.string.error_compressing_image); + } + os.flush(); + } catch (IOException e) { + throw new FileCopyException(R.string.error_io_exception, e); + } catch (SecurityException e) { + throw new FileCopyException(R.string.error_security_exception_during_image_copy); + } catch (NullPointerException e) { + throw new FileCopyException(R.string.error_io_exception); + } finally { + StreamUtil.close(os); + } + return file; + } + + public static Uri getTakePhotoUri() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); pathBuilder.append('/'); pathBuilder.append("Camera"); pathBuilder.append('/'); - pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg"); + pathBuilder.append("IMG_" + imageDateFormat.format(new Date()) + ".jpg"); Uri uri = Uri.parse("file://" + pathBuilder.toString()); File file = new File(uri.toString()); file.getParentFile().mkdirs(); return uri; } - public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { - try { - Avatar avatar = new Avatar(); - Bitmap bm = 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 boolean isAvatarCached(Avatar avatar) { - File file = new File(getAvatarPath(avatar.getFilename())); - return file.exists(); - } - - public 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 { - Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); - file.delete(); - return false; - } - } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { - return false; - } finally { - close(os); - } - } - avatar.size = file.length(); - return true; - } - - public String getAvatarPath(String avatar) { - return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar; - } - - public Uri getAvatarUri(String avatar) { - return Uri.parse("file:" + getAvatarPath(avatar)); - } - - public 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 = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - Bitmap input = BitmapFactory.decodeStream(is, null, options); - if (input == null) { - return null; - } else { - input = rotate(input, getRotation(image)); - return cropCenterSquare(input, size); - } - } catch (SecurityException e) { - return null; // happens for example on Android 6.0 if contacts permissions get revoked - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public 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 = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - 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); - if (source != null && !source.isRecycled()) { - source.recycle(); - } - return dest; - } catch (SecurityException e) { - return null; //android 6.0 with revoked permissions for example - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public 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); - if (input != null && !input.isRecycled()) { - input.recycle(); - } - return output; - } - - private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options); - return calcSampleSize(options, size); - } - - private 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; - } - - public Uri getJingleFileUri(Message message) { + public static Uri getJingleFileUri(Message message) { File file = getFile(message); return Uri.parse("file://" + file.getAbsolutePath()); } - public void updateFileParams(Message message) { - updateFileParams(message,null); - } - - public void updateFileParams(Message message, URL url) { - DownloadableFile file = getFile(message); - if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int rotation = getRotation(file); - boolean rotated = rotation == 90 || rotation == 270; - int imageHeight = rotated ? options.outWidth : options.outHeight; - int imageWidth = rotated ? options.outHeight : options.outWidth; - if (url == null) { - message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } else { - message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } - } else { - if (url != null) { - message.setBody(url.toString()+"|"+Long.toString(file.getSize())); - } else { - message.setBody(Long.toString(file.getSize())); - } - } - - } - - public class FileCopyException extends Exception { - private static final long serialVersionUID = -1010013599132881427L; - private int resId; - - public FileCopyException(int resId) { - this.resId = resId; - } - - public int getResId() { - return resId; - } - } - - public Bitmap getAvatar(String avatar, int size) { - if (avatar == null) { - return null; - } - Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); - if (bm == null) { - return null; - } - return bm; - } - - public boolean isFileAvailable(Message message) { + public static boolean isFileAvailable(Message message) { return getFile(message).exists(); } - public static void close(Closeable stream) { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - } - } - } - - public static void close(Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - } - } - } + private FileBackend() { + // Static helper class + } } diff --git a/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java b/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java new file mode 100644 index 00000000..2146ea53 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/providers/ConversationsPlusFileProvider.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.providers; + +import android.net.Uri; +import android.support.v4.content.FileProvider; + +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import eu.siacs.conversations.entities.DownloadableFile; + +/** + * Created by lookshe on 27.03.16. + */ +public class ConversationsPlusFileProvider extends FileProvider { + + private static final String SCHEME = "content"; + private static final String AUTHORITY = "de.thedevstack.conversationsplus"; + + public static Uri createUriForPrivateFile(DownloadableFile file) { + return FileProvider.getUriForFile(ConversationsPlusApplication.getAppContext(), AUTHORITY, file); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 8d02f975..7728c38a 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -44,16 +44,6 @@ public class AbstractConnectionManager { return this.mXmppConnectionService; } - public long getAutoAcceptFileSize() { - String config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size", "524288"); - try { - return Long.parseLong(config); - } catch (NumberFormatException e) { - return 524288; - } - } - public boolean hasStoragePermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 276be10d..f4cbdf3d 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -8,9 +8,20 @@ import android.graphics.Typeface; import android.net.Uri; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Locale; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.utils.AvatarUtil; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketGenerator; +import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; @@ -18,7 +29,14 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.generator.IqGenerator; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class AvatarService { @@ -30,31 +48,31 @@ public class AvatarService { private static final String PREFIX_CONVERSATION = "conversation"; private static final String PREFIX_ACCOUNT = "account"; private static final String PREFIX_GENERIC = "generic"; + private static final AvatarService INSTANCE = new AvatarService(); - final private ArrayList<Integer> sizes = new ArrayList<>(); - - protected XmppConnectionService mXmppConnectionService = null; - - public AvatarService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } + final private ArrayList<Integer> sizes = new ArrayList<>(); + private final List<String> mInProgressAvatarFetches = new ArrayList<>(); + + public static AvatarService getInstance() { + return INSTANCE; + } private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { final String KEY = key(contact, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); if (avatar != null || cachedOnly) { return avatar; } if (contact.getProfilePhoto() != null) { - avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); + avatar = ImageUtil.cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); } if (avatar == null && contact.getAvatar() != null) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); + avatar = AvatarUtil.getAvatar(contact.getAvatar(), size); } if (avatar == null) { avatar = get(contact.getDisplayName(), size, cachedOnly); } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + ImageUtil.addBitmapToCache(KEY, avatar); return avatar; } @@ -69,12 +87,12 @@ public class AvatarService { private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) { final String KEY = key(user, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); if (avatar != null || cachedOnly) { return avatar; } if (user.getAvatar() != null) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(user.getAvatar(), size); + avatar = AvatarUtil.getAvatar(user.getAvatar(), size); } if (avatar == null) { Contact contact = user.getContact(); @@ -84,15 +102,14 @@ public class AvatarService { avatar = get(user.getName(), size, cachedOnly); } } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + ImageUtil.addBitmapToCache(KEY, avatar); return avatar; } public void clear(Contact contact) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove( - key(contact, size)); + ImageUtil.removeBitmapFromCache(key(contact, size)); } } } @@ -158,7 +175,7 @@ public class AvatarService { private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { final String KEY = key(mucOptions, size); - Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY); if (bitmap != null || cachedOnly) { return bitmap; } @@ -195,14 +212,14 @@ public class AvatarService { drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, size, size); } - this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } public void clear(MucOptions options) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(options, size)); + ImageUtil.removeBitmapFromCache(key(options, size)); } } } @@ -223,16 +240,15 @@ public class AvatarService { public Bitmap get(Account account, int size, boolean cachedOnly) { final String KEY = key(account, size); - Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); if (avatar != null || cachedOnly) { return avatar; } - avatar = mXmppConnectionService.getFileBackend().getAvatar( - account.getAvatar(), size); + avatar = AvatarUtil.getAvatar(account.getAvatar(), size); if (avatar == null) { avatar = get(account.getJid().toBareJid().toString(), size,false); } - mXmppConnectionService.getBitmapCache().put(KEY, avatar); + ImageUtil.addBitmapToCache(KEY, avatar); return avatar; } @@ -257,7 +273,7 @@ public class AvatarService { public void clear(Account account) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(account, size)); + ImageUtil.removeBitmapFromCache(key(account, size)); } } } @@ -265,7 +281,7 @@ public class AvatarService { public void clear(MucOptions.User user) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(user, size)); + ImageUtil.removeBitmapFromCache(key(user, size)); } } } @@ -286,15 +302,14 @@ public class AvatarService { public Bitmap get(final String name, final int size, boolean cachedOnly) { final String KEY = key(name, size); - Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY); if (bitmap != null || cachedOnly) { return bitmap; } bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); - final String trimmedName = name.trim(); - drawTile(canvas, trimmedName, 0, 0, size, size); - mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + drawTile(canvas, name, 0, 0, size, size); + ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } @@ -335,14 +350,13 @@ public class AvatarService { if (contact.getProfilePhoto() != null) { uri = Uri.parse(contact.getProfilePhoto()); } else if (contact.getAvatar() != null) { - uri = mXmppConnectionService.getFileBackend().getAvatarUri( - contact.getAvatar()); + uri = AvatarUtil.getAvatarUri(contact.getAvatar()); } if (drawTile(canvas, uri, left, top, right, bottom)) { return true; } } else if (user.getAvatar() != null) { - Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar()); + Uri uri = AvatarUtil.getAvatarUri(user.getAvatar()); if (drawTile(canvas, uri, left, top, right, bottom)) { return true; } @@ -355,7 +369,7 @@ public class AvatarService { private boolean drawTile(Canvas canvas, Account account, int left, int top, int right, int bottom) { String avatar = account.getAvatar(); if (avatar != null) { - Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(avatar); + Uri uri = AvatarUtil.getAvatarUri(avatar); if (uri != null) { if (drawTile(canvas, uri, left, top, right, bottom)) { return true; @@ -367,7 +381,8 @@ public class AvatarService { private boolean drawTile(Canvas canvas, String name, int left, int top, int right, int bottom) { if (name != null) { - final String letter = name.isEmpty() ? "X" : name.substring(0, 1); + String trimmedName = name.trim(); + final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0, 1); final int color = UIHelper.getColorForName(name); drawTile(canvas, letter, color, left, top, right, bottom); return true; @@ -377,8 +392,7 @@ public class AvatarService { private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) { if (uri != null) { - Bitmap bitmap = mXmppConnectionService.getFileBackend() - .cropCenter(uri, bottom - top, right - left); + Bitmap bitmap = ImageUtil.cropCenter(uri, bottom - top, right - left); if (bitmap != null) { drawTile(canvas, bitmap, left, top, right, bottom); return true; @@ -393,4 +407,221 @@ public class AvatarService { canvas.drawBitmap(bm, null, dst, null); return true; } + + public void publishAvatar(final Account account, + final Uri image, + final UiCallback<Avatar> callback) { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = AvatarUtil.getPepAvatar(image, size, format); + if (avatar != null) { + avatar.height = size; + avatar.width = size; + if (format.equals(Bitmap.CompressFormat.WEBP)) { + avatar.type = "image/webp"; + } else if (format.equals(Bitmap.CompressFormat.JPEG)) { + avatar.type = "image/jpeg"; + } else if (format.equals(Bitmap.CompressFormat.PNG)) { + avatar.type = "image/png"; + } + if (!AvatarUtil.save(avatar)) { + callback.error(R.string.error_saving_avatar, avatar); + return; + } + final IqPacket packet = AvatarPacketGenerator.generatePublishAvatarPacket(avatar); + XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE.RESULT) { + final IqPacket packet = AvatarPacketGenerator.generatePublishAvatarMetadataPacket(avatar); + XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket result) { + if (result.getType() == IqPacket.TYPE.RESULT) { + if (account.setAvatar(avatar.getFilename())) { + AvatarService.getInstance().clear(account); + DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account); + } + callback.success(avatar); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_converting, null); + } + } + + private static String generateFetchKey(Account account, final Avatar avatar) { + return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum; + } + + public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { + final String KEY = generateFetchKey(account, avatar); + synchronized(this.mInProgressAvatarFetches) { + if (this.mInProgressAvatarFetches.contains(KEY)) { + return; + } else { + switch (avatar.origin) { + case PEP: + this.mInProgressAvatarFetches.add(KEY); + fetchAvatarPep(account, avatar, callback); + break; + case VCARD: + this.mInProgressAvatarFetches.add(KEY); + fetchAvatarVcard(account, avatar, callback); + break; + } + } + } + } + + private void fetchAvatarPep(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) { + IqPacket packet = AvatarPacketGenerator.generateRetrieveAvatarPacket(avatar); + XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); + } + final String ERROR = account.getJid().toBareJid() + + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == IqPacket.TYPE.RESULT) { + avatar.image = AvatarPacketParser.parseAvatarData(result); + if (avatar.image != null) { + if (AvatarUtil.save(avatar)) { + if (account.getJid().toBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account); + } + AvatarService.this.clear(account); + UiUpdateHelper.updateConversationUi(); + UiUpdateHelper.updateAccountUi(); + } else { + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar); + AvatarService.this.clear(contact); + UiUpdateHelper.updateConversationUi(); + UiUpdateHelper.updateRosterUi(); + } + if (callback != null) { + callback.success(avatar); + } + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + + ": succesfuly fetched pep avatar for " + avatar.owner); + return; + } + } else { + + Logging.d(Config.LOGTAG, ERROR + "(parsing error)"); + } + } else { + Element error = result.findChild("error"); + if (error == null) { + Logging.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Logging.d(Config.LOGTAG, ERROR + error.toString()); + } + } + if (callback != null) { + callback.error(0, null); + } + + } + }); + } + + private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) { + IqPacket packet = IqGenerator.retrieveVcardAvatar(avatar); + XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); + } + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element vCard = packet.findChild("vCard", "vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; + if (image != null) { + avatar.image = image; + if (AvatarUtil.save(avatar)) { + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner); + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar); + AvatarService.this.clear(contact); + UiUpdateHelper.updateConversationUi(); + UiUpdateHelper.updateRosterUi(); + } + } + } + } + }); + } + + public void checkForAvatar(Account account, final UiCallback<Avatar> callback) { + IqPacket packet = AvatarPacketGenerator.generateRetrieveAvatarMetadataPacket(null); + XmppSendUtil.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid().toBareJid(); + if (AvatarUtil.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateAccount(account); + } + AvatarService.this.clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } + + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } + + public void clearFetchInProgress(Account account) { + synchronized (this.mInProgressAvatarFetches) { + for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) { + final String KEY = iterator.next(); + if (KEY.startsWith(account.getJid().toBareJid()+"_")) { + iterator.remove(); + } + } + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java index c2a45bf2..52894f07 100644 --- a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java +++ b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.services; import android.annotation.TargetApi; -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -11,11 +10,8 @@ import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; import android.os.IBinder; -import android.os.SystemClock; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; -import android.util.DisplayMetrics; -import android.util.Log; import java.util.ArrayList; import java.util.List; @@ -51,7 +47,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements for(int i = 0; i < Math.min(conversations.size(),MAX_TARGETS); ++i) { final Conversation conversation = conversations.get(i); final String name = conversation.getName(); - final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel)); + final Icon icon = Icon.createWithBitmap(AvatarService.getInstance().get(conversation, pixel)); final float score = (1.0f / MAX_TARGETS) * i; final Bundle extras = new Bundle(); extras.putString("uuid", conversation.getUuid()); diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java index 87e65931..53d0caaf 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.support.v4.app.NotificationCompat; + import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index e8616bd8..40dbf1f5 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.services; -import android.util.Log; import android.util.Pair; import java.math.BigInteger; @@ -9,6 +8,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -30,7 +30,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { public enum PagingOrder { NORMAL, REVERSE - } + }; public MessageArchiveService(final XmppConnectionService service) { this.mXmppConnectionService = service; @@ -96,18 +96,25 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return this.query(conversation,conversation.getLastMessageTransmitted(),end); } - public Query query(Conversation conversation, long start, long end) { - synchronized (this.queries) { - if (start > end) { - return null; - } - final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); - query.reference = conversation.getFirstMamReference(); - this.queries.add(query); - this.execute(query); - return query; - } - } + public Query query(Conversation conversation, long start, long end) { + return this.query(conversation, start, end, null); + } + + public Query query(Conversation conversation, long start, long end, XmppConnectionService.OnMoreMessagesLoaded callback) { + synchronized (this.queries) { + if (start > end) { + return null; + } + final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); + query.reference = conversation.getFirstMamReference(); + this.queries.add(query); + if (null != callback) { + query.setCallback(callback); + } + this.execute(query); + return query; + } + } public void executePendingQueries(final Account account) { List<Query> pending = new ArrayList<>(); @@ -128,7 +135,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { private void execute(final Query query) { final Account account= query.getAccount(); if (account.getStatus() == Account.State.ONLINE) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString()); IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override @@ -137,11 +144,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { synchronized (MessageArchiveService.this.queries) { MessageArchiveService.this.queries.remove(query); if (query.hasCallback()) { - query.callback(false); + query.callback(); } } } else if (packet.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); finalizeQuery(query, true); } } @@ -169,8 +176,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } if (query.hasCallback()) { - query.callback(done); + query.callback(); } else { + if (null != conversation) { + conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); + } this.mXmppConnectionService.updateConversationUi(); } } @@ -213,7 +223,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { if (complete || relevant == null || abort) { final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0; this.finalizeQuery(query, done); - Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); + Logging.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); if (query.getWith() == null && query.getMessageCount() > 0) { mXmppConnectionService.getNotificationService().finishBacklog(true); } @@ -332,10 +342,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.callback = callback; } - public void callback(boolean done) { + public void callback() { if (this.callback != null) { this.callback.onMoreMessagesLoaded(messageCount,conversation); - if (done) { + if (messageCount <= 0) { this.callback.informUser(R.string.no_more_history_on_server); } } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index cec9a3ef..c54e2f31 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -5,7 +5,6 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; @@ -26,9 +25,13 @@ import java.util.Calendar; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -36,7 +39,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; -import eu.siacs.conversations.ui.TimePreference; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; @@ -60,10 +62,11 @@ public class NotificationService { public boolean notify(final Message message) { return (message.getStatus() == Message.STATUS_RECEIVED) - && notificationsEnabled() - && !message.getConversation().isMuted() - && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message) - ); + && !message.isRead() + && ConversationsPlusPreferences.showNotification() + && !message.getConversation().isMuted() + && (message.getConversation().alwaysNotify() || wasHighlightedOrPrivate(message) + ); } public void notifyPebble(final Message message) { @@ -77,7 +80,7 @@ public class NotificationService { final String notificationData = new JSONArray().put(jsonData).toString(); i.putExtra("messageType", "PEBBLE_ALERT"); - i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */ + i.putExtra("sender", ConversationsPlusApplication.getName()); i.putExtra("notificationData", notificationData); // notify Pebble App i.setPackage("com.getpebble.android"); @@ -87,17 +90,12 @@ public class NotificationService { mXmppConnectionService.sendBroadcast(i); } - - public boolean notificationsEnabled() { - return mXmppConnectionService.getPreferences().getBoolean("show_notification", true); - } - public boolean isQuietHours() { - if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) { + if (!ConversationsPlusPreferences.enableQuietHours()) { return false; } - final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; - final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; + final long startTime = ConversationsPlusPreferences.quietHoursStart() % Config.MILLISECONDS_IN_DAY; + final long endTime = ConversationsPlusPreferences.quietHoursEnd() % Config.MILLISECONDS_IN_DAY; final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY; if (endTime < startTime) { @@ -134,10 +132,10 @@ public class NotificationService { } public void push(final Message message) { - mXmppConnectionService.updateUnreadCountBadge(); if (!notify(message)) { return; } + mXmppConnectionService.updateUnreadCountBadge(); final boolean isScreenOn = mXmppConnectionService.isInteractive(); if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { return; @@ -170,16 +168,14 @@ public class NotificationService { } private void setNotificationColor(final Builder mBuilder) { - mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary)); + mBuilder.setColor(ConversationsPlusColors.notification()); } public void updateNotification(final boolean notify) { - final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService - .getSystemService(Context.NOTIFICATION_SERVICE); - final SharedPreferences preferences = mXmppConnectionService.getPreferences(); + final NotificationManager notificationManager = (NotificationManager) ConversationsPlusApplication.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); - final String ringtone = preferences.getString("notification_ringtone", null); - final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true); + final String ringtone = ConversationsPlusPreferences.notificationRingtone(); + final boolean vibrate = ConversationsPlusPreferences.vibrateOnNotification(); if (notifications.size() == 0) { notificationManager.cancel(NOTIFICATION_ID); @@ -210,7 +206,7 @@ public class NotificationService { mBuilder.setDefaults(0); mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent()); - mBuilder.setLights(0xff00FF00, 2000, 3000); + mBuilder.setLights(Settings.LED_COLOR, 2000, 4000); final Notification notification = mBuilder.build(); notificationManager.notify(NOTIFICATION_ID, notification); } @@ -262,8 +258,7 @@ public class NotificationService { final ArrayList<Message> messages = notifications.values().iterator().next(); if (messages.size() >= 1) { final Conversation conversation = messages.get(0).getConversation(); - mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() - .get(conversation, getPixel(64))); + mBuilder.setLargeIcon(AvatarService.getInstance().get(conversation, getPixel(64))); mBuilder.setContentTitle(conversation.getName()); if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { int count = messages.size(); @@ -300,8 +295,7 @@ public class NotificationService { private void modifyForImage(final Builder builder, final Message message, final ArrayList<Message> messages, final boolean notify) { try { - final Bitmap bitmap = mXmppConnectionService.getFileBackend() - .getThumbnail(message, getPixel(288), false); + final Bitmap bitmap = ImageUtil.getThumbnail(message, getPixel(288), false); final ArrayList<Message> tmp = new ArrayList<>(); for (final Message msg : messages) { if (msg.getType() == Message.TYPE_TEXT @@ -361,7 +355,7 @@ public class NotificationService { && message.getEncryption() != Message.ENCRYPTION_PGP && message.getFileParams().height > 0) { return message; - } + } } return null; } @@ -467,23 +461,7 @@ public class NotificationService { } private 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); + return MessageUtil.wasHighlightedOrPrivate(message); } public void setOpenConversation(final Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 28ac4d19..f1a46f86 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -47,7 +47,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -59,6 +58,15 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import de.duenndns.ssl.MemorizingTrustManager; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -89,10 +97,10 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; @@ -113,7 +121,6 @@ import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; -import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; @@ -128,11 +135,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh"; public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received"; - private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); - private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); private final IBinder mBinder = new XmppConnectionBinder(); private final List<Conversation> conversations = new CopyOnWriteArrayList<>(); - private final IqGenerator mIqGenerator = new IqGenerator(this); + private final IqGenerator mIqGenerator = new IqGenerator(); private final List<String> mInProgressAvatarFetches = new ArrayList<>(); public DatabaseBackend databaseBackend; private ContentObserver contactObserver = new ContentObserver(null) { @@ -145,7 +150,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa startService(intent); } }; - private FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; private NotificationService mNotificationService = new NotificationService( this); @@ -159,13 +163,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Element error = packet.findChild("error"); String text = error != null ? error.findChildContent("text") : null; if (text != null) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text); } } } }; - private MessageGenerator mMessageGenerator = new MessageGenerator(this); - private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); + private MessageGenerator mMessageGenerator = new MessageGenerator(); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(); private List<Account> accounts; private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( this); @@ -197,7 +201,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa }; private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); - private AvatarService mAvatarService = new AvatarService(this); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private PushManagementService mPushManagementService = new PushManagementService(this); private OnConversationUpdate mOnConversationUpdate = null; @@ -286,10 +289,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mMessageArchiveService.executePendingQueries(account); if (connection != null && connection.getFeatures().csi()) { if (checkListeners()) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive"); connection.sendInactive(); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active"); connection.sendActive(); } } @@ -298,7 +301,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getAccount() == account && !account.pendingConferenceJoins.contains(conversation)) { if (!conversation.startOtrIfNeeded()) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed"); } sendUnsentMessages(conversation); } @@ -318,7 +321,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final boolean pushMode = Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account) && checkListeners(); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode)); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode)); if (!disabled && !pushMode) { int timeToReconnect = mRandom.nextInt(20) + 10; scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); @@ -330,7 +333,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa && (account.getStatus() != Account.State.NO_INTERNET)) { if (connection != null) { int next = connection.getTimeToNextAttempt(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": error connecting account. try again in " + next + "s for the " + (connection.getAttempt() + 1) + " time"); @@ -350,10 +353,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private boolean mRestoredFromDatabase = false; - private static String generateFetchKey(Account account, final Avatar avatar) { - return account.getJid().toBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum; - } - public boolean areMessagesInitialized() { return this.mRestoredFromDatabase; } @@ -371,15 +370,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { return null; } - - } - - public FileBackend getFileBackend() { - return this.fileBackend; - } - - public AvatarService getAvatarService() { - return this.mAvatarService; } public void attachLocationToConversation(final Conversation conversation, @@ -411,67 +401,33 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); - String path = getFileBackend().getOriginalPath(uri); + String path = FileUtils.getPath(uri); if (path != null) { message.setRelativeFilePath(path); - getFileBackend().updateFileParams(message); + MessageUtil.updateFileParams(message); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); } } else { - mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - getFileBackend().copyFileToPrivateStorage(message, uri); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); - } - } - - public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - final String compressPictures = getCompressPicturesPreference(); - if ("never".equals(compressPictures) - || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))) { - Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file"); - attachFileToConversation(conversation, uri, callback); - return; - } - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + @Override + public void run() { + try { + FileBackend.copyFileToPrivateStorage(message, uri); + MessageUtil.updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileCopyException e) { + callback.error(e.getResId(), message); + } + } + }); } - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); - mFileAddingExecutor.execute(new Runnable() { - - @Override - public void run() { - try { - getFileBackend().copyImageToPrivateStorage(message, uri); - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (final FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); } public Conversation find(Bookmark bookmark) { @@ -505,7 +461,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mNotificationService.clear(); break; case ACTION_DISABLE_FOREGROUND: - getPreferences().edit().putBoolean("keep_foreground_service", false).commit(); + ConversationsPlusPreferences.commitKeepForegroundService(false); toggleForegroundService(); break; case ACTION_TRY_AGAIN: @@ -566,7 +522,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime(); if (lastSent > lastReceived) { if (pingTimeoutIn < 0) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); this.reconnectAccount(account, true, interactive); } else { int secs = (int) (pingTimeoutIn / 1000); @@ -574,7 +530,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } else if (msToNextPing <= 0) { account.getXmppConnection().sendPing(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping"); this.scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode()); } else { this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); @@ -587,7 +543,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; if (timeout < 0) { - Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); + Logging.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting"); reconnectAccount(account, true, interactive); } else if (discoTimeout < 0) { account.getXmppConnection().sendDiscoTimeout(); @@ -600,6 +556,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa reconnectAccount(account, true, interactive); } } + } if (mOnAccountUpdate != null) { mOnAccountUpdate.onAccountUpdate(); @@ -627,10 +584,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("away_when_screen_off", false); } - private String getCompressPicturesPreference() { - return getPreferences().getString("picture_compression", "auto"); - } - private Presence.Status getTargetPresence() { if (xaOnSilentMode() && isPhoneSilenced()) { return Presence.Status.XA; @@ -665,7 +618,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } private void resetAllAttemptCounts(boolean reallyAll) { - Log.d(Config.LOGTAG, "resetting all attempt counts"); + Logging.d(Config.LOGTAG, "resetting all attempt counts"); for (Account account : accounts) { if (account.hasErrorStatus() || reallyAll) { final XmppConnection connection = account.getXmppConnection(); @@ -683,6 +636,25 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return activeNetwork != null && activeNetwork.isConnected(); } + /** + * check whether we are allowed to download at the moment + */ + public boolean isDownloadAllowedInConnection() { + if (ConversationsPlusPreferences.autoDownloadFileWLAN()) { + return isWifiConnected(); + } + return true; + } + + /** + * check whether wifi is connected + */ + public boolean isWifiConnected() { + ConnectivityManager cm = (ConnectivityManager) ConversationsPlusApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo niWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return niWifi.isConnected(); + } + @SuppressLint("TrulyRandom") @Override public void onCreate() { @@ -729,6 +701,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); toggleForegroundService(); updateUnreadCountBadge(); + UiUpdateHelper.initXmppConnectionService(this); toggleScreenEventReceiver(); } @@ -766,7 +739,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void toggleForegroundService() { - if (getPreferences().getBoolean("keep_foreground_service", false)) { + if (ConversationsPlusPreferences.keepForegroundService()) { startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification()); } else { stopForeground(true); @@ -776,7 +749,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if (!getPreferences().getBoolean("keep_foreground_service", false)) { + if (!ConversationsPlusPreferences.keepForegroundService()) { this.logoutAndSave(); } } @@ -794,7 +767,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa cancelWakeUpCall(account.getUuid().hashCode()); } } - Log.d(Config.LOGTAG, "good bye"); + Logging.d(Config.LOGTAG, "good bye"); stopSelf(); } @@ -818,9 +791,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public XmppConnection createConnection(final Account account) { - final SharedPreferences sharedPref = getPreferences(); - account.setResource(sharedPref.getString("resource", "mobile") - .toLowerCase(Locale.getDefault())); + account.setResource(ConversationsPlusPreferences.resource().toLowerCase(Locale.getDefault())); final XmppConnection connection = new XmppConnection(account, this); connection.setOnMessagePacketReceivedListener(this.mMessageParser); connection.setOnStatusChangedListener(this.statusListener); @@ -838,16 +809,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void sendChatState(Conversation conversation) { - if (sendChatStates()) { + if (ConversationsPlusPreferences.chatStates()) { MessagePacket packet = mMessageGenerator.generateChatState(conversation); sendMessagePacket(conversation.getAccount(), packet); } } private void sendFileMessage(final Message message, final boolean delay) { - Log.d(Config.LOGTAG, "send file message"); + Logging.d(Config.LOGTAG, "send file message"); final Account account = message.getConversation().getAccount(); - if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())) { + if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())) { mHttpConnectionManager.createNewUploadConnection(message, delay); } else { mJingleConnectionManager.createNewConnection(message); @@ -884,7 +855,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa switch (message.getEncryption()) { case Message.ENCRYPTION_NONE: if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize()) + if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize()) || message.fixCounterpart()) { this.sendFileMessage(message, delay); } else { @@ -897,7 +868,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa case Message.ENCRYPTION_PGP: case Message.ENCRYPTION_DECRYPTED: if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize()) + if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize()) || message.fixCounterpart()) { this.sendFileMessage(message, delay); } else { @@ -924,17 +895,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.fixCounterpart()) { conversation.startOtrSession(message.getCounterpart().getResourcepart(), true); } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid()); break; } } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString()); } break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize()) + if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize()) || message.fixCounterpart()) { this.sendFileMessage(message, delay); } else { @@ -974,7 +945,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case Message.ENCRYPTION_OTR: if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid()); conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; @@ -1010,7 +981,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mMessageGenerator.addDelay(packet, message.getTimeSent()); } if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { - if (this.sendChatStates()) { + if (ConversationsPlusPreferences.chatStates()) { packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); } } @@ -1021,11 +992,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private void sendUnsentMessages(final Conversation conversation) { conversation.findWaitingMessages(new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - resendMessage(message, true); - } - }); + @Override + public void onMessageFound(Message message) { + resendMessage(message, true); + } + }); } public void resendMessage(final Message message, final boolean delay) { @@ -1035,10 +1006,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void fetchRosterFromServer(final Account account) { final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); if (!"".equals(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster version " + account.getRosterVersion()); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion()); sendIqPacket(account, iqPacket, mIqParser); @@ -1079,7 +1050,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } account.setBookmarks(new ArrayList<>(bookmarks.values())); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks"); } } }; @@ -1087,7 +1058,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void pushBookmarks(Account account) { - Log.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks"); IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); @@ -1104,12 +1075,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mPhoneContactMergerThread = new Thread(new Runnable() { @Override public void run() { - Log.d(Config.LOGTAG, "start merging phone contacts with roster"); + Logging.d(Config.LOGTAG, "start merging phone contacts with roster"); for (Account account : accounts) { List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(); for (Bundle phoneContact : phoneContacts) { if (Thread.interrupted()) { - Log.d(Config.LOGTAG, "interrupted merging phone contacts"); + Logging.d(Config.LOGTAG,"interrupted merging phone contacts"); return; } Jid jid; @@ -1124,7 +1095,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa + phoneContact.getString("lookup"); contact.setSystemAccount(systemAccount); if (contact.setPhotoUri(phoneContact.getString("photouri"))) { - getAvatarService().clear(contact); + AvatarService.getInstance().clear(contact); } contact.setSystemName(phoneContact.getString("displayname")); withSystemAccounts.remove(contact); @@ -1133,11 +1104,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa contact.setSystemAccount(null); contact.setSystemName(null); if (contact.setPhotoUri(null)) { - getAvatarService().clear(contact); + AvatarService.getInstance().clear(contact); } } } - Log.d(Config.LOGTAG, "finished merging phone contacts"); + Logging.d(Config.LOGTAG,"finished merging phone contacts"); updateAccountUi(); } }); @@ -1158,15 +1129,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Runnable runnable = new Runnable() { @Override public void run() { - Log.d(Config.LOGTAG, "restoring roster"); + Logging.d(Config.LOGTAG, "restoring roster"); for (Account account : accounts) { databaseBackend.readRoster(account.getRoster()); account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage } - getBitmapCache().evictAll(); + ImageUtil.evictBitmapCache(); Looper.prepare(); loadPhoneContacts(); - Log.d(Config.LOGTAG, "restoring messages"); + Logging.d(Config.LOGTAG, "restoring messages"); for (Conversation conversation : conversations) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); checkDeletedFiles(conversation); @@ -1179,11 +1150,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } mNotificationService.finishBacklog(false); mRestoredFromDatabase = true; - Log.d(Config.LOGTAG, "restored all messages"); + Logging.d(Config.LOGTAG,"restored all messages"); updateConversationUi(); } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); } } @@ -1202,7 +1173,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onMessageFound(Message message) { - if (!getFileBackend().isFileAvailable(message)) { + if (!FileBackend.isFileAvailable(message)) { 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) { @@ -1217,7 +1188,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Conversation conversation : getConversations()) { Message message = conversation.findMessageWithFileAndUuid(uuid); if (message != null) { - if (!getFileBackend().isFileAvailable(message)) { + if (!FileBackend.isFileAvailable(message)) { 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) { @@ -1248,52 +1219,72 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } Collections.sort(list, new Comparator<Conversation>() { - @Override - public int compare(Conversation lhs, Conversation rhs) { - Message left = lhs.getLatestMessage(); - Message right = rhs.getLatestMessage(); - if (left.getTimeSent() > right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() < right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { + Logging.d("mam", "Query in progress"); return; } else if (timestamp == 0) { + Logging.d("mam", "Query stopped due to timestamp"); return; } - Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); + //TODO Create a separate class for this runnable to store if messages are getting loaded or not. Not really a good idea to do this in the callback. + Logging.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); Runnable runnable = new Runnable() { @Override public void run() { + if (null == callback || !callback.isLoadingInProgress()) { // if a callback is set, ensure that there is no loading in progress + if (null != callback) { + callback.setLoadingInProgress(); // Tell the callback that the loading is in progress + } final Account account = conversation.getAccount(); List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp); + Logging.d("mam", "runnable load more messages"); if (messages.size() > 0) { + Logging.d("mam", "At least one message"); conversation.addAll(0, messages); checkDeletedFiles(conversation); callback.onMoreMessagesLoaded(messages.size(), conversation); } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && conversation.getLastClearHistory() == 0) { + && account.isOnlineAndConnected()) { + Logging.d("mam", "account online and connected and messages left on server"); + //TODO Check if this needs to be checked before trying anything with regards to MAM if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam()) || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) { - MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp); - if (query != null) { - query.setCallback(callback); - } + Logging.d("mam", "mam active"); + getMessageArchiveService().query(conversation, 0, timestamp - 1, callback); callback.informUser(R.string.fetching_history_from_server); - } - } - } + } else { + Logging.d("mam", "mam inactive"); + callback.onMoreMessagesLoaded(0, conversation); + } + } else { + Logging.d("mam", ((!conversation.hasMessagesLeftOnServer()) ? "no" : "") + + " more messages left on server, mam " + + ((account.isOnlineAndConnected() && account.getXmppConnection().getFeatures().mam()) ? "" : "not") + + " activated, account is " + ((account.isOnlineAndConnected()) ? "" : "not") + + " online or connected)"); + callback.onMoreMessagesLoaded(0, conversation); + callback.informUser(R.string.no_more_history_on_server); + } + } + } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public List<Account> getAccounts() { @@ -1522,7 +1513,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.deleteAccount(account); } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); this.accounts.remove(account); updateAccountUi(); getNotificationService().updateErrorNotification(); @@ -1754,7 +1745,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } - Log.d(Config.LOGTAG, "app switched into foreground"); + Logging.d(Config.LOGTAG, "app switched into foreground"); } private void switchToBackground() { @@ -1773,7 +1764,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } this.mNotificationService.setIsInForeground(false); - Log.d(Config.LOGTAG, "app switched into background"); + Logging.d(Config.LOGTAG, "app switched into background"); } private void connectMultiModeConversations(Account account) { @@ -1970,7 +1961,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa sendPresencePacket(conversation.getAccount(), packet); conversation.getMucOptions().setOffline(); conversation.deregisterWithBookmark(); - Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": leaving muc " + conversation.getJid()); } else { account.pendingConferenceLeaves.add(conversation); @@ -1997,7 +1988,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString()); if (account.getStatus() == Account.State.ONLINE) { try { String server = findConferenceServer(account); @@ -2083,7 +2074,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (callback != null) { callback.onConferenceConfigurationFetched(conversation); } - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString()); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetched muc configuration for " + conversation.getJid().toBareJid() + " - " + features.toString()); updateConversationUi(); } else if (packet.getType() == IqPacket.TYPE.ERROR) { if (callback != null) { @@ -2173,11 +2164,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) { IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - Log.d(Config.LOGTAG, request.toString()); + Logging.d(Config.LOGTAG, request.toString()); sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, packet.toString()); + Logging.d(Config.LOGTAG, packet.toString()); if (packet.getType() == IqPacket.TYPE.RESULT) { callback.onRoleChangedSuccessful(nick); } else { @@ -2198,7 +2189,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa leaveMuc(conversation, true); } else { if (conversation.endOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": ended otr session with " + conversation.getJid()); } @@ -2238,8 +2229,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void createContact(Contact contact) { - boolean autoGrant = getPreferences().getBoolean("grant_new_contacts", true); - if (autoGrant) { + if (ConversationsPlusPreferences.grantNewContacts()) { contact.setOption(Contact.Options.PREEMPTIVE_GRANT); contact.setOption(Contact.Options.ASKING); } @@ -2249,10 +2239,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void onOtrSessionEstablished(Conversation conversation) { final Account account = conversation.getAccount(); final Session otrSession = conversation.getOtrSession(); - Log.d(Config.LOGTAG, - account.getJid().toBareJid() + " otr session established with " - + conversation.getJid() + "/" - + otrSession.getSessionID().getUserID()); + Logging.d(Config.LOGTAG, + account.getJid().toBareJid() + " otr session established with " + + conversation.getJid() + "/" + + otrSession.getSessionID().getUserID()); conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { @Override @@ -2290,7 +2280,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa packet.setFrom(account.getJid()); MessageGenerator.addMessageHints(packet); packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" - + otrSession.getSessionID().getUserID()); + + otrSession.getSessionID().getUserID()); try { packet.setBody(otrSession .transformSending(CryptoHelper.FILETRANSFER @@ -2328,221 +2318,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } - public void publishAvatar(final Account account, - final Uri image, - final UiCallback<Avatar> callback) { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - avatar.height = size; - avatar.width = size; - if (format.equals(Bitmap.CompressFormat.WEBP)) { - avatar.type = "image/webp"; - } else if (format.equals(Bitmap.CompressFormat.JPEG)) { - avatar.type = "image/jpeg"; - } else if (format.equals(Bitmap.CompressFormat.PNG)) { - avatar.type = "image/png"; - } - if (!getFileBackend().save(avatar)) { - callback.error(R.string.error_saving_avatar, avatar); - return; - } - final IqPacket packet = this.mIqGenerator.publishAvatar(avatar); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - final IqPacket packet = XmppConnectionService.this.mIqGenerator - .publishAvatarMetadata(avatar); - sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - if (account.setAvatar(avatar.getFilename())) { - getAvatarService().clear(account); - databaseBackend.updateAccount(account); - } - callback.success(avatar); - } else { - callback.error( - R.string.error_publish_avatar_server_reject, - avatar); - } - } - }); - } else { - callback.error( - R.string.error_publish_avatar_server_reject, - avatar); - } - } - }); - } else { - callback.error(R.string.error_publish_avatar_converting, null); - } - } - - public void fetchAvatar(Account account, Avatar avatar) { - fetchAvatar(account, avatar, null); - } - - public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { - final String KEY = generateFetchKey(account, avatar); - synchronized (this.mInProgressAvatarFetches) { - if (this.mInProgressAvatarFetches.contains(KEY)) { - return; - } else { - switch (avatar.origin) { - case PEP: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarPep(account, avatar, callback); - break; - case VCARD: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarVcard(account, avatar, callback); - break; - } - } - } - } - - private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { - IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); - } - final String ERROR = account.getJid().toBareJid() - + ": fetching avatar for " + avatar.owner + " failed "; - if (result.getType() == IqPacket.TYPE.RESULT) { - avatar.image = mIqParser.avatarData(result); - if (avatar.image != null) { - if (getFileBackend().save(avatar)) { - if (account.getJid().toBareJid().equals(avatar.owner)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - updateConversationUi(); - updateAccountUi(); - } else { - Contact contact = account.getRoster() - .getContact(avatar.owner); - contact.setAvatar(avatar); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } - if (callback != null) { - callback.success(avatar); - } - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": succesfuly fetched pep avatar for " + avatar.owner); - return; - } - } else { - - Log.d(Config.LOGTAG, ERROR + "(parsing error)"); - } - } else { - Element error = result.findChild("error"); - if (error == null) { - Log.d(Config.LOGTAG, ERROR + "(server error)"); - } else { - Log.d(Config.LOGTAG, ERROR + error.toString()); - } - } - if (callback != null) { - callback.error(0, null); - } - - } - }); - } - - private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) { - IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": successfully fetched vCard avatar for " + avatar.owner); - if (avatar.owner.isBareJid()) { - Contact contact = account.getRoster() - .getContact(avatar.owner); - contact.setAvatar(avatar); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } else { - Conversation conversation = find(account, avatar.owner.toBareJid()); - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - MucOptions.User user = conversation.getMucOptions().findUser(avatar.owner.getResourcepart()); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - } - } - } - } - } - } - } - }); - } - - public void checkForAvatar(Account account, final UiCallback<Avatar> callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().toBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - return; - } - } - } - } - callback.error(0, null); - } - }); - } - public void deleteContactOnServer(Contact contact) { contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); contact.resetOption(Contact.Options.DIRTY_PUSH); @@ -2572,7 +2347,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (!force) { disconnect(account, false); try { - Log.d(Config.LOGTAG, "wait for disconnect"); + Logging.d(Config.LOGTAG, "wait for disconnect"); Thread.sleep(500); //sleep wait for disconnect } catch (InterruptedException e) { //ignored @@ -2601,7 +2376,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void invite(Conversation conversation, Jid contact) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid()); + Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid()); MessagePacket packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } @@ -2671,18 +2446,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa .getDefaultSharedPreferences(getApplicationContext()); } - public boolean confirmMessages() { - return getPreferences().getBoolean("confirm_messages", true); - } - - public boolean allowMessageCorrection() { - return getPreferences().getBoolean("allow_message_correction", false); - } - - public boolean sendChatStates() { - return getPreferences().getBoolean("chat_states", false); - } - public boolean saveEncryptedMessages() { return !getPreferences().getBoolean("dont_save_encrypted", false); } @@ -2691,14 +2454,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("autojoin", true); } - public boolean indicateReceived() { - return getPreferences().getBoolean("indicate_received", false); - } - - public boolean useTorToConnect() { - return Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false); - } - public boolean showExtendedConnectionOptions() { return getPreferences().getBoolean("show_connection_options", false); } @@ -2798,7 +2553,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); updateUnreadCountBadge(); return true; } else { @@ -2809,7 +2564,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public synchronized void updateUnreadCountBadge() { int count = unreadCount(); if (unreadCount != count) { - Log.d(Config.LOGTAG, "update unread count to " + count); + Logging.d(Config.LOGTAG, "update unread count to " + count); if (count > 0) { ShortcutBadger.applyCount(getApplicationContext(), count); } else { @@ -2821,11 +2576,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void sendReadMarker(final Conversation conversation) { final Message markable = conversation.getLatestMarkableMessage(); + Logging.d("markRead", "XmppConnectionService.sendReadMarker (" + conversation.getName() + ")"); if (this.markRead(conversation)) { updateConversationUi(); } - if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); + if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) { + Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); Account account = conversation.getAccount(); final Jid to = markable.getCounterpart(); MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); @@ -2847,9 +2603,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void updateMemorizingTrustmanager() { final MemorizingTrustManager tm; - final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false); - if (dontTrustSystemCAs) { - tm = new MemorizingTrustManager(getApplicationContext(), null); + if (ConversationsPlusPreferences.dontTrustSystemCAs()) { + tm = new MemorizingTrustManager(getApplicationContext(), null); } else { tm = new MemorizingTrustManager(getApplicationContext()); } @@ -2872,8 +2627,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.writeRoster(account.getRoster()); } }; - mDatabaseExecutor.execute(runnable); - + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public List<String> getKnownHosts() { @@ -2907,18 +2661,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return mucServers; } + @Deprecated public void sendMessagePacket(Account account, MessagePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendMessagePacket(packet); - } + XmppSendUtil.sendMessagePacket(account, packet); } + @Deprecated public void sendPresencePacket(Account account, PresencePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendPresencePacket(packet); - } + XmppSendUtil.sendPresencePacket(account, packet); } public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { @@ -2928,15 +2678,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + @Deprecated public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendIqPacket(packet, callback); - } + XmppSendUtil.sendIqPacket(account, packet, callback); } public void sendPresence(final Account account) { - sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence())); + XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence())); } public void refreshAllPresences() { @@ -2956,7 +2704,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void sendOfflinePresence(final Account account) { - sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); + XmppSendUtil.sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); } public MessageGenerator getMessageGenerator() { @@ -3005,34 +2753,28 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void resendFailedMessages(final Message message) { - final Collection<Message> messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - msg.setTime(System.currentTimeMillis()); - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg, false); - } + if (message.getStatus() == Message.STATUS_SEND_FAILED) { + message.setTime(System.currentTimeMillis()); + markMessage(message, Message.STATUS_WAITING); + this.resendMessage(message, false); + } } public void clearConversationHistory(final Conversation conversation) { conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam - conversation.setLastClearHistory(System.currentTimeMillis()); + /* + * In case the history was loaded completely before. + * The flag "hasMessagesLeftOnServer" is set to false and no messages will be loaded anymore + * Therefore set this flag to true and try to get messages from server + */ + conversation.setHasMessagesLeftOnServer(true); Runnable runnable = new Runnable() { @Override public void run() { databaseBackend.deleteMessagesInConversation(conversation); } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public void sendBlockRequest(final Blockable blockable) { @@ -3074,7 +2816,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not publish nick"); } } }); @@ -3115,7 +2857,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.insertDiscoveryResult(disco); injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer()); } } account.inProgressDiscoFetches.remove(key); @@ -3176,6 +2918,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa void onMoreMessagesLoaded(int count, Conversation conversation); void informUser(int r); + + void setLoadingInProgress(); + + boolean isLoadingInProgress(); } public interface OnAccountPasswordChanged { diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java index bd2042fb..b9e5c367 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java @@ -5,7 +5,7 @@ import android.content.Intent; import android.preference.Preference; import android.util.AttributeSet; -import eu.siacs.conversations.utils.PhoneHelper; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; public class AboutPreference extends Preference { public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { @@ -26,7 +26,7 @@ public class AboutPreference extends Preference { } private void setSummary() { - setSummary("Conversations " + PhoneHelper.getVersionName(getContext())); + setSummary(ConversationsPlusApplication.getNameAndVersion()); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java index ccb3a22e..9f4a4bc3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java @@ -6,11 +6,12 @@ import android.widget.Button; import android.widget.EditText; import android.widget.Toast; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; + import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { @@ -26,19 +27,20 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti mCurrentPassword.requestFocus(); mCurrentPassword.setError(getString(R.string.account_status_unauthorized)); } else if (!newPassword.equals(newPasswordConfirm)) { - mNewPasswordConfirm.requestFocus(); - mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); + mNewPasswordConfirm.requestFocus(); + mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); + } else if (newPassword.isEmpty()) { + mNewPassword.requestFocus(); + mNewPassword.setError(getString(R.string.password_should_not_be_empty)); } else if (newPassword.trim().isEmpty()) { mNewPassword.requestFocus(); - mNewPassword.setError(getString(R.string.password_should_not_be_empty)); + mNewPassword.setError(getString(R.string.password_should_not_contain_only_spaces)); } else { mCurrentPassword.setError(null); mNewPassword.setError(null); mNewPasswordConfirm.setError(null); xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this); - mChangePasswordButton.setEnabled(false); - mChangePasswordButton.setTextColor(getSecondaryTextColor()); - mChangePasswordButton.setText(R.string.updating); + TextViewUtil.disable(mChangePasswordButton, ConversationsPlusColors.secondaryText(), R.string.updating); } } } @@ -89,9 +91,7 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti @Override public void run() { mNewPassword.setError(getString(R.string.could_not_change_password)); - mChangePasswordButton.setEnabled(true); - mChangePasswordButton.setTextColor(getPrimaryTextColor()); - mChangePasswordButton.setText(R.string.change_password); + TextViewUtil.enable(mChangePasswordButton, ConversationsPlusColors.primaryText(), R.string.change_password); } }); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index adbb0953..dec387b6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -38,6 +38,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; @@ -509,7 +510,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers account = mConversation.getAccount().getJid().toBareJid().toString(); } mAccountJid.setText(getString(R.string.using_account, account)); - mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48))); + mYourPhoto.setImageBitmap(AvatarService.getInstance().get(mConversation.getAccount(), getPixel(48))); setTitle(mConversation.getName()); if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && mConversation.getJid().getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) { mFullJid.setText(mConversation.getJid().getLocalpart()); @@ -603,7 +604,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); - iv.setImageBitmap(avatarService().get(user, getPixel(48), false)); + iv.setImageBitmap(AvatarService.getInstance().get(user, getPixel(48), false)); membersView.addView(view); if (mConversation.getMucOptions().canInvite()) { mInviteButton.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index e4fc59fa..b11564a9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -6,10 +6,8 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender.SendIntentException; -import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; @@ -34,6 +32,8 @@ import org.openintents.openpgp.util.OpenPgpUtils; import java.security.cert.X509Certificate; import java.util.List; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -42,6 +42,7 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.utils.CryptoHelper; @@ -220,8 +221,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd getActionBar().setDisplayHomeAsUpEnabled(true); } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); + this.showDynamicTags = ConversationsPlusPreferences.showDynamicTags(); } @Override @@ -369,8 +369,9 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } else { account = contact.getAccount().getJid().toBareJid().toString(); } + contactJidTv.setOnClickListener(new ShowResourcesListDialogListener(ContactDetailsActivity.this, contact)); accountJidTv.setText(getString(R.string.using_account, account)); - badge.setImageBitmap(avatarService().get(contact, getPixel(72))); + badge.setImageBitmap(AvatarService.getInstance().get(contact, getPixel(72))); badge.setOnClickListener(this.onBadgeClick); keys.removeAllViews(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 184b5dc6..20c4685f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -43,10 +43,15 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog; +import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener; import de.timroes.android.listview.EnhancedListView; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; @@ -61,6 +66,7 @@ import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdat import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -225,6 +231,7 @@ public class ConversationActivity extends XmppActivity return null; } listAdapter.remove(swipedConversation); + Logging.d("markRead", "EnhancedListView.OnDismissCallback.onDismiss (" + swipedConversation.getName() + ")"); xmppConnectionService.markRead(swipedConversation); final boolean formerlySelected = (getSelectedConversation() == swipedConversation); @@ -274,6 +281,7 @@ public class ConversationActivity extends XmppActivity } }); listView.enableSwipeToDismiss(); + listView.setSwipeDirection(EnhancedListView.SwipeDirection.START); listView.setSwipingLayout(R.id.swipeable_item); listView.setUndoStyle(EnhancedListView.UndoStyle.SINGLE_POPUP); listView.setUndoHideDelay(5000); @@ -285,9 +293,10 @@ public class ConversationActivity extends XmppActivity } if (mContentView instanceof SlidingPaneLayout) { SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + // Move the conversation list when sliding the selected conversation mSlidingPaneLayout.setParallaxDistance(150); - mSlidingPaneLayout - .setShadowResource(R.drawable.es_slidingpane_shadow); + // The shadow between conversation list and selected conversation + mSlidingPaneLayout.setShadowResourceLeft(R.drawable.es_slidingpane_shadow); mSlidingPaneLayout.setSliderFadeColor(0); mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { @@ -341,7 +350,7 @@ public class ConversationActivity extends XmppActivity if (titleShouldBeName && conversation != null) { ab.setDisplayHomeAsUpEnabled(true); ab.setHomeButtonEnabled(true); - if (conversation.getMode() == Conversation.MODE_SINGLE || useSubjectToIdentifyConference()) { + if (conversation.getMode() == Conversation.MODE_SINGLE || ConversationsPlusPreferences.useSubject()) { ab.setTitle(conversation.getName()); } else { ab.setTitle(conversation.getJid().toBareJid().toString()); @@ -444,7 +453,7 @@ public class ConversationActivity extends XmppActivity chooser = true; break; case ATTACHMENT_CHOICE_TAKE_PHOTO: - Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + Uri uri = FileBackend.getTakePhotoUri(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); mPendingImageUris.clear(); @@ -505,16 +514,16 @@ public class ConversationActivity extends XmppActivity } switch (attachmentChoice) { case ATTACHMENT_CHOICE_LOCATION: - getPreferences().edit().putString("recently_used_quick_action", "location").apply(); + ConversationsPlusPreferences.applyRecentlyUsedQuickAction("location"); break; case ATTACHMENT_CHOICE_RECORD_VOICE: - getPreferences().edit().putString("recently_used_quick_action", "voice").apply(); + ConversationsPlusPreferences.applyRecentlyUsedQuickAction("voice"); break; case ATTACHMENT_CHOICE_TAKE_PHOTO: - getPreferences().edit().putString("recently_used_quick_action", "photo").apply(); + ConversationsPlusPreferences.applyRecentlyUsedQuickAction("photo"); break; case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - getPreferences().edit().putString("recently_used_quick_action", "picture").apply(); + ConversationsPlusPreferences.applyRecentlyUsedQuickAction("picture"); break; } final Conversation conversation = getSelectedConversation(); @@ -830,7 +839,7 @@ public class ConversationActivity extends XmppActivity } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) + Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(conversation.getAccount()) + "Enabled axolotl for Contact " + conversation.getContact().getJid()); conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); item.setChecked(true); @@ -1071,7 +1080,7 @@ public class ConversationActivity extends XmppActivity public void onResume() { super.onResume(); final int theme = findTheme(); - final boolean usingEnterKey = usingEnterKey(); + final boolean usingEnterKey = ConversationsPlusPreferences.displayEnterKey(); if (this.mTheme != theme || usingEnterKey != mUsingEnterKey) { recreate(); } @@ -1156,7 +1165,6 @@ public class ConversationActivity extends XmppActivity setSelectedConversation(conversationList.get(0)); this.mConversationFragment.reInit(getSelectedConversation()); } else { - this.mConversationFragment.messageListAdapter.updatePreferences(); this.mConversationFragment.messagesView.invalidateViews(); this.mConversationFragment.setupIme(); } @@ -1318,7 +1326,7 @@ public class ConversationActivity extends XmppActivity } }; if (c.getMode() == Conversation.MODE_MULTI - || FileBackend.allFilesUnderSize(this, uris, max) + || FileUtils.allFilesUnderSize(this, uris, max) || c.getNextEncryption() == Message.ENCRYPTION_OTR) { callback.onPresenceSelected(); } else { @@ -1462,28 +1470,9 @@ public class ConversationActivity extends XmppActivity if (conversation == null) { return; } - final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_image), Toast.LENGTH_LONG); - prepareFileToast.show(); - xmppConnectionService.attachImageToConversation(conversation, uri, - new UiCallback<Message>() { - - @Override - public void userInputRequried(PendingIntent pi, Message object) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void success(Message message) { - hidePrepareFileToast(prepareFileToast); - xmppConnectionService.sendMessage(message); - } - - @Override - public void error(int error, Message message) { - hidePrepareFileToast(prepareFileToast); - displayErrorDialog(error); - } - }); + ResizePictureUserDecisionListener userDecisionListener = new ResizePictureUserDecisionListener(this, conversation, uri, xmppConnectionService); + UserDecisionDialog userDecisionDialog = new UserDecisionDialog(this, R.string.userdecision_question_resize_picture, userDecisionListener); + userDecisionDialog.decide(ConversationsPlusPreferences.resizePicture()); } private void hidePrepareFileToast(final Toast prepareFileToast) { @@ -1543,18 +1532,6 @@ public class ConversationActivity extends XmppActivity }); } - public boolean useSendButtonToIndicateStatus() { - return getPreferences().getBoolean("send_button_status", false); - } - - public boolean indicateReceived() { - return getPreferences().getBoolean("indicate_received", false); - } - - public boolean useWhiteBackground() { - return getPreferences().getBoolean("use_white_background",false); - } - protected boolean trustKeysIfNeeded(int requestCode) { return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID); } @@ -1624,10 +1601,6 @@ public class ConversationActivity extends XmppActivity xmppConnectionService.sendUnblockRequest(conversation); } - public boolean enterIsSend() { - return getPreferences().getBoolean("enter_is_send",false); - } - @Override public void onShowErrorToast(final int resId) { runOnUiThread(new Runnable() { @@ -1637,15 +1610,4 @@ public class ConversationActivity extends XmppActivity } }); } - - public boolean highlightSelectedConversations() { - return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard; - } - - public void setMessagesLoaded() { - if (mConversationFragment != null) { - mConversationFragment.setMessagesLoaded(); - mConversationFragment.updateMessages(); - } - } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 1a834ae5..50002189 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -8,6 +8,7 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.support.annotation.Nullable; @@ -28,19 +29,27 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.ListView; +import android.widget.PopupWindow; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; +import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout; + import net.java.otr4j.session.SessionStatus; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.UUID; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -51,9 +60,11 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.http.HttpDownloadConnection; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; @@ -61,11 +72,15 @@ import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.ui.listeners.ConversationSwipeRefreshListener; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; +import github.ankushsachdeva.emojicon.EmojiconGridView; +import github.ankushsachdeva.emojicon.EmojiconsPopup; +import github.ankushsachdeva.emojicon.emoji.Emojicon; public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener { @@ -104,113 +119,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } }; protected ListView messagesView; + protected SwipyRefreshLayout swipeLayout; final protected List<Message> messageList = new ArrayList<>(); protected MessageAdapter messageListAdapter; private EditMessage mEditMessage; private ImageButton mSendButton; + private ImageView mEmojButton; + private View mRootView; + private EmojiconsPopup mEmojPopup; private RelativeLayout snackbar; private TextView snackbarMessage; private TextView snackbarAction; private boolean messagesLoaded = true; private Toast messageLoaderToast; - - private OnScrollListener mOnScrollListener = new OnScrollListener() { - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // TODO Auto-generated method stub - - } - - private int getIndexOf(String uuid, List<Message> messages) { - if (uuid == null) { - return messages.size() - 1; - } - for(int i = 0; i < messages.size(); ++i) { - if (uuid.equals(messages.get(i).getUuid())) { - return i; - } else { - Message next = messages.get(i); - while(next != null && next.wasMergedIntoPrevious()) { - if (uuid.equals(next.getUuid())) { - return i; - } - next = next.next(); - } - - } - } - return 0; - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { - long timestamp; - if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { - timestamp = messageList.get(1).getTimeSent(); - } else { - timestamp = messageList.get(0).getTimeSent(); - } - messagesLoaded = false; - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int c, Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - return; - } - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - final int oldPosition = messagesView.getFirstVisiblePosition(); - final Message message; - if (oldPosition < messageList.size()) { - message = messageList.get(oldPosition); - } else { - message = null; - } - String uuid = message != null ? message.getUuid() : null; - View v = messagesView.getChildAt(0); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - updateStatusMessages(); - messageListAdapter.notifyDataSetChanged(); - int pos = getIndexOf(uuid,messageList); - messagesView.setSelectionFromTop(pos, pxOffset); - messagesLoaded = true; - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - } - }); - } - - @Override - public void informUser(final int resId) { - - activity.runOnUiThread(new Runnable() { - @Override - public void run() { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); - messageLoaderToast.show(); - } - }); - - } - }); - - } - } - } - }; private final int KEYCHAIN_UNLOCK_NOT_REQUIRED = 0; private final int KEYCHAIN_UNLOCK_REQUIRED = 1; private final int KEYCHAIN_UNLOCK_PENDING = 2; @@ -300,14 +221,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE); break; case CANCEL: - if (conversation != null) { - if (conversation.getCorrectingMessage() != null) { - conversation.setCorrectingMessage(null); - mEditMessage.getEditableText().clear(); - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - } + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); updateChatMsgHint(); updateSendButton(); } @@ -342,21 +257,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (body.length() == 0 || this.conversation == null) { return; } - final Message message; - if (conversation.getCorrectingMessage() == null) { - message = new Message(conversation, body, conversation.getNextEncryption()); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getNextCounterpart() != null) { - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_PRIVATE); - } + Message message = new Message(conversation, body, conversation.getNextEncryption()); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_PRIVATE); } - } else { - message = conversation.getCorrectingMessage(); - message.setBody(body); - message.setEdited(message.getUuid()); - message.setUuid(UUID.randomUUID().toString()); - conversation.setCorrectingMessage(null); } switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_OTR: @@ -377,9 +283,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public void updateChatMsgHint() { final boolean multi = conversation.getMode() == Conversation.MODE_MULTI; - if (conversation.getCorrectingMessage() != null) { - this.mEditMessage.setHint(R.string.send_corrected_message); - } else if (multi && conversation.getNextCounterpart() != null) { + if (multi && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( R.string.send_private_message_to, conversation.getNextCounterpart().getResourcepart())); @@ -415,7 +319,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public void setupIme() { if (activity == null) { return; - } else if (activity.usingEnterKey() && activity.enterIsSend()) { + } else if (activity.usingEnterKey() && ConversationsPlusPreferences.enterIsSend()) { mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); } else if (activity.usingEnterKey()) { @@ -443,6 +347,107 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa }); mEditMessage.setOnEditorActionListener(mEditorActionListener); + // Start of emojicon + mEmojButton = (ImageView) view.findViewById(R.id.emoji_btn); + mRootView = view.findViewById(R.id.textsend); + + // Give the topmost view of your activity layout hierarchy. This will be used to measure soft keyboard height + mEmojPopup = new EmojiconsPopup(mRootView, this.getActivity()); + + //Will automatically set size according to the soft keyboard size + mEmojPopup.setSizeForSoftKeyboard(); + + //If the emoji popup is dismissed, change emojiButton to smiley icon + mEmojPopup.setOnDismissListener(new PopupWindow.OnDismissListener() { + + @Override + public void onDismiss() { + changeEmojiKeyboardIcon(mEmojButton, R.drawable.smiley); + } + }); + + //If the text keyboard closes, also dismiss the emoji popup + mEmojPopup.setOnSoftKeyboardOpenCloseListener(new EmojiconsPopup.OnSoftKeyboardOpenCloseListener() { + + @Override + public void onKeyboardOpen(int keyBoardHeight) { + + } + + @Override + public void onKeyboardClose() { + if (mEmojPopup.isShowing()) + mEmojPopup.dismiss(); + } + }); + + //On emoji clicked, add it to edittext + mEmojPopup.setOnEmojiconClickedListener(new EmojiconGridView.OnEmojiconClickedListener() { + + @Override + public void onEmojiconClicked(Emojicon emojicon) { + if (mEditMessage == null || emojicon == null) { + return; + } + + int start = mEditMessage.getSelectionStart(); + int end = mEditMessage.getSelectionEnd(); + if (start < 0) { + mEditMessage.append(emojicon.getEmoji()); + } else { + mEditMessage.getText().replace(Math.min(start, end), + Math.max(start, end), emojicon.getEmoji(), 0, + emojicon.getEmoji().length()); + } + } + }); + + //On backspace clicked, emulate the KEYCODE_DEL key event + mEmojPopup.setOnEmojiconBackspaceClickedListener(new EmojiconsPopup.OnEmojiconBackspaceClickedListener() { + + @Override + public void onEmojiconBackspaceClicked(View v) { + KeyEvent event = new KeyEvent( + 0, 0, 0, KeyEvent.KEYCODE_DEL, 0, 0, 0, 0, KeyEvent.KEYCODE_ENDCALL); + mEditMessage.dispatchKeyEvent(event); + } + }); + + // To toggle between text keyboard and emoji keyboard keyboard(Popup) + mEmojButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + + //If popup is not showing => emoji keyboard is not visible, we need to show it + if(!mEmojPopup.isShowing()){ + + //If keyboard is visible, simply show the emoji popup + if(mEmojPopup.isKeyBoardOpen()){ + mEmojPopup.showAtBottom(); + changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); + } + + //else, open the text keyboard first and immediately after that show the emoji popup + else{ + mEditMessage.setFocusableInTouchMode(true); + mEditMessage.requestFocus(); + mEmojPopup.showAtBottomPending(); + final InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT); + changeEmojiKeyboardIcon(mEmojButton, R.drawable.ic_action_keyboard); + } + } + + //If popup is showing, simply dismiss it to show the undelying text keyboard + else{ + mEmojPopup.dismiss(); + } + } + }); + + // End of emojicon + mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); mSendButton.setOnClickListener(this.mSendButtonListener); @@ -451,7 +456,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); messagesView = (ListView) view.findViewById(R.id.messages_view); - messagesView.setOnScrollListener(mOnScrollListener); messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList); messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() { @@ -506,6 +510,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa registerForContextMenu(messagesView); + // Start of swipe refresh + // New Swipe refresh + swipeLayout = (SwipyRefreshLayout) view.findViewById(R.id.swipe_refresh_container); + swipeLayout.setOnRefreshListener(new ConversationSwipeRefreshListener(messageList, swipeLayout, this, messagesView, messageListAdapter)); + // End of swipe refresh + return view; } @@ -522,10 +532,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void populateContextMenu(ContextMenu menu) { final Message m = this.selectedMessage; final Transferable t = m.getTransferable(); - Message relevantForCorrection = m; - while(relevantForCorrection.mergeable(relevantForCorrection.next())) { - relevantForCorrection = relevantForCorrection.next(); - } if (m.getType() != Message.TYPE_STATUS) { final boolean treatAsFile = m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE @@ -534,7 +540,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa menu.setHeaderTitle(R.string.message_options); MenuItem copyText = menu.findItem(R.id.copy_text); MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); - MenuItem correctMessage = menu.findItem(R.id.correct_message); MenuItem shareWith = menu.findItem(R.id.share_with); MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem copyUrl = menu.findItem(R.id.copy_url); @@ -549,10 +554,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { retryDecryption.setVisible(true); } - if (relevantForCorrection.getType() == Message.TYPE_TEXT - && relevantForCorrection.isLastCorrectableMessage()) { - correctMessage.setVisible(true); - } if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) { shareWith.setVisible(true); } @@ -566,7 +567,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa copyUrl.setVisible(true); } if ((m.getType() == Message.TYPE_TEXT && t == null && m.treatAsDownloadable() != Message.Decision.NEVER) - || (m.isFileOrImage() && t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){ + || (t instanceof TransferablePlaceholder && m.hasFileOnRemoteHost())){ downloadFile.setVisible(true); downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m))); } @@ -585,15 +586,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.msg_ctx_mnu_details: + new MessageDetailsDialog(getActivity(), selectedMessage).show(); + return true; case R.id.share_with: shareWith(selectedMessage); return true; case R.id.copy_text: copyText(selectedMessage); return true; - case R.id.correct_message: - correctMessage(selectedMessage); - return true; case R.id.send_again: resendMessage(selectedMessage); return true; @@ -625,8 +626,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa shareIntent.setType("text/plain"); } else { shareIntent.putExtra(Intent.EXTRA_STREAM, - activity.xmppConnectionService.getFileBackend() - .getJingleFileUri(message)); + FileBackend.getJingleFileUri(message)); shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); String mime = message.getMimeType(); if (mime == null) { @@ -643,7 +643,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } private void copyText(Message message) { - if (activity.copyTextToClipboard(message.getMergedBody(), + if (activity.copyTextToClipboard(message.getBody(), R.string.message_text)) { Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); @@ -651,21 +651,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } private void deleteFile(Message message) { - if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { + if (FileBackend.deleteFile(message, activity.xmppConnectionService)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - activity.updateConversationList(); - updateMessages(); + activity.xmppConnectionService.updateConversationUi(); } } private void resendMessage(Message message) { if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + DownloadableFile file = FileBackend.getFile(message); if (!file.exists()) { Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show(); message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - activity.updateConversationList(); - updateMessages(); + activity.xmppConnectionService.updateConversationUi(); return; } } @@ -682,7 +680,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa resId = R.string.file_url; url = message.getFileParams().url.toString(); } else { - url = message.getBody().trim(); + url = message.getBody(); resId = R.string.file_url; } if (activity.copyTextToClipboard(url, resId)) { @@ -707,8 +705,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void retryDecryption(Message message) { message.setEncryption(Message.ENCRYPTION_PGP); - activity.updateConversationList(); - updateMessages(); + activity.xmppConnectionService.updateConversationUi(); conversation.getAccount().getPgpDecryptionService().add(message); } @@ -719,18 +716,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } - private void correctMessage(Message message) { - while(message.mergeable(message.next())) { - message = message.next(); - } - this.conversation.setCorrectingMessage(message); - this.mEditMessage.getEditableText().clear(); - this.mEditMessage.getEditableText().append(message.getBody()); - - } - protected void highlightInConference(String nick) { - String oldString = mEditMessage.getText().toString().trim(); + String oldString = mEditMessage.getText().toString(); if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { mEditMessage.getText().insert(0, nick + ": "); } else { @@ -785,7 +772,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mEditMessage.setText(""); this.mEditMessage.append(this.conversation.getNextMessage()); this.mEditMessage.setKeyboardListener(this); - messageListAdapter.updatePreferences(); this.messagesView.setAdapter(messageListAdapter); updateMessages(); this.messagesLoaded = true; @@ -793,6 +779,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (size > 0) { messagesView.setSelection(size - 1); } + swipeLayout.setRefreshing(false); } private OnClickListener mEnableAccountListener = new OnClickListener() { @@ -1051,9 +1038,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString(); final boolean empty = text.length() == 0; final boolean conference = c.getMode() == Conversation.MODE_MULTI; - if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) { - action = SendButtonAction.CANCEL; - } else if (conference && !c.getAccount().httpUploadAvailable()) { + if (conference && !c.getAccount().httpUploadAvailable()) { if (empty && c.getNextCounterpart() != null) { action = SendButtonAction.CANCEL; } else { @@ -1064,11 +1049,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (conference && c.getNextCounterpart() != null) { action = SendButtonAction.CANCEL; } else { - String setting = activity.getPreferences().getString("quick_action", "recent"); + String setting = ConversationsPlusPreferences.quickAction(); if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) { setting = "location"; } else if (setting.equals("recent")) { - setting = activity.getPreferences().getString("recently_used_quick_action", "text"); + setting = ConversationsPlusPreferences.recentlyUsedQuickAction(); } switch (setting) { case "photo": @@ -1092,7 +1077,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa action = SendButtonAction.TEXT; } } - if (activity.useSendButtonToIndicateStatus() && c != null + if (ConversationsPlusPreferences.sendButtonStatus() && c != null && c.getAccount().getStatus() == Account.State.ONLINE) { if (c.getMode() == Conversation.MODE_SINGLE) { status = c.getContact().getMostAvailableStatus(); @@ -1106,11 +1091,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); } - protected void updateStatusMessages() { + public void updateStatusMessages() { synchronized (this.messageList) { - if (showLoadMoreMessages(conversation)) { - this.messageList.add(0, Message.createLoadMoreMessage(conversation)); - } if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); if (state == ChatState.COMPOSING) { @@ -1134,21 +1116,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } - private boolean showLoadMoreMessages(final Conversation c) { - final boolean mam = hasMamSupport(c); - final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService(); - return mam && (c.getLastClearHistory() != 0 || (c.countMessages() == 0 && c.hasMessagesLeftOnServer() && !service.queryInProgress(c))); - } - - private boolean hasMamSupport(final Conversation c) { - if (c.getMode() == Conversation.MODE_SINGLE) { - final XmppConnection connection = c.getAccount().getXmppConnection(); - return connection != null && connection.getFeatures().mam(); - } else { - return c.getMucOptions().mamSupport(); - } - } - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) { snackbar.setVisibility(View.VISIBLE); snackbar.setOnClickListener(null); @@ -1309,7 +1276,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public boolean onEnterPressed() { - if (activity.enterIsSend()) { + if (ConversationsPlusPreferences.enterIsSend()) { sendMessage(); return true; } else { @@ -1344,13 +1311,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } - @Override - public void onTextChanged() { - if (conversation != null && conversation.getCorrectingMessage() != null) { - updateSendButton(); - } - } - private int completionIndex = 0; private int lastCompletionLength = 0; private String incomplete; @@ -1417,4 +1377,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } + private void changeEmojiKeyboardIcon(ImageView iconToBeChanged, int drawableResourceId){ + iconToBeChanged.setImageResource(drawableResourceId); + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 9d73290a..13fcc9a5 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -38,13 +38,18 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.UIHelper; @@ -114,6 +119,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited()) { mAccount.setOption(Account.OPTION_DISABLED, false); + mAccount.setStatus(Account.State.CONNECTING); xmppConnectionService.updateAccount(mAccount); return; } @@ -239,7 +245,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) { if (!mFetchingAvatar) { mFetchingAvatar = true; - xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback); + AvatarService.getInstance().checkForAvatar(mAccount, mAvatarFetchCallback); } } else { updateSaveButton(); @@ -332,26 +338,18 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate protected void updateSaveButton() { if (accountInfoEdited() && !mInitMode) { - this.mSaveButton.setText(R.string.save); - this.mSaveButton.setEnabled(true); - this.mSaveButton.setTextColor(getPrimaryTextColor()); + TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText(), R.string.save); } else if (mAccount != null && (mAccount.getStatus() == Account.State.CONNECTING || mFetchingAvatar)) { - this.mSaveButton.setEnabled(false); - this.mSaveButton.setTextColor(getSecondaryTextColor()); - this.mSaveButton.setText(R.string.account_status_connecting); + TextViewUtil.disable(mSaveButton, ConversationsPlusColors.secondaryText(), R.string.account_status_connecting); } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) { - this.mSaveButton.setEnabled(true); - this.mSaveButton.setTextColor(getPrimaryTextColor()); - this.mSaveButton.setText(R.string.enable); + TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText(), R.string.enable); } else { - this.mSaveButton.setEnabled(true); - this.mSaveButton.setTextColor(getPrimaryTextColor()); + TextViewUtil.enable(mSaveButton, ConversationsPlusColors.primaryText()); if (!mInitMode) { if (mAccount != null && mAccount.isOnlineAndConnected()) { this.mSaveButton.setText(R.string.save); if (!accountInfoEdited()) { - this.mSaveButton.setEnabled(false); - this.mSaveButton.setTextColor(getSecondaryTextColor()); + TextViewUtil.disable(mSaveButton, ConversationsPlusColors.secondaryText()); } } else { this.mSaveButton.setText(R.string.connect); @@ -526,9 +524,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } } SharedPreferences preferences = getPreferences(); - boolean useTor = Config.FORCE_ORBOT || preferences.getBoolean("use_tor", false); - this.mShowOptions = useTor || preferences.getBoolean("show_connection_options", false); - mHostname.setHint(useTor ? R.string.hostname_or_onion : R.string.hostname_example); + this.mShowOptions = preferences.getBoolean("show_connection_options", false); this.mNamePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); } @@ -550,9 +546,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate getActionBar().setDisplayHomeAsUpEnabled(false); getActionBar().setDisplayShowHomeEnabled(false); getActionBar().setHomeButtonEnabled(false); - } - this.mCancelButton.setEnabled(false); - this.mCancelButton.setTextColor(getSecondaryTextColor()); + } + TextViewUtil.disable(mCancelButton, ConversationsPlusColors.secondaryText()); } if (Config.DOMAIN_LOCK == null) { final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, @@ -630,7 +625,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (!mInitMode) { this.mAvatar.setVisibility(View.VISIBLE); - this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(72))); + this.mAvatar.setImageBitmap(AvatarService.getInstance().get(this.mAccount, getPixel(72))); } if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { this.mRegisterNew.setVisibility(View.VISIBLE); @@ -641,6 +636,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mRegisterNew.setChecked(false); } if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { + this.findViewById(R.id.editAccountBoxes).setVisibility(View.GONE); + this.findViewById(R.id.displayAccountFrame).setVisibility(View.VISIBLE); + TextView detailsAccountJid = (TextView)this.findViewById(R.id.detailsAccountJid); + if (this.mAccount.countPresences() > 0) { + detailsAccountJid.setText(this.mAccount.getJid().toBareJid().toString() + " (" + this.mAccount.countPresences() + ")"); + detailsAccountJid.setOnClickListener(new ShowResourcesListDialogListener(EditAccountActivity.this, this.mAccount.getRoster().getContact(this.mAccount.getJid().toBareJid()))); + } else { + detailsAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); + } Features features = this.mAccount.getXmppConnection().getFeatures(); this.mStats.setVisibility(View.VISIBLE); boolean showOptimizingWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery(); diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java index e3841d1d..06868a98 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -4,11 +4,11 @@ import android.content.Context; import android.os.Handler; import android.util.AttributeSet; import android.view.KeyEvent; -import android.widget.EditText; import eu.siacs.conversations.Config; +import github.ankushsachdeva.emojicon.EmojiconEditText; -public class EditMessage extends EditText { +public class EditMessage extends EmojiconEditText { public EditMessage(Context context, AttributeSet attrs) { super(context, attrs); @@ -69,7 +69,6 @@ public class EditMessage extends EditText { this.isUserTyping = false; this.keyboardListener.onTextDeleted(); } - this.keyboardListener.onTextChanged(); } } @@ -85,7 +84,6 @@ public class EditMessage extends EditText { void onTypingStarted(); void onTypingStopped(); void onTextDeleted(); - void onTextChanged(); boolean onTabPressed(boolean repeated); } diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index feac2c62..c83a0275 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -21,6 +21,8 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import android.widget.Toast; +import org.openintents.openpgp.util.OpenPgpApi; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -34,8 +36,6 @@ import eu.siacs.conversations.ui.adapter.AccountAdapter; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -import org.openintents.openpgp.util.OpenPgpApi; - public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated { private final String STATE_SELECTED_ACCOUNT = "selected_account"; diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 88645c4a..1916947b 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -4,6 +4,7 @@ import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Bundle; import android.view.Menu; @@ -19,10 +20,17 @@ import android.widget.Toast; import com.soundcloud.android.crop.Crop; import java.io.File; +import java.io.FileNotFoundException; + +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xmpp.pep.Avatar; @@ -80,9 +88,8 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void run() { hintOrWarning.setText(errorCode); - hintOrWarning.setTextColor(getWarningTextColor()); - publishButton.setText(R.string.publish); - enablePublishButton(); + hintOrWarning.setTextColor(ConversationsPlusColors.warning()); + TextViewUtil.enable(publishButton, ConversationsPlusColors.primaryText(), R.string.publish); } }); @@ -108,9 +115,8 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void onClick(View v) { if (avatarUri != null) { - publishButton.setText(R.string.publishing); - disablePublishButton(); - xmppConnectionService.publishAvatar(account, avatarUri, + TextViewUtil.disable(publishButton, ConversationsPlusColors.secondaryText(), R.string.publishing); + AvatarService.getInstance().publishAvatar(account, avatarUri, avatarPublication); } } @@ -190,7 +196,7 @@ public class PublishProfilePictureActivity extends XmppActivity { switch (requestCode) { case REQUEST_CHOOSE_FILE_AND_CROP: Uri source = data.getData(); - String original = FileUtils.getPath(this, source); + String original = FileUtils.getPath(source); if (original != null) { source = Uri.parse("file://"+original); } @@ -231,7 +237,7 @@ public class PublishProfilePictureActivity extends XmppActivity { if (this.avatarUri == null) { if (this.account.getAvatar() != null || this.defaultUri == null) { - this.avatar.setImageBitmap(avatarService().get(account, getPixel(192))); + this.avatar.setImageBitmap(AvatarService.getInstance().get(account, getPixel(192))); if (this.defaultUri != null) { this.avatar .setOnLongClickListener(this.backToDefaultListener); @@ -240,7 +246,7 @@ public class PublishProfilePictureActivity extends XmppActivity { } if (!support) { this.hintOrWarning - .setTextColor(getWarningTextColor()); + .setTextColor(ConversationsPlusColors.warning()); this.hintOrWarning .setText(R.string.error_publish_avatar_no_server_support); } @@ -276,27 +282,26 @@ public class PublishProfilePictureActivity extends XmppActivity { protected void loadImageIntoPreview(Uri uri) { Bitmap bm = null; try { - bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(192)); + bm = ImageUtil.cropCenterSquare(uri, getPixel(192)); } catch (Exception e) { e.printStackTrace(); } if (bm == null) { - disablePublishButton(); - this.hintOrWarning.setTextColor(getWarningTextColor()); + TextViewUtil.disable(this.publishButton, ConversationsPlusColors.secondaryText()); + this.hintOrWarning.setTextColor(ConversationsPlusColors.warning()); this.hintOrWarning .setText(R.string.error_publish_avatar_converting); return; } this.avatar.setImageBitmap(bm); if (support) { - enablePublishButton(); - this.publishButton.setText(R.string.publish); + TextViewUtil.enable(this.publishButton, ConversationsPlusColors.primaryText(), R.string.publish); this.hintOrWarning.setText(R.string.publish_avatar_explanation); - this.hintOrWarning.setTextColor(getPrimaryTextColor()); + this.hintOrWarning.setTextColor(ConversationsPlusColors.primaryText()); } else { - disablePublishButton(); - this.hintOrWarning.setTextColor(getWarningTextColor()); + TextViewUtil.disable(this.publishButton, ConversationsPlusColors.secondaryText()); + this.hintOrWarning.setTextColor(ConversationsPlusColors.warning()); this.hintOrWarning .setText(R.string.error_publish_avatar_no_server_support); } @@ -309,16 +314,6 @@ public class PublishProfilePictureActivity extends XmppActivity { } } - protected void enablePublishButton() { - this.publishButton.setEnabled(true); - this.publishButton.setTextColor(getPrimaryTextColor()); - } - - protected void disablePublishButton() { - this.publishButton.setEnabled(false); - this.publishButton.setTextColor(getSecondaryTextColor()); - } - public void refreshUiReal() { //nothing to do. This Activity doesn't implement any listeners } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 750a7421..5b1978c4 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -9,6 +9,7 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceCategory; @@ -25,11 +26,14 @@ import java.util.List; import java.util.Locale; import de.duenndns.ssl.MemorizingTrustManager; +import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.ExportLogsService; import eu.siacs.conversations.xmpp.XmppConnection; +import github.ankushsachdeva.emojicon.EmojiconHandler; public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener { @@ -67,14 +71,6 @@ public class SettingsActivity extends XmppActivity implements } } - if (Config.FORCE_ORBOT) { - PreferenceCategory connectionOptions = (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); - PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); - if (connectionOptions != null) { - expert.removePreference(connectionOptions); - } - } - final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates"); removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override @@ -106,45 +102,42 @@ public class SettingsActivity extends XmppActivity implements } }); - dialogBuilder.setPositiveButton( - getResources().getString(R.string.dialog_manage_certs_positivebutton), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - int count = selectedItems.size(); - if (count > 0) { - for (int i = 0; i < count; i++) { - try { - Integer item = Integer.valueOf(selectedItems.get(i).toString()); - String alias = aliases.get(item); - mtm.deleteCertificate(alias); - } catch (KeyStoreException e) { - e.printStackTrace(); - displayToast("Error: " + e.getLocalizedMessage()); - } - } - if (xmppConnectionServiceBound) { - reconnectAccounts(); - } - displayToast(getResources().getQuantityString(R.plurals.toast_delete_certificates, count, count)); - } - } - }); - dialogBuilder.setNegativeButton(getResources().getString(R.string.dialog_manage_certs_negativebutton), null); - AlertDialog removeCertsDialog = dialogBuilder.create(); - removeCertsDialog.show(); - removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - } - }); - - final Preference exportLogsPreference = mSettingsFragment.findPreference("export_logs"); - exportLogsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - hasStoragePermission(REQUEST_WRITE_LOGS); - return true; - } - }); + dialogBuilder.setPositiveButton( + getResources().getString(R.string.dialog_manage_certs_positivebutton), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + int count = selectedItems.size(); + if (count > 0) { + for (int i = 0; i < count; i++) { + try { + Integer item = Integer.valueOf(selectedItems.get(i).toString()); + String alias = aliases.get(item); + mtm.deleteCertificate(alias); + } catch (KeyStoreException e) { + e.printStackTrace(); + displayToast("Error: " + e.getLocalizedMessage()); + } + } + if (xmppConnectionServiceBound) { + reconnectAccounts(); + } + displayToast(getResources().getQuantityString(R.plurals.toast_delete_certificates, count, count)); + } + } + }); + dialogBuilder.setNegativeButton(getResources().getString(R.string.dialog_manage_certs_negativebutton), null); + AlertDialog removeCertsDialog = dialogBuilder.create(); + removeCertsDialog.show(); + removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + return true; + } + }); + // Avoid appearence of setting to enable or disable omemo in screen + Preference omemoEnabledPreference = this.mSettingsFragment.findPreference("omemo_enabled"); + PreferenceCategory otherExpertSettingsGroup = (PreferenceCategory) this.mSettingsFragment.findPreference("other_expert_settings"); + if (null != omemoEnabledPreference && null != otherExpertSettingsGroup) { + otherExpertSettingsGroup.removePreference(omemoEnabledPreference); + } } @Override @@ -157,11 +150,12 @@ public class SettingsActivity extends XmppActivity implements @Override public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { final List<String> resendPresence = Arrays.asList( - "confirm_messages", + "confirm_messages_list", "xa_on_silent_mode", "away_when_screen_off", - "allow_message_correction", "treat_vibrate_as_silent"); + // need to synchronize the settings class first + Settings.synchronizeSettingsClassWithPreferences(preferences, name); if (name.equals("resource")) { String resource = preferences.getString("resource", "mobile") .toLowerCase(Locale.US); @@ -190,8 +184,10 @@ public class SettingsActivity extends XmppActivity implements } else if (name.equals("dont_trust_system_cas")) { xmppConnectionService.updateMemorizingTrustmanager(); reconnectAccounts(); - } else if (name.equals("use_tor")) { - reconnectAccounts(); + } else if ("parse_emoticons".equals(name)) { + EmojiconHandler.setParseEmoticons(Settings.PARSE_EMOTICONS); + } else if ("file_transfer_folder".equals(name)) { + FileBackend.createNoMedia(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 8cd017bf..6ea011f8 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -19,6 +19,10 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog; +import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener; +import de.thedevstack.conversationsplus.ui.listeners.ShareWithResizePictureUserDecisionListener; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -27,6 +31,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -275,30 +280,35 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer final long max = account.getXmppConnection() .getFeatures() .getMaxHttpUploadSize(); - OnPresenceSelected callback = new OnPresenceSelected() { - @Override - public void onPresenceSelected() { - attachmentCounter.set(share.uris.size()); - if (share.image) { - share.multiple = share.uris.size() > 1; - replaceToast(getString(share.multiple ? R.string.preparing_images : R.string.preparing_image)); - for (Iterator<Uri> i = share.uris.iterator(); i.hasNext(); i.remove()) { - ShareWithActivity.this.xmppConnectionService - .attachImageToConversation(conversation, i.next(), - attachFileCallback); - } - } else { + OnPresenceSelected callback; + if (this.share.image) { + // TODO: attachementCounter should be set and decremented correctly + callback = new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + ResizePictureUserDecisionListener userDecisionListener = new ShareWithResizePictureUserDecisionListener(ShareWithActivity.this, conversation, xmppConnectionService, share.uris); + UserDecisionDialog userDecisionDialog = new UserDecisionDialog(ShareWithActivity.this, R.string.userdecision_question_resize_picture, userDecisionListener); + userDecisionDialog.decide(ConversationsPlusPreferences.resizePicture()); + } + }; + } else { + attachmentCounter.set(share.uris.size()); + callback = new OnPresenceSelected() { + @Override + public void onPresenceSelected() { replaceToast(getString(R.string.preparing_file)); ShareWithActivity.this.xmppConnectionService .attachFileToConversation(conversation, share.uris.get(0), attachFileCallback); + switchToConversation(conversation, null, true); + finish(); } - } - }; + }; + } if (account.httpUploadAvailable() && ((share.image && !neverCompressPictures()) || conversation.getMode() == Conversation.MODE_MULTI - || FileBackend.allFilesUnderSize(this, share.uris, max)) + || FileUtils.allFilesUnderSize(this, share.uris, max)) && conversation.getNextEncryption() != Message.ENCRYPTION_OTR) { callback.onPresenceSelected(); } else { @@ -306,6 +316,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer } } else { switchToConversation(conversation, this.share.text, true); + finish(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 7d650e5b..a6468970 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -26,7 +26,6 @@ import android.support.v13.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.KeyEvent; @@ -56,6 +55,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -192,6 +193,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_start_conversation); + this.mHideOfflineContacts = ConversationsPlusPreferences.hideOffline(); mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager); ActionBar actionBar = getActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); @@ -248,7 +250,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } }); - this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false); } @@ -549,12 +550,13 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU new IntentIntegrator(this).initiateScan(); return true; case R.id.action_hide_offline: - mHideOfflineContacts = !item.isChecked(); - getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit(); + mHideOfflineContacts = !item.isChecked(); // the item is the menu item which is displayed, the inversion here calculates the new value + ConversationsPlusPreferences.commitHideOffline(mHideOfflineContacts); if (mSearchEditText != null) { filter(mSearchEditText.getText().toString()); } - invalidateOptionsMenu(); + invalidateOptionsMenu(); // Since the selection of this item changed the checked value, the options menu is now invalid + return true; } return super.onOptionsItemSelected(item); } @@ -668,12 +670,12 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU switch (intent.getAction()) { case Intent.ACTION_SENDTO: case Intent.ACTION_VIEW: - Log.d(Config.LOGTAG, "received uri=" + intent.getData()); + Logging.d(Config.LOGTAG, "received uri=" + intent.getData()); return new Invite(intent.getData()).invite(); case NfcAdapter.ACTION_NDEF_DISCOVERED: for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) { if (message instanceof NdefMessage) { - Log.d(Config.LOGTAG, "received message=" + message); + Logging.d(Config.LOGTAG, "received message=" + message); for (NdefRecord record : ((NdefMessage) message).getRecords()) { switch (record.getTnf()) { case NdefRecord.TNF_WELL_KNOWN: @@ -705,7 +707,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU Contact contact = contacts.get(0); if (invite.getFingerprint() != null) { if (contact.addOtrFingerprint(invite.getFingerprint())) { - Log.d(Config.LOGTAG,"added new fingerprint"); + Logging.d(Config.LOGTAG,"added new fingerprint"); xmppConnectionService.syncRosterToDisk(contact.getAccount()); } } @@ -740,7 +742,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU Presence.Status s = p == null ? Presence.Status.OFFLINE : p.getStatus(); if (contact.showInRoster() && contact.match(needle) && (!this.mHideOfflineContacts - || (needle != null && !needle.trim().isEmpty()) + || (needle != null && !needle.isEmpty()) || s.compareTo(Presence.Status.OFFLINE) < 0)) { this.contacts.add(contact); } diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index cc4ba7b2..02a9823d 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -18,6 +18,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; + import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; @@ -159,8 +162,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE); foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE); if(hasPendingKeyFetches()) { - setFetching(); - lock(); + TextViewUtil.disable(this.mSaveButton, ConversationsPlusColors.secondaryText(), R.string.fetching_keys); } else { if (!hasForeignKeys && hasNoOtherTrustedKeys()) { keyErrorMessageCard.setVisibility(View.VISIBLE); @@ -176,7 +178,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate foreignKeys.setVisibility(View.GONE); } lockOrUnlockAsNeeded(); - setDone(); + mSaveButton.setText(R.string.done); } } @@ -303,35 +305,16 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate } } - private void unlock() { - mSaveButton.setEnabled(true); - mSaveButton.setTextColor(getPrimaryTextColor()); - } - - private void lock() { - mSaveButton.setEnabled(false); - mSaveButton.setTextColor(getSecondaryTextColor()); - } - private void lockOrUnlockAsNeeded() { synchronized (this.foreignKeysToTrust) { for (Jid jid : contactJids) { Map<String, Boolean> fingerprints = foreignKeysToTrust.get(jid); if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) { - lock(); + TextViewUtil.disable(this.mSaveButton, ConversationsPlusColors.secondaryText()); return; } } } - unlock(); - - } - - private void setDone() { - mSaveButton.setText(getString(R.string.done)); - } - - private void setFetching() { - mSaveButton.setText(getString(R.string.fetching_keys)); + TextViewUtil.enable(this.mSaveButton, ConversationsPlusColors.primaryText()); } } diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java index 2e415d5b..a310b6ce 100644 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java @@ -19,6 +19,9 @@ import com.google.zxing.integration.android.IntentResult; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; + import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -379,16 +382,12 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } protected void activateButton(Button button, int text, View.OnClickListener listener) { - button.setEnabled(true); - button.setTextColor(getPrimaryTextColor()); - button.setText(text); + TextViewUtil.enable(button, ConversationsPlusColors.primaryText(), text); button.setOnClickListener(listener); } protected void deactivateButton(Button button, int text) { - button.setEnabled(false); - button.setTextColor(getSecondaryTextColor()); - button.setText(text); + TextViewUtil.disable(button, ConversationsPlusColors.secondaryText(), text); button.setOnClickListener(null); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7d70b20f..29c9ee0c 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -41,7 +41,6 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; import android.util.DisplayMetrics; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -50,6 +49,7 @@ import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; @@ -69,6 +69,11 @@ import java.util.Hashtable; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.ImageUtil; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; @@ -78,10 +83,8 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; -import eu.siacs.conversations.ui.widget.Switch; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -102,16 +105,6 @@ public abstract class XmppActivity extends Activity { public boolean xmppConnectionServiceBound = false; protected boolean registeredListeners = false; - protected int mPrimaryTextColor; - protected int mSecondaryTextColor; - protected int mTertiaryTextColor; - protected int mPrimaryBackgroundColor; - protected int mSecondaryBackgroundColor; - protected int mColorRed; - protected int mColorOrange; - protected int mColorGreen; - protected int mPrimaryColor; - protected boolean mUseSubject = true; private DisplayMetrics metrics; @@ -359,18 +352,9 @@ public abstract class XmppActivity extends Activity { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); ExceptionHelper.init(getApplicationContext()); - mPrimaryTextColor = getResources().getColor(R.color.black87); - mSecondaryTextColor = getResources().getColor(R.color.black54); - mTertiaryTextColor = getResources().getColor(R.color.black12); - mColorRed = getResources().getColor(R.color.red800); - mColorOrange = getResources().getColor(R.color.orange500); - mColorGreen = getResources().getColor(R.color.green500); - mPrimaryColor = getResources().getColor(R.color.primary); - mPrimaryBackgroundColor = getResources().getColor(R.color.grey50); - mSecondaryBackgroundColor = getResources().getColor(R.color.grey200); this.mTheme = findTheme(); setTheme(this.mTheme); - this.mUsingEnterKey = usingEnterKey(); + this.mUsingEnterKey = ConversationsPlusPreferences.displayEnterKey(); mUseSubject = getPreferences().getBoolean("use_subject", true); final ActionBar ab = getActionBar(); if (ab!=null) { @@ -558,7 +542,7 @@ public abstract class XmppActivity extends Activity { }); } - protected void displayErrorDialog(final int errorCode) { + public void displayErrorDialog(final int errorCode) { runOnUiThread(new Runnable() { @Override @@ -743,35 +727,35 @@ public abstract class XmppActivity extends Activity { case UNTRUSTED: case TRUSTED: case TRUSTED_X509: - trustToggle.setChecked(trust.trusted(), false); + trustToggle.setChecked(trust.trusted()); trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509); if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) { trustToggle.setOnClickListener(null); } - key.setTextColor(getPrimaryTextColor()); - keyType.setTextColor(getSecondaryTextColor()); + key.setTextColor(ConversationsPlusColors.primaryText()); + keyType.setTextColor(ConversationsPlusColors.secondaryText()); break; case UNDECIDED: - trustToggle.setChecked(false, false); + trustToggle.setChecked(false); trustToggle.setEnabled(false); - key.setTextColor(getPrimaryTextColor()); - keyType.setTextColor(getSecondaryTextColor()); + key.setTextColor(ConversationsPlusColors.primaryText()); + keyType.setTextColor(ConversationsPlusColors.secondaryText()); break; case INACTIVE_UNTRUSTED: case INACTIVE_UNDECIDED: trustToggle.setOnClickListener(null); - trustToggle.setChecked(false, false); + trustToggle.setChecked(false); trustToggle.setEnabled(false); - key.setTextColor(getTertiaryTextColor()); - keyType.setTextColor(getTertiaryTextColor()); + key.setTextColor(ConversationsPlusColors.tertiaryText()); + keyType.setTextColor(ConversationsPlusColors.tertiaryText()); break; case INACTIVE_TRUSTED: case INACTIVE_TRUSTED_X509: trustToggle.setOnClickListener(null); - trustToggle.setChecked(true, false); + trustToggle.setChecked(true); trustToggle.setEnabled(false); - key.setTextColor(getTertiaryTextColor()); - keyType.setTextColor(getTertiaryTextColor()); + key.setTextColor(ConversationsPlusColors.tertiaryText()); + keyType.setTextColor(ConversationsPlusColors.tertiaryText()); break; } @@ -781,7 +765,7 @@ public abstract class XmppActivity extends Activity { keyType.setVisibility(View.GONE); } if (highlight) { - keyType.setTextColor(getResources().getColor(R.color.accent)); + keyType.setTextColor(ConversationsPlusColors.accent()); keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message)); } else { keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); @@ -943,34 +927,6 @@ public abstract class XmppActivity extends Activity { } }; - public int getTertiaryTextColor() { - return this.mTertiaryTextColor; - } - - public int getSecondaryTextColor() { - return this.mSecondaryTextColor; - } - - public int getPrimaryTextColor() { - return this.mPrimaryTextColor; - } - - public int getWarningTextColor() { - return this.mColorRed; - } - - public int getOnlineColor() { - return this.mColorGreen; - } - - public int getPrimaryBackgroundColor() { - return this.mPrimaryBackgroundColor; - } - - public int getSecondaryBackgroundColor() { - return this.mSecondaryBackgroundColor; - } - public int getPixel(int dp) { DisplayMetrics metrics = getResources().getDisplayMetrics(); return ((int) (dp * metrics.density)); @@ -1026,7 +982,7 @@ public abstract class XmppActivity extends Activity { } protected int findTheme() { - if (getPreferences().getBoolean("use_larger_font", false)) { + if (ConversationsPlusPreferences.useLargerFont()) { return R.style.ConversationsTheme_LargerText; } else { return R.style.ConversationsTheme; @@ -1055,7 +1011,7 @@ public abstract class XmppActivity extends Activity { } protected Bitmap createQrCodeBitmap(String input, int size) { - Log.d(Config.LOGTAG,"qr code requested size: "+size); + Logging.d(Config.LOGTAG,"qr code requested size: "+size); try { final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter(); final Hashtable<EncodeHintType, Object> hints = new Hashtable<>(); @@ -1071,7 +1027,7 @@ public abstract class XmppActivity extends Activity { } } final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Log.d(Config.LOGTAG,"output size: "+width+"x"+height); + Logging.d(Config.LOGTAG,"output size: "+width+"x"+height); bitmap.setPixels(pixels, 0, width, 0, 0, width, height); return bitmap; } catch (final WriterException e) { @@ -1130,24 +1086,21 @@ public abstract class XmppActivity extends Activity { } } - public AvatarService avatarService() { - return xmppConnectionService.getAvatarService(); - } - class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; - private Message message = null; + private final boolean setSize; + private Message message = null; - public BitmapWorkerTask(ImageView imageView) { + public BitmapWorkerTask(ImageView imageView, boolean setSize) { imageViewReference = new WeakReference<>(imageView); + this.setSize = setSize; } @Override protected Bitmap doInBackground(Message... params) { message = params[0]; try { - return xmppConnectionService.getFileBackend().getThumbnail( - message, (int) (metrics.density * 288), false); + return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { return null; } @@ -1160,27 +1113,35 @@ public abstract class XmppActivity extends Activity { if (imageView != null) { imageView.setImageBitmap(bitmap); imageView.setBackgroundColor(0x00000000); + if (setSize) { + imageView.setLayoutParams(new LinearLayout.LayoutParams( + bitmap.getWidth(), bitmap.getHeight())); + } } } } } - public void loadBitmap(Message message, ImageView imageView) { + public void loadBitmap(Message message, ImageView imageView, boolean setSize) { Bitmap bm; try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, - (int) (metrics.density * 288), true); + bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true); } catch (FileNotFoundException e) { bm = null; } + if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); + if (setSize) { + imageView.setLayoutParams(new LinearLayout.LayoutParams( + bm.getWidth(), bm.getHeight())); + } } else { if (cancelPotentialWork(message, imageView)) { imageView.setBackgroundColor(0xff333333); imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView, setSize); final AsyncDrawable asyncDrawable = new AsyncDrawable( getResources(), null, task); imageView.setImageDrawable(asyncDrawable); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 98250af9..55d3f5a7 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -7,16 +7,19 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.CompoundButton; import android.widget.ImageView; +import android.widget.Switch; import android.widget.TextView; import java.util.List; +import de.thedevstack.conversationsplus.ConversationsPlusColors; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.ManageAccountActivity; import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.widget.Switch; public class AccountAdapter extends ArrayAdapter<Account> { @@ -43,28 +46,29 @@ public class AccountAdapter extends ArrayAdapter<Account> { } TextView statusView = (TextView) view.findViewById(R.id.account_status); ImageView imageView = (ImageView) view.findViewById(R.id.account_image); - imageView.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48))); + imageView.setImageBitmap(AvatarService.getInstance().get(account, activity.getPixel(48))); statusView.setText(getContext().getString(account.getStatus().getReadableId())); switch (account.getStatus()) { case ONLINE: - statusView.setTextColor(activity.getOnlineColor()); + statusView.setTextColor(ConversationsPlusColors.online()); break; case DISABLED: case CONNECTING: - statusView.setTextColor(activity.getSecondaryTextColor()); + statusView.setTextColor(ConversationsPlusColors.secondaryText()); break; default: - statusView.setTextColor(activity.getWarningTextColor()); + statusView.setTextColor(ConversationsPlusColors.warning()); break; } final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status); final boolean isDisabled = (account.getStatus() == Account.State.DISABLED); - tglAccountState.setChecked(!isDisabled,false); + tglAccountState.setChecked(!isDisabled); tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - if (b == isDisabled && activity instanceof ManageAccountActivity) { - ((ManageAccountActivity) activity).onClickTglAccountState(account,b); + // Condition compoundButton.isPressed() added because of http://stackoverflow.com/a/28219410 + if (compoundButton.isPressed() && b == isDisabled && activity instanceof ManageAccountActivity) { + ((ManageAccountActivity) activity).onClickTglAccountState(account, b); } } }); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index f5f48a26..49e35168 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; @@ -19,10 +20,16 @@ import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; +import de.tzur.conversations.Settings; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.utils.UIHelper; @@ -44,11 +51,12 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { view = inflater.inflate(R.layout.conversation_list_row,parent, false); } Conversation conversation = getItem(position); + // Highlight the currently selected conversation if (this.activity instanceof ConversationActivity) { - View swipeableItem = view.findViewById(R.id.swipeable_item); ConversationActivity a = (ConversationActivity) this.activity; - int c = a.highlightSelectedConversations() && conversation == a.getSelectedConversation() ? a.getSecondaryBackgroundColor() : a.getPrimaryBackgroundColor(); - swipeableItem.setBackgroundColor(c); + int c = conversation == a.getSelectedConversation() ? ConversationsPlusColors.secondaryBackground() : ConversationsPlusColors.primaryBackground(); + view.findViewById(R.id.conversationListRowContent).setBackgroundColor(c); + view.findViewById(R.id.conversationListRowFrame).setBackgroundColor(c); } TextView convName = (TextView) view.findViewById(R.id.conversation_name); if (conversation.getMode() == Conversation.MODE_SINGLE || activity.useSubjectToIdentifyConference()) { @@ -61,6 +69,19 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage); ImageView notificationStatus = (ImageView) view.findViewById(R.id.notification_status); + if (Settings.SHOW_ONLINE_STATUS) { + int color = ConversationsPlusColors.offline(); + if (conversation.getAccount().getStatus() == Account.State.ONLINE) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + color = UIHelper.getStatusColor(conversation.getContact().getMostAvailableStatus()); + } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().online()) { + color = ConversationsPlusColors.online(); + } + } + TextView status = (TextView) view.findViewById(R.id.status); + status.setBackgroundColor(color); + } + Message message = conversation.getLatestMessage(); if (!conversation.isRead()) { @@ -74,12 +95,23 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { || message.getTransferable().getStatus() != Transferable.STATUS_DELETED)) { mLastMessage.setVisibility(View.GONE); imagePreview.setVisibility(View.VISIBLE); - activity.loadBitmap(message, imagePreview); + activity.loadBitmap(message, imagePreview, false); } else { Pair<String,Boolean> preview = UIHelper.getMessagePreview(activity,message); mLastMessage.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); - mLastMessage.setText(preview.first); + CharSequence msgText = preview.first; + String msgPrefix = null; + if (message.getStatus() == Message.STATUS_SEND + || message.getStatus() == Message.STATUS_SEND_DISPLAYED + || message.getStatus() == Message.STATUS_SEND_FAILED + || message.getStatus() == Message.STATUS_SEND_RECEIVED) { + msgPrefix = activity.getString(R.string.cplus_me); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + msgPrefix = UIHelper.getMessageDisplayName(message); + } + String lastMessagePreview = ((null == msgPrefix || msgPrefix.isEmpty()) ? "" : (msgPrefix + ": ")) + msgText; + mLastMessage.setText(lastMessagePreview); if (preview.second) { if (conversation.isRead()) { mLastMessage.setTypeface(null, Typeface.ITALIC); @@ -103,15 +135,23 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { notificationStatus.setVisibility(View.VISIBLE); notificationStatus.setImageResource(R.drawable.ic_notifications_paused_grey600_24dp); } else if (conversation.alwaysNotify()) { - notificationStatus.setVisibility(View.GONE); + notificationStatus.setImageResource(R.drawable.ic_notifications_grey600_24dp); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + notificationStatus.setVisibility(View.GONE); + } else { + notificationStatus.setVisibility(View.VISIBLE); + } } else { notificationStatus.setVisibility(View.VISIBLE); notificationStatus.setImageResource(R.drawable.ic_notifications_none_grey600_24dp); } - mTimestamp.setText(UIHelper.readableTimeDifference(activity,conversation.getLatestMessage().getTimeSent())); + mTimestamp.setText(UIHelper.readableTimeDifference(activity, message.getTimeSent())); ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image); - loadAvatar(conversation,profilePicture); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + profilePicture.setOnLongClickListener(new ShowResourcesListDialogListener(activity, conversation.getContact())); + } + loadAvatar(conversation, profilePicture); return view; } @@ -126,7 +166,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { @Override protected Bitmap doInBackground(Conversation... params) { - return activity.avatarService().get(params[0], activity.getPixel(56)); + return AvatarService.getInstance().get(params[0], activity.getPixel(56)); } @Override @@ -143,7 +183,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { public void loadAvatar(Conversation conversation, ImageView imageView) { if (cancelPotentialWork(conversation, imageView)) { - final Bitmap bm = activity.avatarService().get(conversation, activity.getPixel(56), true); + final Bitmap bm = AvatarService.getInstance().get(conversation, activity.getPixel(56), true); if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index da8e3910..29d706c7 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -1,13 +1,11 @@ package eu.siacs.conversations.ui.adapter; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; -import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -20,8 +18,11 @@ import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.tzur.conversations.Settings; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jid.Jid; @@ -45,8 +46,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { public ListItemAdapter(XmppActivity activity, List<ListItem> objects) { super(activity, 0, objects); this.activity = activity; - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); + this.showDynamicTags = ConversationsPlusPreferences.showDynamicTags(); } @Override @@ -57,6 +57,12 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { if (view == null) { view = inflater.inflate(R.layout.contact, parent, false); } + + if (Settings.SHOW_ONLINE_STATUS) { + TextView tvStatus = (TextView) view.findViewById(R.id.contact_status); + tvStatus.setBackgroundColor(item.getStatusColor()); + } + TextView tvName = (TextView) view.findViewById(R.id.contact_display_name); TextView tvJid = (TextView) view.findViewById(R.id.contact_jid); ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); @@ -106,7 +112,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { @Override protected Bitmap doInBackground(ListItem... params) { - return activity.avatarService().get(params[0], activity.getPixel(48)); + return AvatarService.getInstance().get(params[0], activity.getPixel(48)); } @Override @@ -123,7 +129,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { public void loadAvatar(ListItem item, ImageView imageView) { if (cancelPotentialWork(item, imageView)) { - final Bitmap bm = activity.avatarService().get(item,activity.getPixel(48),true); + final Bitmap bm = AvatarService.getInstance().get(item,activity.getPixel(48),true); if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 0268097f..9d2917d5 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -38,6 +38,10 @@ import java.util.concurrent.RejectedExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusColors; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import eu.siacs.conversations.providers.ConversationsPlusFileProvider; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; @@ -46,6 +50,8 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.GeoHelper; @@ -56,6 +62,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { private static final int SENT = 0; private static final int RECEIVED = 1; private static final int STATUS = 2; + private static final int NULL = 3; private static final Pattern XMPP_PATTERN = Pattern .compile("xmpp\\:(?:(?:[" + Patterns.GOOD_IRI_CHAR @@ -77,14 +84,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { return true; } }; - private boolean mIndicateReceived = false; - private boolean mUseWhiteBackground = false; public MessageAdapter(ConversationActivity activity, List<Message> messages) { super(activity, 0, messages); this.activity = activity; metrics = getContext().getResources().getDisplayMetrics(); - updatePreferences(); } public void setOnContactPictureClicked(OnContactPictureClicked listener) { @@ -118,9 +122,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { private int getMessageTextColor(boolean onDark, boolean primary) { if (onDark) { - return activity.getResources().getColor(primary ? R.color.white : R.color.white70); + return primary ? ConversationsPlusColors.primaryTextOnDark() : ConversationsPlusColors.secondaryTextOnDark(); } else { - return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54); + return primary ? ConversationsPlusColors.primaryText() : ConversationsPlusColors.secondaryText(); } } @@ -132,17 +136,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.indicatorReceived.setVisibility(View.GONE); } - if (viewHolder.edit_indicator != null) { - if (message.edited()) { - viewHolder.edit_indicator.setVisibility(View.VISIBLE); - viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); - viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); - } else { - viewHolder.edit_indicator.setVisibility(View.GONE); - } - } boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getMergedStatus() <= Message.STATUS_RECEIVED; + && message.getStatus() <= Message.STATUS_RECEIVED; if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) { FileParams params = message.getFileParams(); if (params.size > (1.5 * 1024 * 1024)) { @@ -154,7 +149,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { error = true; } } - switch (message.getMergedStatus()) { + switch (message.getStatus()) { case Message.STATUS_WAITING: info = getContext().getString(R.string.waiting); break; @@ -170,12 +165,12 @@ public class MessageAdapter extends ArrayAdapter<Message> { info = getContext().getString(R.string.offering); break; case Message.STATUS_SEND_RECEIVED: - if (mIndicateReceived) { + if (ConversationsPlusPreferences.indicateReceived()) { viewHolder.indicatorReceived.setVisibility(View.VISIBLE); } break; case Message.STATUS_SEND_DISPLAYED: - if (mIndicateReceived) { + if (ConversationsPlusPreferences.indicateReceived()) { viewHolder.indicatorReceived.setVisibility(View.VISIBLE); } break; @@ -190,7 +185,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { break; } if (error && type == SENT) { - viewHolder.time.setTextColor(activity.getWarningTextColor()); + viewHolder.time.setTextColor(ConversationsPlusColors.warning()); } else { viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground,false)); } @@ -205,7 +200,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { message.getFingerprint()); if(trust == null || (!trust.trusted() && !trust.trustedInactive())) { - viewHolder.indicator.setColorFilter(activity.getWarningTextColor()); + viewHolder.indicator.setColorFilter(ConversationsPlusColors.warning()); viewHolder.indicator.setAlpha(1.0f); } else { viewHolder.indicator.clearColorFilter(); @@ -226,7 +221,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), - message.getMergedTimeSent()); + message.getTimeSent()); if (message.getStatus() <= Message.STATUS_RECEIVED) { if ((filesize != null) && (info != null)) { viewHolder.time.setText(formatedTime + " \u00B7 " + filesize +" \u00B7 " + info); @@ -279,19 +274,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setTextIsSelectable(false); } - private void displayHeartMessage(final ViewHolder viewHolder, final String body) { - if (viewHolder.download_button != null) { - viewHolder.download_button.setVisibility(View.GONE); - } - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setIncludeFontPadding(false); - Spannable span = new SpannableString(body); - span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(span); - } - private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); @@ -302,10 +284,10 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (message.getBody() != null) { final String nick = UIHelper.getMessageDisplayName(message); String body; - try { - body = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND, nick + " "); - } catch (ArrayIndexOutOfBoundsException e) { - body = message.getMergedBody(); + if (message.hasMeCommand()) { + body = message.getBodyReplacedMeCommand(nick); + } else { + body = message.getBody(); } final SpannableString formattedBody = new SpannableString(body); int i = body.indexOf(Message.MERGE_SEPARATOR); @@ -351,26 +333,54 @@ public class MessageAdapter extends ArrayAdapter<Message> { } viewHolder.messageBody.setText(span); } - int urlCount = 0; - Matcher matcher = Patterns.WEB_URL.matcher(body); - while (matcher.find()) { - urlCount++; + int patternMatchCount = 0; + int oldAutoLinkMask = viewHolder.messageBody.getAutoLinkMask(); + + // first check if we have a match on XMPP_PATTERN so we do not have to check for EMAIL_ADDRESSES + patternMatchCount += countMatches(XMPP_PATTERN, body); + if ((Linkify.EMAIL_ADDRESSES & oldAutoLinkMask) != 0 && patternMatchCount > 0) { + oldAutoLinkMask -= Linkify.EMAIL_ADDRESSES; + } + + // count matches for all patterns + if ((Linkify.WEB_URLS & oldAutoLinkMask) != 0) { + patternMatchCount += countMatches(Patterns.WEB_URL, body); + } + if ((Linkify.EMAIL_ADDRESSES & oldAutoLinkMask) != 0) { + patternMatchCount += countMatches(Patterns.EMAIL_ADDRESS, body); } - viewHolder.messageBody.setTextIsSelectable(urlCount <= 1); + if ((Linkify.PHONE_NUMBERS & oldAutoLinkMask) != 0) { + patternMatchCount += countMatches(Patterns.PHONE, body); + } + + viewHolder.messageBody.setTextIsSelectable(patternMatchCount <= 1); viewHolder.messageBody.setAutoLinkMask(0); - Linkify.addLinks(viewHolder.messageBody, Linkify.WEB_URLS); Linkify.addLinks(viewHolder.messageBody, XMPP_PATTERN, "xmpp"); + viewHolder.messageBody.setAutoLinkMask(oldAutoLinkMask); } else { viewHolder.messageBody.setText(""); viewHolder.messageBody.setTextIsSelectable(false); } viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true)); - viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true)); - viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? R.color.grey800 : R.color.grey500)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setOnLongClickListener(openContextMenu); } + /** + * Counts the number of occurrences of the pattern in body. + * @param pattern the pattern to match + * @param body the body to find the pattern + * @return the number of occurrences + */ + private int countMatches(Pattern pattern, String body) { + Matcher matcher = pattern.matcher(body); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text) { viewHolder.image.setVisibility(View.GONE); @@ -425,7 +435,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setVisibility(View.GONE); viewHolder.image.setVisibility(View.VISIBLE); FileParams params = message.getFileParams(); - double target = metrics.density * 288; + //TODO: Check what value add the following lines have (compared with setting height/width in XmppActivity.loadBitmap from thumbnail after thumbnail is created) + /*double target = metrics.density * 288; int scalledW; int scalledH; if (params.width <= params.height) { @@ -437,8 +448,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { } LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH); layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.image.setLayoutParams(layoutParams); - activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setLayoutParams(layoutParams);*/ + //TODO Why should this be calculated by hand??? + activity.loadBitmap(message, viewHolder.image, true); viewHolder.image.setOnClickListener(new OnClickListener() { @Override @@ -449,19 +461,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.image.setOnLongClickListener(openContextMenu); } - private void loadMoreMessages(Conversation conversation) { - conversation.setLastClearHistory(0); - conversation.setHasMessagesLeftOnServer(true); - conversation.setFirstMamReference(null); - long timestamp = conversation.getLastMessageTransmitted(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - activity.setMessagesLoaded(); - activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp); - Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show(); - } - @Override public View getView(int position, View view, ViewGroup parent) { final Message message = getItem(position); @@ -484,7 +483,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { .findViewById(R.id.download_button); viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); - viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); viewHolder.image = (ImageView) view .findViewById(R.id.message_image); viewHolder.messageBody = (TextView) view @@ -505,7 +503,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { .findViewById(R.id.download_button); viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); - viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); viewHolder.image = (ImageView) view .findViewById(R.id.message_image); viewHolder.messageBody = (TextView) view @@ -520,7 +517,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo); viewHolder.status_message = (TextView) view.findViewById(R.id.status_message); - viewHolder.load_more_messages = (Button) view.findViewById(R.id.load_more_messages); break; default: viewHolder = null; @@ -534,31 +530,17 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - boolean darkBackground = (type == RECEIVED && (!isInValidSession || !mUseWhiteBackground)); + boolean darkBackground = (type == RECEIVED && !isInValidSession); if (type == STATUS) { - if ("LOAD_MORE".equals(message.getBody())) { - viewHolder.status_message.setVisibility(View.GONE); - viewHolder.contact_picture.setVisibility(View.GONE); - viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - loadMoreMessages(message.getConversation()); - } - }); - } else { - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.contact_picture.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setVisibility(View.GONE); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - viewHolder.contact_picture.setImageBitmap(activity - .avatarService().get(conversation.getContact(), - activity.getPixel(32))); - viewHolder.contact_picture.setAlpha(0.5f); - } - viewHolder.status_message.setText(message.getBody()); + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(AvatarService.getInstance().get(conversation.getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); } + viewHolder.status_message.setText(message.getBody()); return view; } else { loadAvatar(message, viewHolder.contact_picture); @@ -633,8 +615,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else { if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder,message); - } else if (message.bodyIsHeart()) { - displayHeartMessage(viewHolder, message.getBody().trim()); } else if (message.treatAsDownloadable() == Message.Decision.MUST) { try { URL url = new URL(message.getBody()); @@ -656,11 +636,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (type == RECEIVED) { if(isInValidSession) { - if (mUseWhiteBackground) { - viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_white); - } else { - viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received); - } viewHolder.encryption.setVisibility(View.GONE); } else { viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); @@ -675,21 +650,40 @@ public class MessageAdapter extends ArrayAdapter<Message> { } public void openDownloadable(Message message) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + DownloadableFile file = FileBackend.getFile(message); if (!file.exists()) { Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); return; } + boolean bInPrivateStorage = false; + if (file.getAbsolutePath().startsWith(FileBackend.getPrivateFileDirectoryPath())) { + bInPrivateStorage = true; + } Intent openIntent = new Intent(Intent.ACTION_VIEW); String mime = file.getMimeType(); if (mime == null) { mime = "*/*"; } - openIntent.setDataAndType(Uri.fromFile(file), mime); + Uri uri; + if (bInPrivateStorage) { + uri = ConversationsPlusFileProvider.createUriForPrivateFile(file); + } else { + uri = Uri.fromFile(file); + } + openIntent.setDataAndType(uri, mime); PackageManager manager = activity.getPackageManager(); List<ResolveInfo> infos = manager.queryIntentActivities(openIntent, 0); + if (bInPrivateStorage) { + for (ResolveInfo info : infos) { + ConversationsPlusApplication.getAppContext().grantUriPermission(info.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } if (infos.size() == 0) { - openIntent.setDataAndType(Uri.fromFile(file),"*/*"); + openIntent.setDataAndType(uri,"*/*"); + } + if (bInPrivateStorage) { + openIntent.putExtra(Intent.EXTRA_STREAM, uri); + openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } try { getContext().startActivity(openIntent); @@ -710,11 +704,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show(); } - public void updatePreferences() { - this.mIndicateReceived = activity.indicateReceived(); - this.mUseWhiteBackground = activity.useWhiteBackground(); - } - public interface OnContactPictureClicked { void onContactPictureClicked(Message message); } @@ -735,8 +724,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { protected ImageView contact_picture; protected TextView status_message; protected TextView encryption; - public Button load_more_messages; - public ImageView edit_indicator; } class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { @@ -749,7 +736,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { @Override protected Bitmap doInBackground(Message... params) { - return activity.avatarService().get(params[0], activity.getPixel(48), isCancelled()); + return AvatarService.getInstance().get(params[0], activity.getPixel(48), isCancelled()); } @Override @@ -766,7 +753,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { public void loadAvatar(Message message, ImageView imageView) { if (cancelPotentialWork(message, imageView)) { - final Bitmap bm = activity.avatarService().get(message, activity.getPixel(48), true); + final Bitmap bm = AvatarService.getInstance().get(message, activity.getPixel(48), true); if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java index 3a21ade3..cb504576 100644 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java +++ b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java @@ -4,13 +4,13 @@ import android.content.Context; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import java.util.List; -import eu.siacs.conversations.Config; +import de.thedevstack.conversationsplus.ConversationsPlusColors; + import eu.siacs.conversations.R; import eu.siacs.conversations.xmpp.forms.Field; @@ -59,7 +59,7 @@ public abstract class FormFieldWrapper { int start = label.length(); int end = label.length() + 2; spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0); - spannableString.setSpan(new ForegroundColorSpan(context.getResources().getColor(R.color.accent)), start, end, 0); + spannableString.setSpan(new ForegroundColorSpan(ConversationsPlusColors.accent()), start, end, 0); } return spannableString; } diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java new file mode 100644 index 00000000..08916206 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java @@ -0,0 +1,143 @@ +package eu.siacs.conversations.ui.listeners; + +import android.view.View; +import android.widget.ListView; +import android.widget.Toast; + +import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout; + +import java.util.List; + +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.adapter.MessageAdapter; + +/** + * This listener updates the UI when messages are loaded from the server. + */ +public class ConversationMoreMessagesLoadedListener implements XmppConnectionService.OnMoreMessagesLoaded { + private SwipyRefreshLayout swipeLayout; + private List<Message> messageList; + private ConversationFragment fragment; + private ListView messagesView; + private MessageAdapter messageListAdapter; + private Toast messageLoaderToast; + /* + The current loading status + */ + private boolean loadingMessages = false; + /** + * Whether the user is loading only history messages or not. + * History messages are messages which are older than the oldest in the database. + */ + private boolean loadHistory = true; + + public ConversationMoreMessagesLoadedListener(SwipyRefreshLayout swipeLayout, List<Message> messageList, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) { + this.swipeLayout = swipeLayout; + this.messageList = messageList; + this.fragment = fragment; + this.messagesView = messagesView; + this.messageListAdapter = messageListAdapter; + } + + public void setLoadHistory(boolean value) { + this.loadHistory = value; + } + + public void setLoadingInProgress() { + this.loadingMessages = true; + } + + public boolean isLoadingInProgress() { + return this.loadingMessages; + } + + @Override + public void onMoreMessagesLoaded(final int c, final Conversation conversation) { + ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + // Current selected conversation is not the same the messages are loaded - skip updating message view and hide loading graphic + if (activity.getSelectedConversation() != conversation) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + swipeLayout.setRefreshing(false); + } + }); + return; + } + // No new messages are loaded + if (0 == c) { + if (this.loadHistory) { + conversation.setHasMessagesLeftOnServer(false); + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + swipeLayout.setRefreshing(false); + } + }); + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final int oldPosition = messagesView.getFirstVisiblePosition(); // Always 0 - because loading starts always when hitting the top + String uuid = null; + boolean oldMessageListWasEmpty = messageList.isEmpty(); + if (-1 < oldPosition && messageList.size() > oldPosition) { + Message message = messageList.get(oldPosition); + uuid = message != null ? message.getUuid() : null; + } + View v = messagesView.getChildAt(0); + final int pxOffset = (v == null) ? 0 : v.getTop(); + + conversation.populateWithMessages(messageList); // This overrides the old message list + fragment.updateStatusMessages(); // This adds "messages" to the list for the status + messageListAdapter.notifyDataSetChanged(); + loadingMessages = false; // Loading of messages is finished - next query can be loaded + + int pos = getIndexOf(uuid, messageList); + + if (!oldMessageListWasEmpty) { + messagesView.setSelectionFromTop(pos, pxOffset); + } + + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + swipeLayout.setRefreshing(false); + } + }); + } + + @Override + public void informUser(final int resId) { + final ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); + messageLoaderToast.show(); + } + }); + + } + + private int getIndexOf(String uuid, List<Message> messages) { + if (uuid == null) { + return 0; + } + for (int i = 0; i < messages.size(); ++i) { + if (uuid.equals(messages.get(i).getUuid())) { + return i; + } + } + return 0; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java new file mode 100644 index 00000000..0cbde814 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java @@ -0,0 +1,103 @@ +package eu.siacs.conversations.ui.listeners; + +import android.widget.ListView; + +import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout; +import com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection; + +import java.util.List; + +import de.thedevstack.android.logcat.Logging; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.MessageArchiveService; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.adapter.MessageAdapter; + +/** + * This listener starts loading messages from the server. + */ +public class ConversationSwipeRefreshListener implements SwipyRefreshLayout.OnRefreshListener { + private List<Message> messageList; + private ConversationFragment fragment; + private ConversationMoreMessagesLoadedListener listener; + private SwipyRefreshLayout swipeLayout; + + public ConversationSwipeRefreshListener(List<Message> messageList, SwipyRefreshLayout swipeLayout, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) { + this.messageList = messageList; + this.fragment = fragment; + this.swipeLayout = swipeLayout; + this.listener = new ConversationMoreMessagesLoadedListener(swipeLayout, messageList, fragment, messagesView, messageListAdapter); + } + + @Override + public void onRefresh(SwipyRefreshLayoutDirection direction) { + Logging.d(Config.LOGTAG, "Refresh swipe container"); + Logging.d(Config.LOGTAG, "Refresh direction " + direction); + final ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + if (activity.getSelectedConversation().getAccount().getStatus() != Account.State.DISABLED) { + synchronized (this.messageList) { + long timestamp; + if (SwipyRefreshLayoutDirection.TOP == direction) { // Load history -> messages sent/received before first message in database + if (messageList.isEmpty()) { + timestamp = System.currentTimeMillis(); + } else { + timestamp = this.messageList.get(0).getTimeSent(); // works only because of the ordering (last msg = first msg in list) + } + this.listener.setLoadHistory(true); + activity.xmppConnectionService.loadMoreMessages(activity.getSelectedConversation(), timestamp, this.listener); + } else if (SwipyRefreshLayoutDirection.BOTTOM == direction) { // load messages sent/received between last received or last session establishment and now + if (activity.getSelectedConversation().getAccount().isOnlineAndConnected()) { + Logging.d("mam", "loading missing messages from mam (last session establishing or last received message)"); + long lastSessionEstablished = this.getTimestampOfLastSessionEstablished(activity.getSelectedConversation()); + long lastReceivedMessage = this.getTimestampOfLastReceivedOrTransmittedMessage(); + long startTimestamp = Math.min(lastSessionEstablished, lastReceivedMessage); + MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(activity.getSelectedConversation(), startTimestamp, System.currentTimeMillis(), this.listener); + if (query != null) { + this.listener.setLoadHistory(false); + } else { + Logging.d("mam", "no query built - no messages loaded"); + this.listener.onMoreMessagesLoaded(0, activity.getSelectedConversation()); + this.listener.informUser(R.string.no_more_history_on_server); + } + this.listener.informUser(R.string.fetching_history_from_server); + } else { + this.listener.informUser(R.string.not_connected_try_again); + swipeLayout.setRefreshing(false); + } + } + } + } else { + this.listener.informUser(R.string.this_account_is_disabled); + swipeLayout.setRefreshing(false); + } + Logging.d(Config.LOGTAG, "End Refresh swipe container"); + } + + private long getTimestampOfLastReceivedOrTransmittedMessage() { + long lastReceivedOrTransmittedMessage = Long.MAX_VALUE; + if (null != this.messageList + && !this.messageList.isEmpty()) { + int lastMessageIndex = this.messageList.size() - 1; + if (0 <= lastMessageIndex && this.messageList.size() > lastMessageIndex) { + lastReceivedOrTransmittedMessage = this.messageList.get(lastMessageIndex).getTimeSent(); + } + } + + return lastReceivedOrTransmittedMessage; + } + + private long getTimestampOfLastSessionEstablished(Conversation conversation) { + long lastSessionEstablished = Long.MAX_VALUE; + if (null != conversation + && null != conversation.getAccount() + && null != conversation.getAccount().getXmppConnection()) { + lastSessionEstablished = conversation.getAccount().getXmppConnection().getLastSessionEstablished(); + } + return lastSessionEstablished; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Switch.java b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java deleted file mode 100644 index fd3b5553..00000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/Switch.java +++ /dev/null @@ -1,68 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.ViewConfiguration; - -import com.kyleduo.switchbutton.SwitchButton; - -public class Switch extends SwitchButton { - - private int mTouchSlop; - private int mClickTimeout; - private float mStartX; - private float mStartY; - private OnClickListener mOnClickListener; - - public Switch(Context context) { - super(context); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); - } - - public Switch(Context context, AttributeSet attrs) { - super(context, attrs); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); - } - - public Switch(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); - } - - @Override - public void setOnClickListener(OnClickListener onClickListener) { - this.mOnClickListener = onClickListener; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (!isEnabled()) { - float deltaX = event.getX() - mStartX; - float deltaY = event.getY() - mStartY; - int action = event.getAction(); - switch (action) { - case MotionEvent.ACTION_DOWN: - mStartX = event.getX(); - mStartY = event.getY(); - break; - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: - float time = event.getEventTime() - event.getDownTime(); - if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) { - if (mOnClickListener != null) { - this.mOnClickListener.onClick(this); - } - } - break; - default: - break; - } - return true; - } - return super.onTouchEvent(event); - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 306d50c2..1568eb8c 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -7,20 +7,13 @@ import android.net.LinkProperties; import android.net.Network; import android.net.RouteInfo; import android.os.Build; -import android.os.Bundle; -import android.os.Parcelable; -import android.util.Log; import java.io.IOException; import java.net.InetAddress; -import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Random; -import java.util.TreeMap; -import java.util.Map; +import java.util.TreeSet; import java.util.regex.Pattern; import de.measite.minidns.Client; @@ -28,47 +21,58 @@ import de.measite.minidns.DNSMessage; import de.measite.minidns.Record; import de.measite.minidns.Record.CLASS; import de.measite.minidns.Record.TYPE; -import de.measite.minidns.record.A; -import de.measite.minidns.record.AAAA; import de.measite.minidns.record.Data; import de.measite.minidns.record.SRV; import de.measite.minidns.util.NameUtil; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.dto.SrvRecord; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jid.Jid; public class DNSHelper { - - public static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - public static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - public static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + private static final String CLIENT_SRV_PREFIX = "_xmpp-client._tcp."; + private static final String SECURE_CLIENT_SRV_PREFIX = "_xmpps-client._tcp."; + private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); protected static Client client = new Client(); - public static Bundle getSRVRecord(final Jid jid, Context context) throws IOException { - final String host = jid.getDomainpart(); - final List<InetAddress> servers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDnsServers(context) : getDnsServersPreLollipop(); - Bundle b = new Bundle(); - for(InetAddress server : servers) { - b = queryDNS(host, server); - if (b.containsKey("values")) { - return b; - } - } - if (!b.containsKey("values")) { - Log.d(Config.LOGTAG,"all dns queries failed. provide fallback A record"); - ArrayList<Parcelable> values = new ArrayList<>(); - values.add(createNamePortBundle(host, 5222, false)); - b.putParcelableArrayList("values",values); - } - return b; - } + static { + client.setTimeout(Config.PING_TIMEOUT * 1000); + } + + /** + * Queries the SRV record for the server JID. + * This method uses all available Domain Name Servers. + * @param jid the server JID + * @return TreeSet with SrvRecords. If no SRV record is found for JID an empty TreeSet is returned. + */ + public static final TreeSet<SrvRecord> querySrvRecord(Jid jid) { + String host = jid.getDomainpart(); + TreeSet<SrvRecord> result = new TreeSet<>(); + + final List<InetAddress> dnsServers = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? getDnsServers() : getDnsServersPreLollipop(); + + if (dnsServers != null) { + for (InetAddress dnsServer : dnsServers) { + result = querySrvRecord(host, dnsServer); + if (!result.isEmpty()) { + break; + } + } + } + + return result; + } @TargetApi(21) - private static List<InetAddress> getDnsServers(Context context) { + private static List<InetAddress> getDnsServers() { List<InetAddress> servers = new ArrayList<>(); - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + ConnectivityManager connectivityManager = (ConnectivityManager) ConversationsPlusApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks(); if (networks == null) { return getDnsServersPreLollipop(); @@ -84,7 +88,7 @@ public class DNSHelper { } } if (servers.size() > 0) { - Log.d(Config.LOGTAG, "used lollipop variant to discover dns servers in " + networks.length + " networks"); + Logging.d("dns", "used lollipop variant to discover dns servers in " + networks.length + " networks"); } return servers.size() > 0 ? servers : getDnsServersPreLollipop(); } @@ -112,155 +116,38 @@ public class DNSHelper { return servers; } - private static class TlsSrv { - private final SRV srv; - private final boolean tls; - - public TlsSrv(SRV srv, boolean tls) { - this.srv = srv; - this.tls = tls; - } - } - - private static void fillSrvMaps(final String qname, final InetAddress dnsServer, final Map<Integer, List<TlsSrv>> priorities, final Map<String, List<String>> ips4, final Map<String, List<String>> ips6, final boolean tls) throws IOException { - final DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); - for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { - for (Record rr : rrset) { - Data d = rr.getPayload(); - if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { - SRV srv = (SRV) d; - if (!priorities.containsKey(srv.getPriority())) { - priorities.put(srv.getPriority(),new ArrayList<TlsSrv>()); - } - priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls)); - } - if (d instanceof A) { - A a = (A) d; - if (!ips4.containsKey(rr.getName())) { - ips4.put(rr.getName(), new ArrayList<String>()); - } - ips4.get(rr.getName()).add(a.toString()); - } - if (d instanceof AAAA) { - AAAA aaaa = (AAAA) d; - if (!ips6.containsKey(rr.getName())) { - ips6.put(rr.getName(), new ArrayList<String>()); - } - ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); - } - } - } - } - - public static Bundle queryDNS(String host, InetAddress dnsServer) { - Bundle bundle = new Bundle(); - try { - client.setTimeout(Config.PING_TIMEOUT * 1000); - final String qname = "_xmpp-client._tcp." + host; - final String tlsQname = "_xmpps-client._tcp." + host; - Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); - - final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>(); - final Map<String, List<String>> ips4 = new TreeMap<>(); - final Map<String, List<String>> ips6 = new TreeMap<>(); - - fillSrvMaps(qname, dnsServer, priorities, ips4, ips6, false); - fillSrvMaps(tlsQname, dnsServer, priorities, ips4, ips6, true); - - final List<TlsSrv> result = new ArrayList<>(); - for (final List<TlsSrv> s : priorities.values()) { - result.addAll(s); - } - - final ArrayList<Bundle> values = new ArrayList<>(); - if (result.size() == 0) { - DNSMessage response; - try { - response = client.query(host, TYPE.A, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying A record on "+dnsServer.getHostAddress()); - } - try { - response = client.query(host, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(host, 5222, response.getAnswers()[i].getPayload(), false)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); - } - values.add(createNamePortBundle(host, 5222, false)); - bundle.putParcelableArrayList("values", values); - return bundle; - } - for (final TlsSrv tlsSrv : result) { - final SRV srv = tlsSrv.srv; - if (ips6.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls)); - } else { - try { - DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); - for (int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls)); - } - } catch (SocketTimeoutException e) { - Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress()); - } - } - if (ips4.containsKey(srv.getName())) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls)); - } else { - DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress()); - for(int i = 0; i < response.getAnswers().length; ++i) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls)); - } - } - values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls)); - } - bundle.putParcelableArrayList("values", values); - } catch (SocketTimeoutException e) { - bundle.putString("error", "timeout"); - } catch (Exception e) { - bundle.putString("error", "unhandled"); - } - return bundle; - } - - private static Bundle createNamePortBundle(String name, int port, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - return namePort; - } - - private static Bundle createNamePortBundle(String name, int port, Map<String, List<String>> ips, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - if (ips!=null) { - List<String> ip = ips.get(name); - Collections.shuffle(ip, new Random()); - namePort.putString("ip", ip.get(0)); - } - return namePort; - } - - private static Bundle createNamePortBundle(String name, int port, Data data, final boolean tls) { - Bundle namePort = new Bundle(); - namePort.putString("name", name); - namePort.putBoolean("tls", tls); - namePort.putInt("port", port); - if (data instanceof A) { - namePort.putString("ip", data.toString()); - } else if (data instanceof AAAA) { - namePort.putString("ip","["+data.toString()+"]"); - } - return namePort; - } + /** + * Queries the SRV record for an host from the given Domain Name Server. + * @param host the host to query for + * @param dnsServerAddress the DNS to query on + * @return TreeSet with SrvRecords. + */ + private static final TreeSet<SrvRecord> querySrvRecord(String host, InetAddress dnsServerAddress) { + TreeSet<SrvRecord> result = new TreeSet<>(); + querySrvRecord(host, dnsServerAddress, false, result); + querySrvRecord(host, dnsServerAddress, true, result); + return result; + } + + private static final void querySrvRecord(String host, InetAddress dnsServerAddress, boolean tlsSrvRecord, TreeSet<SrvRecord> result) { + String qname = (tlsSrvRecord ? SECURE_CLIENT_SRV_PREFIX : CLIENT_SRV_PREFIX) + host; + String dnsServerHostAddress = dnsServerAddress.getHostAddress(); + Logging.d("dns", "using dns server: " + dnsServerHostAddress + " to look up " + qname); + try { + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServerHostAddress); + Record[] rrset = message.getAnswers(); + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord); + result.add(srvRecord); + } + } + } catch (IOException e) { + Logging.d("dns", "Error while retrieving SRV record '" + qname + "' for '" + host + "' from DNS '" + dnsServerHostAddress + "': " + e.getMessage()); + } + } public static boolean isIp(final String server) { return server != null && ( diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index 8799b4a5..9c8db210 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -4,13 +4,10 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; -import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.preference.PreferenceManager; import android.text.format.DateUtils; -import android.util.Log; import java.io.BufferedReader; import java.io.FileInputStream; @@ -18,6 +15,8 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.List; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -36,11 +35,9 @@ public class ExceptionHelper { } } - public static boolean checkForCrash(ConversationActivity activity, final XmppConnectionService service) { + public static boolean checkForCrash(final ConversationActivity activity, final XmppConnectionService service) { try { - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(activity); - boolean neverSend = preferences.getBoolean("never_send", false); + boolean neverSend = ConversationsPlusPreferences.neverSend(); if (neverSend) { return false; } @@ -89,13 +86,13 @@ public class ExceptionHelper { @Override public void onClick(DialogInterface dialog, int which) { - Log.d(Config.LOGTAG, "using account=" + Logging.d(Config.LOGTAG, "using account=" + finalAccount.getJid().toBareJid() + " to send in stack trace"); Conversation conversation = null; try { conversation = service.findOrCreateConversation(finalAccount, - Jid.fromString("bugs@siacs.eu"), false); + Jid.fromString(activity.getString(R.string.cplus_bugreport_jabberid)), false); } catch (final InvalidJidException ignored) { } Message message = new Message(conversation, report @@ -108,8 +105,7 @@ public class ExceptionHelper { @Override public void onClick(DialogInterface dialog, int which) { - preferences.edit().putBoolean("never_send", true) - .apply(); + ConversationsPlusPreferences.applyNeverSend(true); } }); builder.create().show(); diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java index ceda7293..5e465e94 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java @@ -16,11 +16,11 @@ package eu.siacs.conversations.utils; -import android.util.Log; - import java.io.IOException; import java.io.InputStream; +import de.thedevstack.android.logcat.Logging; + public class ExifHelper { private static final String TAG = "CameraExif"; @@ -56,7 +56,7 @@ public class ExifHelper { } length = pack(buf, 0, 2, false); if (length < 2) { - Log.e(TAG, "Invalid length"); + Logging.e(TAG, "Invalid length"); return 0; } length -= 2; @@ -91,7 +91,7 @@ public class ExifHelper { // Identify the byte order. int tag = pack(jpeg, offset, 4, false); if (tag != 0x49492A00 && tag != 0x4D4D002A) { - Log.e(TAG, "Invalid byte order"); + Logging.e(TAG, "Invalid byte order"); return 0; } boolean littleEndian = (tag == 0x49492A00); @@ -99,7 +99,7 @@ public class ExifHelper { // Get the offset and check if it is reasonable. int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; if (count < 10 || count > length) { - Log.e(TAG, "Invalid offset"); + Logging.e(TAG, "Invalid offset"); return 0; } offset += count; @@ -123,7 +123,7 @@ public class ExifHelper { case 8: return 270; } - Log.i(TAG, "Unsupported orientation"); + Logging.i(TAG, "Unsupported orientation"); return 0; } offset += 12; @@ -131,7 +131,7 @@ public class ExifHelper { } } - Log.i(TAG, "Orientation not found"); + Logging.i(TAG, "Orientation not found"); return 0; } diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java index 6e75d41c..1f2a71ca 100644 --- a/src/main/java/eu/siacs/conversations/utils/FileUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.utils; import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; @@ -9,28 +10,31 @@ import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; +import android.provider.OpenableColumns; import java.io.File; +import java.util.List; -public class FileUtils { +import de.thedevstack.conversationsplus.ConversationsPlusApplication; + +public final class FileUtils { /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and * other file-based ContentProviders. * - * @param context The context. * @param uri The Uri to query. * @author paulburke */ @SuppressLint("NewApi") - public static String getPath(final Context context, final Uri uri) { + public static String getPath(final Uri uri) { if (uri == null) { return null; } final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - + final Context context = ConversationsPlusApplication.getAppContext(); // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider @@ -78,7 +82,7 @@ public class FileUtils { } } // MediaStore (and general) - else if ("content".equalsIgnoreCase(uri.getScheme())) { + else if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { String path = getDataColumn(context, uri, null, null); if (path != null) { File file = new File(path); @@ -89,7 +93,7 @@ public class FileUtils { return path; } // File - else if ("file".equalsIgnoreCase(uri.getScheme())) { + else if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return uri.getPath(); } @@ -106,7 +110,7 @@ public class FileUtils { * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ - public static String getDataColumn(Context context, Uri uri, String selection, + private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; @@ -155,4 +159,71 @@ public class FileUtils { public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } + + /** + * @param filename The filename to extract extension from + * @return last extension or empty string + */ + public static String getLastExtension(final String filename) { + if (filename == null || filename.isEmpty()) { + return ""; + } + final int lastDotPosition = filename.lastIndexOf('.'); + final String lastPart = lastDotPosition != -1 ? + filename.substring(lastDotPosition + 1) : ""; + return lastPart; + } + + /** + * @param filename The filename to extract extension from + * @return second to last extension or empty string + */ + public static String getSecondToLastExtension(final String filename) { + if (filename == null || filename.isEmpty()) { + return ""; + } + final int lastDotPosition = filename.lastIndexOf('.'); + final int secondToLastDotPosition = filename.lastIndexOf('.', lastDotPosition - 1); + final String secondToLastPart = secondToLastDotPosition != -1 ? + filename.substring(secondToLastDotPosition + 1, lastDotPosition) : ""; + return secondToLastPart; + } + + /** + * Retrieve file size from given uri + * @param context actual Context + * @param uri uri to file + * @return file size or -1 in case of error + */ + private static long getFileSize(Context context, Uri uri) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); + } else { + return -1; + } + } + + /** + * Check for given list of uris if corresponding file sizes are all smaller than given maximum + * @param context actual Context + * @param uris list of uris + * @param max maximum file size + * @return true if all file sizes are smaller than max, false otherwise + */ + public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) { + if (max <= 0) { + return true; //exception to be compatible with HTTP Upload < v0.2 + } + for(Uri uri : uris) { + if (getFileSize(context, uri) > max) { + return false; + } + } + return true; + } + + private FileUtils() { + // Utility class - do not instantiate + } } diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java index 8fe67234..5faa1fa7 100644 --- a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java +++ b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.utils; import android.os.Build; import android.os.Process; -import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -19,6 +18,8 @@ import java.security.SecureRandom; import java.security.SecureRandomSpi; import java.security.Security; +import de.thedevstack.android.logcat.Logging; + /** * Fixes for the output of the default PRNG having low entropy. * @@ -209,7 +210,7 @@ public final class PRNGFixes { } catch (IOException e) { // On a small fraction of devices /dev/urandom is not writable. // Log and ignore. - Log.w(PRNGFixes.class.getSimpleName(), + Logging.w(PRNGFixes.class.getSimpleName(), "Failed to mix seed into " + URANDOM_FILE); } finally { mSeeded = true; diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 768e9f17..04cfa2eb 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -43,10 +43,6 @@ public class SocksSocketFactory { return socket; } - public static Socket createSocketOverTor(String destination, int port) throws IOException { - return createSocket(new InetSocketAddress(InetAddress.getLocalHost(), 9050), destination, port); - } - static class SocksConnectionException extends IOException { } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index add3d80c..a97b16a4 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -5,27 +5,25 @@ import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Pair; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Locale; +import de.thedevstack.conversationsplus.ConversationsPlusColors; + import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.xmpp.jid.Jid; public class UIHelper { - private static String BLACK_HEART_SUIT = "\u2665"; - private static String HEAVY_BLACK_HEART_SUIT = "\u2764"; - private static String WHITE_HEART_SUIT = "\u2661"; - - public static final ArrayList<String> HEARTS = new ArrayList<>(Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT)); - private static final ArrayList<String> LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList( "where are you", //en "where are you now", //en @@ -182,9 +180,8 @@ public class UIHelper { return new Pair<>(getFileDescriptionString(context,message),true); } } else { - if (message.getBody().startsWith(Message.ME_COMMAND)) { - return new Pair<>(message.getBody().replaceAll("^" + Message.ME_COMMAND, - UIHelper.getMessageDisplayName(message) + " "), false); + if (message.hasMeCommand()) { + return new Pair<>(message.getBodyReplacedMeCommand(UIHelper.getMessageDisplayName(message)), false); } else if (GeoHelper.isGeoUri(message.getBody())) { if (message.getStatus() == Message.STATUS_RECEIVED) { return new Pair<>(context.getString(R.string.received_location), true); @@ -195,7 +192,7 @@ public class UIHelper { return new Pair<>(context.getString(R.string.x_file_offered_for_download, getFileDescriptionString(context,message)),true); } else{ - return new Pair<>(message.getBody().trim(), false); + return new Pair<>(message.getBody(), false); } } } @@ -220,7 +217,7 @@ public class UIHelper { } else if (mime.contains("vcard")) { return context.getString(R.string.vcard) ; } else { - return mime; + return message.getRelativeFilePath(); } } @@ -247,13 +244,29 @@ public class UIHelper { } } + public static int getStatusColor(Presence.Status status) { + switch (status) { + case ONLINE: + return ConversationsPlusColors.online(); + case CHAT: + return ConversationsPlusColors.chat(); + case AWAY: + return ConversationsPlusColors.away(); + case XA: + return ConversationsPlusColors.xa(); + case DND: + return ConversationsPlusColors.dnd(); + } + return ConversationsPlusColors.offline(); + } + private static String getDisplayedMucCounterpart(final Jid counterpart) { if (counterpart==null) { return ""; } else if (!counterpart.isBareJid()) { - return counterpart.getResourcepart().trim(); + return counterpart.getResourcepart(); } else { - return counterpart.toString().trim(); + return counterpart.toString(); } } @@ -263,8 +276,24 @@ public class UIHelper { || message.getType() != Message.TYPE_TEXT) { return false; } - String body = message.getBody() == null ? null : message.getBody().trim().toLowerCase(Locale.getDefault()); + String body = message.getBody() == null ? null : message.getBody().toLowerCase(Locale.getDefault()); body = body.replace("?","").replace("¿",""); return LOCATION_QUESTIONS.contains(body); } + + public static String getHumanReadableFileSize(long filesize) { + if (0 > filesize) { + return "?"; + } + double size = Double.valueOf(filesize); + String[] sizes = {" bytes", " Kb", " Mb", " Gb", " Tb"}; + int i = 0; + while (1023 < size) { + size /= 1024d; + ++i; + } + BigDecimal readableSize = new BigDecimal(size); + readableSize = readableSize.setScale(2, BigDecimal.ROUND_HALF_UP); + return readableSize.doubleValue() + sizes[i]; + } } diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 34794be1..9152c679 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,11 +1,10 @@ package eu.siacs.conversations.xml; -import android.util.Log; - import java.util.ArrayList; import java.util.Hashtable; import java.util.List; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -128,7 +127,7 @@ public class Element { try { return Jid.fromString(jid); } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "could not parse jid " + jid); + Logging.e(Config.LOGTAG, "could not parse jid " + jid); return null; } } diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index aeaaa593..74e65fcd 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.xml; import android.os.PowerManager; import android.os.PowerManager.WakeLock; -import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; @@ -12,6 +11,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; public class XmlReader { @@ -25,7 +25,7 @@ public class XmlReader { this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); } catch (XmlPullParserException e) { - Log.d(Config.LOGTAG, "error setting namespace feature on parser"); + Logging.d(Config.LOGTAG, "error setting namespace feature on parser"); } this.wakeLock = wakeLock; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6371f115..c0b1ff1f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -39,6 +39,7 @@ import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; +import java.util.TreeSet; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; @@ -49,6 +50,8 @@ import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import de.duenndns.ssl.MemorizingTrustManager; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.dto.SrvRecord; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.sasl.DigestMd5; @@ -88,7 +91,7 @@ import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; public class XmppConnection implements Runnable { - + private static final int DEFAULT_PORT = 5222; private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; @@ -227,7 +230,7 @@ public class XmppConnection implements Runnable { } protected void connect() { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); features.encryptionEnabled = false; this.attempt++; switch (account.getJid().getDomainpart()) { @@ -246,19 +249,8 @@ public class XmppConnection implements Runnable { tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); this.changeStatus(Account.State.CONNECTING); - final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); final boolean extended = mXmppConnectionService.showExtendedConnectionOptions(); - if (useTor) { - String destination; - if (account.getHostname() == null || account.getHostname().isEmpty()) { - destination = account.getServer().toString(); - } else { - destination = account.getHostname(); - } - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via TOR"); - socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort()); - startXmpp(); - } else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) { + if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) { socket = new Socket(); try { socket.connect(new InetSocketAddress(account.getHostname(), account.getPort()), Config.SOCKET_TIMEOUT * 1000); @@ -269,75 +261,60 @@ public class XmppConnection implements Runnable { } else if (DNSHelper.isIp(account.getServer().toString())) { socket = new Socket(); try { - socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000); + socket.connect(new InetSocketAddress(account.getServer().toString(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000); } catch (IOException e) { throw new UnknownHostException(); } startXmpp(); } else { - final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService); - final ArrayList<Parcelable>values = result.getParcelableArrayList("values"); - for(Iterator<Parcelable> iterator = values.iterator(); iterator.hasNext();) { - final Bundle namePort = (Bundle) iterator.next(); - try { - String srvRecordServer; - try { - srvRecordServer = IDN.toASCII(namePort.getString("name")); - } catch (final IllegalArgumentException e) { - // TODO: Handle me?` - srvRecordServer = ""; - } - final int srvRecordPort = namePort.getInt("port"); - final String srvIpServer = namePort.getString("ip"); - // if tls is true, encryption is implied and must not be started - features.encryptionEnabled = namePort.getBoolean("tls"); - final InetSocketAddress addr; - if (srvIpServer != null) { - addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort + " tls: " + features.encryptionEnabled); - } else { - addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort + " tls: " + features.encryptionEnabled); - } - - if (!features.encryptionEnabled) { - socket = new Socket(); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - } else { - final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier(); - socket = tlsFactoryVerifier.factory.createSocket(); - - if (socket == null) { - throw new IOException("could not initialize ssl socket"); - } - - SSLSocketHelper.setSecurity((SSLSocket) socket); - SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); - SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); - - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - - if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); - throw new SecurityException(); - } - } - - if (startXmpp()) - break; // successfully connected to server that speaks xmpp - } catch(final SecurityException e) { - throw e; - } catch (final Throwable e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() +"("+e.getClass().getName()+")"); - if (!iterator.hasNext()) { - throw new UnknownHostException(); - } - } - } + final TreeSet<SrvRecord> srvRecords = DNSHelper.querySrvRecord(account.getServer()); + if (srvRecords.isEmpty()) { + socket = new Socket(); + try { + socket.connect(new InetSocketAddress(account.getServer().getDomainpart(), DEFAULT_PORT), Config.SOCKET_TIMEOUT * 1000); + } catch (IOException e) { + throw new UnknownHostException(); + } + startXmpp(); + } else { + for (SrvRecord srvRecord : srvRecords) { + // if tls is true, encryption is implied and must not be started + features.encryptionEnabled = srvRecord.isUseTls(); + TlsFactoryVerifier tlsFactoryVerifier = null; + if (features.encryptionEnabled) { + try { + tlsFactoryVerifier = getTlsFactoryVerifier(); + socket = tlsFactoryVerifier.factory.createSocket(); + + if (socket == null) { + throw new IOException("could not initialize ssl socket"); + } + + SSLSocketHelper.setSecurity((SSLSocket) socket); + SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart()); + SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client"); + } catch (SecurityException e) { + throw e; + } catch (KeyManagementException e) { + Logging.e("connection-init", "Error while creating TLS verifier factory: " + e.getMessage(), e); + throw new SecurityException(); + } + } else { + socket = new Socket(); + } + + socket.connect(new InetSocketAddress(srvRecord.getName(), srvRecord.getPort()), Config.SOCKET_TIMEOUT * 1000); + + if (null != tlsFactoryVerifier && !tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed"); + throw new SecurityException(); + } + + if (startXmpp()) { + break; // successfully connected to server that speaks xmpp + } + } + } } processStream(); } catch (final IncompatibleServerException e) { @@ -348,10 +325,8 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.UNAUTHORIZED); } catch (final UnknownHostException | ConnectException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); - } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { - this.changeStatus(Account.State.TOR_NOT_AVAILABLE); } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); this.changeStatus(Account.State.OFFLINE); this.attempt--; //don't count attempt when reconnecting instantly anyway } finally { @@ -651,7 +626,7 @@ public class XmppConnection implements Runnable { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { - Log.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet"); + Logging.e(Config.LOGTAG, account.getJid().toBareJid().toString() + ": ignoring spoofed iq packet"); } } } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { @@ -758,16 +733,16 @@ public class XmppConnection implements Runnable { try { if (keys.has(Account.PINNED_MECHANISM_KEY) && keys.getInt(Account.PINNED_MECHANISM_KEY) > saslMechanism.getPriority()) { - Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + + Logging.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + "). Possible downgrade attack?"); throw new SecurityException(); } } catch (final JSONException e) { - Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); + Logging.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); } - Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism()); + Logging.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism()); auth.setAttribute("mechanism", saslMechanism.getMechanism()); if (!saslMechanism.getClientFirstMessage().isEmpty()) { auth.setContent(saslMechanism.getClientFirstMessage()); @@ -778,7 +753,7 @@ public class XmppConnection implements Runnable { } } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) { if (Config.EXTENDED_SM_LOGGING) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": resuming after stanza #"+stanzasReceived); } final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); this.tagWriter.writeStanzaAsync(resume); @@ -850,7 +825,7 @@ public class XmppConnection implements Runnable { URL uri = new URL(urlString); captcha = BitmapFactory.decodeStream(uri.openConnection().getInputStream()); } catch (IOException e) { - Log.e(Config.LOGTAG, e.toString()); + Logging.e(Config.LOGTAG, e.toString()); } } @@ -919,11 +894,11 @@ public class XmppConnection implements Runnable { sendPostBindInitialization(); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); + Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure. (no jid)"); disconnect(true); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure ("+packet.toString()); + Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure ("+packet.toString()); disconnect(true); } } @@ -983,7 +958,7 @@ public class XmppConnection implements Runnable { if (packet.getType() == IqPacket.TYPE.RESULT) { sendPostBindInitialization(); } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not init sessions"); disconnect(true); } } @@ -1074,13 +1049,13 @@ public class XmppConnection implements Runnable { enableAdvancedStreamFeatures(); } } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not query disco info for " + jid.toString()); } if (packet.getType() != IqPacket.TYPE.TIMEOUT) { mPendingServiceDiscoveries--; if (mPendingServiceDiscoveries == 0) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done with service discovery"); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": done with service discovery"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource()); if (bindListener != null) { bindListener.onBind(account); } @@ -1140,11 +1115,11 @@ public class XmppConnection implements Runnable { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": successfully enabled carbons"); features.carbonsEnabled = true; } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": error enableing carbons " + packet.toString()); } } @@ -1157,11 +1132,11 @@ public class XmppConnection implements Runnable { if (streamError != null && streamError.hasChild("conflict")) { final String resource = account.getResource().split("\\.")[0]; account.setResource(resource + "." + nextRandomId()); - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": switching resource due to conflict (" + account.getResource() + ")"); } else if (streamError != null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": stream error "+streamError.toString()); } } @@ -1285,7 +1260,7 @@ public class XmppConnection implements Runnable { Thread.sleep(10); } socket.close(); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream"); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { @@ -1295,7 +1270,7 @@ public class XmppConnection implements Runnable { }).start(); } else { forceCloseSocket(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)"); } } @@ -1304,13 +1279,13 @@ public class XmppConnection implements Runnable { try { socket.close(); } catch (IOException e) { - e.printStackTrace(); + Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")"); } } } public void disconnect(final boolean force) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force)); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force)); if (force) { forceCloseSocket(); return; @@ -1404,7 +1379,12 @@ public class XmppConnection implements Runnable { } public long getLastSessionEstablished() { - final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; + final long diff; + if (this.lastSessionStarted == 0) { + diff = SystemClock.elapsedRealtime() - this.lastConnect; + } else { + diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; + } return System.currentTimeMillis() - diff; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index beed92fa..ecaa9c13 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -1,18 +1,24 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Log; +import android.content.Intent; +import android.net.Uri; import android.util.Pair; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback; @@ -26,6 +32,7 @@ import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.Jid; @@ -94,7 +101,7 @@ public class JingleConnection implements Transferable { public void onFileTransmitted(DownloadableFile file) { if (responder.equals(account.getJid())) { sendSuccess(); - mXmppConnectionService.getFileBackend().updateFileParams(message); + MessageUtil.updateFileParams(message); mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED); if (acceptedAutomatically) { @@ -106,9 +113,9 @@ public class JingleConnection implements Transferable { file.delete(); } } - Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")"); + Logging.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")"); if (message.getEncryption() != Message.ENCRYPTION_PGP) { - mXmppConnectionService.getFileBackend().updateMediaScanner(file); + FileBackend.updateMediaScanner(file, mXmppConnectionService); } else { account.getPgpDecryptionService().add(message); } @@ -134,17 +141,17 @@ public class JingleConnection implements Transferable { @Override public void success() { if (initiator.equals(account.getJid())) { - Log.d(Config.LOGTAG, "we were initiating. sending file"); + Logging.d(Config.LOGTAG, "we were initiating. sending file"); transport.send(file, onFileTransmissionSatusChanged); } else { transport.receive(file, onFileTransmissionSatusChanged); - Log.d(Config.LOGTAG, "we were responding. receiving file"); + Logging.d(Config.LOGTAG, "we were responding. receiving file"); } } @Override public void failed() { - Log.d(Config.LOGTAG, "proxy activation failed"); + Logging.d(Config.LOGTAG, "proxy activation failed"); } }; @@ -190,13 +197,13 @@ public class JingleConnection implements Transferable { returnResult = this.receiveFallbackToIbb(packet); } else { returnResult = false; - Log.d(Config.LOGTAG, "trying to fallback to something unknown" + Logging.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString()); } } else if (packet.isAction("transport-accept")) { returnResult = this.receiveTransportAccept(packet); } else { - Log.d(Config.LOGTAG, "packet arrived in connection. action was " + Logging.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction()); returnResult = false; } @@ -258,14 +265,14 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "connection to our own primary candidete failed"); sendInitRequest(); } @Override public void established() { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "succesfully connected to our own primary candidate"); mergeCandidate(candidate); sendInitRequest(); @@ -273,7 +280,8 @@ public class JingleConnection implements Transferable { }); mergeCandidate(candidate); } else { - Log.d(Config.LOGTAG, "no primary candidate of our own was found"); + Logging.d(Config.LOGTAG, + "no primary candidate of our own was found"); sendInitRequest(); } } @@ -314,64 +322,59 @@ public class JingleConnection implements Transferable { Element fileSize = fileOffer.findChild("size"); Element fileNameElement = fileOffer.findChild("name"); if (fileNameElement != null) { - String[] filename = fileNameElement.getContent() - .toLowerCase(Locale.US).toLowerCase().split("\\."); - String extension = filename[filename.length - 1]; - if (VALID_IMAGE_EXTENSIONS.contains(extension)) { - message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid()+"."+extension); - } else if (VALID_CRYPTO_EXTENSIONS.contains( - filename[filename.length - 1])) { - if (filename.length == 3) { - extension = filename[filename.length - 2]; - if (VALID_IMAGE_EXTENSIONS.contains(extension)) { - message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid()+"."+extension); - } else { - message.setType(Message.TYPE_FILE); - } - if (filename[filename.length - 1].equals("otr")) { - message.setEncryption(Message.ENCRYPTION_OTR); - } else { - message.setEncryption(Message.ENCRYPTION_PGP); + String filename = fileNameElement.getContent() + .toLowerCase(Locale.US).toLowerCase(); + final String lastPart = FileUtils.getLastExtension(filename); + final String secondToLastPart = FileUtils.getSecondToLastExtension(filename); + if (!lastPart.isEmpty()) { + if (VALID_IMAGE_EXTENSIONS.contains(lastPart)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+lastPart); + } else if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + if (!secondToLastPart.isEmpty()) { + if (VALID_IMAGE_EXTENSIONS.contains(secondToLastPart)) { + message.setType(Message.TYPE_IMAGE); + message.setRelativeFilePath(message.getUuid()+"."+secondToLastPart); + } else { + message.setType(Message.TYPE_FILE); + message.setRelativeFilePath(message.getUuid() + "_" + + filename.substring(0, filename.length() - (secondToLastPart.length() + 1))); + } + if (lastPart.equals("otr")) { + message.setEncryption(Message.ENCRYPTION_OTR); + } else { + message.setEncryption(Message.ENCRYPTION_PGP); + } } + } else { + message.setType(Message.TYPE_FILE); + message.setRelativeFilePath(message.getUuid() + "_" + filename); } } else { message.setType(Message.TYPE_FILE); + message.setRelativeFilePath(message.getUuid() + "_" + filename); } - if (message.getType() == Message.TYPE_FILE) { - String suffix = ""; - if (!fileNameElement.getContent().isEmpty()) { - String parts[] = fileNameElement.getContent().split("/"); - suffix = parts[parts.length - 1]; - if (message.getEncryption() == Message.ENCRYPTION_OTR && suffix.endsWith(".otr")) { - suffix = suffix.substring(0,suffix.length() - 4); - } else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) { - suffix = suffix.substring(0,suffix.length() - 4); - } - } - message.setRelativeFilePath(message.getUuid()+"_"+suffix); - } + long size = Long.parseLong(fileSize.getContent()); message.setBody(Long.toString(size)); conversation.add(message); mXmppConnectionService.updateConversationUi(); if (mJingleConnectionManager.hasStoragePermission() - && size < this.mJingleConnectionManager.getAutoAcceptFileSize()) { - Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom()); + && size <= ConversationsPlusPreferences.autoAcceptFileSize() + && mXmppConnectionService.isDownloadAllowedInConnection()) { + Logging.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom()); this.acceptedAutomatically = true; this.sendAccept(); } else { message.markUnread(); - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "not auto accepting new file offer with size: " + size + " allowed size:" - + this.mJingleConnectionManager - .getAutoAcceptFileSize()); + + ConversationsPlusPreferences.autoAcceptFileSize()); this.mXmppConnectionService.getNotificationService().push(message); } - this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); if (mXmppAxolotlMessage != null) { XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage); if (transportMessage != null) { @@ -380,7 +383,7 @@ public class JingleConnection implements Transferable { this.file.setIv(transportMessage.getIv()); message.setFingerprint(transportMessage.getFingerprint()); } else { - Log.d(Config.LOGTAG,"could not process KeyTransportMessage"); + Logging.d(Config.LOGTAG,"could not process KeyTransportMessage"); } } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); @@ -398,7 +401,7 @@ public class JingleConnection implements Transferable { } else { this.file.setExpectedSize(size); } - Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); + Logging.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); } else { this.sendCancel(); this.fail(); @@ -414,13 +417,13 @@ public class JingleConnection implements Transferable { Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); Pair<InputStream,Integer> pair; try { if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key"); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key"); cancel(); } this.file.setKeyAndIv(conversation.getSymmetricKey()); @@ -452,7 +455,7 @@ public class JingleConnection implements Transferable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer"); mJingleStatus = JINGLE_STATUS_INITIATED; mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); } else { @@ -494,7 +497,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG,"connection to our own primary candidate failed"); + Logging.d(Config.LOGTAG,"connection to our own primary candidate failed"); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); sendJinglePacket(packet); @@ -503,7 +506,7 @@ public class JingleConnection implements Transferable { @Override public void established() { - Log.d(Config.LOGTAG, "connected to primary candidate"); + Logging.d(Config.LOGTAG, "connected to primary candidate"); mergeCandidate(candidate); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); @@ -512,7 +515,7 @@ public class JingleConnection implements Transferable { } }); } else { - Log.d(Config.LOGTAG,"did not find a primary candidate for ourself"); + Logging.d(Config.LOGTAG,"did not find a primary candidate for ourself"); content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); sendJinglePacket(packet); @@ -558,13 +561,13 @@ public class JingleConnection implements Transferable { onProxyActivated.success(); } else { String cid = content.socks5transport().findChild("activated").getAttribute("cid"); - Log.d(Config.LOGTAG, "received proxy activated (" + cid + Logging.d(Config.LOGTAG, "received proxy activated (" + cid + ")prior to choosing our own transport"); JingleSocks5Transport connection = this.connections.get(cid); if (connection != null) { connection.setActivated(true); } else { - Log.d(Config.LOGTAG, "activated connection not found"); + Logging.d(Config.LOGTAG, "activated connection not found"); this.sendCancel(); this.fail(); } @@ -574,7 +577,7 @@ public class JingleConnection implements Transferable { onProxyActivated.failed(); return true; } else if (content.socks5transport().hasChild("candidate-error")) { - Log.d(Config.LOGTAG, "received candidate error"); + Logging.d(Config.LOGTAG, "received candidate error"); this.receivedCandidate = true; if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) && (this.sentCandidate)) { @@ -585,7 +588,7 @@ public class JingleConnection implements Transferable { String cid = content.socks5transport() .findChild("candidate-used").getAttribute("cid"); if (cid != null) { - Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); + Logging.d(Config.LOGTAG, "candidate used by counterpart:" + cid); JingleCandidate candidate = getCandidate(cid); candidate.flagAsUsedByCounterpart(); this.receivedCandidate = true; @@ -593,7 +596,7 @@ public class JingleConnection implements Transferable { && (this.sentCandidate)) { this.connect(); } else { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "ignoring because file is already in transmission or we havent sent our candidate yet"); } return true; @@ -612,7 +615,7 @@ public class JingleConnection implements Transferable { final JingleSocks5Transport connection = chooseConnection(); this.transport = connection; if (connection == null) { - Log.d(Config.LOGTAG, "could not find suitable candidate"); + Logging.d(Config.LOGTAG, "could not find suitable candidate"); this.disconnectSocks5Connections(); if (this.initiator.equals(account.getJid())) { this.sendFallbackToIbb(); @@ -621,7 +624,7 @@ public class JingleConnection implements Transferable { this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { - Log.d(Config.LOGTAG, "candidate " + Logging.d(Config.LOGTAG, "candidate " + connection.getCandidate().getCid() + " was our proxy. going to activate"); IqPacket activation = new IqPacket(IqPacket.TYPE.SET); @@ -645,17 +648,17 @@ public class JingleConnection implements Transferable { } }); } else { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "candidate " + connection.getCandidate().getCid() + " was a proxy. waiting for other party to activate"); } } else { if (initiator.equals(account.getJid())) { - Log.d(Config.LOGTAG, "we were initiating. sending file"); + Logging.d(Config.LOGTAG, "we were initiating. sending file"); connection.send(file, onFileTransmissionSatusChanged); } else { - Log.d(Config.LOGTAG, "we were responding. receiving file"); + Logging.d(Config.LOGTAG, "we were responding. receiving file"); connection.receive(file, onFileTransmissionSatusChanged); } } @@ -667,11 +670,11 @@ public class JingleConnection implements Transferable { for (Entry<String, JingleSocks5Transport> cursor : connections .entrySet()) { JingleSocks5Transport currentConnection = cursor.getValue(); - // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); + // Logging.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); if (currentConnection.isEstablished() && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection .getCandidate().isOurs()))) { - // Log.d(Config.LOGTAG,"is usable"); + // Logging.d(Config.LOGTAG,"is usable"); if (connection == null) { connection = currentConnection; } else { @@ -680,7 +683,7 @@ public class JingleConnection implements Transferable { connection = currentConnection; } else if (connection.getCandidate().getPriority() == currentConnection .getCandidate().getPriority()) { - // Log.d(Config.LOGTAG,"found two candidates with same priority"); + // Logging.d(Config.LOGTAG,"found two candidates with same priority"); if (initiator.equals(account.getJid())) { if (currentConnection.getCandidate().isOurs()) { connection = currentConnection; @@ -712,7 +715,7 @@ public class JingleConnection implements Transferable { } private void sendFallbackToIbb() { - Log.d(Config.LOGTAG, "sending fallback to ibb"); + Logging.d(Config.LOGTAG, "sending fallback to ibb"); JinglePacket packet = this.bootstrapPacket("transport-replace"); Content content = new Content(this.contentCreator, this.contentName); this.transportId = this.mJingleConnectionManager.nextRandomId(); @@ -724,7 +727,7 @@ public class JingleConnection implements Transferable { } private boolean receiveFallbackToIbb(JinglePacket packet) { - Log.d(Config.LOGTAG, "receiving fallack to ibb"); + Logging.d(Config.LOGTAG, "receiving fallack to ibb"); String receivedBlockSize = packet.getJingleContent().ibbTransport() .getAttribute("block-size"); if (receivedBlockSize != null) { @@ -760,7 +763,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, "ibb open failed"); + Logging.d(Config.LOGTAG, "ibb open failed"); } @Override @@ -812,8 +815,8 @@ public class JingleConnection implements Transferable { if (this.transport != null && this.transport instanceof JingleInbandTransport) { this.transport.disconnect(); } - FileBackend.close(mFileInputStream); - FileBackend.close(mFileOutputStream); + StreamUtil.close(mFileInputStream); + StreamUtil.close(mFileOutputStream); if (this.message != null) { if (this.responder.equals(account.getJid())) { this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED)); @@ -857,7 +860,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "connection failed with " + candidate.getHost() + ":" + candidate.getPort()); connectNextCandidate(); @@ -865,7 +868,7 @@ public class JingleConnection implements Transferable { @Override public void established() { - Log.d(Config.LOGTAG, + Logging.d(Config.LOGTAG, "established connection with " + candidate.getHost() + ":" + candidate.getPort()); sendCandidateUsed(candidate.getCid()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0f0361cd..f4a069bc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; import android.annotation.SuppressLint; -import android.util.Log; import java.math.BigInteger; import java.security.SecureRandom; @@ -9,6 +8,7 @@ import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; @@ -155,9 +155,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } - Log.d(Config.LOGTAG,"couldn't deliver payload: " + payload.toString()); + Logging.d(Config.LOGTAG,"couldn't deliver payload: " + payload.toString()); } else { - Log.d(Config.LOGTAG, "no sid found in incoming ibb packet"); + Logging.d(Config.LOGTAG, "no sid found in incoming ibb packet"); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 0b0cb408..3800b94f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Base64; -import android.util.Log; import java.io.IOException; import java.io.InputStream; @@ -10,10 +9,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -95,13 +95,13 @@ public class JingleInbandTransport extends JingleTransport { file.createNewFile(); this.fileOutputStream = connection.getFileOutputStream(); if (this.fileOutputStream == null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); callback.onFileTransferAborted(); return; } this.remainingSize = this.fileSize = file.getExpectedSize(); } catch (final NoSuchAlgorithmException | IOException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage()); callback.onFileTransferAborted(); } } @@ -118,7 +118,7 @@ public class JingleInbandTransport extends JingleTransport { this.digest.reset(); fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); callback.onFileTransferAborted(); return; } @@ -127,7 +127,7 @@ public class JingleInbandTransport extends JingleTransport { } } catch (NoSuchAlgorithmException e) { callback.onFileTransferAborted(); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); } } @@ -186,8 +186,8 @@ public class JingleInbandTransport extends JingleTransport { fileInputStream.close(); } } catch (IOException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); - FileBackend.close(fileInputStream); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); + StreamUtil.close(fileInputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } } @@ -210,8 +210,8 @@ public class JingleInbandTransport extends JingleTransport { connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); } } catch (IOException e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); - FileBackend.close(fileOutputStream); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); + StreamUtil.close(fileOutputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 9240bd2c..76cd0c87 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -1,25 +1,22 @@ package eu.siacs.conversations.xmpp.jingle; import android.os.PowerManager; -import android.util.Log; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.Proxy; import java.net.Socket; import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.utils.StreamUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SocksSocketFactory; @@ -62,14 +59,10 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { try { - final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); - if (useTor) { - socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(),candidate.getPort()); - } else { - socket = new Socket(); - SocketAddress address = new InetSocketAddress(candidate.getHost(),candidate.getPort()); - socket.connect(address,Config.SOCKET_TIMEOUT * 1000); - } + socket = new Socket(); + SocketAddress address = new InetSocketAddress(candidate.getHost(),candidate.getPort()); + socket.connect(address,Config.SOCKET_TIMEOUT * 1000); + inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); SocksSocketFactory.createSocksConnection(socket,destination,0); @@ -98,7 +91,7 @@ public class JingleSocks5Transport extends JingleTransport { digest.reset(); fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); callback.onFileTransferAborted(); return; } @@ -118,16 +111,16 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransmitted(file); } } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (IOException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { - FileBackend.close(fileInputStream); + StreamUtil.close(fileInputStream); wakeLock.release(); } } @@ -153,7 +146,7 @@ public class JingleSocks5Transport extends JingleTransport { fileOutputStream = connection.getFileOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); return; } double size = file.getExpectedSize(); @@ -164,7 +157,7 @@ public class JingleSocks5Transport extends JingleTransport { count = inputStream.read(buffer); if (count == -1) { callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": file ended prematurely with "+remainingSize+" bytes remaining"); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": file ended prematurely with "+remainingSize+" bytes remaining"); return; } else { fileOutputStream.write(buffer, 0, count); @@ -178,18 +171,18 @@ public class JingleSocks5Transport extends JingleTransport { file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); callback.onFileTransmitted(file); } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (IOException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); + Logging.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { wakeLock.release(); - FileBackend.close(fileOutputStream); - FileBackend.close(inputStream); + StreamUtil.close(fileOutputStream); + StreamUtil.close(inputStream); } } }).start(); @@ -204,9 +197,9 @@ public class JingleSocks5Transport extends JingleTransport { } public void disconnect() { - FileBackend.close(inputStream); - FileBackend.close(outputStream); - FileBackend.close(socket); + StreamUtil.close(inputStream); + StreamUtil.close(outputStream); + StreamUtil.close(socket); } public boolean isEstablished() { |