diff options
Diffstat (limited to 'src/main')
146 files changed, 4815 insertions, 1166 deletions
diff --git a/src/main/.project b/src/main/.project new file mode 100644 index 00000000..d2f05ef9 --- /dev/null +++ b/src/main/.project @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ConversationActivity</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index dda0fbe9..999ec516 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ <manifest package="eu.siacs.conversations" xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools"> + xmlns:tools="http://schemas.android.com/tools" + android:installLocation="auto"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> @@ -25,8 +26,9 @@ android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/ConversationsTheme" - tools:replace="android:label"> - <service android:name=".services.XmppConnectionService"/> + tools:replace="android:label" + android:name="de.thedevstack.conversationsplus.ConversationsPlusApplication"> + <service android:name=".services.XmppConnectionService" /> <receiver android:name=".services.EventReceiver"> <intent-filter> @@ -157,6 +159,14 @@ android:name="android.support.PARENT_ACTIVITY" android:value="eu.siacs.conversations.ui.SettingsActivity"/> </activity> + <activity + android:name="de.thedevstack.conversationsplus.ui.LogCatOutputActivity" + android:label="@string/title_activity_loginformation" + android:parentActivityName="de.thedevstack.conversationsplus.ui.SettingsActivity" > + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.thedevstack.conversationsplus.ui.SettingsActivity" /> + </activity> <activity android:name="com.soundcloud.android.crop.CropImageActivity" /> <service android:name=".services.ExportLogsService"/> <service android:name=".services.ContactChooserTargetService" 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..9c217e35 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -0,0 +1,103 @@ +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.utils.SerialSingleThreadExecutor; + +import eu.siacs.conversations.R; + +/** + * 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(); + } + + /** + * 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/ConversationsPlusPreferences.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java new file mode 100644 index 00000000..17829998 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java @@ -0,0 +1,331 @@ +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 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 alwaysNotifyInConference() { + return getBoolean("always_notify_in_conference", false); + } + + 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 confirmMessages() { + return getBoolean("confirm_messages", true); + } + + 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); + } + + 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..3bc79c4f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/dto/SrvRecord.java @@ -0,0 +1,53 @@ +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; + + public SrvRecord(int priority, String name, int port) { + this.priority = priority; + this.name = name; + this.port = port; + } + + /** + * 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; + } +} 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/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..14872cb3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/LogCatOutputActivity.java @@ -0,0 +1,29 @@ +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..96faee91 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/PresencesArrayAdapter.java @@ -0,0 +1,63 @@ +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.ImageView; +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(Color.parseColor(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, Integer> entry : presences.getPresences().entrySet()) { + Presence p = new Presence(); + p.resource = entry.getKey(); + p.status = entry.getValue(); + presenceArrayList.add(p); + } + presenceArrayList.trimToSize(); + } + return presenceArrayList.toArray(new Presence[0]); + } +} + +class Presence { + String resource; + int 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..af875251 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java @@ -0,0 +1,181 @@ +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 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; + int msgStatusColorResId = R.color.primaryText; + 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; + msgStatusColorResId = R.color.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); + msgStatusTextView.setTextColor(getContext().getResources().getColor(msgStatusColorResId)); + } + + /** + * 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.getMergedTimeSent()))); + } +} 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..2d919528 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java @@ -0,0 +1,70 @@ +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; + +/** + * Created by tzur on 31.10.2015. + */ +public class UserDecisionDialog extends AbstractAlertDialog { + protected final UserDecisionListener listener; + protected final CheckBox rememberCheckBox; + + public UserDecisionDialog(Activity context, int questionResourceId, UserDecisionListener userDecisionListener) { + super(context, "User Decision"); + this.listener = userDecisionListener; + + int viewId = R.layout.dialog_userdecision; + View view = context.getLayoutInflater().inflate(viewId, null); + + ((TextView)view.findViewById(R.id.dlgUserDecQuestion)).setText(questionResourceId); + 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..ac58e698 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java @@ -0,0 +1,186 @@ +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.FileHelper; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; + +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; + +/** + * 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(); + 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; + final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption)); + } + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_IMAGE); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + + @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(forceEncryption) == 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); + } + } + }); + } + + @Override + public void onNo() { + this.showPrepareFileToast(); + final Message message; + final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption)); + } + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_IMAGE); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + @Override + public void run() { + InputStream is = null; + try { + is = ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(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 = FileHelper.getRealPathFromUri(uri); + MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight); + if (conversation.getNextEncryption(forceEncryption) == 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()); + } + } + } + } + }); + } + + @Override + public void onRemember(UserDecision decision) { + ConversationsPlusPreferences.applyResizePicture(decision); + } +} 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..b082c5c4 --- /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 eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import de.thedevstack.conversationsplus.ui.adapter.PresencesArrayAdapter; +import de.thedevstack.conversationsplus.ui.dialogs.AbstractAlertDialog; + +/** + * 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..401c77fc --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java @@ -0,0 +1,164 @@ +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/FileHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/FileHelper.java new file mode 100644 index 00000000..3384b54e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/FileHelper.java @@ -0,0 +1,43 @@ +package de.thedevstack.conversationsplus.utils; + +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; + +import de.thedevstack.conversationsplus.ConversationsPlusApplication; + +/** + * Created by tzur on 30.10.2015. + */ +public final class FileHelper { + + /** + * Get the real path from an Uri. + * @param uri the uri to convert to the real path + * @return the real path or <code>null</code> + */ + public static String getRealPathFromUri(Uri uri) { + String path = null; + if (uri.getScheme().equals("file")) { + return uri.getPath(); + } else if (uri.toString().startsWith("content://media/")) { + String[] projection = {MediaStore.MediaColumns.DATA}; + Cursor metaCursor = ConversationsPlusApplication.getInstance().getContentResolver().query(uri, + projection, null, null, null); + if (metaCursor != null) { + try { + if (metaCursor.moveToFirst()) { + path = metaCursor.getString(0); + } + } finally { + metaCursor.close(); + } + } + } + return path; + } + + private FileHelper() { + // Utility class - do not instantiate + } +} 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..f51ca7ea --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java @@ -0,0 +1,375 @@ +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.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +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..9fedada6 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -0,0 +1,72 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.BitmapFactory; + +import java.net.URL; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.persistance.FileBackend; + +/** + * Created by tzur on 15.12.2015. + */ +public final class MessageUtil { + 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..64f46314 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java @@ -0,0 +1,48 @@ +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 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) { + } + } + } + + /** + * 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..082c8f7e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/UiUpdateHelper.java @@ -0,0 +1,53 @@ +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..74e65857 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/XmppSendUtil.java @@ -0,0 +1,26 @@ +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.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); + } + } +} 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..ed8a898d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketGenerator.java @@ -0,0 +1,125 @@ +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..cf993462 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/avatar/AvatarPacketParser.java @@ -0,0 +1,30 @@ +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..2484a88f --- /dev/null +++ b/src/main/java/de/tzur/conversations/Settings.java @@ -0,0 +1,77 @@ +package de.tzur.conversations; + +import android.content.SharedPreferences; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; + +/** + * 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 9ee221cc..2ba1177e 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -32,7 +32,7 @@ public final class Config { public static final int MINI_GRACE_PERIOD = 750; 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; @@ -41,9 +41,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..8a6bfc44 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java @@ -26,11 +26,18 @@ import java.security.spec.DSAPrivateKeySpec; import java.security.spec.DSAPublicKeySpec; import java.security.spec.InvalidKeySpecException; +import org.json.JSONException; +import org.json.JSONObject; + +import de.thedevstack.android.logcat.Logging; + import eu.siacs.conversations.Config; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -110,7 +117,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 +158,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 +192,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 +224,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 +259,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 +274,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 +290,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/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 624d1b13..1f889cd8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -17,6 +17,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 eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -61,7 +64,9 @@ public class PgpEngine { final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER - && manager.getAutoAcceptFileSize() > 0) { + && ConversationsPlusPreferences.autoDownloadFileLink() + && mXmppConnectionService.isDownloadAllowedInConnection() + && ConversationsPlusPreferences.autoAcceptFileSize() > 0) { manager.createNewDownloadConnection(message); } mXmppConnectionService.updateMessage(message); @@ -102,7 +107,7 @@ 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); @@ -168,7 +173,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 @@ -250,7 +255,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(); @@ -330,7 +335,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/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 4abfc801..0f37ea61 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -162,6 +162,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); diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index acb4bf1a..d0a951b3 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; @@ -74,6 +76,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 c2d8b278..6d252376 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; @@ -155,11 +156,16 @@ public class Contact implements ListItem, Blockable { return tags; } + @Override + public int getStatusColor() { + return Color.parseColor(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 22607fc6..38791d28 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -20,6 +20,7 @@ import java.util.Iterator; import java.util.List; import eu.siacs.conversations.Config; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -824,10 +825,16 @@ public class Conversation extends AbstractEntity implements Blockable { 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 (getMode() == Conversation.MODE_SINGLE + || ConversationsPlusPreferences.alwaysNotifyInConference() + || account.getXmppConnectionService().getNotificationService().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 efc1c2b9..aba2aef2 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> { public List<Tag> getTags(); + public int getStatusColor(); + public final class Tag { private final String name; private final int color; diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index f3d891e8..8d0571a4 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -347,16 +347,16 @@ 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) { + } else if (this.getBody() == null || this.counterpart == null) { return false; } else { String body, otherBody; if (this.hasFileOnRemoteHost()) { body = getFileParams().url.toString(); - otherBody = message.body == null ? null : message.body.trim(); + otherBody = message.getBody() == null ? null : message.getBody(); } else { - body = this.body; - otherBody = message.body; + body = this.getBody(); + otherBody = message.getBody(); } if (message.getRemoteMsgId() != null) { return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) @@ -415,13 +415,11 @@ public class Message extends AbstractEntity { this.getCounterpart().equals(message.getCounterpart()) && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && !GeoHelper.isGeoUri(message.getBody()) && - !GeoHelper.isGeoUri(this.body) && + !GeoHelper.isGeoUri(this.getBody()) && message.treatAsDownloadable() == Decision.NEVER && this.treatAsDownloadable() == Decision.NEVER && !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) && - !this.bodyIsHeart() && - !message.bodyIsHeart() && + !this.getBody().startsWith(ME_COMMAND) this.isTrusted() == message.isTrusted() ); } @@ -443,7 +441,7 @@ public class Message extends AbstractEntity { while(current.mergeable(current.next())) { current = current.next(); body.append(MERGE_SEPARATOR); - body.append(current.getBody().trim()); + body.append(current.getBody()); } return body.toString(); } @@ -543,7 +541,7 @@ 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; } @@ -551,7 +549,12 @@ public class Message extends AbstractEntity { } public Decision treatAsDownloadable() { - if (body.trim().contains(" ")) { + /** + * 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(" ")) { return Decision.NEVER; } try { @@ -584,10 +587,6 @@ public class Message extends AbstractEntity { } } - public boolean bodyIsHeart() { - return body != null && UIHelper.HEARTS.contains(body.trim()); - } - public FileParams getFileParams() { FileParams params = getLegacyFileParams(); if (params != null) { @@ -597,10 +596,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 { @@ -659,10 +658,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/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 7f4ded11..068dbf83 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -7,13 +7,17 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; @SuppressLint("DefaultLocale") public class MucOptions { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 5741af53..c30ae5d8 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -15,6 +15,8 @@ import java.util.TimeZone; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.tzur.conversations.Settings; public abstract class AbstractGenerator { private final String[] FEATURES = { @@ -61,7 +63,7 @@ public abstract class AbstractGenerator { 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"); @@ -73,7 +75,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) { @@ -84,7 +86,7 @@ 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)); } Collections.sort(features); diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 345f68ae..6bc1f0fc 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -15,6 +15,8 @@ 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; @@ -45,7 +47,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 +57,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; } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index b849f56f..9b5dabb0 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -9,6 +9,11 @@ import java.util.Date; import java.util.Locale; import java.util.TimeZone; +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; + +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; + import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -32,13 +37,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 { diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 455a0b14..516d3141 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -2,33 +2,29 @@ package eu.siacs.conversations.http; import android.content.Intent; import android.net.Uri; -import android.os.PowerManager; -import android.util.Log; +import android.os.SystemClock; import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.MalformedURLException; -import java.net.Proxy; import java.net.URL; import java.util.Arrays; -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.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.MessageUtil; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Transferable; -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.CryptoHelper; @@ -43,13 +39,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 long mLastGuiRefresh = 0; public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; this.mXmppConnectionService = manager.getXmppConnectionService(); - this.mUseTor = mXmppConnectionService.useTorToConnect(); } @Override @@ -74,18 +68,13 @@ public class HttpDownloadConnection implements Transferable { this.message = message; this.message.setTransferable(this); try { - if (message.hasFileOnRemoteHost()) { - mUrl = message.getFileParams().url; - } else { - mUrl = new URL(message.getBody()); - } + 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)) { this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR - && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { + } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; @@ -95,14 +84,13 @@ public class HttpDownloadConnection implements Transferable { 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)); + this.file.setKey(CryptoHelper.hexToBytes(reference)); } - if ((this.message.getEncryption() == Message.ENCRYPTION_OTR - || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) + if (this.message.getEncryption() == Message.ENCRYPTION_OTR && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } @@ -116,15 +104,9 @@ public class HttpDownloadConnection implements Transferable { new Thread(new FileSizeChecker(interactive)).start(); } - @Override public void cancel() { - this.canceled = true; mHttpConnectionManager.finishConnection(this); - if (message.isFileOrImage()) { - message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - } else { - message.setTransferable(null); - } + message.setTransferable(null); mXmppConnectionService.updateConversationUi(); } @@ -134,9 +116,6 @@ public class HttpDownloadConnection implements Transferable { mXmppConnectionService.sendBroadcast(intent); message.setTransferable(null); mHttpConnectionManager.finishConnection(this); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - message.getConversation().getAccount().getPgpDecryptionService().add(message); - } mXmppConnectionService.updateConversationUi(); if (acceptedAutomatically) { mXmppConnectionService.getNotificationService().push(message); @@ -148,17 +127,6 @@ public class HttpDownloadConnection implements Transferable { mXmppConnectionService.updateConversationUi(); } - private void showToastForException(Exception e) { - e.printStackTrace(); - if (e instanceof java.net.UnknownHostException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found); - } else if (e instanceof java.net.ConnectException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); - } else if (!(e instanceof CancellationException)) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); - } - } - private class FileSizeChecker implements Runnable { private boolean interactive = false; @@ -178,15 +146,15 @@ public class HttpDownloadConnection implements Transferable { HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); return; } catch (IOException e) { - Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); + Logging.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); if (interactive) { - showToastForException(e); + mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); } cancel(); return; } file.setExpectedSize(size); - if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + if (size != -1 && size <= ConversationsPlusPreferences.autoAcceptFileSize()) { HttpDownloadConnection.this.acceptedAutomatically = true; new Thread(new FileDownloader(interactive)).start(); } else { @@ -197,33 +165,22 @@ public class HttpDownloadConnection implements Transferable { } private long retrieveFileSize() throws IOException { + Logging.d(Config.LOGTAG,"retrieve file size. interactive:"+String.valueOf(interactive)); + changeStatus(STATUS_CHECKING); + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + connection.setRequestMethod("HEAD"); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + String contentLength = connection.getHeaderField("Content-Length"); + if (contentLength == null) { + return -1; + } try { - Log.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(); - } - 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()); - if (connection instanceof HttpsURLConnection) { - mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); - } - connection.connect(); - String contentLength = connection.getHeaderField("Content-Length"); - connection.disconnect(); - if (contentLength == null) { - throw new IOException(); - } return Long.parseLong(contentLength, 10); - } catch (IOException e) { - throw e; } catch (NumberFormatException e) { - throw new IOException(); + return -1; } } @@ -233,8 +190,6 @@ public class HttpDownloadConnection implements Transferable { private boolean interactive = false; - private OutputStream os; - public FileDownloader(boolean interactive) { this.interactive = interactive; } @@ -248,79 +203,42 @@ public class HttpDownloadConnection implements Transferable { finish(); } catch (SSLHandshakeException e) { changeStatus(STATUS_OFFER); - } catch (Exception e) { - if (interactive) { - showToastForException(e); - } + } catch (IOException e) { + mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host); cancel(); } } - 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 { + HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + if (connection instanceof HttpsURLConnection) { + mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); } + connection.connect(); + BufferedInputStream is = new BufferedInputStream(connection.getInputStream()); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream os = file.createOutputStream(); + if (os == null) { + throw new IOException(); + } + long transmitted = 0; + long expected = file.getExpectedSize(); + 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)); + } + os.flush(); + os.close(); + is.close(); } private void updateImageBounds() { message.setType(Message.TYPE_FILE); - mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl); + MessageUtil.updateFileParams(message, mUrl); mXmppConnectionService.updateMessage(message); } @@ -328,7 +246,10 @@ public class HttpDownloadConnection implements Transferable { public void updateProgress(int i) { this.mProgress = i; - mXmppConnectionService.updateConversationUi(); + if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) { + this.mLastGuiRefresh = SystemClock.elapsedRealtime(); + mXmppConnectionService.updateConversationUi(); + } } @Override diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 1e4b9102..553988ae 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -20,6 +20,10 @@ import java.net.URL; import javax.net.ssl.HttpsURLConnection; +import de.thedevstack.android.logcat.Logging; +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; @@ -162,7 +166,7 @@ public class HttpUploadConnection implements Transferable { PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid()); try { wakeLock.acquire(); - Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); + Logging.d(Config.LOGTAG, "uploading to " + mPutUrl.toString()); if (mUseTor) { connection = (HttpURLConnection) mPutUrl.openConnection(mHttpConnectionManager.getProxy()); } else { @@ -191,11 +195,11 @@ 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); + MessageUtil.updateFileParams(message, mGetUrl); Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); mXmppConnectionService.sendBroadcast(intent); @@ -225,12 +229,11 @@ public class HttpUploadConnection implements Transferable { fail(); } } catch (IOException e) { - e.printStackTrace(); - Log.d(Config.LOGTAG,"http upload failed "+e.getMessage()); + Logging.d(Config.LOGTAG, e.getMessage()); fail(); } finally { - FileBackend.close(mFileInputStream); - FileBackend.close(os); + StreamUtil.close(is); + StreamUtil.close(os); 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..d825543c 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.parser; +import android.util.Log; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -21,6 +22,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 +50,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 1761e0df..4406a28a 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -23,10 +23,13 @@ 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; @@ -70,7 +73,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { contact.parseSubscriptionFromElement(item); } } - mXmppConnectionService.getAvatarService().clear(contact); + AvatarService.getInstance().clear(contact); } } mXmppConnectionService.updateConversationUi(); @@ -281,7 +284,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() : @@ -309,7 +312,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 d8eae969..99354100 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -9,6 +9,9 @@ import net.java.otr4j.session.SessionStatus; import java.util.ArrayList; import java.util.Set; +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.axolotl.AxolotlService; @@ -20,6 +23,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; @@ -182,23 +186,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)) { @@ -207,7 +211,7 @@ 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(); } @@ -273,7 +277,7 @@ public class MessageParser extends AbstractParser implements serverMsgId = result.getAttribute("id"); query.incrementTotalCount(); } 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; @@ -417,7 +421,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); } - if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { + if (ConversationsPlusPreferences.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { ArrayList<String> receiptsNamespaces = new ArrayList<>(); if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { receiptsNamespaces.add("urn:xmpp:chat-markers:0"); @@ -441,11 +445,15 @@ 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) { + if (message.trusted() + && message.treatAsDownloadable() != Message.Decision.NEVER + && ConversationsPlusPreferences.autoAcceptFileSize() > 0 + && ConversationsPlusPreferences.autoDownloadFileLink() + && mXmppConnectionService.isDownloadAllowedInConnection()) { manager.createNewDownloadConnection(message); } else if (!message.isRead()) { if (query == null) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index a07b42a9..bbb21c84 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -4,6 +4,8 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; +import de.thedevstack.conversationsplus.utils.AvatarUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import eu.siacs.conversations.Config; @@ -15,6 +17,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; 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.OnPresencePacketReceived; @@ -39,13 +42,13 @@ public class PresenceParser extends AbstractParser implements processConferencePresence(packet, mucOptions); final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5); if (!tileUserAfter.equals(tileUserBefore)) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": update tiles for "+conversation.getName()); - mXmppConnectionService.getAvatarService().clear(mucOptions); + Loggin.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(); } } } @@ -172,14 +175,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(); @@ -222,7 +225,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 77c16f25..79d69348 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import de.thedevstack.android.logcat.Logging; + import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; @@ -44,6 +46,12 @@ import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; @@ -232,7 +240,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; @@ -257,7 +265,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; @@ -286,7 +294,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; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 4bdf080c..f242b928 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -30,8 +30,21 @@ import java.util.Arrays; import java.util.Date; import java.util.Locale; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Environment; +import android.webkit.MimeTypeMap; + +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.Transferable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; @@ -81,52 +94,20 @@ public class FileBackend { } 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/"; + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator; } - 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; - } - } + private static String getPrivateFileDirectoryPath() { + return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath(); + } - 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; - } + private static String getPrivateImageDirectoryPath() { + return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator; + } public boolean useImageAsIs(Uri uri) { String path = getOriginalPath(uri); @@ -162,7 +143,7 @@ public class FileBackend { 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) { @@ -175,10 +156,11 @@ 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); + return file; } public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { @@ -240,8 +222,8 @@ public class FileBackend { } catch (NullPointerException e) { throw new FileCopyException(R.string.error_io_exception); } finally { - close(os); - close(is); + StreamUtil.close(os); + StreamUtil.close(is); } } @@ -265,41 +247,7 @@ public class FileBackend { 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() { + public static Uri getTakePhotoUri() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); pathBuilder.append('/'); @@ -312,283 +260,16 @@ public class FileBackend { 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/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 8d02f975..d4626fc9 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -32,6 +32,7 @@ import javax.crypto.spec.SecretKeySpec; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; public class AbstractConnectionManager { protected XmppConnectionService mXmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 276be10d..1308f32b 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -8,9 +8,21 @@ 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.R; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; @@ -18,7 +30,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 +49,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; } @@ -91,8 +110,7 @@ public class AvatarService { 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 +176,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 +213,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 +241,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 +274,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)); } } } @@ -286,7 +303,7 @@ 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; } @@ -294,7 +311,7 @@ public class AvatarService { Canvas canvas = new Canvas(bitmap); final String trimmedName = name.trim(); drawTile(canvas, trimmedName, 0, 0, size, size); - mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } @@ -335,8 +352,7 @@ 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; @@ -393,4 +409,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/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 4403f99c..4400105e 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -9,6 +9,8 @@ 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; @@ -127,7 +129,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 @@ -140,7 +142,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } } 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); } } @@ -159,7 +161,6 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { final Conversation conversation = query.getConversation(); if (conversation != null) { conversation.sort(); - conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); } else { for(Conversation tmp : this.mXmppConnectionService.getConversations()) { if (tmp.getAccount() == query.getAccount()) { @@ -170,6 +171,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { if (query.hasCallback()) { query.callback(); } else { + conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); this.mXmppConnectionService.updateConversationUi(); } } @@ -204,7 +206,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; if (complete || relevant == null || abort) { this.finalizeQuery(query); - Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); + Logging.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); if (query.getWith() == null && query.getMessageCount() > 0) { mXmppConnectionService.getNotificationService().finishBacklog(true); } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index cec9a3ef..5a50a8df 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.services; +import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -9,6 +10,7 @@ import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; +import android.os.PowerManager; import android.os.SystemClock; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.BigPictureStyle; @@ -16,6 +18,7 @@ import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.TaskStackBuilder; import android.text.Html; import android.util.DisplayMetrics; +import android.util.Log; import org.json.JSONArray; import org.json.JSONObject; @@ -29,6 +32,10 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -39,6 +46,7 @@ import eu.siacs.conversations.ui.ManageAccountActivity; import eu.siacs.conversations.ui.TimePreference; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.XmppConnection; public class NotificationService { @@ -60,10 +68,13 @@ 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().getMode() == Conversation.MODE_SINGLE + || ConversationsPlusPreferences.alwaysNotifyInConference() + || wasHighlightedOrPrivate(message) + ); } public void notifyPebble(final Message message) { @@ -77,7 +88,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"); @@ -93,11 +104,11 @@ public class NotificationService { } 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 +145,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; @@ -174,12 +185,10 @@ public class NotificationService { } 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 +219,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 +271,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 +308,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 +368,7 @@ public class NotificationService { && message.getEncryption() != Message.ENCRYPTION_PGP && message.getFileParams().height > 0) { return message; - } + } } return null; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 99183dff..09a409fc 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -59,6 +59,18 @@ 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.FileHelper; +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; @@ -69,6 +81,8 @@ import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; @@ -115,12 +129,14 @@ 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; + import me.leolin.shortcutbadger.ShortcutBadger; public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener { public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground"; + private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; @@ -155,7 +171,7 @@ 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); } } } @@ -269,10 +285,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (account.getStatus() == Account.State.ONLINE) { 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(); } } @@ -305,7 +321,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"); @@ -384,32 +400,32 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); - String path = getFileBackend().getOriginalPath(uri); + String path = FileHelper.getRealPathFromUri(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); - } - } - }); + 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); + } + } + }); } } @@ -478,7 +494,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: @@ -534,7 +550,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); @@ -542,7 +558,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()); @@ -555,7 +571,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(); @@ -626,7 +642,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } private void resetAllAttemptCounts(boolean reallyAll) { - Log.d(Config.LOGTAG, "resetting all attepmt counts"); + Logging.d(Config.LOGTAG, "resetting all attepmt counts"); for (Account account : accounts) { if (account.hasErrorStatus() || reallyAll) { final XmppConnection connection = account.getXmppConnection(); @@ -644,6 +660,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() { @@ -687,6 +722,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService"); toggleForegroundService(); updateUnreadCountBadge(); + UiUpdateHelper.initXmppConnectionService(this); toggleScreenEventReceiver(); } @@ -724,7 +760,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); @@ -734,7 +770,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(); } } @@ -757,7 +793,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa .getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(context, EventReceiver.class); alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); - Log.d(Config.LOGTAG, "good bye"); + Logging.d(Config.LOGTAG, "good bye"); stopSelf(); } @@ -774,9 +810,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); @@ -794,14 +828,14 @@ 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(); final XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().httpUpload()) { @@ -955,7 +989,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())); } } @@ -980,10 +1014,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); @@ -1024,7 +1058,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"); } } }; @@ -1032,7 +1066,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"); @@ -1049,12 +1083,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; @@ -1069,7 +1103,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); @@ -1078,11 +1112,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(); } }); @@ -1103,15 +1137,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); @@ -1124,11 +1158,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); } } @@ -1147,7 +1181,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) { @@ -1162,7 +1196,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) { @@ -1210,20 +1244,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa 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; } - 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()) { + Logging.d("mam", "mam activate, account online and connected and messages left on server"); 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 - 1); @@ -1232,10 +1275,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } callback.informUser(R.string.fetching_history_from_server); } - } + } else { + Logging.d("mam", ((!conversation.hasMessagesLeftOnServer()) ? "no" : "") + " more messages left on server, mam " + ((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() { @@ -1464,7 +1512,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.deleteAccount(account); } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation.execute(runnable); this.accounts.remove(account); updateAccountUi(); getNotificationService().updateErrorNotification(); @@ -1696,7 +1744,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() { @@ -1709,7 +1757,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) { @@ -1867,7 +1915,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); @@ -1894,7 +1942,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); @@ -2069,11 +2117,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 { @@ -2094,7 +2142,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()); } @@ -2129,8 +2177,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); } @@ -2140,7 +2187,7 @@ 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, + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + " otr session established with " + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); @@ -2472,7 +2519,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } if (!force) { 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 @@ -2499,7 +2546,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); } @@ -2692,7 +2739,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation.execute(runnable); } updateUnreadCountBadge(); } @@ -2700,7 +2747,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.with(getApplicationContext()).count(count); } else { @@ -2713,8 +2760,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void sendReadMarker(final Conversation conversation) { final Message markable = conversation.getLatestMarkableMessage(); this.markRead(conversation); - 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()); @@ -2737,9 +2784,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()); } @@ -2762,8 +2808,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.writeRoster(account.getRoster()); } }; - mDatabaseExecutor.execute(runnable); - + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public List<String> getKnownHosts() { @@ -2906,14 +2951,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void clearConversationHistory(final Conversation conversation) { conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + /* + * 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); + ConversationsPlusPreferences.dontTrustSystemCAs().execute(runnable); } public void sendBlockRequest(final Blockable blockable) { 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..f55c99f3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java @@ -26,11 +26,14 @@ 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index be454936..e42d3e61 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -39,6 +39,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; diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index f8d1c97f..2a8bb1ce 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,6 +35,9 @@ 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; @@ -221,8 +224,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 @@ -370,8 +372,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 b4857067..1f163b82 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -36,6 +36,10 @@ import android.widget.Toast; import net.java.otr4j.session.SessionStatus; +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 org.openintents.openpgp.util.OpenPgpApi; import java.util.ArrayList; @@ -54,6 +58,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; @@ -273,6 +278,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); @@ -284,9 +290,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() { @@ -340,7 +347,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()); @@ -446,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(); @@ -507,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(); @@ -1072,7 +1079,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(); } @@ -1339,51 +1346,13 @@ public class ConversationActivity extends XmppActivity } } else { - mPendingImageUris.clear(); - mPendingFileUris.clear(); - if (requestCode == ConversationActivity.REQUEST_DECRYPT_PGP) { - mConversationFragment.onActivityResult(requestCode, resultCode, data); - } - if (requestCode == REQUEST_BATTERY_OP) { - setNeverAskForBatteryOptimizationsAgain(); + if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + mPendingImageUri = null; } } } - private void setNeverAskForBatteryOptimizationsAgain() { - getPreferences().edit().putBoolean("show_battery_optimization", false).commit(); - } - - private void openBatteryOptimizationDialogIfNeeded() { - if (showBatteryOptimizationWarning() && getPreferences().getBoolean("show_battery_optimization", true)) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.battery_optimizations_enabled); - builder.setMessage(R.string.battery_optimizations_enabled_dialog); - builder.setPositiveButton(R.string.next, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - startActivityForResult(intent, REQUEST_BATTERY_OP); - } - }); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - builder.setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - setNeverAskForBatteryOptimizationsAgain(); - } - }); - } - builder.create().show(); - } - } - private void attachLocationToConversation(Conversation conversation, Uri uri) { - if (conversation == null) { - return; - } xmppConnectionService.attachLocationToConversation(conversation,uri, new UiCallback<Message>() { @Override diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index c23c20f4..568189a8 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -29,12 +29,16 @@ 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; @@ -43,6 +47,10 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; + +import eu.siacs.conversations.ui.listeners.ConversationSwipeRefreshListener; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -50,22 +58,27 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.entities.TransferablePlaceholder; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; 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.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,10 +117,14 @@ 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; @@ -384,7 +401,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()) { @@ -412,6 +429,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); @@ -475,6 +593,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; } @@ -594,7 +718,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa 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)); @@ -614,7 +738,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)) { @@ -645,7 +769,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } 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 { @@ -711,6 +835,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (size > 0) { messagesView.setSelection(size - 1); } + swipeLayout.setRefreshing(false); } private OnClickListener mUnblockClickListener = new OnClickListener() { @@ -872,6 +997,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case Presences.AWAY: return R.drawable.ic_send_text_away; case Presences.XA: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; case Presences.DND: return R.drawable.ic_send_text_dnd; default: @@ -963,11 +1091,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": @@ -991,7 +1119,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(); @@ -1189,7 +1317,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public boolean onEnterPressed() { - if (activity.enterIsSend()) { + if (ConversationsPlusPreferences.enterIsSend()) { sendMessage(); return true; } else { @@ -1290,4 +1418,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 5c783f54..f7d4564f 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -32,12 +32,14 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; import java.util.Set; 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.AvatarService; import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -618,6 +620,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()); + } this.mStats.setVisibility(View.VISIBLE); this.mBatteryOptimizations.setVisibility(showBatteryOptimizationWarning() ? View.VISIBLE : View.GONE); this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java index fc655b0c..4f273882 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -7,8 +7,9 @@ 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index f2511ecb..a6fb0fea 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -15,6 +15,7 @@ import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index a457bf96..9f8ac45c 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -15,6 +15,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import de.thedevstack.conversationsplus.utils.ImageUtil; import com.soundcloud.android.crop.Crop; import java.io.File; @@ -23,6 +24,7 @@ import java.io.FileNotFoundException; 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.persistance.FileBackend; import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.utils.FileUtils; @@ -113,7 +115,7 @@ public class PublishProfilePictureActivity extends XmppActivity { if (avatarUri != null) { publishButton.setText(R.string.publishing); disablePublishButton(); - xmppConnectionService.publishAvatar(account, avatarUri, + AvatarService.getInstance().publishAvatar(account, avatarUri, avatarPublication); } } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index aa23e36d..05c9eea7 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -1,5 +1,20 @@ package eu.siacs.conversations.ui; +import java.security.KeyStoreException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; + +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.tzur.conversations.Settings; +import de.duenndns.ssl.MemorizingTrustManager; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.XmppConnection; +import github.ankushsachdeva.emojicon.EmojiconHandler; + import android.app.AlertDialog; import android.app.FragmentManager; import android.content.DialogInterface; diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 09bbe0df..d0cfe558 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -19,6 +19,11 @@ import java.util.Iterator; import java.util.List; import eu.siacs.conversations.Config; +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.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -234,34 +239,35 @@ public class ShareWithActivity extends XmppActivity { return; } if (share.uris.size() != 0) { - OnPresenceSelected callback = new OnPresenceSelected() { - @Override - public void onPresenceSelected() { - if (share.image) { - mToast = Toast.makeText(getApplicationContext(), - getText(R.string.preparing_image), - Toast.LENGTH_LONG); - mToast.show(); - for (Iterator<Uri> i = share.uris.iterator(); i.hasNext(); i.remove()) { - ShareWithActivity.this.xmppConnectionService - .attachImageToConversation(conversation, i.next(), - attachFileCallback); - } - } else { - mToast = Toast.makeText(getApplicationContext(), - getText(R.string.preparing_file), - Toast.LENGTH_LONG); - mToast.show(); - ShareWithActivity.this.xmppConnectionService - .attachFileToConversation(conversation, share.uris.get(0), - attachFileCallback); - } + OnPresenceSelected callback; + if (this.share.image) { + 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 { + callback = new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + mToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_file), + Toast.LENGTH_LONG); + mToast.show(); + ShareWithActivity.this.xmppConnectionService + .attachFileToConversation(conversation, share.uris.get(0), + attachFileCallback); if (share.uuid == null) { switchToConversation(conversation, null, true); } - finish(); - } - }; + finish(); + } + }; + } + if (conversation.getAccount().httpUploadAvailable()) { callback.onPresenceSelected(); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 355ee93c..c9638e18 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; @@ -55,6 +54,9 @@ 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; @@ -191,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); @@ -247,7 +250,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } }); - this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false); } @@ -538,12 +540,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); } @@ -657,12 +660,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: @@ -694,7 +697,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()); } } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 003b802c..3d3207e4 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -68,6 +68,10 @@ import java.util.Hashtable; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.android.logcat.Logging; +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; @@ -358,18 +362,18 @@ 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); + mPrimaryTextColor = getResources().getColor(R.color.primaryText); + mSecondaryTextColor = getResources().getColor(R.color.secondaryText); mTertiaryTextColor = getResources().getColor(R.color.black12); - mColorRed = getResources().getColor(R.color.red800); + mColorRed = getResources().getColor(R.color.warning); mColorOrange = getResources().getColor(R.color.orange500); - mColorGreen = getResources().getColor(R.color.green500); + mColorGreen = getResources().getColor(R.color.online); mPrimaryColor = getResources().getColor(R.color.primary); - mPrimaryBackgroundColor = getResources().getColor(R.color.grey50); - mSecondaryBackgroundColor = getResources().getColor(R.color.grey200); + mPrimaryBackgroundColor = getResources().getColor(R.color.primaryBackground); + mSecondaryBackgroundColor = getResources().getColor(R.color.secondaryBackground); 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) { @@ -1007,7 +1011,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; @@ -1036,7 +1040,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<>(); @@ -1052,7 +1056,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) { @@ -1111,24 +1115,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; } @@ -1141,26 +1142,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); - 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..24b58af4 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -1,5 +1,13 @@ package eu.siacs.conversations.ui.adapter; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; + import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -43,7 +51,7 @@ 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: 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..b4069fd1 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -19,13 +19,35 @@ import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; +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.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.utils.UIHelper; +import github.ankushsachdeva.emojicon.EmojiconTextView; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; public class ConversationAdapter extends ArrayAdapter<Conversation> { @@ -44,11 +66,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() ? a.getSecondaryBackgroundColor() : a.getPrimaryBackgroundColor(); + 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 +84,30 @@ 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 && conversation.getAccount().getStatus() == Account.State.ONLINE) { + TextView status = (TextView) view.findViewById(R.id.status); + + String color = "#000000"; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + switch (conversation.getContact().getMostAvailableStatus()) { + case Presences.ONLINE: + case Presences.CHAT: + color = "#259B23"; + break; + case Presences.AWAY: + case Presences.XA: + color = "#FF9800"; + break; + case Presences.DND: + color = "#E51C23"; + break; + } + } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().online()) { + color = "#259B23"; + } + status.setBackgroundColor(Color.parseColor(color)); + } + Message message = conversation.getLatestMessage(); if (!conversation.isRead()) { @@ -74,7 +121,7 @@ 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); @@ -111,7 +158,8 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { mTimestamp.setText(UIHelper.readableTimeDifference(activity,conversation.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image); - loadAvatar(conversation,profilePicture); + profilePicture.setOnLongClickListener(new ShowResourcesListDialogListener(activity, conversation.getContact())); + loadAvatar(conversation, profilePicture); return view; } @@ -126,7 +174,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 +191,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 47414f90..90d5a382 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -1,5 +1,20 @@ package eu.siacs.conversations.ui.adapter; +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; + import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; @@ -16,16 +31,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.jid.Jid; - public class ListItemAdapter extends ArrayAdapter<ListItem> { protected XmppActivity activity; @@ -45,8 +50,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 +61,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 +116,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 +133,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 a9234e1a..ed2a23d2 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -11,6 +11,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.preference.PreferenceManager; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -35,24 +36,33 @@ import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.regex.Matcher; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; + import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Transferable; 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.services.AvatarService; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; +import github.ankushsachdeva.emojicon.EmojiconTextView; + 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 ConversationActivity activity; @@ -152,12 +162,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; @@ -404,7 +414,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) { @@ -416,8 +427,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 @@ -501,8 +513,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (type == STATUS) { if (conversation.getMode() == Conversation.MODE_SINGLE) { - viewHolder.contact_picture.setImageBitmap(activity - .avatarService().get(conversation.getContact(), + viewHolder.contact_picture.setImageBitmap(AvatarService.getInstance().get(conversation.getContact(), activity.getPixel(32))); viewHolder.contact_picture.setAlpha(0.5f); viewHolder.status_message.setText(message.getBody()); 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..a5c5e2aa --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationMoreMessagesLoadedListener.java @@ -0,0 +1,152 @@ +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; + } else { + Message next = messages.get(i); + while(next != null && next.wasMergedIntoPrevious()) { + if (uuid.equals(next.getUuid())) { + return i; + } + next = next.next(); + } + + } + } + 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..e698c9b2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/listeners/ConversationSwipeRefreshListener.java @@ -0,0 +1,93 @@ +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.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; + + public ConversationSwipeRefreshListener(List<Message> messageList, SwipyRefreshLayout swipeLayout, ConversationFragment fragment, ListView messagesView, MessageAdapter messageListAdapter) { + this.messageList = messageList; + this.fragment = fragment; + 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); + 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) + } + ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + 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 + Logging.d("mam", "loading missing messages from mam (last session establishing or last received message)"); + final ConversationActivity activity = (ConversationActivity) fragment.getActivity(); + 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()); + if (query != null) { + this.listener.setLoadHistory(false); + query.setCallback(this.listener); + } 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); + } + } + 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/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 1ef5fb3f..4850f19d 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -12,6 +12,7 @@ import java.security.MessageDigest; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; +import java.security.SecureRandom; import java.text.Normalizer; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 306d50c2..58d53216 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -28,11 +28,22 @@ 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.CLASS; +import de.measite.minidns.record.SRV; import de.measite.minidns.record.A; import de.measite.minidns.record.AAAA; import de.measite.minidns.record.Data; import de.measite.minidns.record.SRV; import de.measite.minidns.util.NameUtil; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.dto.SrvRecord; + import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jid.Jid; diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index 8799b4a5..892d5a00 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -18,6 +18,9 @@ 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; @@ -28,6 +31,17 @@ import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.text.format.DateUtils; + public class ExceptionHelper { public static void init(Context context) { if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { @@ -38,9 +52,7 @@ public class ExceptionHelper { public static boolean checkForCrash(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,7 +101,7 @@ 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; @@ -108,8 +120,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..eac2d0f0 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java @@ -16,7 +16,7 @@ package eu.siacs.conversations.utils; -import android.util.Log; +import de.thedevstack.android.logcat.Logging; import java.io.IOException; import java.io.InputStream; @@ -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/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/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index 6c1b4bef..774532d1 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -1,5 +1,9 @@ package eu.siacs.conversations.utils; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + import android.Manifest; import android.content.Context; import android.content.CursorLoader; @@ -8,13 +12,11 @@ import android.content.Loader.OnLoadCompleteListener; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.provider.ContactsContract.Profile; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; public class PhoneHelper { diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index cf1e0d3b..e1b1a2a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -5,19 +5,39 @@ import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Pair; +import java.math.BigDecimal; +import java.text.NumberFormat; import java.util.ArrayList; import java.util.Arrays; +import java.net.URLConnection; import java.util.Calendar; import java.util.Date; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.xmpp.jid.Jid; +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.text.Spannable.Factory; +import android.text.style.ImageSpan; +import android.text.Spannable; +import android.util.Pair; + public class UIHelper { private static String BLACK_HEART_SUIT = "\u2665"; @@ -190,7 +210,7 @@ public class UIHelper { return new Pair<>(context.getString(R.string.location), true); } } else{ - return new Pair<>(message.getBody().trim(), false); + return new Pair<>(message.getBody(), false); } } } @@ -241,13 +261,27 @@ public class UIHelper { } } + public static String getStatusColor(int status) { + switch (status) { + case Presences.ONLINE: + case Presences.CHAT: + return "#259B23"; + case Presences.AWAY: + case Presences.XA: + return "#FF9800"; + case Presences.DND: + return "#E51C23"; + } + return "#CCCCCC"; + } + private static String getDisplayedMucCounterpart(final Jid counterpart) { if (counterpart==null) { return ""; } else if (!counterpart.isBareJid()) { - return counterpart.getResourcepart().trim(); + return counterpart.getResourcepart(); } else { - return counterpart.toString().trim(); + return counterpart.toString(); } } @@ -257,8 +291,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..90eb112d 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,11 +1,11 @@ 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 +128,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..a58dc43f 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -12,6 +12,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import de.thedevstack.android.logcat.Logging; + import eu.siacs.conversations.Config; public class XmlReader { @@ -25,7 +30,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 37b2da68..ab647a15 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; @@ -13,6 +14,7 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; @@ -20,6 +22,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.math.BigInteger; import java.net.ConnectException; import java.net.IDN; @@ -34,11 +37,16 @@ import java.security.Principal; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.TreeSet; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; @@ -48,6 +56,9 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.dto.SrvRecord; + import de.duenndns.ssl.MemorizingTrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -88,7 +99,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; @@ -220,7 +231,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; lastConnect = SystemClock.elapsedRealtime(); lastPingSent = SystemClock.elapsedRealtime(); @@ -265,7 +276,7 @@ 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(); } @@ -347,7 +358,7 @@ public class XmppConnection implements Runnable { } 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 { @@ -659,7 +670,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) { @@ -766,16 +777,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()); @@ -786,7 +797,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); @@ -858,7 +869,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()); } } @@ -927,11 +938,11 @@ public class XmppConnection implements Runnable { sendPostBindInitialization(); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure"); + Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure"); disconnect(true); } } else { - Log.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure"); + Logging.d(Config.LOGTAG, account.getJid() + ": disconnecting because of bind failure"); disconnect(true); } } @@ -991,7 +1002,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); } } @@ -1080,13 +1091,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()); changeStatus(Account.State.ONLINE); if (bindListener != null) { bindListener.onBind(account); @@ -1146,11 +1157,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()); } } @@ -1163,11 +1174,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()); } } @@ -1281,12 +1292,12 @@ public class XmppConnection implements Runnable { } 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) { try { socket.close(); } catch(Exception e) { - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")"); + Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")"); } return; } else { @@ -1379,7 +1390,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 56582f97..11bfdd8d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -16,13 +16,23 @@ import java.util.Locale; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; + +import de.thedevstack.android.logcat.Logging; +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.crypto.axolotl.OnMessageCreatedCallback; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; @@ -97,7 +107,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) { @@ -109,7 +119,7 @@ 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) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); @@ -139,17 +149,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"); } }; @@ -195,13 +205,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; } @@ -263,14 +273,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(); @@ -278,7 +288,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(); } } @@ -363,20 +374,19 @@ public class JingleConnection implements Transferable { mXmppConnectionService.updateConversationUi(); if (mJingleConnectionManager.hasStoragePermission() && size < this.mJingleConnectionManager.getAutoAcceptFileSize()) { - Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom()); + 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) { @@ -385,7 +395,7 @@ public class JingleConnection implements Transferable { this.file.setIv(transportMessage.getIv()); message.setAxolotlFingerprint(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(); @@ -403,7 +413,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(); @@ -419,13 +429,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()); @@ -457,7 +467,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 { @@ -499,7 +509,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); @@ -508,7 +518,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); @@ -517,7 +527,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); @@ -563,13 +573,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(); } @@ -579,7 +589,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)) { @@ -590,7 +600,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; @@ -598,7 +608,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; @@ -617,7 +627,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(); @@ -626,7 +636,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); @@ -650,17 +660,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); } } @@ -672,11 +682,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 { @@ -685,7 +695,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; @@ -717,7 +727,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(); @@ -729,7 +739,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) { @@ -765,7 +775,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 @@ -817,8 +827,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)); @@ -862,7 +872,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(); @@ -870,7 +880,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..3be1276d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -8,9 +8,13 @@ import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import android.annotation.SuppressLint; + +import de.thedevstack.android.logcat.Logging; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -155,9 +159,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..d1cd20fa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -10,6 +10,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import android.util.Base64; + +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; @@ -95,13 +100,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 +123,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 +132,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 +191,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 +215,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..c4c96ea2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -17,6 +17,9 @@ 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; @@ -98,7 +101,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 +121,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 +156,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 +167,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 +181,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 +207,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() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 941b4b5f..e7539c62 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -2,6 +2,8 @@ package eu.siacs.conversations.xmpp.stanzas; import android.util.Pair; +import java.text.ParseException; + import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.xml.Element; diff --git a/src/main/project.properties b/src/main/project.properties new file mode 100644 index 00000000..4ab12569 --- /dev/null +++ b/src/main/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 diff --git a/src/main/res/drawable-hdpi/ic_action_keyboard.png b/src/main/res/drawable-hdpi/ic_action_keyboard.png Binary files differnew file mode 100644 index 00000000..37c04af2 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_keyboard.png diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png Binary files differindex 690400eb..1bea2f74 100644 --- a/src/main/res/drawable-hdpi/ic_launcher.png +++ b/src/main/res/drawable-hdpi/ic_launcher.png diff --git a/src/main/res/drawable-hdpi/ic_notification.png b/src/main/res/drawable-hdpi/ic_notification.png Binary files differindex 31c0ee1a..fd5d1d8b 100644 --- a/src/main/res/drawable-hdpi/ic_notification.png +++ b/src/main/res/drawable-hdpi/ic_notification.png diff --git a/src/main/res/drawable-hdpi/smiley.png b/src/main/res/drawable-hdpi/smiley.png Binary files differnew file mode 100644 index 00000000..c841c4c0 --- /dev/null +++ b/src/main/res/drawable-hdpi/smiley.png diff --git a/src/main/res/drawable-mdpi/ic_action_keyboard.png b/src/main/res/drawable-mdpi/ic_action_keyboard.png Binary files differnew file mode 100644 index 00000000..481e4222 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_keyboard.png diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png Binary files differindex 9e076434..f2df046e 100644 --- a/src/main/res/drawable-mdpi/ic_launcher.png +++ b/src/main/res/drawable-mdpi/ic_launcher.png diff --git a/src/main/res/drawable-mdpi/ic_notification.png b/src/main/res/drawable-mdpi/ic_notification.png Binary files differindex aafc54f5..5cf7593b 100644 --- a/src/main/res/drawable-mdpi/ic_notification.png +++ b/src/main/res/drawable-mdpi/ic_notification.png diff --git a/src/main/res/drawable-mdpi/smiley.png b/src/main/res/drawable-mdpi/smiley.png Binary files differnew file mode 100644 index 00000000..9f6d6511 --- /dev/null +++ b/src/main/res/drawable-mdpi/smiley.png diff --git a/src/main/res/drawable-xhdpi/ic_action_keyboard.png b/src/main/res/drawable-xhdpi/ic_action_keyboard.png Binary files differnew file mode 100644 index 00000000..c1af1a2f --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_keyboard.png diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png Binary files differindex c49b2cb1..2c0f1647 100644 --- a/src/main/res/drawable-xhdpi/ic_launcher.png +++ b/src/main/res/drawable-xhdpi/ic_launcher.png diff --git a/src/main/res/drawable-xhdpi/ic_notification.png b/src/main/res/drawable-xhdpi/ic_notification.png Binary files differindex 042d2cda..76a9414a 100644 --- a/src/main/res/drawable-xhdpi/ic_notification.png +++ b/src/main/res/drawable-xhdpi/ic_notification.png diff --git a/src/main/res/drawable-xhdpi/smiley.png b/src/main/res/drawable-xhdpi/smiley.png Binary files differnew file mode 100644 index 00000000..b06e0073 --- /dev/null +++ b/src/main/res/drawable-xhdpi/smiley.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_keyboard.png b/src/main/res/drawable-xxhdpi/ic_action_keyboard.png Binary files differnew file mode 100644 index 00000000..a4668c41 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_keyboard.png diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png Binary files differindex 873d4b14..f8edda90 100644 --- a/src/main/res/drawable-xxhdpi/ic_launcher.png +++ b/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/src/main/res/drawable-xxhdpi/ic_notification.png b/src/main/res/drawable-xxhdpi/ic_notification.png Binary files differindex 42c62d32..68f051b3 100644 --- a/src/main/res/drawable-xxhdpi/ic_notification.png +++ b/src/main/res/drawable-xxhdpi/ic_notification.png diff --git a/src/main/res/drawable-xxhdpi/smiley.png b/src/main/res/drawable-xxhdpi/smiley.png Binary files differnew file mode 100644 index 00000000..2f11d408 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/smiley.png diff --git a/src/main/res/drawable-xxxhdpi/ic_launcher.png b/src/main/res/drawable-xxxhdpi/ic_launcher.png Binary files differindex baadfa78..d9aa6c0e 100644 --- a/src/main/res/drawable-xxxhdpi/ic_launcher.png +++ b/src/main/res/drawable-xxxhdpi/ic_launcher.png diff --git a/src/main/res/drawable-xxxhdpi/ic_notification.png b/src/main/res/drawable-xxxhdpi/ic_notification.png Binary files differindex c3439f1a..ba656860 100644 --- a/src/main/res/drawable-xxxhdpi/ic_notification.png +++ b/src/main/res/drawable-xxxhdpi/ic_notification.png diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index cac9a9fa..c6569d5a 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -33,7 +33,7 @@ android:layout_height="wrap_content" android:scrollHorizontally="false" android:singleLine="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" /> <TextView diff --git a/src/main/res/layout/activity_about.xml b/src/main/res/layout/activity_about.xml index 247e96e5..8f1113ad 100644 --- a/src/main/res/layout/activity_about.xml +++ b/src/main/res/layout/activity_about.xml @@ -15,7 +15,7 @@ android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:typeface="monospace" android:fontFamily="monospace"/> diff --git a/src/main/res/layout/activity_change_password.xml b/src/main/res/layout/activity_change_password.xml index 1a4d00d8..df75e61f 100644 --- a/src/main/res/layout/activity_change_password.xml +++ b/src/main/res/layout/activity_change_password.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/current_password" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText @@ -32,15 +32,15 @@ android:layout_marginBottom="8dp" android:hint="@string/password" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/new_password" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText @@ -50,15 +50,15 @@ android:layout_marginBottom="8dp" android:hint="@string/password" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_confirm_password" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText @@ -67,8 +67,8 @@ android:layout_height="wrap_content" android:hint="@string/password" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout> </ScrollView> diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index c35f26bb..598003e3 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -5,7 +5,6 @@ android:background="@color/grey200" > <LinearLayout - android:id="@+id/details_main_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > @@ -40,7 +39,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_example_jabber_id" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold" /> @@ -62,7 +61,7 @@ android:id="@+id/details_lastseen" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeBody" /> </LinearLayout> @@ -79,7 +78,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/send_presence_updates" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> <CheckBox @@ -87,7 +86,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/receive_presence_updates" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> </LinearLayout> @@ -99,7 +98,7 @@ android:layout_below="@+id/details_jidbox" android:layout_marginTop="32dp" android:text="@string/using_account" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo" /> </RelativeLayout> diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index ed935819..9bacf9a2 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -41,13 +41,14 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_toRightOf="@+id/avater" - android:orientation="vertical"> + android:orientation="vertical" + android:id="@+id/editAccountBoxes"> <TextView android:id="@+id/account_jid_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_jabber_id" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <AutoCompleteTextView @@ -56,8 +57,8 @@ android:layout_height="wrap_content" android:hint="@string/account_settings_example_jabber_id" android:inputType="textEmailAddress" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -65,7 +66,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/account_settings_password" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText @@ -74,8 +75,8 @@ android:layout_height="wrap_content" android:hint="@string/password" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <LinearLayout @@ -95,7 +96,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_hostname" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText android:id="@+id/hostname" @@ -103,8 +104,8 @@ android:layout_height="wrap_content" android:hint="@string/hostname_or_onion" android:inputType="textNoSuggestions" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout> <LinearLayout @@ -117,7 +118,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_port" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <EditText android:id="@+id/port" @@ -125,8 +126,8 @@ android:layout_height="match_parent" android:inputType="number" android:maxLength="5" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout> </LinearLayout> @@ -136,7 +137,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/register_account" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -144,7 +145,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/account_settings_confirm_password" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:visibility="gone"/> @@ -155,11 +156,27 @@ android:layout_marginTop="8dp" android:hint="@string/confirm_password" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody" android:visibility="gone"/> </LinearLayout> + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_toRightOf="@+id/avater" + android:id="@+id/displayAccountFrame" + android:visibility="gone"> + <TextView + android:id="@+id/detailsAccountJid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_settings_example_jabber_id" + android:textColor="@color/primaryText" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" /> + </LinearLayout> </RelativeLayout> <RelativeLayout @@ -179,7 +196,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/battery_optimizations_enabled" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold"/> <TextView @@ -190,7 +207,7 @@ android:layout_marginBottom="8dp" android:layout_marginTop="8dp" android:text="@string/battery_optimizations_enabled_explained" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <Button android:id="@+id/batt_op_disable" @@ -232,7 +249,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_session_established" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -240,7 +257,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -261,7 +278,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_pep" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -269,7 +286,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -282,7 +299,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_blocking" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -290,7 +307,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -303,7 +320,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_stream_management" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -311,7 +328,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -324,7 +341,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_roster_version" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -332,7 +349,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -345,7 +362,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_carbon_messages" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -353,7 +370,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -366,7 +383,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_mam" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -374,7 +391,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -387,7 +404,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_csi" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -395,7 +412,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -407,7 +424,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_http_upload" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <TextView @@ -415,7 +432,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" tools:ignore="RtlHardcoded"/> </TableRow> @@ -439,7 +456,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="monospace" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:typeface="monospace"/> @@ -447,7 +464,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/otr_fingerprint" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> </LinearLayout> @@ -481,7 +498,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="monospace" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:typeface="monospace"/> @@ -489,7 +506,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/this_device_omemo_fingerprint" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> </LinearLayout> @@ -541,7 +558,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/other_devices" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold"/> @@ -574,7 +591,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/cancel" - android:textColor="@color/black87"/> + android:textColor="@color/primaryText"/> <View android:layout_width="1dp" @@ -591,7 +608,7 @@ android:layout_weight="1" android:enabled="false" android:text="@string/save" - android:textColor="@color/black54"/> + android:textColor="@color/secondaryText"/> </LinearLayout> </RelativeLayout> diff --git a/src/main/res/layout/activity_logcatoutput.xml b/src/main/res/layout/activity_logcatoutput.xml new file mode 100644 index 00000000..302991b1 --- /dev/null +++ b/src/main/res/layout/activity_logcatoutput.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <Button + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/cplus_copy_to_clipboard" + android:id="@+id/actLogOutputCopyButton" /> + + <ListView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/actLogInfoOutput" + android:scrollbars="vertical" + android:textSize="12sp" + android:textIsSelectable="true"/> +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index e7b11d9f..4c6b0301 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -28,7 +28,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:text="@string/account_settings_example_jabber_id" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold"/> @@ -58,7 +58,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline"/> <TextView @@ -66,7 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout> @@ -90,7 +90,7 @@ android:layout_height="wrap_content" android:text="@string/private_conference" android:layout_centerVertical="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/change_conference_button" @@ -104,7 +104,7 @@ android:layout_alignParentRight="true" android:layout_centerVertical="true" android:background="?android:selectableItemBackground" - android:padding="@dimen/image_button_padding" + android:padding="8dp" android:src="?attr/icon_settings"/> </RelativeLayout> @@ -117,7 +117,7 @@ android:layout_height="wrap_content" android:text="@string/notify_on_all_messages" android:layout_centerVertical="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/notification_status_button" @@ -150,7 +150,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_mam" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> <TextView @@ -158,7 +158,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> </TableRow> @@ -171,7 +171,7 @@ android:layout_gravity="right" android:layout_marginTop="32dp" android:text="@string/using_account" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> </LinearLayout> diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index 8c7c359b..1520b330 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -27,7 +27,7 @@ android:layout_below="@id/account_image_wrapper" android:layout_centerHorizontal="true" android:text="@string/touch_to_choose_picture" - android:textColor="@color/black54" /> + android:textColor="@color/secondaryText" /> <TextView android:id="@+id/secondary_hint" @@ -36,7 +36,7 @@ android:layout_below="@id/hint" android:layout_centerHorizontal="true" android:text="@string/or_long_press_for_default" - android:textColor="@color/black54" /> + android:textColor="@color/secondaryText" /> <LinearLayout android:id="@+id/button_bar" @@ -53,7 +53,7 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/cancel" - android:textColor="@color/black87" /> + android:textColor="@color/primaryText" /> <View android:layout_width="1dp" @@ -70,7 +70,7 @@ android:layout_weight="1" android:enabled="false" android:text="@string/publish" - android:textColor="@color/black54" /> + android:textColor="@color/secondaryText" /> </LinearLayout> <LinearLayout @@ -89,7 +89,7 @@ android:id="@+id/account" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" /> <TextView @@ -99,7 +99,7 @@ android:layout_marginTop="8dp" android:minLines="3" android:text="@string/publish_avatar_explanation" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> </LinearLayout> diff --git a/src/main/res/layout/activity_verify_otr.xml b/src/main/res/layout/activity_verify_otr.xml index c15f19d5..2d5821a4 100644 --- a/src/main/res/layout/activity_verify_otr.xml +++ b/src/main/res/layout/activity_verify_otr.xml @@ -34,7 +34,7 @@ android:id="@+id/your_fingerprint" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:typeface="monospace" android:fontFamily="monospace"/> @@ -43,7 +43,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/your_fingerprint" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> <TextView @@ -51,7 +51,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:typeface="monospace" android:fontFamily="monospace"/> @@ -61,7 +61,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="20dp" android:text="@string/remote_fingerprint" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> </LinearLayout> @@ -79,7 +79,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="@string/verified" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold" android:visibility="gone"/> @@ -89,7 +89,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" android:textStyle="bold" android:visibility="gone"/> @@ -101,8 +101,8 @@ android:layout_marginBottom="8dp" android:hint="@string/shared_secret_hint" android:inputType="textAutoComplete" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <EditText @@ -112,8 +112,8 @@ android:layout_marginTop="8dp" android:hint="@string/shared_secret_secret" android:inputType="textPassword" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout> </LinearLayout> diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml index 56443c34..faf017d0 100644 --- a/src/main/res/layout/contact.xml +++ b/src/main/res/layout/contact.xml @@ -14,6 +14,14 @@ android:scaleType="centerCrop" android:src="@drawable/ic_profile" app:riv_corner_radius="2dp" /> + <TextView + android:layout_width="48dp" + android:layout_height="4dp" + android:textAppearance="?android:attr/textAppearanceSmall" + android:id="@+id/contact_status" + android:layout_below="@+id/contact_photo" + android:paddingTop="8dp" + android:paddingRight="8dp"/> <LinearLayout android:layout_width="wrap_content" @@ -28,7 +36,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" /> <TextView @@ -36,7 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> <LinearLayout android:id="@+id/tags" @@ -49,7 +57,7 @@ android:id="@+id/key" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:typeface="monospace" android:fontFamily="monospace" diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index a4fd29e9..6d61026d 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -16,7 +16,7 @@ android:id="@+id/key" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/tgl_trust" android:textSize="?attr/TextSizeBody" @@ -27,7 +27,7 @@ android:id="@+id/key_type" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:layout_alignParentLeft="true" android:layout_below="@+id/key" android:maxLines="1" @@ -40,7 +40,7 @@ android:layout_alignParentRight="true" android:layout_below="@+id/key" android:visibility="gone" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> <ImageButton diff --git a/src/main/res/layout/conversation_list_row.xml b/src/main/res/layout/conversation_list_row.xml index cd3f9266..cde94087 100644 --- a/src/main/res/layout/conversation_list_row.xml +++ b/src/main/res/layout/conversation_list_row.xml @@ -1,13 +1,18 @@ -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:descendantFocusability="blocksDescendants"> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:emojicon="http://schemas.android.com/apk/res-auto" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="8dp" + android:descendantFocusability="blocksDescendants" + android:id="@+id/conversationListRowFrame" + android:background="@color/primaryBackground"> <View android:layout_width="fill_parent" android:layout_height="match_parent" - android:background="@color/primary"/> + android:background="@color/red500"/> <FrameLayout android:id="@+id/swipeable_item" @@ -20,7 +25,8 @@ android:layout_height="wrap_content" android:background="?android:selectableItemBackground" android:orientation="horizontal" - android:padding="8dp"> + android:padding="8dp" + android:id="@+id/conversationListRowContent"> <com.makeramen.roundedimageview.RoundedImageView android:id="@+id/conversation_image" @@ -30,6 +36,15 @@ android:scaleType="centerCrop" app:riv_corner_radius="2dp"/> + <TextView + android:layout_width="56dp" + android:layout_height="4dp" + android:textAppearance="?android:attr/textAppearanceSmall" + android:id="@+id/status" + android:layout_below="@+id/conversation_image" + android:paddingTop="8dp" + android:paddingRight="8dp"/> + <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" @@ -46,7 +61,7 @@ android:paddingRight="4dp" android:singleLine="true" android:text="Awesome groupchat" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeHeadline" android:typeface="sans"/> @@ -63,21 +78,23 @@ android:layout_centerVertical="true" android:layout_toLeftOf="@+id/notification_status" android:orientation="vertical"> - <TextView - android:id="@+id/conversation_lastmsg" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:scrollHorizontally="false" - android:singleLine="true" + + <github.ankushsachdeva.emojicon.EmojiconTextView + android:id="@+id/conversation_lastmsg" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollHorizontally="false" + android:singleLine="true" android:text="This is a placeholder text to show the last messages" - android:textColor="@color/black87" - android:textSize="?attr/TextSizeBody"/> + android:textColor="@color/primaryText" + android:textSize="?attr/TextSizeBody" + emojicon:emojiconSize="20sp" /> <com.makeramen.roundedimageview.RoundedImageView android:id="@+id/conversation_lastimage" android:layout_width="fill_parent" android:layout_height="36dp" - android:background="@color/black87" + android:background="@color/primaryText" android:scaleType="centerCrop" android:visibility="gone" app:riv_corner_radius="2dp"/> @@ -101,7 +118,7 @@ android:layout_alignParentRight="true" android:gravity="right" android:text="23:42" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo"/> </RelativeLayout> </RelativeLayout> diff --git a/src/main/res/layout/dialog_message_details.xml b/src/main/res/layout/dialog_message_details.xml new file mode 100644 index 00000000..84159f44 --- /dev/null +++ b/src/main/res/layout/dialog_message_details.xml @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="8dp"> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_time_sent" + android:id="@+id/dlgMsgDetLblTimeSent" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/dlgMsgDetTimeSent" + android:width="200dp" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:textAlignment="viewEnd" + android:gravity="end" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_msg_type" + android:id="@+id/dlgMsgDetLblMsgType" + android:layout_below="@+id/dlgMsgDetLblTimeSent" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/dlgMsgDetMsgType" + android:layout_below="@+id/dlgMsgDetTimeSent" + android:layout_alignRight="@+id/dlgMsgDetTimeSent" + android:layout_alignEnd="@+id/dlgMsgDetTimeSent" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_msg_status" + android:id="@+id/dlgMsgDetLblMsgStatus" + android:layout_below="@+id/dlgMsgDetLblMsgType" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/dlgMsgDetMsgStatus" + android:layout_below="@+id/dlgMsgDetMsgType" + android:layout_alignRight="@+id/dlgMsgDetMsgType" + android:layout_alignEnd="@+id/dlgMsgDetMsgType" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_sender_resource" + android:id="@+id/dlgMsgDetLblSender" + android:layout_below="@+id/dlgMsgDetLblMsgStatus" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/dlgMsgDetSender" + android:layout_below="@+id/dlgMsgDetMsgStatus" + android:layout_alignRight="@+id/dlgMsgDetMsgStatus" + android:layout_alignEnd="@+id/dlgMsgDetMsgStatus" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_receipient_resource" + android:id="@+id/dlgMsgDetLblReceipient" + android:layout_below="@+id/dlgMsgDetLblSender" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/dlgMsgDetReceipient" + android:layout_below="@+id/dlgMsgDetSender" + android:layout_alignRight="@+id/dlgMsgDetSender" + android:layout_alignEnd="@+id/dlgMsgDetSender" /> + + <TableLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/dlgMsgDetLblReceipient" + android:layout_marginTop="11dp" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:stretchColumns="1" + android:background="@color/primaryText" + android:id="@+id/dlgMsgDetFileTable" + android:visibility="gone"> + + <TableRow + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/white" + android:layout_marginTop="0.3dp" + android:layout_marginLeft="0.3dp" + android:layout_marginRight="0.3dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_file_details" + android:layout_column="0"/> + </TableRow> + + <TableRow + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/white" + android:layout_marginTop="0.3dp" + android:layout_marginLeft="0.3dp" + android:layout_marginRight="0.3dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_file_mime" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="" + android:textAlignment="textEnd" + android:gravity="end" + android:id="@+id/dlgMsgDetFileMimeType" /> + </TableRow> + + <TableRow + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/white" + android:layout_marginLeft="0.3dp" + android:layout_marginTop="0.3dp" + android:layout_marginRight="0.3dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/dlg_msg_details_file_size" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="" + android:textAlignment="textEnd" + android:gravity="end" + android:id="@+id/dlgMsgDetFileSize"/> + </TableRow> + </TableLayout> + </RelativeLayout> + + + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/dialog_resources_status.xml b/src/main/res/layout/dialog_resources_status.xml new file mode 100644 index 00000000..4dd511f5 --- /dev/null +++ b/src/main/res/layout/dialog_resources_status.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="Medium Text" + android:id="@+id/dlg_res_stat_resource_name" + android:layout_marginLeft="10dp" /> +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/dialog_userdecision.xml b/src/main/res/layout/dialog_userdecision.xml new file mode 100644 index 00000000..edeff812 --- /dev/null +++ b/src/main/res/layout/dialog_userdecision.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:layout_width="match_parent" + android:layout_height="32dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="Medium Text" + android:id="@+id/dlgUserDecQuestion" /> + + <CheckBox + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/cplus_remember_userdecision" + android:id="@+id/dlgUserDecRemember" + android:checked="false" /> +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/enter_jid_dialog.xml b/src/main/res/layout/enter_jid_dialog.xml index d4af0dfc..f75f2674 100644 --- a/src/main/res/layout/enter_jid_dialog.xml +++ b/src/main/res/layout/enter_jid_dialog.xml @@ -13,7 +13,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/your_account" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <Spinner @@ -27,7 +27,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/account_settings_jabber_id" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> <AutoCompleteTextView @@ -36,8 +36,8 @@ android:layout_height="wrap_content" android:hint="@string/account_settings_example_jabber_id" android:inputType="textEmailAddress" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody" /> </LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index db92c05f..d4c380d3 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -1,10 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/grey200" > + <com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/snackbar" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:id="@+id/swipe_refresh_container" + app:direction="both"> <ListView android:id="@+id/messages_view" android:layout_width="fill_parent" @@ -20,6 +29,7 @@ android:transcriptMode="normal" tools:listitem="@layout/message_sent"> </ListView> + </com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout> <RelativeLayout android:id="@+id/textsend" @@ -35,6 +45,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/textSendButton" + android:layout_toRightOf="@+id/emoji_btn" android:background="@color/grey50" android:ems="10" android:imeOptions="flagNoExtractUi|actionSend" @@ -45,7 +56,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingTop="12dp" - android:textColor="@color/black87" > + android:textColor="@color/primaryText" > <requestFocus /> </eu.siacs.conversations.ui.EditMessage> @@ -58,6 +69,15 @@ android:layout_centerVertical="true" android:background="?android:selectableItemBackground" android:src="@drawable/ic_send_text_offline" /> + + <ImageView + android:layout_width="48dp" + android:layout_height="48dp" + android:id="@+id/emoji_btn" + android:layout_centerVertical="true" + android:layout_alignParentLeft="true" + android:padding="4dp" + android:src="@drawable/smiley" /> </RelativeLayout> <RelativeLayout diff --git a/src/main/res/layout/join_conference_dialog.xml b/src/main/res/layout/join_conference_dialog.xml index f7aa3c46..d172142a 100644 --- a/src/main/res/layout/join_conference_dialog.xml +++ b/src/main/res/layout/join_conference_dialog.xml @@ -13,7 +13,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/your_account" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> <Spinner @@ -27,7 +27,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/conference_address" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody" /> <AutoCompleteTextView @@ -36,8 +36,8 @@ android:layout_height="wrap_content" android:hint="@string/conference_address_example" android:inputType="textEmailAddress" - android:textColor="@color/black87" - android:textColorHint="@color/black54" + android:textColor="@color/primaryText" + android:textColorHint="@color/secondaryText" android:textSize="?attr/TextSizeBody"/> <CheckBox @@ -47,7 +47,7 @@ android:layout_marginTop="8dp" android:checked="true" android:text="@string/save_as_bookmark" - android:textColor="@color/black87" + android:textColor="@color/primaryText" android:textSize="?attr/TextSizeBody"/> </LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/list_item_logcatoutput.xml b/src/main/res/layout/list_item_logcatoutput.xml new file mode 100644 index 00000000..9a31be17 --- /dev/null +++ b/src/main/res/layout/list_item_logcatoutput.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/listItemLogcatoutputText" + android:scrollbars="vertical" + android:textSize="12sp"/>
\ No newline at end of file diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml index a998bf37..389b0f48 100644 --- a/src/main/res/layout/message_received.xml +++ b/src/main/res/layout/message_received.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:emojicon="http://schemas.android.com/apk/res-auto" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" @@ -34,6 +34,7 @@ <LinearLayout android:layout_width="wrap_content" android:layout_height="fill_parent" + android:background="@color/primarybackground" android:gravity="center_vertical" android:orientation="vertical" android:padding="2dp"> @@ -48,15 +49,16 @@ android:background="@color/black87" android:scaleType="centerCrop" /> - <TextView + <github.ankushsachdeva.emojicon.EmojiconTextView android:id="@+id/message_body" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:autoLink="web" + android:autoLink="web|phone|email" android:textColorLink="@color/white" android:textColor="@color/white" android:textColorHighlight="@color/grey800" - android:textSize="?attr/TextSizeBody" /> + android:textSize="?attr/TextSizeBody" + emojicon:emojiconSize="28sp" /> <Button android:id="@+id/download_button" diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index 55f874e6..d75f4bdb 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/res/layout/message_sent.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:emojicon="http://schemas.android.com/apk/res-auto" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" @@ -46,16 +46,16 @@ android:layout_marginTop="8dp" android:layout_marginBottom="4dp" android:adjustViewBounds="true" - android:background="@color/black87" + android:background="@color/primaryText" android:scaleType="centerCrop" /> <TextView android:id="@+id/message_body" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:autoLink="web" - android:textColorLink="@color/black87" - android:textColor="@color/black87" + android:autoLink="web|phone|email" + android:textColorLink="@color/primaryText" + android:textColor="@color/primaryText" android:textColorHighlight="@color/grey500" android:textSize="?attr/TextSizeBody" /> @@ -80,7 +80,7 @@ android:layout_gravity="center_vertical" android:gravity="center_vertical" android:text="@string/sending" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo" /> <ImageView diff --git a/src/main/res/layout/message_status.xml b/src/main/res/layout/message_status.xml index ad2579fa..aa02e154 100644 --- a/src/main/res/layout/message_status.xml +++ b/src/main/res/layout/message_status.xml @@ -30,7 +30,7 @@ android:layout_toEndOf="@+id/message_photo" android:layout_toRightOf="@+id/message_photo" android:text="@string/contact_has_read_up_to_this_point" - android:textColor="@color/black54" + android:textColor="@color/secondaryText" android:textSize="?attr/TextSizeInfo" android:textStyle="italic"/> diff --git a/src/main/res/layout/quickedit.xml b/src/main/res/layout/quickedit.xml index ff6b0413..74e8dcf8 100644 --- a/src/main/res/layout/quickedit.xml +++ b/src/main/res/layout/quickedit.xml @@ -11,7 +11,7 @@ android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" - android:textColor="@color/black87" > + android:textColor="@color/primaryText" > <requestFocus /> </EditText> diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index 4e1316d3..2fe1ca22 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -1,6 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" > - + <item + android:id="@+id/msg_ctx_mnu_details" + android:title="@string/msg_ctx_mnu_details" + android:visible="true"/> <item android:id="@+id/copy_text" android:title="@string/copy_text" diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 5b89e0e5..9ceeaec7 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -100,8 +100,14 @@ <string name="pref_general">Allgemeines</string> <string name="pref_xmpp_resource">XMPP-Ressource</string> <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string> - <string name="pref_accept_files">Dateien annehmen</string> - <string name="pref_accept_files_summary">Dateien automatisch annehmen, die kleiner sind als …</string> + <string name="pref_accept_files">Dateiannahme</string> + <string name="pref_accept_files_summary">Einstellungen für Dateiannahme und automatischen Download</string> + <string name="pref_accept_files_size">Größe</string> + <string name="pref_accept_files_size_summary">Dateien, die kleiner sind als …, automatisch annehmen</string> + <string name="pref_accept_files_download">nur WLAN</string> + <string name="pref_accept_files_download_summary">Automatisches Herunterladen und Akzeptieren von Dateien nur im WLAN</string> + <string name="pref_accept_files_download_link">Bilder-Links</string> + <string name="pref_accept_files_download_link_summary">Bilder-Links automatisch herunterladen</string> <string name="pref_notification_settings">Benachrichtigungen</string> <string name="pref_notifications">Benachrichtigungen</string> <string name="pref_notifications_summary">Benachrichtigen bei Erhalt einer neuen Nachricht</string> @@ -116,7 +122,12 @@ <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string> <string name="pref_confirm_messages">Lese- und Empfangsbestätigung senden</string> <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfangen und gelesen hast</string> + <string name="pref_confirm_messages_none">Keine Bestätigungen</string> + <string name="pref_confirm_messages_received">Nur Empfangsbestätigung</string> + <string name="pref_confirm_messages_read_and_received">Lese- und Empfangsbestätigung</string> <string name="pref_ui_options">Benutzeroberfläche</string> + <string name="pref_parse_emoticons">Smilies ersetzen</string> + <string name="pref_parse_emoticons_summary">Zeige Smilie-Bilder anstelle von Emoticons.</string> <string name="openpgp_error">Fehler mit OpenKeychain</string> <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string> <string name="accept">Annehmen</string> @@ -394,6 +405,7 @@ <string name="current_password">Aktuelles Passwort</string> <string name="new_password">Neues Passwort</string> <string name="password_should_not_be_empty">Das Passwort darf nicht leer sein</string> + <string name="password_should_not_contain_only_spaces">Das Passwort darf nicht nur aus Leerzeichen bestehen</string> <string name="enable_all_accounts">Alle Konten anschalten</string> <string name="disable_all_accounts">Alle Konten abschalten</string> <string name="perform_action_with">Aktion durchführen mit</string> @@ -479,6 +491,54 @@ <string name="recently_used">zuletzt verwendet</string> <string name="choose_quick_action">wähle Schnell-Taste</string> <string name="search_for_contacts_or_groups">Nach Kontakten oder Konferenzen suchen</string> + <string name="pref_led_notification_color">LED-Benachrichtigung Farbe</string> + <string name="pref_led_notification_color_summary">Setze die Farbe der LED-Benachrichtigung</string> + <string name="msg_ctx_mnu_details">Nachrichtendetails</string> + <string name="dlg_msg_details_title">Nachrichtendetails</string> + <string name="cplus_ok">Ok</string> + <string name="dlg_resources_title">%1$s (%2$d)</string><!-- %s = bare jid, %d = count of online resources --> + <string name="dlg_msg_details_receipient_resource">Empfänger Resource:</string> + <string name="dlg_msg_details_receipient_nick">Empfänger Nickname:</string> + <string name="dlg_msg_details_sender_resource">Absender Resource:</string> + <string name="dlg_msg_details_sender_nick">Absender Nickname:</string> + <string name="dlg_msg_details_time_sent">Gesendet:</string> + <string name="dlg_msg_details_msg_type">Nachrichtentyp:</string> + <string name="dlg_msg_details_msg_status">Nachrichtenstatus:</string> + <string name="dlg_msg_details_file_details">Dateidetails</string> + <string name="dlg_msg_details_file_mime">Mime Type</string> + <string name="dlg_msg_details_file_size">Größe</string> + <string name="dlg_msg_details_msg_type_text">Text</string> + <string name="dlg_msg_details_msg_type_image">Bild</string> + <string name="dlg_msg_details_msg_type_file">Datei</string> + <string name="dlg_msg_details_msg_type_status">Status</string> + <string name="dlg_msg_details_msg_type_private">Privat</string> + <string name="dlg_msg_details_msg_status_sent">Gesendet</string> + <string name="dlg_msg_details_msg_status_received">Empfangen</string> + <string name="dlg_msg_details_msg_status_waiting">Wartend</string> + <string name="dlg_msg_details_msg_status_unsend">In Übertragung</string> + <string name="dlg_msg_details_msg_status_offered">Angeboten</string> + <string name="dlg_msg_details_msg_status_failed">Fehlgeschlagen</string> + <string name="pref_resize_picture_ask">nachfragen</string> + <string name="pref_resize_picture_always">immer</string> + <string name="pref_resize_picture_never">nie</string> + <string name="pref_resize_picture_summary">Sollen Bilder vor dem Senden verkleinert werden oder nicht?</string> + <string name="pref_resize_picture">Bilder verkleinern</string> + <string name="cplus_yes">Ja</string> + <string name="cplus_no">Nein</string> + <string name="cplus_remember_userdecision">Diese Entscheidung merken</string> + <string name="userdecision_question_resize_picture">Soll das Bild verkleinert werden?</string> + <string name="title_activity_loginformation">Logausgaben</string> + <string name="cplus_copy_to_clipboard">Kopieren</string> + <string name="cplus_copied_to_clipboard">In Zwischenablage kopiert</string> + <string name="pref_show_logcat_title">Zeige logcat Ausgabe</string> + <string name="pref_show_logcat_summary">Zeigt die Ausgabe von logcat an. Hilfreich für die Fehlersuche.</string> + <string name="pref_file_transfer">Ordnername, um eingehnde Datein zu speichern</string> + <string name="pref_file_transfer_folder_summary">Unterordner des globalen Dateiordners, um eingehende Dateien zu speichern.</string> + <string name="pref_img_file_transfer">Ordnername, um eingehende Bilder zu speichern</string> + <string name="pref_img_file_transfer_summary">Unterordner des globalen Bilderordners, um eingehende Bilder zu speichern.</string> + <string name="pref_file_transfer_category">Dateiübertragung</string> + <string name="cplus_not_copied_to_clipboard_empty">Nichts zu kopieren.</string> + <string name="cplus_me">Ich</string> <string name="send_private_message">Private Nachricht senden</string> <string name="user_has_left_conference">%s hat die Konferenz verlassen!</string> <string name="username">Benutzername</string> diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index c0e0b9aa..656c20d5 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -57,7 +57,7 @@ <string name="pref_xmpp_resource">Recurso</string> <string name="pref_xmpp_resource_summary">O nome que identifica o cliente que estás a empregar</string> <string name="pref_accept_files">Aceptar arquivos</string> - <string name="pref_accept_files_summary">De forma automática aceptar arquivos menores de…</string> + <string name="pref_accept_files_size_summary">De forma automática aceptar arquivos menores de…</string> <string name="pref_notification_settings">Axustes de notificación</string> <string name="pref_notifications">Notificacións</string> <string name="pref_notifications_summary">Notifica cuando chega unha nova mensaxe</string> diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index b66c87c7..3a5ce0fe 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -76,7 +76,7 @@ <string name="pref_xmpp_resource">XMPP 資源</string> <string name="pref_xmpp_resource_summary">客戶端標示名稱</string> <string name="pref_accept_files">接收文件</string> - <string name="pref_accept_files_summary">自動接收小於 … 的文件</string> + <string name="pref_accept_files_size_summary">自動接收小於 … 的文件</string> <string name="pref_notification_settings">通知設定</string> <string name="pref_notifications">通知</string> <string name="pref_notifications_summary">收到新訊息時通知</string> diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index ca63a0f1..be52464a 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -58,6 +58,28 @@ <item>location</item> </string-array> + <string-array name="confirm_strings"> + <item>@string/pref_confirm_messages_none</item> + <item>@string/pref_confirm_messages_received</item> + <item>@string/pref_confirm_messages_read_and_received</item> + </string-array> + + <string-array name="confirm_values"> + <item>0</item> + <item>1</item> + <item>2</item> + </string-array> + <string-array name="resize_picture_setting_entries"> + <item>@string/pref_resize_picture_ask</item> + <item>@string/pref_resize_picture_always</item> + <item>@string/pref_resize_picture_never</item> + </string-array> + <string-array name="resize_picture_setting_values"> + <item>ASK</item> + <item>ALWAYS</item> + <item>NEVER</item> + </string-array> + <string-array name="picture_compression_values"> <item>never</item> <item>auto</item> diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index d471e54a..0d6d19b1 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -25,5 +25,8 @@ <attr name="icon_secure" format="reference"/> <attr name="icon_settings" format="reference"/> <attr name="icon_import_export" format="reference"/> - + <declare-styleable name="AmbilWarnaPreference"> + <attr name="supportsAlpha" + format="boolean"/> + </declare-styleable> </resources>
\ No newline at end of file diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index b3567b44..3ecc2225 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -1,7 +1,16 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="primary">#ff259b24</color> - <color name="primary_dark">#ff0a7e07</color> + <color name="primaryBackground">@color/grey50</color> + <color name="secondaryBackground">@color/grey200</color> + <color name="primaryText">@color/black87</color> + <color name="secondaryText">@color/black54</color> + <color name="warning">@color/red500</color> + <color name="error">@color/red500</color> + <color name="online">@color/green500</color> + <color name="notification">@color/green500</color> + + <color name="primary">@color/green500</color> + <color name="primary_dark">@color/green700</color> <color name="accent">#ff0091ea</color> <color name="black87">#de000000</color> <color name="black54">#8a000000</color> diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 95e80055..d5d4aad4 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -5,4 +5,8 @@ <dimen name="infocard_padding">16dp</dimen> <dimen name="conversations_overview_width">288dp</dimen> <dimen name="image_button_padding">8dp</dimen> + <dimen name="ambilwarna_hsvHeight">240dp</dimen> + <dimen name="ambilwarna_hsvWidth">240dp</dimen> + <dimen name="ambilwarna_hueWidth">30dp</dimen> + <dimen name="ambilwarna_spacer">8dp</dimen> </resources> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9c472d0c..a7ded490 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -104,6 +104,12 @@ <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string> <string name="pref_accept_files">Accept files</string> <string name="pref_accept_files_summary">Automatically accept files smaller than…</string> + <string name="pref_accept_files_size">Size</string> + <string name="pref_accept_files_size_summary">Automatically accept files smaller than…</string> + <string name="pref_accept_files_download">Wi-Fi only</string> + <string name="pref_accept_files_download_summary">Download and accept files automatically only when using Wi-Fi</string> + <string name="pref_accept_files_download_link">Image links</string> + <string name="pref_accept_files_download_link_summary">Automatically download image links</string> <string name="pref_notification_settings">Notification Settings</string> <string name="pref_notifications">Notifications</string> <string name="pref_notifications_summary">Notify when a new message arrives</string> @@ -118,7 +124,12 @@ <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string> <string name="pref_confirm_messages">Confirm Messages</string> <string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string> + <string name="pref_confirm_messages_none">No confirmation</string> + <string name="pref_confirm_messages_received">Confirmation for received message</string> + <string name="pref_confirm_messages_read_and_received">Confirmation for received and read message</string> <string name="pref_ui_options">UI Options</string> + <string name="pref_parse_emoticons">Parse Emoticons</string> + <string name="pref_parse_emoticons_summary">Replace emoticons with smilies.</string> <string name="openpgp_error">OpenKeychain reported an error</string> <string name="error_decrypting_file">I/O Error decrypting file</string> <string name="accept">Accept</string> @@ -430,6 +441,7 @@ <string name="current_password">Current password</string> <string name="new_password">New password</string> <string name="password_should_not_be_empty">Password should not be empty</string> + <string name="password_should_not_contain_only_spaces">Password should not contain only spaces</string> <string name="enable_all_accounts">Enable all accounts</string> <string name="disable_all_accounts">Disable all accounts</string> <string name="perform_action_with">Perform action with</string> @@ -515,6 +527,55 @@ <string name="recently_used">Most recently used</string> <string name="choose_quick_action">Choose quick action</string> <string name="search_for_contacts_or_groups">Search for contacts or groups</string> + <string name="pref_led_notification_color">LED notification color</string> + <string name="pref_led_notification_color_summary">Change the color of the LED notification</string> + <string name="msg_ctx_mnu_details">Message Details</string> + <string name="dlg_msg_details_title">Message Details</string> + <string name="cplus_ok">Ok</string> + <string name="dlg_resources_title">%1$s (%2$d)</string><!-- %s = bare jid, %d = count of online resources --> + <string name="dlg_msg_details_receipient_resource">Receipient Resource:</string> + <string name="dlg_msg_details_receipient_nick">Receipient Nick:</string> + <string name="dlg_msg_details_sender_resource">Sender Resource:</string> + <string name="dlg_msg_details_sender_nick">Sender Nick:</string> + <string name="dlg_msg_details_time_sent">Time Sent:</string> + <string name="dlg_msg_details_msg_type">Message Type:</string> + <string name="dlg_msg_details_msg_status">Message Status:</string> + <string name="dlg_msg_details_file_details">File Details</string> + <string name="dlg_msg_details_file_mime">Mime Type</string> + <string name="dlg_msg_details_file_size">Size</string> + <string name="dlg_msg_details_msg_type_text">Text</string> + <string name="dlg_msg_details_msg_type_image">Image</string> + <string name="dlg_msg_details_msg_type_file">File</string> + <string name="dlg_msg_details_msg_type_status">Status</string> + <string name="dlg_msg_details_msg_type_private">Private</string> + <string name="dlg_msg_details_msg_status_sent">Sent</string> + <string name="dlg_msg_details_msg_status_received">Received</string> + <string name="dlg_msg_details_msg_status_waiting">Waiting</string> + <string name="dlg_msg_details_msg_status_unsend">Unsend</string> + <string name="dlg_msg_details_msg_status_offered">Offered</string> + <string name="dlg_msg_details_msg_status_failed">Failed</string> + <string name="pref_resize_picture_ask">ask</string> + <string name="pref_resize_picture_always">always</string> + <string name="pref_resize_picture_never">never</string> + <string name="pref_resize_picture_summary">Whether pictures shall be resized or not</string> + <string name="pref_resize_picture">Resize pictures</string> + <string name="cplus_yes">Yes</string> + <string name="cplus_no">No</string> + <string name="cplus_remember_userdecision">Remember this decision</string> + <string name="userdecision_question_resize_picture">Shall the picture be resized?</string> + <string name="title_activity_loginformation">Logoutput</string> + <string name="cplus_copy_to_clipboard">Copy to clipboard</string> + <string name="cplus_copied_to_clipboard">Copied to clipboard</string> + <string name="pref_show_logcat_title">Show logcat output</string> + <string name="pref_show_logcat_summary">Shows the output of logcat. This is useful for debugging.</string> + <string name="cplus_bugreport_jabberid">c+bugs@conference.thedevstack.de</string> + <string name="pref_file_transfer">Folder to save incoming files</string> + <string name="pref_file_transfer_folder_summary">This is the subdirectory for incoming files.</string> + <string name="pref_img_file_transfer">Folder to save incoming pictures</string> + <string name="pref_img_file_transfer_summary">This is the subdirectory in the pictures directory for incoming files.</string> + <string name="pref_file_transfer_category">File Transfer</string> + <string name="cplus_not_copied_to_clipboard_empty">Nothing to copy.</string> + <string name="cplus_me">Me</string> <string name="send_private_message">Send private message</string> <string name="user_has_left_conference">%s has left the conference!</string> <string name="username">Username</string> diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 382b3199..ea685c14 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" > <PreferenceCategory android:title="@string/pref_general"> <CheckBoxPreference @@ -16,78 +17,137 @@ android:summary="@string/pref_xmpp_resource_summary" android:title="@string/pref_xmpp_resource"/> <ListPreference - android:defaultValue="524288" - android:entries="@array/filesizes" - android:entryValues="@array/filesizes_values" - android:key="auto_accept_file_size" - android:summary="@string/pref_accept_files_summary" - android:title="@string/pref_accept_files"/> - <ListPreference android:defaultValue="auto" android:entries="@array/picture_compression_entries" android:entryValues="@array/picture_compression_values" android:key="picture_compression" android:summary="@string/pref_picture_compression_summary" android:title="@string/pref_picture_compression"/> - <CheckBoxPreference - android:defaultValue="true" - android:key="confirm_messages" + <ListPreference + android:defaultValue="2" + android:entries="@array/confirm_strings" + android:entryValues="@array/confirm_values" + android:key="confirm_messages_list" android:summary="@string/pref_confirm_messages_summary" - android:title="@string/pref_confirm_messages"/> + android:title="@string/pref_confirm_messages" /> <CheckBoxPreference android:defaultValue="false" android:key="chat_states" android:summary="@string/pref_chat_states_summary" android:title="@string/pref_chat_states"/> - - </PreferenceCategory> - <PreferenceCategory - android:key="notifications" - android:title="@string/pref_notification_settings"> <CheckBoxPreference android:defaultValue="true" - android:key="show_notification" - android:summary="@string/pref_notifications_summary" - android:title="@string/pref_notifications"/> + android:key="parse_emoticons" + android:summary="@string/pref_parse_emoticons_summary" + android:title="@string/pref_parse_emoticons"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_file_transfer_category"> <PreferenceScreen - android:dependency="show_notification" - android:key="quiet_hours" - android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_quiet_hours"> + android:summary="@string/pref_accept_files_summary" + android:title="@string/pref_accept_files"> + <CheckBoxPreference - android:defaultValue="false" - android:key="enable_quiet_hours" - android:summary="@string/pref_quiet_hours_summary" - android:title="@string/title_pref_enable_quiet_hours"/> - <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_start" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_start_time"/> - <eu.siacs.conversations.ui.TimePreference - android:dependency="enable_quiet_hours" - android:key="quiet_hours_end" - android:negativeButtonText="@string/cancel" - android:positiveButtonText="@string/set" - android:title="@string/title_pref_quiet_hours_end_time"/> + android:defaultValue="true" + android:key="auto_download_file_link" + android:summary="@string/pref_accept_files_download_link_summary" + android:title="@string/pref_accept_files_download_link" /> + + <ListPreference + android:defaultValue="524288" + android:entries="@array/filesizes" + android:entryValues="@array/filesizes_values" + android:key="auto_accept_file_size" + android:summary="@string/pref_accept_files_size_summary" + android:title="@string/pref_accept_files_size" /> + + <CheckBoxPreference + android:defaultValue="true" + android:key="auto_download_file_wlan" + android:summary="@string/pref_accept_files_download_summary" + android:title="@string/pref_accept_files_download" /> + </PreferenceScreen> - <CheckBoxPreference - android:defaultValue="true" - android:dependency="show_notification" - android:key="vibrate_on_notification" - android:summary="@string/pref_vibrate_summary" - android:title="@string/pref_vibrate"/> + <EditTextPreference + android:title="@string/pref_img_file_transfer" + android:summary="@string/pref_file_transfer_folder_summary" + android:key="file_transfer_folder" + android:defaultValue="Conversations+"/> + <EditTextPreference + android:title="@string/pref_file_transfer" + android:summary="@string/pref_img_file_transfer_summary" + android:key="img_transfer_folder" + android:defaultValue="Conversations+"/> - <RingtonePreference - android:defaultValue="content://settings/system/notification_sound" - android:dependency="show_notification" - android:key="notification_ringtone" - android:ringtoneType="notification" - android:summary="@string/pref_sound_summary" - android:title="@string/pref_sound"/> + <ListPreference + android:defaultValue="ASK" + android:entries="@array/resize_picture_setting_entries" + android:entryValues="@array/resize_picture_setting_values" + android:key="resize_picture" + android:summary="@string/pref_resize_picture_summary" + android:title="@string/pref_resize_picture"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_notification_settings" > + <PreferenceScreen + android:summary="@string/pref_notification_settings" + android:title="@string/pref_notifications" > + <CheckBoxPreference + android:defaultValue="true" + android:key="show_notification" + android:summary="@string/pref_notifications_summary" + android:title="@string/pref_notifications" /> + <PreferenceScreen + android:dependency="show_notification" + android:key="quiet_hours" + android:summary="@string/pref_quiet_hours_summary" + android:title="@string/title_pref_quiet_hours"> + <CheckBoxPreference + android:defaultValue="false" + android:key="enable_quiet_hours" + android:summary="@string/pref_quiet_hours_summary" + android:title="@string/title_pref_enable_quiet_hours" /> + <eu.siacs.conversations.ui.TimePreference + android:dependency="enable_quiet_hours" + android:key="quiet_hours_start" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_start_time" /> + <eu.siacs.conversations.ui.TimePreference + android:dependency="enable_quiet_hours" + android:key="quiet_hours_end" + android:negativeButtonText="@string/cancel" + android:positiveButtonText="@string/set" + android:title="@string/title_pref_quiet_hours_end_time" /> + </PreferenceScreen> + <CheckBoxPreference + android:defaultValue="true" + android:dependency="show_notification" + android:key="vibrate_on_notification" + android:summary="@string/pref_vibrate_summary" + android:title="@string/pref_vibrate" /> + + <RingtonePreference + android:defaultValue="content://settings/system/notification_sound" + android:dependency="show_notification" + android:key="notification_ringtone" + android:ringtoneType="notification" + android:summary="@string/pref_sound_summary" + android:title="@string/pref_sound" /> + <yuku.ambilwarna.widget.AmbilWarnaPreference + android:defaultValue="0xffffffff" + android:key="led_notify_color" + android:title="@string/pref_led_notification_color" + app:supportsAlpha="true" + android:summary="@string/pref_led_notification_color_summary"/> + + <CheckBoxPreference + android:defaultValue="true" + android:dependency="show_notification" + android:key="always_notify_in_conference" + android:summary="@string/pref_conference_notifications_summary" + android:title="@string/pref_conference_notifications" /> + </PreferenceScreen> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_ui_options"> <CheckBoxPreference @@ -206,6 +266,9 @@ android:key="export_logs" android:summary="@string/pref_export_logs_summary" android:title="@string/pref_export_logs"/> + <de.thedevstack.conversationsplus.ui.preferences.LogInformationPreference + android:summary="@string/pref_show_logcat_summary" + android:title="@string/pref_show_logcat_title"/> </PreferenceCategory> </PreferenceScreen> |