diff options
88 files changed, 4848 insertions, 2822 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8c90d092..97a27abb 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="eu.siacs.conversations" - android:versionCode="11" - android:versionName="0.2" > + android:versionCode="16" + android:versionName="0.3" > <uses-sdk android:minSdkVersion="14" @@ -32,14 +32,16 @@ <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> + <action android:name="android.intent.action.ACTION_SHUTDOWN" /> </intent-filter> </receiver> <activity android:name="eu.siacs.conversations.ui.ConversationActivity" android:configChanges="orientation|screenSize" - android:label="Conversations" - android:windowSoftInputMode="stateHidden" > + android:label="@string/title_activity_conversations" + android:windowSoftInputMode="stateHidden" + android:launchMode="singleTask" > <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -48,23 +50,23 @@ </activity> <activity android:name="eu.siacs.conversations.ui.SettingsActivity" - android:label="Settings" + android:label="@string/title_activity_settings" android:parentActivityName="eu.siacs.conversations.ui.ConversationActivity" > </activity> <activity android:name="eu.siacs.conversations.ui.ManageAccountActivity" - android:label="Manage Accounts" + android:label="@string/title_activity_manage_accounts" android:configChanges="orientation|screenSize" android:parentActivityName="eu.siacs.conversations.ui.ConversationActivity" > </activity> <activity android:name="eu.siacs.conversations.ui.MucDetailsActivity" - android:label="Conference Details" + android:label="@string/title_activity_conference_details" android:windowSoftInputMode="stateHidden" > </activity> <activity android:name="eu.siacs.conversations.ui.ContactDetailsActivity" - android:label="Contact Details" + android:label="@string/title_activity_contact_details" android:windowSoftInputMode="stateHidden" > </activity> <activity @@ -84,7 +86,7 @@ </activity> <activity android:name="eu.siacs.conversations.ui.ShareWithActivity" - android:label="Conversations" + android:label="@string/title_activity_conversations" android:theme="@android:style/Theme.Holo.Light.DialogWhenLarge" android:icon="@drawable/ic_launcher"> <intent-filter> @@ -92,6 +94,11 @@ <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:mimeType="image/*" /> + </intent-filter> </activity> </application> diff --git a/CHANGELOG.md b/CHANGELOG.md index ecfd13f5..1db04218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ ###Changelog +####Version 0.3 +* Mostly bug fixes and internal rewrites +* Touch contact picture in conference to highlight +* Long press on received image to share +* made OTR more reliable +* improved issues with occasional message lost +* experimental conference encryption. (see FAQ) + +####Version 0.2.3 +* regression fix with receiving encrypted images + +####Version 0.2.2 +* Ability to take photos directly +* Improved openPGP offline handling +* Various bug fixes +* Updated Translations + +####Version 0.2.1 +* Various bug fixes +* Updated Translations + ####Version 0.2 * Image file transfer * Better integration with OpenKeychain (PGP encryption) @@ -42,11 +42,15 @@ These XEPs are - as of now: (In order of appearance) ###Code -* Rene Treffer @rtreffer -* Andreas Straub @strb +* [Rene Treffer](https://github.com/rtreffer) +* [Andreas Straub](https://github.com/strb) +* [Alethea Butler](https://github.com/alethea) ###Translations -* @beriain (Spanish and Basque) +* [Sergio Cárdenas](https://github.com/kruks23) (Spanish) +* [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French) +* [Daniel Gultsch](https://github.com/iNPUTmice) (German) +* [Aitor Beriain](https://github.com/beriain) (Basque) ##FAQ ###General @@ -86,7 +90,7 @@ not supported for simplicity reasons. Users tend to forget their status, other users ignore them and setting the status automatically would mean too much of an impact on privacy. ###Security -####Why are there to end-to-end encryption methods and which one should I choose? +####Why are there two end-to-end encryption methods and which one should I choose? In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. However PGP can be in some cases (carbonated messages to multiple clients) be more flexible. @@ -94,9 +98,22 @@ more flexible. Before you continue reading you should notice that the openPGP support in Conversations is marked as experimental. This is not because it will make the app unstable but because the fundamental concepts of PGP aren't ready for a -widespread use. The way PGP works is that you trust Key IDs instead of XMPP- or email addresses. So in theory your contact list should consist of Public-Key-IDs instead of email addresses. But of course no email or xmpp client out there implements these concepts. Plus PGP in the context of instant messaging has a couple of downsides. It is vulnerable to replay attacs, it is rather verbose, decryping and encrypting takes longer than OTR. It is however asynchronous and works well with carbonated messages. +widespread use. The way PGP works is that you trust Key IDs instead of XMPP- or email addresses. So in theory your contact list should consist of Public-Key-IDs instead of email addresses. But of course no email or xmpp client out there implements these concepts. Plus PGP in the context of instant messaging has a couple of downsides. It is vulnerable to replay attacks, it is rather verbose, and decryping and encrypting takes longer than OTR. It is however asynchronous and works well with carbonated messages. To use openpgp you have to install the opensource app OpenKeychain (www.openkeychain.org) and then long press on the account in manage accounts and choose renew PGP announcement from the contextual menu. +####How does the encryption for conferences work? +For conferences the only supported encryption method is OpenPGP. (OTR does not +work with multiple participents.) Every participant has to announce their +OpenPGP key. (See answer above). If you would like to send encrypted messages to +a conference you have to make sure that you have every participants public key +in your OpenKeychain. Right now there is no check in Conversations to ensure +that. You have to take care of that yourself. Go to the conference details and +touch every key id (The hexadecimal number below a contact). This will send you +to OpenKeychain which will assist you on adding the key. +This works best in very small conferences with contacts you are already using +OpenPGP with. This feature is regarded experimental. Conversations is the only +client that uses XEP-0027 with confercenes. (The XEP neither specifically allows +nor disallows this.) ###Development ####How do I build Conversations Make sure to have ANDROID_HOME point to your Android SDK diff --git a/res/drawable-hdpi/ic_action_search.png b/res/drawable-hdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..f594b4e4 --- /dev/null +++ b/res/drawable-hdpi/ic_action_search.png diff --git a/res/drawable-mdpi/ic_action_search.png b/res/drawable-mdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..2e446ec0 --- /dev/null +++ b/res/drawable-mdpi/ic_action_search.png diff --git a/res/drawable-xhdpi/ic_action_search.png b/res/drawable-xhdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..aad535e9 --- /dev/null +++ b/res/drawable-xhdpi/ic_action_search.png diff --git a/res/drawable-xxhdpi/ic_action_search.png b/res/drawable-xxhdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..9c0ea3ca --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_search.png diff --git a/res/layout/fragment_conversations_overview.xml b/res/layout-sw320dp/fragment_conversations_overview.xml index 5548cce6..fcb1949c 100644 --- a/res/layout/fragment_conversations_overview.xml +++ b/res/layout-sw320dp/fragment_conversations_overview.xml @@ -4,7 +4,7 @@ android:layout_height="match_parent" android:id="@+id/slidingpanelayout"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="336dp" + android:layout_width="288dp" android:layout_height="match_parent" android:orientation="vertical" android:background="#eeeeee" @@ -22,7 +22,7 @@ </LinearLayout> <LinearLayout android:id="@+id/selected_conversation" - android:layout_width="400dp" + android:layout_width="fill_parent" android:layout_height="match_parent" android:layout_weight="1" android:background="#e5e5e5" diff --git a/res/layout-sw360dp/fragment_conversations_overview.xml b/res/layout-sw360dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..939950c2 --- /dev/null +++ b/res/layout-sw360dp/fragment_conversations_overview.xml @@ -0,0 +1,31 @@ +<android.support.v4.widget.SlidingPaneLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/slidingpanelayout"> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="324dp" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#eeeeee" + > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="#b5b5b5" + android:dividerHeight="1dp" + android:background="#eeeeee" + /> + +</LinearLayout> +<LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="#e5e5e5" + android:orientation="vertical"> +</LinearLayout> +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/res/layout-sw384dp/fragment_conversations_overview.xml b/res/layout-sw384dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..e48cf9ec --- /dev/null +++ b/res/layout-sw384dp/fragment_conversations_overview.xml @@ -0,0 +1,31 @@ +<android.support.v4.widget.SlidingPaneLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/slidingpanelayout"> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="345dp" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#eeeeee" + > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="#b5b5b5" + android:dividerHeight="1dp" + android:background="#eeeeee" + /> + +</LinearLayout> +<LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="#e5e5e5" + android:orientation="vertical"> +</LinearLayout> +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/res/layout-sw600dp/fragment_conversations_overview.xml b/res/layout-sw600dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..fac95f9c --- /dev/null +++ b/res/layout-sw600dp/fragment_conversations_overview.xml @@ -0,0 +1,31 @@ +<android.support.v4.widget.SlidingPaneLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/slidingpanelayout"> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="240dp" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#eeeeee" + > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="#b5b5b5" + android:dividerHeight="1dp" + android:background="#eeeeee" + /> + +</LinearLayout> +<LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="#e5e5e5" + android:orientation="vertical"> +</LinearLayout> +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/res/layout-sw720dp/fragment_conversations_overview.xml b/res/layout-sw720dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..fcb1949c --- /dev/null +++ b/res/layout-sw720dp/fragment_conversations_overview.xml @@ -0,0 +1,31 @@ +<android.support.v4.widget.SlidingPaneLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/slidingpanelayout"> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="288dp" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="#eeeeee" + > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="#b5b5b5" + android:dividerHeight="1dp" + android:background="#eeeeee" + /> + +</LinearLayout> +<LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="#e5e5e5" + android:orientation="vertical"> +</LinearLayout> +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/res/layout/account_row.xml b/res/layout/account_row.xml index 402cb965..6cfbe9e3 100644 --- a/res/layout/account_row.xml +++ b/res/layout/account_row.xml @@ -19,7 +19,6 @@ android:id="@+id/account_jid" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="inputmice@jabber.ccc.de" android:textSize="20sp" android:singleLine="true" android:scrollHorizontally="false"/> @@ -34,7 +33,7 @@ android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Status: " + android:text="@string/account_status" android:textStyle="bold" android:textSize="16sp" /> @@ -42,8 +41,9 @@ android:id="@+id/account_status" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingLeft="4dp" android:textColor="#669900" - android:text="unknown" + android:text="@string/account_status_unknown" android:textStyle="bold" android:textSize="16sp"/> diff --git a/res/layout/activity_contact_details.xml b/res/layout/activity_contact_details.xml index cb001b79..5eecfa87 100644 --- a/res/layout/activity_contact_details.xml +++ b/res/layout/activity_contact_details.xml @@ -17,7 +17,8 @@ android:text="@string/action_contact_details" /> <RelativeLayout android:layout_width="wrap_content" - android:layout_height="88dp" + android:layout_height="wrap_content" + android:minHeight="88dp" android:padding="8dp"> <QuickContactBadge @@ -47,11 +48,19 @@ <TextView android:id="@+id/details_contactstatus" - android:layout_width="fill_parent" - android:layout_height="fill_parent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:paddingLeft="16dp" android:textSize="24sp" android:textStyle="bold" /> + <TextView + android:id="@+id/details_lastseen" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="8dp" + android:singleLine="true" + android:textColor="#5b5b5b" + android:textSize="14sp" /> </LinearLayout> </RelativeLayout> diff --git a/res/layout/activity_muc_details.xml b/res/layout/activity_muc_details.xml index 73f21ad0..f866a485 100644 --- a/res/layout/activity_muc_details.xml +++ b/res/layout/activity_muc_details.xml @@ -13,7 +13,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" - android:text="Conference" /> + android:text="@string/muc_details_conference" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" @@ -28,7 +28,7 @@ android:layout_toLeftOf="@+id/muc_edit_subject" android:background="#eee" android:ems="10" - android:hint="Conferenece Subject" + android:hint="@string/muc_details_conference_subject" android:inputType="textAutoComplete" android:paddingBottom="12dp" android:paddingLeft="8dp" @@ -62,7 +62,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingTop="8dp" - android:text="Your Nickname" /> + android:text="@string/muc_details_your_nickname" /> <RelativeLayout android:layout_width="match_parent" @@ -78,7 +78,7 @@ android:layout_toLeftOf="@+id/muc_edit_nick" android:background="#eee" android:ems="10" - android:hint="Your nickname" + android:hint="@string/muc_details_your_nickname" android:inputType="textEmailAddress" android:paddingBottom="12dp" android:paddingLeft="8dp" @@ -119,7 +119,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingTop="8dp" - android:text="Other Members" /> + android:text="@string/muc_details_other_members" /> <LinearLayout android:id="@+id/muc_members" diff --git a/res/layout/activity_new_conversation.xml b/res/layout/activity_new_conversation.xml index a2b00af1..78500ead 100644 --- a/res/layout/activity_new_conversation.xml +++ b/res/layout/activity_new_conversation.xml @@ -23,7 +23,7 @@ android:layout_alignParentTop="true" android:background="#eee" android:ems="10" - android:hint="Search or enter Jabber ID" + android:hint="@string/search_jabber_id" android:inputType="textEmailAddress" android:paddingBottom="12dp" android:paddingLeft="8dp" diff --git a/res/layout/contact.xml b/res/layout/contact.xml index fcf8e8b9..7303d2ba 100644 --- a/res/layout/contact.xml +++ b/res/layout/contact.xml @@ -35,6 +35,15 @@ android:textColor="#5b5b5b" android:singleLine="true" /> + <TextView + android:id="@+id/key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="18sp" + android:textColor="#5b5b5b" + android:typeface="monospace" + android:visibility="gone" + /> </LinearLayout> </RelativeLayout>
\ No newline at end of file diff --git a/res/layout/dialog_verify_otr.xml b/res/layout/dialog_verify_otr.xml index 41ce3b07..6dfe149d 100644 --- a/res/layout/dialog_verify_otr.xml +++ b/res/layout/dialog_verify_otr.xml @@ -20,7 +20,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="8dp" - android:text="julia@jabber.example.com" android:textSize="14sp" /> <TextView android:layout_width="wrap_content" @@ -35,7 +34,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="8dp" - android:text="2674D6A0 0B1421B1 BFC42AEC C56F3719 672437D8" android:textSize="14sp" android:typeface="monospace"/> <TextView @@ -51,7 +49,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="8dp" - android:text="2674D6A0 0B1421B1 BFC42AEC C56F3719 672437D8" android:textSize="14sp" android:typeface="monospace"/> </LinearLayout> diff --git a/res/layout/edit_account_dialog.xml b/res/layout/edit_account_dialog.xml index 0a86039d..12fbe468 100644 --- a/res/layout/edit_account_dialog.xml +++ b/res/layout/edit_account_dialog.xml @@ -9,7 +9,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" - android:text="Jabber ID:" /> + android:text="@string/account_settings_jabber_id" /> @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:ems="10" android:inputType="textEmailAddress" - android:hint="username@jabber.example.com"> + android:hint="@string/account_settings_example_jabber_id"> </EditText> @@ -30,7 +30,7 @@ android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Password:" + android:text="@string/account_settings_password" android:textSize="14sp"/> <EditText @@ -39,14 +39,14 @@ android:layout_height="wrap_content" android:ems="10" android:inputType="textPassword" - android:hint="Password" + android:hint="@string/password" android:fontFamily="sans-serif" /> <CheckBox android:id="@+id/edit_account_register_new" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Register a new account on server"/> + android:text="@string/register_account"/> <TextView @@ -54,7 +54,7 @@ android:id="@+id/account_confirm_password_desc" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Confirm password:" + android:text="@string/account_settings_confirm_password" android:textSize="14sp" android:visibility="gone"/> @@ -64,7 +64,7 @@ android:layout_height="wrap_content" android:ems="10" android:inputType="textPassword" - android:hint="Confirm password" + android:hint="@string/confirm_password" android:visibility="gone" android:fontFamily="sans-serif" /> diff --git a/res/layout/fragment_conversation.xml b/res/layout/fragment_conversation.xml index ca09d770..703a525e 100644 --- a/res/layout/fragment_conversation.xml +++ b/res/layout/fragment_conversation.xml @@ -3,32 +3,33 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#e5e5e5"> + android:background="#e5e5e5" > <RelativeLayout - android:background="#eee" android:id="@+id/textsend" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - android:layout_alignParentLeft="true"> + android:layout_alignParentLeft="true" + android:background="#eee" > <EditText android:id="@+id/textinput" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minHeight="48dp" android:layout_alignParentLeft="true" - android:paddingBottom="12dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="12dp" android:layout_toLeftOf="@+id/textSendButton" android:background="#eee" android:ems="10" android:inputType="textShortMessage|textMultiLine|textCapSentences" - android:minLines="1" > - <requestFocus /> + android:minHeight="48dp" + android:minLines="1" + android:paddingBottom="12dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="12dp" > + + <requestFocus /> </EditText> <ImageButton @@ -52,101 +53,98 @@ android:divider="@null" android:dividerHeight="0dp" android:listSelector="@android:color/transparent" - android:transcriptMode="alwaysScroll" - tools:listitem="@layout/message_sent" - android:stackFromBottom="true"> - + android:stackFromBottom="true" + android:transcriptMode="normal" + tools:listitem="@layout/message_sent" > </ListView> + <LinearLayout android:id="@+id/info_box" - android:layout_height="wrap_content" - android:layout_width="fill_parent" - android:orientation="vertical" - > - - <LinearLayout - android:id="@+id/muc_error" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:background="@drawable/redbackground" - android:orientation="vertical" - android:visibility="gone" - > + android:orientation="vertical" > - <TextView - android:id="@+id/muc_error_msg" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textColor="#eee" - android:textStyle="bold" - android:padding="8dp" - android:textSize="20sp"/> - <TextView - android:layout_width="wrap_content" + <LinearLayout + android:id="@+id/muc_error" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="Click to edit conference details" - android:textColor="#eee" - android:paddingLeft="8dp" - android:paddingBottom="8dp" - android:textSize="14sp"/> - - </LinearLayout> - - - <LinearLayout - android:id="@+id/new_fingerprint" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@drawable/redbackground" - android:orientation="vertical" - android:visibility="gone" - > + android:background="@drawable/redbackground" + android:orientation="vertical" + android:visibility="gone" > - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Unknown OTR Fingerprint" - android:textColor="#eee" - android:textStyle="bold" - android:padding="8dp" - android:textSize="20sp"/> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@+id/otr_fingerprint" - android:textColor="#eee" - android:paddingLeft="8dp" - android:paddingBottom="8dp" - android:textSize="14sp" - android:typeface="monospace"/> - - </LinearLayout> - <LinearLayout - android:id="@+id/pgp_keyentry" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="@drawable/bluebackground" - android:orientation="vertical" - android:visibility="gone" - > + <TextView + android:id="@+id/muc_error_msg" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textColor="#eee" + android:textSize="20sp" + android:textStyle="bold" /> - <TextView - android:layout_width="wrap_content" + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:text="Click to edit conference details" + android:textColor="#eee" + android:textSize="14sp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/new_fingerprint" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="OpenPGP encrypted messages found" - android:textColor="#eee" - android:textStyle="bold" - android:padding="8dp" - android:textSize="20sp"/> - <TextView - android:layout_width="wrap_content" + android:background="@drawable/redbackground" + android:orientation="vertical" + android:visibility="gone" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:text="Unknown OTR Fingerprint" + android:textColor="#eee" + android:textSize="20sp" + android:textStyle="bold" /> + + <TextView + android:id="@+id/otr_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:textColor="#eee" + android:textSize="14sp" + android:typeface="monospace" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/pgp_keyentry" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:textColor="#eee" - android:text="Click here to enter passphrase and decrypt messages" - android:paddingLeft="8dp" - android:paddingBottom="8dp" - android:textSize="14sp"/> - - </LinearLayout> + android:background="@drawable/bluebackground" + android:orientation="vertical" + android:visibility="gone" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:text="OpenPGP encrypted messages found" + android:textColor="#eee" + android:textSize="20sp" + android:textStyle="bold" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:text="Click here to enter passphrase and decrypt messages" + android:textColor="#eee" + android:textSize="14sp" /> + </LinearLayout> </LinearLayout> + </RelativeLayout>
\ No newline at end of file diff --git a/res/layout/message_recieved.xml b/res/layout/message_recieved.xml index 9f0c41e5..6e4577fb 100644 --- a/res/layout/message_recieved.xml +++ b/res/layout/message_recieved.xml @@ -6,7 +6,7 @@ android:padding="8dp" > <LinearLayout - android:id="@+id/linearLayout1" + android:id="@+id/message_box" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" diff --git a/res/layout/message_sent.xml b/res/layout/message_sent.xml index 990df59b..ebba6ac5 100644 --- a/res/layout/message_sent.xml +++ b/res/layout/message_sent.xml @@ -6,6 +6,7 @@ android:padding="8dp" > <LinearLayout + android:id="@+id/message_box" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" diff --git a/res/layout/message_status.xml b/res/layout/message_status.xml new file mode 100644 index 00000000..33487984 --- /dev/null +++ b/res/layout/message_status.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="6dp" + android:paddingBottom="6dp" + android:paddingRight="6dp" + android:paddingLeft="8dp"> + + <ImageView + android:id="@+id/message_photo" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_marginRight="-1.5dp" + android:padding="0dp" + android:scaleType="fitXY" + android:src="@drawable/ic_profile"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/server_info.xml b/res/layout/server_info.xml index 25e5a471..499a20de 100644 --- a/res/layout/server_info.xml +++ b/res/layout/server_info.xml @@ -12,14 +12,14 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:paddingBottom="8dp" - android:text="Statistics" /> + android:text="@string/server_info_statistics" /> <TextView android:layout_below="@+id/stats_header" android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Connection age" + android:text="@string/server_info_connection_age" android:textSize="18sp"/> <TextView android:id="@+id/connection" @@ -36,7 +36,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/textView1" - android:text="Session age" + android:text="@string/server_info_session_age" android:textSize="18sp"/> <TextView android:id="@+id/session" @@ -53,7 +53,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/textView2" - android:text="Packets sent" + android:text="@string/server_info_packets_sent" android:textSize="18sp"/> <TextView android:id="@+id/pcks_sent" @@ -69,7 +69,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/textView3" - android:text="Packets received" + android:text="@string/server_info_packets_received" android:textSize="18sp"/> <TextView android:id="@+id/pcks_received" @@ -85,7 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/textView4" - android:text="Connected accounts" + android:text="@string/server_info_connected_accounts" android:textSize="18sp"/> <TextView android:id="@+id/number_presences" @@ -103,14 +103,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" - android:text="Server Features" /> + android:text="@string/server_info_server_features" /> <TextView android:layout_below="@+id/features_header" android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Roster Versioning" + android:text="@string/server_info_roster_versioning" android:textSize="18sp"/> <TextView android:id="@+id/roster" @@ -127,7 +127,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/textView5" - android:text="Carbon Messages" + android:text="@string/server_info_carbon_messages" android:textSize="18sp"/> <TextView android:id="@+id/carbon" @@ -144,7 +144,7 @@ android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_below="@+id/textView6" - android:text="Stream Managment" + android:text="@string/server_info_stream_management" android:textSize="18sp"/> <TextView android:id="@+id/stream" diff --git a/res/layout/share_with.xml b/res/layout/share_with.xml index ae73a688..a578b2cb 100644 --- a/res/layout/share_with.xml +++ b/res/layout/share_with.xml @@ -12,7 +12,7 @@ android:id="@+id/conversations_header" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Active Conversations" + android:text="@string/share_with_active_conversations" style="@style/sectionHeader" android:paddingLeft="8dp" android:paddingTop="8dp" @@ -32,7 +32,7 @@ android:id="@+id/contacts_header" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Contacts" + android:text="@string/contacts" style="@style/sectionHeader" android:paddingLeft="8dp" android:paddingTop="8dp" diff --git a/res/menu/attachment_choices.xml b/res/menu/attachment_choices.xml new file mode 100644 index 00000000..6a4f295a --- /dev/null +++ b/res/menu/attachment_choices.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/attach_choose_picture" + android:title="@string/attach_choose_picture"/> + <item + android:id="@+id/attach_take_picture" + android:title="@string/attach_take_picture"/> + <item + android:id="@+id/attach_record_voice" + android:title="@string/attach_record_voice" + android:visible="false"/> +</menu>
\ No newline at end of file diff --git a/res/menu/contact_details.xml b/res/menu/contact_details.xml index 5d4a8d56..43e8ea6a 100644 --- a/res/menu/contact_details.xml +++ b/res/menu/contact_details.xml @@ -5,13 +5,13 @@ android:orderInCategory="10" android:showAsAction="always" android:icon="@drawable/ic_action_edit" - android:title="Edit name" /> + android:title="@string/action_edit_contact" /> <item android:id="@+id/action_delete_contact" android:orderInCategory="10" android:showAsAction="always" android:icon="@drawable/ic_action_discard" - android:title="Delete from roster" /> + android:title="@string/action_delete_contact" /> <item android:id="@+id/action_accounts" android:orderInCategory="90" diff --git a/res/menu/encryption_choices.xml b/res/menu/encryption_choices.xml index ade17654..0596bfbc 100644 --- a/res/menu/encryption_choices.xml +++ b/res/menu/encryption_choices.xml @@ -4,14 +4,14 @@ <item android:id="@+id/encryption_choice_none" - android:title="Plain text" + android:title="@string/encryption_choice_none" /> <item android:id="@+id/encryption_choice_otr" - android:title="OTR" + android:title="@string/encryption_choice_otr" /> <item android:id="@+id/encryption_choice_pgp" - android:title="OpenPGP"/> + android:title="@string/encryption_choice_pgp"/> </group> </menu> diff --git a/res/menu/manageaccounts_context.xml b/res/menu/manageaccounts_context.xml index da8c8bf2..5f76b0e0 100644 --- a/res/menu/manageaccounts_context.xml +++ b/res/menu/manageaccounts_context.xml @@ -5,20 +5,20 @@ android:id="@+id/mgmt_account_edit" android:icon="@drawable/ic_action_edit" android:showAsAction="always" - android:title="Edit Account"/> + android:title="@string/mgmt_account_edit"/> <item android:id="@+id/mgmt_account_delete" android:icon="@drawable/ic_action_delete" android:showAsAction="always" - android:title="Delete"/> + android:title="@string/mgmt_account_delete"/> <item android:id="@+id/mgmt_account_disable" android:showAsAction="never" - android:title="Temporarily disable"/> + android:title="@string/mgmt_account_disable"/> <item android:id="@+id/mgmt_account_enable" android:showAsAction="never" - android:title="Enable" + android:title="@string/mgmt_account_enable" android:visible="false"/> <item android:id="@+id/mgmt_account_announce_pgp" diff --git a/res/menu/newconversation.xml b/res/menu/newconversation.xml index ca6a57e1..4f9b6da0 100644 --- a/res/menu/newconversation.xml +++ b/res/menu/newconversation.xml @@ -1,12 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" > <item - android:id="@+id/action_refresh_contacts" - android:orderInCategory="10" - android:showAsAction="always" - android:icon="@drawable/ic_action_refresh" - android:title="Refresh contact list" /> - <item android:id="@+id/action_accounts" android:orderInCategory="90" android:showAsAction="never" diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 4d7bc6cc..76e32e7d 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -63,7 +63,7 @@ <string name="send_message_to_conference">Enviar missatge a conferència</string> <string name="send_plain_text_message">Enviar missatge de text</string> <string name="send_otr_message">Enviar missatge xifrat amb OTR</string> - <string name="send_pgp_message">Enviar missatge xifrat amb openPGP</string> + <string name="send_pgp_message">Enviar missatge xifrat amb OpenPGP</string> <string name="your_nick_has_been_changed">El teu sobrenom s\'ha modificat</string> <string name="download_image">Descarregar imatge</string> <string name="error_loading_image">Error carregant imatge (Fitxer no trobat)</string> @@ -84,14 +84,14 @@ <string name="restart">Reiniciar</string> <string name="install">Instal·lar</string> <string name="offering">oferint…</string> - <string name="no_pgp_key">Clau openPGP no trobada</string> - <string name="contact_has_no_pgp_key">Conversations no ha pogut xifrar els teus missatges perquè el teu contacte no està anunciant la seva clau pública.\n\n<small>Si us plau, demana al teu contacte que configuri openPGP.</small></string> + <string name="no_pgp_key">Clau OpenPGP no trobada</string> + <string name="contact_has_no_pgp_key">Conversations no ha pogut xifrar els teus missatges perquè el teu contacte no està anunciant la seva clau pública.\n\n<small>Si us plau, demana al teu contacte que configuri OpenPGP.</small></string> <string name="encrypted_message_received"><i>Missatge xifrat rebut. Prem per desxifrar i veure-ho.</i></string> <string name="encrypted_image_received"><i>Imatge xifrada rebuda. Prem per desxifrar i veure-la.</i></string> <string name="image_file"><i>Imatge rebuda. Prem per veure</i></string> <string name="otr_file_transfer">Xifratge amb OTR no disponible</string> - <string name="otr_file_transfer_msg">Malauradament el xifratge amb OTR no està disponible per transferència de fitxers. Pots seleccionar xifratge amb openPGP o no usar xifratge.</string> - <string name="use_pgp_encryption">Usa xifratge amb openPGP</string> + <string name="otr_file_transfer_msg">Malauradament el xifratge amb OTR no està disponible per transferència de fitxers. Pots seleccionar xifratge amb OpenPGP o no usar xifratge.</string> + <string name="use_pgp_encryption">Usa xifratge amb OpenPGP</string> <string name="pref_xmpp_resource">Recursos XMPP</string> <string name="pref_xmpp_resource_summary">El nom que identifica aquest client amb</string> <string name="pref_accept_files">Acceptar fitxers</string> @@ -109,7 +109,7 @@ <string name="pref_notification_grace_period_summary">Desactiva les notificacions durant un breu termini després de rebre una còpia de missatges carbon</string> <string name="pref_ui_options">Opcions de UI</string> <string name="pref_use_phone_self_picture">Utilitza l\'avatar de l\'usuari del mòbil</string> - <string name="pref_use_phone_sefl_picture_summary">Podries no diferenciar quin compte estàs usant a la conversa</string> + <string name="pref_use_phone_self_picture_summary">Podries no diferenciar quin compte estàs usant a la conversa</string> <string name="pref_conference_name">Nom de la conferència</string> <string name="pref_conference_name_summary">Utilitza l\'assumpte de la sala per identificar Conferències</string> <string name="pref_advanced_options">Opcions avançades</string> diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 719d769c..9a11ca6a 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -4,14 +4,18 @@ <string name="app_name">Conversations</string> <string name="action_settings">Einstellungen</string> <string name="action_add">Neue Unterhaltung</string> - <string name="action_accounts">Accounts verwalten</string> + <string name="action_accounts">Konten verwalten</string> + <string name="action_refresh">Kontaktliste neu laden</string> <string name="action_end_conversation">Unterhaltung beenden</string> <string name="action_contact_details">Kontaktdetails</string> <string name="action_muc_details">Konferenzdetails</string> <string name="action_secure">Verschlüsselte Unterhaltung</string> <string name="action_add_account">Account hinzufügen</string> + <string name="action_edit_contact">Name bearbeiten</string> + <string name="action_delete_contact">Aus Kontaktliste entfernen</string> <string name="title_activity_contacts">Kontakte</string> <string name="just_now">gerade</string> + <string name="minutes_ago">vor %d Minuten</string> <string name="sending">senden…</string> <string name="announce_pgp">PGP Ankündigung erneuern</string> <string name="encrypted_message">Entschlüssle Nachricht. Bitte warten…</string> @@ -24,7 +28,7 @@ <string name="remove_contact_text">Möchtest du %s von deiner Kontaktliste enfernen? Die Unterhaltung mit diesem Kontakt wird dabei nicht entfernt.</string> <string name="untrusted_cert_hint">Der Server %s hat Dir ein unbekanntes, möglicherweise selbstsigniertes Zertifikat geschickt.</string> <string name="account_info">Server Info</string> - <string name="register_account">Neuen Account auf dem Server erstellen</string> + <string name="register_account">Neues Konto auf dem Server erstellen</string> <string name="share_with">Teile mit…</string> <string name="ask_again"><u>Klick um noch einmal zu fragen</u></string> <string name="show_otr_key">OTR Fingerabdruck</string> @@ -41,12 +45,12 @@ <string name="account_offline">Account offline</string> <string name="cant_invite_while_offline">Du musst online sein um andere Leute zu einer Konferenz einzuladen</string> <string name="crash_report_title">Conversations ist abgestürzt</string> - <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string> + <string name="crash_report_message">Durch das Einsenden von Fehlerberichten hilfst du der stetigen Verbesserung von Conversations.\n<b>Achtung:</b> Dies wird einen von deinen XMPP Konten benutzen um den Entwickler zu kontaktieren.</string> <string name="send_now">Jetzt abschicken</string> <string name="send_never">Nie mehr nachfragen</string> - <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Account</string> - <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Accounts</string> - <string name="touch_to_fix">Drücke hier um den Account zu verwalten</string> + <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string> + <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konto</string> + <string name="touch_to_fix">Drücke hier um das Konto zu verwalten</string> <string name="attach_file">Datei anfügen</string> <string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string> <string name="add_contact">Kontakt hinzufügen</string> @@ -63,17 +67,17 @@ <string name="send_message_to_conference">Nachricht an Konferenz schicken</string> <string name="send_plain_text_message">Unverschlüsselt schreiben</string> <string name="send_otr_message">OTR-verschlüsselt schreiben</string> - <string name="send_pgp_message">openPGP-verschlüsselt schreiben</string> + <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string> <string name="your_nick_has_been_changed">Dein Nickname wurde geändert</string> <string name="download_image">Bild herunter laden</string> <string name="error_loading_image">Fehler beim laden des Bildes. (Datei wurde nicht gefunden)</string> <string name="image_offered_for_download"><i>Bild Datei zum Download angeboten</i></string> <string name="not_connected">Nicht verbunden</string> - <string name="you_are_offline">Du musst online sein um %s zu verschicken. Dein Account der mit dieser Unterhaltung verbunden ist aber gerade offline.</string> + <string name="you_are_offline">Du musst online sein um %s zu verschicken. Das mit dieser Unterhaltung verbundene Konto ist gerade offline.</string> <string name="you_are_offline_blank">Du kannst diese Aktion nicht ausführen so lange du offline bist.</string> <string name="files">Dateien</string> <string name="otr_messages">OTR-verschlüsselte Nachrichten</string> - <string name="manage_account">Account verwalten</string> + <string name="manage_account">Konto verwalten</string> <string name="contact_offline">Dein Kontakt ist offline</string> <string name="contact_offline_otr">Dein Kontakt muss online sein um OTR-verschlüsselte Nachrichten zu empfangen. Möchtest Du die Nachricht gerne im Klartext übertragen?</string> <string name="contact_offline_file">Der Kontakt muss online sein um Datein zu empfangen.</string> @@ -84,14 +88,14 @@ <string name="restart">Neustarten</string> <string name="install">Installieren</string> <string name="offering">angeboten…</string> - <string name="no_pgp_key">Kein openPGP Schlüssel gefunden</string> - <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage deine Nachrichten zu verschlüsseln weil dein Kontakt sein oder ihren Schlüssel nicht preis gibt.\n\n<small>Bitte sag deinem Kontakt er oder sie möge bitte openPGP einrichten.</small></string> + <string name="no_pgp_key">Kein OpenPGP Schlüssel gefunden</string> + <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage deine Nachrichten zu verschlüsseln weil dein Kontakt sein oder ihren Schlüssel nicht preis gibt.\n\n<small>Bitte sag deinem Kontakt er oder sie möge bitte OpenPGP einrichten.</small></string> <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier um sie anzuzeigen und zu entschlüsseln.</i></string> <string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier um es anzuzeigen und zu entschlüsseln.</i></string> <string name="image_file"><i>Bild erhalten. Drücke hier um es anzuzeigen.</i></string> <string name="otr_file_transfer">OTR-Verschlüsselung nicht verfügbar</string> - <string name="otr_file_transfer_msg">Es ist nicht möglich Datein mitels OTR zu verschlüsseln. Du kannst entweder openPGP wählen oder die Datei nicht verschlüsseln.</string> - <string name="use_pgp_encryption">openPGP verwenden</string> + <string name="otr_file_transfer_msg">Es ist nicht möglich Datein mitels OTR zu verschlüsseln. Du kannst entweder OpenPGP wählen oder die Datei nicht verschlüsseln.</string> + <string name="use_pgp_encryption">OpenPGP verwenden</string> <string name="pref_xmpp_resource">XMPP resource</string> <string name="pref_xmpp_resource_summary">Der Name mit dem sich der Client selber identifiziert</string> <string name="pref_accept_files">Dateiannahme</string> @@ -109,7 +113,7 @@ <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach erhalt einer Nachricht die von einem anderen Client von Dir kommt.</string> <string name="pref_ui_options">Aussehen</string> <string name="pref_use_phone_self_picture">Benutze Dein Kontaktbild</string> - <string name="pref_use_phone_sefl_picture_summary">Wenn du mehrere Accounts hast bist du eventuell nicht mehr in der Lage diese auseinander zu halten.</string> + <string name="pref_use_phone_self_picture_summary">Wenn du mehrere Accounts hast bist du eventuell nicht mehr in der Lage diese auseinander zu halten.</string> <string name="pref_conference_name">Konferenznamen</string> <string name="pref_conference_name_summary">Benutze das Thema der Konferenz als Name in der Überschicht</string> <string name="pref_advanced_options">Erweiterte Optionen</string> @@ -127,5 +131,43 @@ <string name="keys">Schlüssel</string> <string name="send_presence_updates">Sende Online Status</string> <string name="receive_presence_updates">Empfange Online Status</string> - <string name="preemptively_grant">Preemptively grant subscription request</string> -</resources> + <string name="ask_for_presence_updates">Frage um Erlaubnis den Online Status sehen zu dürfen</string> + <string name="attach_choose_picture">Photo auswählen</string> + <string name="attach_take_picture">Photo aufnehmen</string> + <string name="preemptively_grant">Erlaube Statusanfrage vorab</string> + <string name="error_not_an_image_file">Die ausgewählte Datei ist kein Bild</string> + <string name="error_compressing_image">Fehler beim umwandeln des Bildes</string> + <string name="error_file_not_found">Datei nicht gefunden</string> + <string name="error_io_exception">Allgemeiner Fehler. Vielleicht hast du keinen Speicherplatz mehr?</string> + <string name="error_security_exception_during_image_copy">Die App mit der du das Bild ausgesucht hast, hat uns keine Rechte eingeräumt das Bild zu betrachten.\n\n<small>Benutze einen anderen Dateimanager</small></string> + <string name="account_status">Status:</string> + <string name="account_status_unknown">Unbekannt</string> + <string name="account_status_disabled">Vorübergehend abgeschaltet</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Verbinde\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Ungültige Zugangsdaten</string> + <string name="account_status_not_found">Server nicht gefunden</string> + <string name="account_status_no_internet">Keine Internetverbindung</string> + <string name="account_status_requires_tls">Server benötigt TLS</string> + <string name="account_status_error">Unbekanntes Zertifikat</string> + <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string> + <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string> + <string name="account_status_regis_success">Registrierung abgeschlossen</string> + <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string> + <string name="certif_no_trust">Nicht verbinden</string> + <string name="certif_trust">Dem Zertifikat vertrauen</string> + <string name="encryption_choice_none">Klartext</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Konto bearbeiten</string> + <string name="mgmt_account_delete">Löschen</string> + <string name="mgmt_account_disable">Vorübergehend abschalten</string> + <string name="mgmt_account_enable">Anschalten</string> + <string name="attach_record_voice">Sprache aufzeichnen</string> + <string name="account_settings">Kontoeinstellungen</string> + <string name="save">Speichern</string> + <string name="passwords_do_not_match">Passwörter stimmen nicht überein</string> + <string name="invalid_jid">Ungültige Jabber ID</string> + +</resources>
\ No newline at end of file diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 12e255e8..6f1715ba 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -5,35 +5,65 @@ <string name="action_settings">Ajustes</string> <string name="action_add">Nueva conversación</string> <string name="action_accounts">Gestionar cuentas</string> + <string name="action_refresh">Actualizar lista de contactos</string> <string name="action_end_conversation">Terminar conversación</string> <string name="action_contact_details">Detalles del contacto</string> <string name="action_muc_details">Detalles de la conferencia</string> <string name="action_secure">Conversación segura</string> <string name="action_add_account">Añadir cuenta</string> + <string name="action_edit_contact">Editar contacto</string> + <string name="action_delete_contact">Eliminar contacto de la lista</string> + <string name="action_add_phone_book">Añadir a contactos del teléfono</string> <string name="title_activity_contacts">Contactos</string> + <string name="title_activity_manage_accounts">Gestionar Cuentas</string> + <string name="title_activity_settings">Ajustes</string> + <string name="title_activity_conference_details">Detalles de Conferencia</string> + <string name="title_activity_contact_details">Detalles del Contacto</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Compartir con Conversación</string> <string name="just_now">ahora</string> + <string name="minute_ago">1 minuto</string> + <string name="minutes_ago">%d minutos</string> + <string name="unread_conversations">conversaciones por leer</string> <string name="sending">enviando…</string> <string name="announce_pgp">Renovar anuncio PGP</string> <string name="encrypted_message">Desencriptando mensaje. Espera por favor…</string> <string name="conference_details">Detalles de la conferencia</string> <string name="nick_in_use">El apodo ya está en uso</string> + <string name="admin">Administrador</string> + <string name="owner">Propietario</string> <string name="moderator">Moderador</string> <string name="participant">Participante</string> <string name="visitor">Visitante</string> <string name="enter_new_name">Introduce un nuevo nombre:</string> - <string name="remove_contact_text">¿Quieres eliminar a %s de tu lista?. La conversación asociada a esta cuenta no se eliminará.</string> + <string name="remove_contact_text">¿Quieres eliminar a %s de tu lista? La conversación asociada a esta cuenta no se eliminará.</string> <string name="untrusted_cert_hint">El servidor %s presenta un certificado no confiable, posiblemente auto firmado.</string> <string name="account_info">Información del servidor</string> <string name="register_account">Registrar nueva cuenta en servidor</string> <string name="share_with">Compartir con</string> <string name="ask_again"><u>Pulsa para preguntar otra vez</u></string> - <string name="show_otr_key">Huella dactilar OTR</string> - <string name="no_otr_fingerprint">No se ha generado una huella dactilar OTR. Simplemente continúa y comienza una conversación encriptada</string> + <string name="show_otr_key">Clave OTR</string> + <string name="no_otr_fingerprint">No se ha generado una clave OTR. Continúa y comienza una conversación encriptada</string> <string name="start_conversation">Comenzar conversación</string> <string name="invite_contacts">Invitar contactos</string> <string name="invite_contacts_to_existing">Invitar a conferencia existente</string> <string name="new_conference">Crear nueva conferencia</string> + <string name="new_contact">Crear nuevo contacto</string> + <string name="contacts">Contactos</string> + <string name="search_jabber_id">Busca o escribe identificador Jabber</string> + <string name="choose_account">Seleccionar cuenta</string> + <string name="multi_user_conference">Conferencia multiusuario</string> + <string name="trying_join_conference">¿Estás intentando unirte a una conferencia?</string> <string name="cancel">Cancelar</string> + <string name="add">Añadir</string> + <string name="edit">Editar</string> + <string name="delete">Eliminar</string> + <string name="save">Guardar</string> + <string name="yes">Sí</string> + <string name="no">No</string> + <string name="ok">OK</string> + <string name="done">Hecho</string> + <string name="hide">Ocultar</string> <string name="create_invite">Crear \u0026 Invitar</string> <string name="new_conference_explained">¿Quieres crear una nueva conferencia con una dirección generada aleatoriamente e invitar a los contactos seleccionados a ella?</string> <string name="no_open_mucs">No hay conferencias existentes</string> @@ -41,13 +71,13 @@ <string name="account_offline">Cuenta desconectada</string> <string name="cant_invite_while_offline">Debes estar conectado para invitar a contactos a la conferencia</string> <string name="crash_report_title">Conversations se ha detenido.</string> - <string name="crash_report_message">Enviando volcados de pilas ayudas al desarrollo de Conversations\n<b>Aviso:</b> Esto usará tu cuenta XMPP para enviar el volcado de pila al desarrollador.</string> + <string name="crash_report_message">Si envías un informe de fallos ayudas al desarrollo de Conversations\n<b>Aviso:</b> Esto usará tu cuenta XMPP para enviar los registros de error al desarrollador.</string> <string name="send_now">Enviar ahora</string> <string name="send_never">No preguntar de nuevo</string> <string name="problem_connecting_to_account">No se ha podido conectar a la cuenta</string> <string name="problem_connecting_to_accounts">No se ha podido conectar a múltiples cuentas</string> <string name="touch_to_fix">Pulsa aquí para gestionar tus cuentas</string> - <string name="attach_file">Enviar archivo</string> + <string name="attach_file">Adjuntar</string> <string name="not_in_roster">El contacto no está en tu lista. ¿Quieres añadirlo?</string> <string name="add_contact">Añadir contacto</string> <string name="send_failed">Error al enviar</string> @@ -60,23 +90,19 @@ <string name="delete_messages">Borrar mensajes</string> <string name="also_end_conversation">Terminar esta conversación más tarde</string> <string name="choose_presence">Selecciona recurso del contacto</string> - <string name="send_message_to_conference">Enviar mensaje a conferencia</string> <string name="send_plain_text_message">Enviar mensaje de texto</string> <string name="send_otr_message">Enviar mensaje encriptado con OTR</string> - <string name="send_pgp_message">Enviar mensaje encriptado con openPGP</string> + <string name="send_pgp_message">Enviar mensaje encriptado con OpenPGP</string> <string name="your_nick_has_been_changed">Tu apodo se ha modificado</string> <string name="download_image">Descargar imagen</string> <string name="error_loading_image">Error cargando imagen (Archivo no encontrado)</string> <string name="image_offered_for_download"><i>Archivo de imagen ofrecido para descarga</i></string> <string name="not_connected">No conectado</string> - <string name="you_are_offline">Debes estar conectado para enviar %s pero tu cuenta asociada a esta conversación está desconectada.</string> - <string name="you_are_offline_blank">No puedes ejecutar esta acción estando desconectado</string> - <string name="files">archivos</string> <string name="otr_messages">Mensajes encriptados con OTR</string> <string name="manage_account">Gestionar cuenta</string> - <string name="contact_offline">Tu contacto está desconectado</string> - <string name="contact_offline_otr">Desgraciadamente no es posible enviar mensajes encriptados con OTR a un contacto desconectado.\n¿Quieres enviar el mensaje en texto plano?</string> - <string name="contact_offline_file">Desgraciadamente no es posible enviar archivos a un contacto desconectado.</string> + <string name="contact_offline">El contacto está desconectado</string> + <string name="contact_offline_otr">No es posible enviar mensajes encriptados con OTR a un contacto desconectado.\n¿Quieres enviar el mensaje en texto plano?</string> + <string name="contact_offline_file">No es posible enviar archivos a un contacto desconectado.</string> <string name="send_unencrypted">Enviar sin encriptar</string> <string name="decryption_failed">Falló la desencriptación. Tal vez no tengas la clave privada apropiada.</string> <string name="openkeychain_required">OpenKeychain</string> @@ -84,18 +110,21 @@ <string name="restart">Reiniciar</string> <string name="install">Instalar</string> <string name="offering">ofreciendo…</string> - <string name="no_pgp_key">Clave openPGP no encontrada</string> - <string name="contact_has_no_pgp_key">Conversations no ha podido encriptar tus mensajes porque tu contacto no está anunciando su clave publica.\n\n<small>Por favor, pide a tu contacto que configure openPGP.</small></string> - <string name="encrypted_message_received"><i>Mensaje encriptado recibido. Pulsa para desencriptar y ver.</i></string> - <string name="encrypted_image_received"><i>Imagen encriptada recibida. Pulsa para desencriptar y ver.</i></string> + <string name="waiting">esperando…</string> + <string name="no_pgp_key">Clave OpenPGP no encontrada</string> + <string name="contact_has_no_pgp_key">Conversations no ha podido encriptar tus mensajes porque el contacto no está anunciando su clave publica.\n\n<small>Por favor, pide a tu contacto que configure OpenPGP.</small></string> + <string name="no_pgp_keys">Claves OpenPGP no encontradas</string> + <string name="contacts_have_no_pgp_keys">Conversations no ha podido encriptar tus mensajes porque tus contactos no están anunciando su clave publica.\n\n<small>Por favor, pide a tus contactos que configuren OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Mensaje encriptado recibido. Pulsa para ver.</i></string> + <string name="encrypted_image_received"><i>Imagen encriptada recibida. Pulsa para ver.</i></string> <string name="image_file"><i>Imagen recibida. Pulsa para ver</i></string> <string name="otr_file_transfer">Encriptación con OTR no disponible</string> - <string name="otr_file_transfer_msg">Desafortunadamente la encriptación con OTR no está disponible para transferencia de archivos. Puedes selecionar encriptación con openPGP o no usar encriptación.</string> - <string name="use_pgp_encryption">Usa encriptación con openPGP</string> + <string name="otr_file_transfer_msg">La encriptación con OTR no está disponible para transferencia de archivos. Puedes selecionar encriptación con OpenPGP o no usar encriptación.</string> + <string name="use_pgp_encryption">Usa encriptación con OpenPGP</string> <string name="pref_xmpp_resource">Recurso</string> <string name="pref_xmpp_resource_summary">El nombre que identifica el cliente que estás utilizando</string> <string name="pref_accept_files">Aceptar archivos</string> - <string name="pref_accept_files_summary">Automáticamente aceptar archivos menores que…</string> + <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que…</string> <string name="pref_notification_settings">Ajustes de notificación</string> <string name="pref_notifications">Notificaciones</string> <string name="pref_notifications_summary">Notifica cuando llega un nuevo mensaje</string> @@ -108,16 +137,111 @@ <string name="pref_notification_grace_period">Notificaciones Carbons</string> <string name="pref_notification_grace_period_summary">Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon</string> <string name="pref_ui_options">Opciones de interfaz</string> - <string name="pref_use_phone_self_picture">Usar foto de contacto del teléfono</string> - <string name="pref_use_phone_sefl_picture_summary">Podrías no ser capaz de distinguir que cuenta está utlizando en una conversación</string> + <string name="pref_use_phone_self_picture">Usar foto del teléfono</string> + <string name="pref_use_phone_self_picture_summary">Si tienes más de una cuenta configurada podrías no distinguir que cuenta está utilizando en una conversación</string> <string name="pref_conference_name">Nombre de la conferencia</string> <string name="pref_conference_name_summary">Usa el nombre de la sala para identificar Conferencias</string> <string name="pref_advanced_options">Opciones avanzadas</string> <string name="pref_never_send_crash">Nunca enviar informe de fallos</string> - <string name="pref_never_send_crash_summary">Enviando volcados de pilas ayudas al desarrollo de Conversations</string> + <string name="pref_never_send_crash_summary">Si envías registros de error ayudas al desarrollo de Conversations</string> + <string name="pref_confirm_messages">Confirmar Mensajes</string> + <string name="pref_confirm_messages_summary">Permitir a tus contactos saber cuando recibes y lees un mensaje</string> + <string name="pref_show_last_seen">Última vez</string> + <string name="pref_show_last_seen_summary">Muestra la última vez que un contacto ha sido visto conectado</string> <string name="openpgp_error">OpenKeychain reportó un error</string> - <string name="error_decrypting_file">I/O Error desencriptando fichero</string> + <string name="error_decrypting_file">Error desencriptando fichero</string> <string name="error_copying_image_file">Error copiando archivo de imagen.</string> <string name="accept">Aceptar</string> <string name="error">Ha ocurrido un error</string> + <string name="pref_grant_presence_updates">Suscripción de presencia</string> + <string name="pref_grant_presence_updates_summary">De forma automática solicitar y conceder suscripciones de presencia de los contactos que has creado</string> + <string name="subscriptions">Suscripciones</string> + <string name="subscription_updated">Suscripción actualizada</string> + <string name="your_account">Tu cuenta</string> + <string name="keys">Claves</string> + <string name="send_presence_updates">Enviar actualizaciones de presencia</string> + <string name="receive_presence_updates">Recibir actualizaciones de presencia</string> + <string name="ask_for_presence_updates">Solicitar actualizaciones de presencia</string> + <string name="asked_for_presence_updates">Solictida actualizaciones de presencia</string> + <string name="attach_choose_picture">Seleccionar imagen</string> + <string name="attach_take_picture">Hacer foto</string> + <string name="preemptively_grant">De forma automática conceder solicitud de suscripción</string> + <string name="error_not_an_image_file">El archivo seleccionado no es una imagen</string> + <string name="error_compressing_image">Error comprimiendo el archivo de imagen</string> + <string name="error_file_not_found">Archivo no encontrado</string> + <string name="error_io_exception">Error general. ¿Puede que no tengas espacio en disco?</string> + <string name="error_security_exception_during_image_copy">La aplicación que usas para seleccionar imágenes no proporciona suficientes permisos para leer el archivo.\n\n<small>Utiliza un explorador de ficheros diferente para seleccionar la imagen</small></string> + <string name="account_status">Estado:</string> + <string name="account_status_unknown">Desconocido</string> + <string name="account_status_disabled">Deshabilitado temporalmente</string> + <string name="account_status_online">Conectado</string> + <string name="account_status_connecting">Conectando\u2026</string> + <string name="account_status_offline">Desconectado</string> + <string name="account_status_unauthorized">No autorizado</string> + <string name="account_status_not_found">Servidor no encontrado</string> + <string name="account_status_no_internet">Sin conectividad</string> + <string name="account_status_requires_tls">El servidor requiere TLS</string> + <string name="account_status_error">Certificado no confiable</string> + <string name="account_status_regis_fail">Error en el registro</string> + <string name="account_status_regis_conflict">El identificador ya está en uso</string> + <string name="account_status_regis_success">Registro completado</string> + <string name="account_status_regis_not_sup">El servidor no soporta registros</string> + <string name="certif_no_trust">No conectar</string> + <string name="certif_trust">Confiar en el certificado</string> + <string name="encryption_choice_none">Texto plano</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Editar cuenta</string> + <string name="mgmt_account_delete">Eliminar cuenta</string> + <string name="mgmt_account_disable">Deshabilitar temporalmente</string> + <string name="mgmt_account_enable">Habilitar</string> + <string name="mgmt_account_are_you_sure">¿Estás seguro?</string> + <string name="mgmt_account_delete_confirm_text">Si eliminas tu cuenta tu historial completo de conversaciones se perderá</string> + <string name="mgmt_account_account_offline">La cuenta está desconectada</string> + <string name="attach_record_voice">Grabar audio</string> + <string name="account_settings">Configuración de cuenta</string> + <string name="account_settings_jabber_id">Identificador Jabber:</string> + <string name="account_settings_password">Contraseña:</string> + <string name="account_settings_example_jabber_id">usuario@ejemplo.com</string> + <string name="account_settings_confirm_password">Confirmar contraseña:</string> + <string name="password">Contraseña</string> + <string name="confirm_password">Confirmar contraseña</string> + <string name="passwords_do_not_match">Las contraseñas no coinciden</string> + <string name="invalid_jid">El identificador no es un identificador de Jabber válido</string> + <string name="error_out_of_memory">Sin memoria. La imagen es demasiado grande</string> + <string name="add_phone_book_text">¿Te gustaría añadir a %s a tus contactos del teléfono?</string> + <string name="contact_status_online">Disponible</string> + <string name="contact_status_free_to_chat">Hablador</string> + <string name="contact_status_away">Ausente</string> + <string name="contact_status_extended_away">Ausencia extendida</string> + <string name="contact_status_do_not_disturb">No molestar</string> + <string name="contact_status_offline">Desconectado</string> + <string name="muc_details_conference">Conferencia</string> + <string name="muc_details_conference_subject">Asunto de la Conferencia</string> + <string name="muc_details_your_nickname">Tu apodo</string> + <string name="muc_details_other_members">Otros Miembros</string> + <string name="subscription_not_updated_offline">Cuenta desconectada. No se puede actualizar suscripciones</string> + <string name="share_with_active_conversations">Conversaciones Activas</string> + <string name="server_info_statistics">Estadísticas</string> + <string name="server_info_connection_age">Tiempo de conexión</string> + <string name="server_info_session_age">Duración de la sesión</string> + <string name="server_info_packets_sent">Paquetes enviados</string> + <string name="server_info_packets_received">Paquetes recibidos</string> + <string name="server_info_connected_accounts">Cuentas conectadas</string> + <string name="server_info_server_features">Características del Servidor</string> + <string name="server_info_roster_versioning">Roster Versioning</string> + <string name="server_info_carbon_messages">Mensajes Carbon</string> + <string name="server_info_stream_management">Stream Management</string> + <string name="hours">horas</string> + <string name="mins">mins</string> + <string name="missing_public_keys">Se han perdido las claves de anuncio públicas</string> + <string name="last_seen_now">Visto última vez ahora</string> + <string name="last_seen_min">Visto última vez hace 1 minuto</string> + <string name="last_seen_mins">Visto última vez hace %d minutos</string> + <string name="last_seen_hour">Visto última vez hace 1 hora</string> + <string name="last_seen_hours">Visto última vez hace %d horas</string> + <string name="last_seen_day">Visto última vez hace 1 día</string> + <string name="last_seen_days">Visto última vez hace %d días</string> + <string name="never_seen">Nunca visto</string> + <string name="install_openkeychain">Mensaje encriptado. Por favor instala OpenKeychain para desencriptar.</string> </resources>
\ No newline at end of file diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 8d78a010..d55caf5b 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -1,248 +1,252 @@ <?xml version="1.0" encoding="UTF-8"?> <resources> - <string name="app_name">Conversations</string> - <string name="action_settings">Ezarpenak</string> - <string name="action_add">Elkarrizketa berria</string> - <string name="action_accounts">Kontuak kudeatu</string> - <string name="action_refresh">Kontaktuen zerrenda freskatu</string> - <string name="action_end_conversation">Elkarrizketa hau amaitu</string> - <string name="action_contact_details">Kontaktuaren xehetasunak</string> - <string name="action_muc_details">Konferentziaren xehetasunak</string> - <string name="action_secure">Elkarrizketa segurua</string> - <string name="action_add_account">Kontua gehitu</string> - <string name="action_edit_contact">Izena editatu</string> - <string name="action_add_phone_book">Telefono kontaktuetara gehitu</string> - <string name="action_delete_contact">Zerrendatik ezabatu</string> - <string name="title_activity_contacts">Kontaktuak</string> - <string name="title_activity_manage_accounts">Kontuak kudeatu</string> - <string name="title_activity_settings">Ezarpenak</string> - <string name="title_activity_conference_details">Konferentziaren xehetasunak</string> - <string name="title_activity_contact_details">Kontaktuaren xehetasunak</string> - <string name="title_activity_conversations">Conversations</string> - <string name="title_activity_sharewith">Elkarrizketa batekin partekatu</string> - <string name="just_now">orain</string> - <string name="minute_ago">1 min lehenago</string> - <string name="minutes_ago">%d min lehenago</string> - <string name="unread_conversations">irakurri gabeko elkarrizketak</string> - <string name="sending">bidaltzen…</string> - <string name="announce_pgp">PGP iragarpena berritu</string> - <string name="encrypted_message">Mezua desenkriptatzen. Mesedez itxaron…</string> - <string name="conference_details">Konferentziaren xehetasunak</string> - <string name="nick_in_use">Ezizena erabilita dagoeneko</string> - <string name="admin">Administratzailea</string> - <string name="owner">Jabea</string> - <string name="moderator">Moderatzailea</string> - <string name="participant">Parte-hartzailea</string> - <string name="visitor">Bisitaria</string> - <string name="enter_new_name">Sartu izen berri bat:</string> - <string name="remove_contact_text">%s zure zerrendatik ezabatu nahi duzu? Kontu honekin lotutako elkarrizketa ez da ezabatuko.</string> - <string name="untrusted_cert_hint">%s zerbitzariak konfiantzarik gabeko, agian bere buruak izenpetutako, ziurtagiri batekin aurkeztu zaitu.</string> - <string name="account_info">Zerbitzariaren informazioa</string> - <string name="register_account">Kontu berria zerbitzarian erregistratu</string> - <string name="share_with">Honekin partekatu</string> - <string name="ask_again"><u>Sakatu berriz galdetzeko</u></string> - <string name="show_otr_key">OTR hatz-marka</string> - <string name="no_otr_fingerprint">Ez da OTR hatz-markarik sortu. Aurrera joan eta enkriptatutako elkarrizketa hasi</string> - <string name="start_conversation">Elkarrizketa hasi</string> - <string name="invite_contacts">Kontaktuak gonbidatu</string> - <string name="invite_contacts_to_existing">Existitzen den konferentzia batera gonbidatu</string> - <string name="new_conference">Konferentzia berria sortu</string> - <string name="new_contact">Kontaktu berria sortu</string> - <string name="contacts">Kontaktuak</string> - <string name="search_jabber_id">Bilatu edo sartu Jabber ID bat</string> - <string name="choose_account">Kontua hautatu</string> - <string name="multi_user_conference">Erabiltzaile ugariko konferentzia</string> - <string name="trying_join_conference">Are you trying to join a conference?</string> - <string name="cancel">Utzi</string> - <string name="add">Gehitu</string> - <string name="edit">Editatu</string> - <string name="delete">Ezabatu</string> - <string name="save">Gorde</string> - <string name="yes">Bai</string> - <string name="no">Ez</string> - <string name="ok">Ados</string> - <string name="done">Eginda</string> - <string name="hide">Ezkutatu</string> - <string name="create_invite">Sortu \u0026 Gonbidatu</string> - <string name="new_conference_explained">Ausazki sortutako helbide batekin konferentzia berri bat sortu eta hautatutako kontaktuak bertara gonbidatu nahi al dituzu?</string> - <string name="no_open_mucs">Ez da konferentziarik existitzen</string> - <string name="invitation_sent">Gonbidapena bidali da</string> - <string name="account_offline">Kontua lineaz kanpo</string> - <string name="cant_invite_while_offline">Konektatuta egon behar zara jendea konferentzietara gonbidatzeko</string> - <string name="crash_report_title">Conversations gelditu da</string> - <string name="crash_report_message">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu\n<b>Abisua:</b> Honek zure XMPP kontua erabiliko du garatzaileari akats harraska bidaltzeko.</string> - <string name="send_now">Bidali orain</string> - <string name="send_never">Ez galdetu berriz</string> - <string name="problem_connecting_to_account">Ezin izan da kontura konektatu</string> - <string name="problem_connecting_to_accounts">Ezin izan da hainbat kontuetara konektatu</string> - <string name="touch_to_fix">Ukitu hemen zure kontuak kudeatzeko</string> - <string name="attach_file">Fitxategia erantsi</string> - <string name="not_in_roster">Kontaktua ez dago zure zerrendan. Gehitu nahiko al zenuke?</string> - <string name="add_contact">Kontaktua gehitu</string> - <string name="send_failed">huts bidaltzerakoan</string> - <string name="send_rejected">ukatua</string> - <string name="receiving_image">Irudi fitxategia jasotzen. Mesedez itxaron…</string> - <string name="preparing_image">Irudia transmisiorako prestatzen. Mesedez itxaron…</string> - <string name="action_clear_history">Historia garbitu</string> - <string name="clear_conversation_history">Elkarrizketa historia garbitu</string> - <string name="clear_histor_msg">Elkarrizketa honetako mezu guztiak ezabatu nahi al dituzu?\n\n<b>Abisua:</b> Honek ez du beste gailu edo zerbitzarietan gordetako mezuetan eraginik izango.</string> - <string name="delete_messages">Mezuak ezabatu</string> - <string name="also_end_conversation">Elkarrizketa hau geroago amaitu</string> - <string name="choose_presence">Hautatu agerpena kontaktuarentzat</string> - <string name="send_plain_text_message">Testu mezua bidali</string> - <string name="send_otr_message">OTRz enkriptatutako mezua bidali</string> - <string name="send_pgp_message">openPGPz enkriptatutako mezua bidali</string> - <string name="your_nick_has_been_changed">Zure ezizena aldatu da</string> - <string name="download_image">Irudia deskargatu</string> - <string name="error_loading_image">Akatsa irudia kargatzen (fitxategia ez da aurkitu)</string> - <string name="image_offered_for_download"><i>Irudi fitxategia deskargarako eskeinia</i></string> - <string name="not_connected">Ez konektaturik</string> - <string name="otr_messages">OTRz enkriptatutako mezuak</string> - <string name="manage_account">Kontua kudeatu</string> - <string name="contact_offline">Zure kontaktua lineaz kanpo dago</string> - <string name="contact_offline_otr">Zoritxarrez ezin da OTRz enkriptatutako mezurik bidali lineaz kanpo dagoen kontaktu bati.\nMezua testu lauean bidali nahiko al zenuke?</string> - <string name="contact_offline_file">Zoritxarrez ezin da fitxategirik bidali lineaz kanpo dagoen kontaktu bati.</string> - <string name="send_unencrypted">Enkriptatu gabe bidali</string> - <string name="decryption_failed">Desenkriptazioak huts egin du. Agian ez duzu gako pribatu egokia.</string> - <string name="openkeychain_required">OpenKeychain</string> - <string name="openkeychain_required_long">Conversationsek <b>OpenKeychain</b> izeneko hirugarren app bat erabiltzen du mezuak enkriptatu eta desenkriptatzeko eta zure gako publikoak kudeatzeko.\n\nOpenKeychain GPLv3 lizentziapean dago eta F-Droid eta Google Playn eskura daiteke.\n\n<small>(Mesedez ondoren Conversations berrabiarazi)</small></string> - <string name="restart">Berrabiarazi</string> - <string name="install">Instalatu</string> - <string name="offering">eskeintzen…</string> - <string name="waiting">itxaroten…</string> - <string name="no_pgp_key">Ez da openPGP gakorik aurkitu</string> - <string name="contact_has_no_pgp_key">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktua bere gako publikoa jakinarazten ez dagoelako.\n\n<small>Mesedez eskatu ezaiozu zure kontaktuari openPGP konfigura dezan.</small></string> - <string name="no_pgp_keys">Ez da OpenPGP gakorik aurkitu</string> - <string name="contacts_have_no_pgp_keys">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktuak haien gako publikoa jakinarazten ez daudelako.\n\n<small>Mesedez eskatu ezaiezu zure kontakuei OpenPGP konfigura dezaten.</small></string> - <string name="encrypted_message_received"><i>Enkriptatutako mezua jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> - <string name="encrypted_image_received"><i>Enkriptatutako irudia jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> - <string name="image_file"><i>Irudia jaso da. Ukitu ikusteko</i></string> - <string name="otr_file_transfer">OTR enkriptazioa ez dago erabilgarri</string> - <string name="otr_file_transfer_msg">Zoritxarrez OTR enkriptazioa ez dago fitxategi transferentzietarako erabilgarri. OpenPGP edo enkriptaziorik ez erabiltzea aukera dezakezu.</string> - <string name="use_pgp_encryption">OpenPGP enkriptazioa erabili</string> - <string name="pref_xmpp_resource">XMPP baliabidea</string> - <string name="pref_xmpp_resource_summary">Bezero honek bere burua aurkezteko erabiltzen duen izena</string> - <string name="pref_accept_files">Fitxategiak onartu</string> - <string name="pref_accept_files_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu…</string> - <string name="pref_notification_settings">Jakinarazpenen ezarpenak</string> - <string name="pref_notifications">Jakinarazpenak</string> - <string name="pref_notifications_summary">Mezu berri bat heltzerakoan jakinarazi</string> - <string name="pref_vibrate">Dardaratu</string> - <string name="pref_vibrate_summary">Dardaratu ere mezu berri bat heltzerakoan</string> - <string name="pref_sound">Soinua</string> - <string name="pref_sound_summary">Dei-tonua jo jakinarazpenarekin</string> - <string name="pref_conference_notifications">Konferentzien jakinarazpenak</string> - <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string> - <string name="pref_notification_grace_period">Jakinarazpenen grazia epea</string> - <string name="pref_notification_grace_period_summary">Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren</string> - <string name="pref_ui_options">Erabiltzaile-interfazearen aukerak</string> - <string name="pref_use_phone_self_picture">Telefonoaren kontaktu argazkia erabili</string> - <string name="pref_use_phone_sefl_picture_summary">Baliteke elkarrizketa batean zein kontu erabiltzen ari zaren gehiago ez bereiztea</string> - <string name="pref_conference_name">Konferentziaren izena</string> - <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko</string> - <string name="pref_advanced_options">Aukera aurreratuak</string> - <string name="pref_never_send_crash">Gelditze txostenik ez bidali inoiz</string> - <string name="pref_never_send_crash_summary">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu</string> - <string name="pref_confirm_messages">Mezuak egiaztatu</string> - <string name="pref_confirm_messages_summary">Zure kontatuak mezu bat noiz jaso eta irakurri duzun jakin dezan baimendu</string> - <string name="pref_show_last_seen">Azkenengoz ikusia erakutsi</string> - <string name="pref_show_last_seen_summary">Kontaktu bat azken aldiz konektatuta ikusi den ordua erakutsi</string> - <string name="openpgp_error">OpenKeychainek akats baten berri eman du</string> - <string name="error_decrypting_file">Sarrera/Irteera akatsa fitxategia desenkriptatzerakoan</string> - <string name="error_copying_image_file">Huts irudi fitxategia kopiatzerakoan.</string> - <string name="accept">Onartu</string> - <string name="error">Akats bat gertatu da</string> - <string name="pref_grant_presence_updates">Presentzia eguneraketak eman</string> - <string name="pref_grant_presence_updates_summary">Prebentiboki presentzia eguneraketak eman eta eskatu sortu dituzun kontaktuetarako</string> - <string name="subscriptions">Harpidetzak</string> - <string name="subscription_updated">Herpidetza eguneratuta</string> - <string name="your_account">Zure kontua</string> - <string name="keys">Gakoak</string> - <string name="send_presence_updates">Presentzia eguneraketak bidali</string> - <string name="receive_presence_updates">Presentzia eguneraketak jaso</string> - <string name="ask_for_presence_updates">Presentzia eguneraketak eskatu</string> - <string name="asked_for_presence_updates">Presentzia eguneraketak eskatu dira</string> - <string name="attach_choose_picture">Argazkia aukeratu</string> - <string name="attach_take_picture">Argazkia egin</string> - <string name="preemptively_grant">Prebentiboki harpidetza eskaera eman</string> - <string name="error_not_an_image_file">Aukeratu duzun fitxategia ez da irudi bat</string> - <string name="error_compressing_image">Huts irudi fitxategia bihurtzerakoan</string> - <string name="error_file_not_found">Fitxategia ez da aurkitu</string> - <string name="error_io_exception">Sarrera/Irteera akats orokorra. Agian biltegian lekurik gabe gelditu zara?</string> - <string name="error_security_exception_during_image_copy">Irudi hau aukeratzeko erabili duzun aplikazioak ez digu fitxategia irakurtzeko baimen nahikorik eman.\n\n<small>Beste fitxategi kudeatzaile bat erabili ezazu irudia aukeratzeko</small></string> - <string name="account_status">Egoera:</string> - <string name="account_status_unknown">Ezezaguna</string> - <string name="account_status_disabled">Aldi baterako ezgaituta</string> - <string name="account_status_online">Konektatuta</string> - <string name="account_status_connecting">Konektatzen\u2026</string> - <string name="account_status_offline">Lineaz kanpo</string> - <string name="account_status_unauthorized">Ez baimenduta</string> - <string name="account_status_not_found">Zerbitzaria ez da aurkitu</string> - <string name="account_status_no_internet">Konektagarritasunik ez</string> - <string name="account_status_requires_tls">Zerbitzariak TLS behar du</string> - <string name="account_status_error">Konfiantzarik gabeko ziurtagiria</string> - <string name="account_status_regis_fail">Erregistroak huts egin du</string> - <string name="account_status_regis_conflict">Erabiltzaile izena dagoeneko erabilita</string> - <string name="account_status_regis_success">Erregistroa burutu da</string> - <string name="account_status_regis_not_sup">Zerbitzariak ez du erregistratzea onartzen</string> - <string name="certif_no_trust">Ez konektatu</string> - <string name="certif_trust">Ziurtagiriaz fidatu</string> - <string name="encryption_choice_none">Testu laua</string> - <string name="encryption_choice_otr">OTR</string> - <string name="encryption_choice_pgp">openPGP</string> - <string name="mgmt_account_edit">Kontua editatu</string> - <string name="mgmt_account_delete">Ezabatu</string> - <string name="mgmt_account_disable">Aldi baterako ezgaitu</string> - <string name="mgmt_account_enable">Gaitu</string> - <string name="mgmt_account_are_you_sure">Ziur al zaude?</string> - <string name="mgmt_account_delete_confirm_text">Zure kontua ezabatzen baduzu zure elkarrizketa historia guztia galduko da</string> - <string name="mgmt_account_account_offline">Kontua lineaz kanpo dago</string> - <string name="attach_record_voice">Ahotsa grabatu</string> - <string name="account_settings">Kontuaren ezarpenak</string> - <string name="account_settings_jabber_id">Jabber IDa:</string> - <string name="account_settings_password">Pasahitza:</string> - <string name="account_settings_example_jabber_id">erabiltzailea@adibidea.com</string> - <string name="account_settings_confirm_password">Pasahitza egiaztatu:</string> - <string name="password">Pasahitza</string> - <string name="confirm_password">Pasahitza egiaztatu</string> - <string name="passwords_do_not_match">Pasahitzak ez dute bat egiten</string> - <string name="invalid_jid">Hau ez da Jabber ID baliodun bat</string> - <string name="error_out_of_memory">Memoriarik gabe. Irudia handiegia da</string> - <string name="add_phone_book_text">%s zure telefono kontaktu zerrendara gehitu nahi al duzu?</string> - <string name="contact_status_online">konektatuta</string> - <string name="contact_status_free_to_chat">hitzegiteko aske</string> - <string name="contact_status_away">kanpoan</string> - <string name="contact_status_extended_away">luzerako kanpoan</string> - <string name="contact_status_do_not_disturb">ez gogaitu</string> - <string name="contact_status_offline">lineaz kanpo</string> - <string name="muc_details_conference">Konferentzia</string> - <string name="muc_details_conference_subject">Konferentziaren gaia</string> - <string name="muc_details_your_nickname">Zure ezizena</string> - <string name="muc_details_other_members">Beste kideak</string> - <string name="subscription_not_updated_offline">Kontua lineaz kanpo. Ezin izan da harpidetza eguneratu</string> - <string name="share_with_active_conversations">Elkarrizketa aktiboak</string> - <string name="server_info_statistics">Estatistikak</string> - <string name="server_info_connection_age">Konexioaren adina</string> - <string name="server_info_session_age">Saioaren adina</string> - <string name="server_info_packets_sent">Bidalitako paketeak</string> - <string name="server_info_packets_received">Jasotako paketeak</string> - <string name="server_info_connected_accounts">Konektatutako kontuak</string> - <string name="server_info_server_features">Zerbitzariaren ezaugarriak</string> - <string name="server_info_roster_versioning">Roster Versioning</string> - <string name="server_info_carbon_messages">Carbon Messages</string> - <string name="server_info_stream_management">Stream Management</string> - <string name="hours">orduak</string> - <string name="mins">minutuak</string> - <string name="missing_public_keys">Gako publikoen iragarpenak faltan</string> - <string name="last_seen_now">azkenegoz ikusia orain</string> - <string name="last_seen_min">azkenegoz ikusia minutu 1 lehenago</string> - <string name="last_seen_mins">azkenegoz ikusia %d minutu lehenago</string> - <string name="last_seen_hour">azkenegoz ikusia ordu 1 lehenago</string> - <string name="last_seen_hours">azkenegoz ikusia %d ordu lehenago</string> - <string name="last_seen_day">azkenengoz ikusia egun 1 lehenago</string> - <string name="last_seen_days">azkenegoz ikusia %d egun lehenago</string> - <string name="never_seen">inoiz ez ikusia</string> - <string name="install_openkeychain">Mezu enkriptatua. Mesedez instalatu OpenKeychain desenkriptatzeko.</string> - -</resources> + <string name="app_name">Conversations</string> + <string name="action_settings">Ezarpenak</string> + <string name="action_add">Elkarrizketa berria</string> + <string name="action_accounts">Kontuak kudeatu</string> + <string name="action_refresh">Kontaktuen zerrenda freskatu</string> + <string name="action_end_conversation">Elkarrizketa hau amaitu</string> + <string name="action_contact_details">Kontaktuaren xehetasunak</string> + <string name="action_muc_details">Konferentziaren xehetasunak</string> + <string name="action_secure">Elkarrizketa segurua</string> + <string name="action_add_account">Kontua gehitu</string> + <string name="action_edit_contact">Izena editatu</string> + <string name="action_add_phone_book">Telefono kontaktuetara gehitu</string> + <string name="action_delete_contact">Zerrendatik ezabatu</string> + <string name="title_activity_contacts">Kontaktuak</string> + <string name="title_activity_manage_accounts">Kontuak kudeatu</string> + <string name="title_activity_settings">Ezarpenak</string> + <string name="title_activity_conference_details">Konferentziaren xehetasunak</string> + <string name="title_activity_contact_details">Kontaktuaren xehetasunak</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Elkarrizketa batekin partekatu</string> + <string name="just_now">orain</string> + <string name="minute_ago">1 min lehenago</string> + <string name="minutes_ago">%d min lehenago</string> + <string name="unread_conversations">irakurri gabeko elkarrizketak</string> + <string name="sending">bidaltzen…</string> + <string name="announce_pgp">PGP iragarpena berritu</string> + <string name="encrypted_message">Mezua desenkriptatzen. Mesedez itxaron…</string> + <string name="conference_details">Konferentziaren xehetasunak</string> + <string name="nick_in_use">Ezizena erabilita dagoeneko</string> + <string name="admin">Administratzailea</string> + <string name="owner">Jabea</string> + <string name="moderator">Moderatzailea</string> + <string name="participant">Parte-hartzailea</string> + <string name="visitor">Bisitaria</string> + <string name="enter_new_name">Sartu izen berri bat:</string> + <string name="remove_contact_text">%s zure zerrendatik ezabatu nahi duzu? Kontu honekin lotutako elkarrizketa ez da ezabatuko.</string> + <string name="untrusted_cert_hint">%s zerbitzariak konfiantzarik gabeko, agian bere buruak izenpetutako, ziurtagiri batekin aurkeztu zaitu.</string> + <string name="account_info">Zerbitzariaren informazioa</string> + <string name="register_account">Kontu berria zerbitzarian erregistratu</string> + <string name="share_with">Honekin partekatu</string> + <string name="ask_again"><u>Sakatu berriz galdetzeko</u></string> + <string name="show_otr_key">OTR hatz-marka</string> + <string name="no_otr_fingerprint">Ez da OTR hatz-markarik sortu. Aurrera joan eta enkriptatutako elkarrizketa hasi</string> + <string name="start_conversation">Elkarrizketa hasi</string> + <string name="invite_contacts">Kontaktuak gonbidatu</string> + <string name="invite_contacts_to_existing">Existitzen den konferentzia batera gonbidatu</string> + <string name="new_conference">Konferentzia berria sortu</string> + <string name="new_contact">Kontaktu berria sortu</string> + <string name="contacts">Kontaktuak</string> + <string name="search_jabber_id">Bilatu edo sartu Jabber ID bat</string> + <string name="choose_account">Kontua hautatu</string> + <string name="multi_user_conference">Erabiltzaile ugariko konferentzia</string> + <string name="trying_join_conference">Are you trying to join a conference?</string> + <string name="cancel">Utzi</string> + <string name="add">Gehitu</string> + <string name="edit">Editatu</string> + <string name="delete">Ezabatu</string> + <string name="save">Gorde</string> + <string name="yes">Bai</string> + <string name="no">Ez</string> + <string name="ok">Ados</string> + <string name="done">Eginda</string> + <string name="hide">Ezkutatu</string> + <string name="create_invite">Sortu \u0026 Gonbidatu</string> + <string name="new_conference_explained">Ausazki sortutako helbide batekin konferentzia berri bat sortu eta hautatutako kontaktuak bertara gonbidatu nahi al dituzu?</string> + <string name="no_open_mucs">Ez da konferentziarik existitzen</string> + <string name="invitation_sent">Gonbidapena bidali da</string> + <string name="account_offline">Kontua lineaz kanpo</string> + <string name="cant_invite_while_offline">Konektatuta egon behar zara jendea konferentzietara gonbidatzeko</string> + <string name="crash_report_title">Conversations gelditu da</string> + <string name="crash_report_message">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu\n<b>Abisua:</b> Honek zure XMPP kontua erabiliko du garatzaileari akats harraska bidaltzeko.</string> + <string name="send_now">Bidali orain</string> + <string name="send_never">Ez galdetu berriz</string> + <string name="problem_connecting_to_account">Ezin izan da kontura konektatu</string> + <string name="problem_connecting_to_accounts">Ezin izan da hainbat kontuetara konektatu</string> + <string name="touch_to_fix">Ukitu hemen zure kontuak kudeatzeko</string> + <string name="attach_file">Fitxategia erantsi</string> + <string name="not_in_roster">Kontaktua ez dago zure zerrendan. Gehitu nahiko al zenuke?</string> + <string name="add_contact">Kontaktua gehitu</string> + <string name="send_failed">huts bidaltzerakoan</string> + <string name="send_rejected">ukatua</string> + <string name="receiving_image">Irudi fitxategia jasotzen. Mesedez itxaron…</string> + <string name="preparing_image">Irudia transmisiorako prestatzen. Mesedez itxaron…</string> + <string name="action_clear_history">Historia garbitu</string> + <string name="clear_conversation_history">Elkarrizketa historia garbitu</string> + <string name="clear_histor_msg">Elkarrizketa honetako mezu guztiak ezabatu nahi al dituzu?\n\n<b>Abisua:</b> Honek ez du beste gailu edo zerbitzarietan gordetako mezuetan eraginik izango.</string> + <string name="delete_messages">Mezuak ezabatu</string> + <string name="also_end_conversation">Elkarrizketa hau geroago amaitu</string> + <string name="choose_presence">Hautatu agerpena kontaktuarentzat</string> + <string name="send_plain_text_message">Testu mezua bidali</string> + <string name="send_otr_message">OTRz enkriptatutako mezua bidali</string> + <string name="send_pgp_message">OpenPGPz enkriptatutako mezua bidali</string> + <string name="your_nick_has_been_changed">Zure ezizena aldatu da</string> + <string name="download_image">Irudia deskargatu</string> + <string name="error_loading_image">Akatsa irudia kargatzen (fitxategia ez da aurkitu)</string> + <string name="image_offered_for_download"><i>Irudi fitxategia deskargarako eskeinia</i></string> + <string name="not_connected">Ez konektaturik</string> + <string name="otr_messages">OTRz enkriptatutako mezuak</string> + <string name="manage_account">Kontua kudeatu</string> + <string name="contact_offline">Zure kontaktua lineaz kanpo dago</string> + <string name="contact_offline_otr">Zoritxarrez ezin da OTRz enkriptatutako mezurik bidali lineaz kanpo dagoen kontaktu bati.\nMezua testu lauean bidali nahiko al zenuke?</string> + <string name="contact_offline_file">Zoritxarrez ezin da fitxategirik bidali lineaz kanpo dagoen kontaktu bati.</string> + <string name="send_unencrypted">Enkriptatu gabe bidali</string> + <string name="decryption_failed">Desenkriptazioak huts egin du. Agian ez duzu gako pribatu egokia.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversationsek <b>OpenKeychain</b> izeneko hirugarren app bat erabiltzen du mezuak enkriptatu eta desenkriptatzeko eta zure gako publikoak kudeatzeko.\n\nOpenKeychain GPLv3 lizentziapean dago eta F-Droid eta Google Playn eskura daiteke.\n\n<small>(Mesedez ondoren Conversations berrabiarazi)</small></string> + <string name="restart">Berrabiarazi</string> + <string name="install">Instalatu</string> + <string name="offering">eskeintzen…</string> + <string name="waiting">itxaroten…</string> + <string name="no_pgp_key">Ez da OpenPGP gakorik aurkitu</string> + <string name="contact_has_no_pgp_key">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktua bere gako publikoa jakinarazten ez dagoelako.\n\n<small>Mesedez eskatu ezaiozu zure kontaktuari openPGP konfigura dezan.</small></string> + <string name="no_pgp_keys">Ez da OpenPGP gakorik aurkitu</string> + <string name="contacts_have_no_pgp_keys">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktuak haien gako publikoa jakinarazten ez daudelako.\n\n<small>Mesedez eskatu ezaiezu zure kontakuei OpenPGP konfigura dezaten.</small></string> + <string name="encrypted_message_received"><i>Enkriptatutako mezua jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> + <string name="encrypted_image_received"><i>Enkriptatutako irudia jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> + <string name="image_file"><i>Irudia jaso da. Ukitu ikusteko</i></string> + <string name="otr_file_transfer">OTR enkriptazioa ez dago erabilgarri</string> + <string name="otr_file_transfer_msg">Zoritxarrez OTR enkriptazioa ez dago fitxategi transferentzietarako erabilgarri. OpenPGP edo enkriptaziorik ez erabiltzea aukera dezakezu.</string> + <string name="use_pgp_encryption">OpenPGP enkriptazioa erabili</string> + <string name="pref_xmpp_resource">XMPP baliabidea</string> + <string name="pref_xmpp_resource_summary">Bezero honek bere burua aurkezteko erabiltzen duen izena</string> + <string name="pref_accept_files">Fitxategiak onartu</string> + <string name="pref_accept_files_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu…</string> + <string name="pref_notification_settings">Jakinarazpenen ezarpenak</string> + <string name="pref_notifications">Jakinarazpenak</string> + <string name="pref_notifications_summary">Mezu berri bat heltzerakoan jakinarazi</string> + <string name="pref_vibrate">Dardaratu</string> + <string name="pref_vibrate_summary">Dardaratu ere mezu berri bat heltzerakoan</string> + <string name="pref_sound">Soinua</string> + <string name="pref_sound_summary">Dei-tonua jo jakinarazpenarekin</string> + <string name="pref_conference_notifications">Konferentzien jakinarazpenak</string> + <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string> + <string name="pref_notification_grace_period">Jakinarazpenen grazia epea</string> + <string name="pref_notification_grace_period_summary">Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren</string> + <string name="pref_ui_options">Erabiltzaile-interfazearen aukerak</string> + <string name="pref_use_phone_self_picture">Telefonoaren kontaktu argazkia erabili</string> + <string name="pref_use_phone_self_picture_summary">Baliteke elkarrizketa batean zein kontu erabiltzen ari zaren gehiago ez bereiztea</string> + <string name="pref_conference_name">Konferentziaren izena</string> + <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko</string> + <string name="pref_advanced_options">Aukera aurreratuak</string> + <string name="pref_never_send_crash">Gelditze txostenik ez bidali inoiz</string> + <string name="pref_never_send_crash_summary">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu</string> + <string name="pref_confirm_messages">Mezuak egiaztatu</string> + <string name="pref_confirm_messages_summary">Zure kontatuak mezu bat noiz jaso eta irakurri duzun jakin dezan baimendu</string> + <string name="pref_show_last_seen">Azkenengoz ikusia erakutsi</string> + <string name="pref_show_last_seen_summary">Kontaktu bat azken aldiz konektatuta ikusi den ordua erakutsi</string> + <string name="openpgp_error">OpenKeychainek akats baten berri eman du</string> + <string name="error_decrypting_file">Sarrera/Irteera akatsa fitxategia desenkriptatzerakoan</string> + <string name="error_copying_image_file">Huts irudi fitxategia kopiatzerakoan.</string> + <string name="accept">Onartu</string> + <string name="error">Akats bat gertatu da</string> + <string name="pref_grant_presence_updates">Presentzia eguneraketak eman</string> + <string name="pref_grant_presence_updates_summary">Prebentiboki presentzia eguneraketak eman eta eskatu sortu dituzun kontaktuetarako</string> + <string name="subscriptions">Harpidetzak</string> + <string name="subscription_updated">Herpidetza eguneratuta</string> + <string name="your_account">Zure kontua</string> + <string name="keys">Gakoak</string> + <string name="send_presence_updates">Presentzia eguneraketak bidali</string> + <string name="receive_presence_updates">Presentzia eguneraketak jaso</string> + <string name="ask_for_presence_updates">Presentzia eguneraketak eskatu</string> + <string name="asked_for_presence_updates">Presentzia eguneraketak eskatu dira</string> + <string name="attach_choose_picture">Argazkia aukeratu</string> + <string name="attach_take_picture">Argazkia egin</string> + <string name="preemptively_grant">Prebentiboki harpidetza eskaera eman</string> + <string name="error_not_an_image_file">Aukeratu duzun fitxategia ez da irudi bat</string> + <string name="error_compressing_image">Huts irudi fitxategia bihurtzerakoan</string> + <string name="error_file_not_found">Fitxategia ez da aurkitu</string> + <string name="error_io_exception">Sarrera/Irteera akats orokorra. Agian biltegian lekurik gabe gelditu zara?</string> + <string name="error_security_exception_during_image_copy">Irudi hau aukeratzeko erabili duzun aplikazioak ez digu fitxategia irakurtzeko baimen nahikorik eman.\n\n<small>Beste fitxategi kudeatzaile bat erabili ezazu irudia aukeratzeko</small></string> + <string name="account_status">Egoera:</string> + <string name="account_status_unknown">Ezezaguna</string> + <string name="account_status_disabled">Aldi baterako ezgaituta</string> + <string name="account_status_online">Konektatuta</string> + <string name="account_status_connecting">Konektatzen\u2026</string> + <string name="account_status_offline">Lineaz kanpo</string> + <string name="account_status_unauthorized">Ez baimenduta</string> + <string name="account_status_not_found">Zerbitzaria ez da aurkitu</string> + <string name="account_status_no_internet">Konektagarritasunik ez</string> + <string name="account_status_requires_tls">Zerbitzariak TLS behar du</string> + <string name="account_status_error">Konfiantzarik gabeko ziurtagiria</string> + <string name="account_status_regis_fail">Erregistroak huts egin du</string> + <string name="account_status_regis_conflict">Erabiltzaile izena dagoeneko erabilita</string> + <string name="account_status_regis_success">Erregistroa burutu da</string> + <string name="account_status_regis_not_sup">Zerbitzariak ez du erregistratzea onartzen</string> + <string name="certif_no_trust">Ez konektatu</string> + <string name="certif_trust">Ziurtagiriaz fidatu</string> + <string name="encryption_choice_none">Testu laua</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Kontua editatu</string> + <string name="mgmt_account_delete">Ezabatu</string> + <string name="mgmt_account_disable">Aldi baterako ezgaitu</string> + <string name="mgmt_account_enable">Gaitu</string> + <string name="mgmt_account_are_you_sure">Ziur al zaude?</string> + <string name="mgmt_account_delete_confirm_text">Zure kontua ezabatzen baduzu zure elkarrizketa historia guztia galduko da</string> + <string name="mgmt_account_account_offline">Kontua lineaz kanpo dago</string> + <string name="attach_record_voice">Ahotsa grabatu</string> + <string name="account_settings">Kontuaren ezarpenak</string> + <string name="account_settings_jabber_id">Jabber IDa:</string> + <string name="account_settings_password">Pasahitza:</string> + <string name="account_settings_example_jabber_id">erabiltzailea@adibidea.com</string> + <string name="account_settings_confirm_password">Pasahitza egiaztatu:</string> + <string name="password">Pasahitza</string> + <string name="confirm_password">Pasahitza egiaztatu</string> + <string name="passwords_do_not_match">Pasahitzak ez dute bat egiten</string> + <string name="invalid_jid">Hau ez da Jabber ID baliodun bat</string> + <string name="error_out_of_memory">Memoriarik gabe. Irudia handiegia da</string> + <string name="add_phone_book_text">%s zure telefono kontaktu zerrendara gehitu nahi al duzu?</string> + <string name="contact_status_online">konektatuta</string> + <string name="contact_status_free_to_chat">hitzegiteko aske</string> + <string name="contact_status_away">kanpoan</string> + <string name="contact_status_extended_away">luzerako kanpoan</string> + <string name="contact_status_do_not_disturb">ez gogaitu</string> + <string name="contact_status_offline">lineaz kanpo</string> + <string name="muc_details_conference">Konferentzia</string> + <string name="muc_details_conference_subject">Konferentziaren gaia</string> + <string name="muc_details_your_nickname">Zure ezizena</string> + <string name="muc_details_other_members">Beste kideak</string> + <string name="subscription_not_updated_offline">Kontua lineaz kanpo. Ezin izan da harpidetza eguneratu</string> + <string name="share_with_active_conversations">Elkarrizketa aktiboak</string> + <string name="server_info_statistics">Estatistikak</string> + <string name="server_info_connection_age">Konexioaren adina</string> + <string name="server_info_session_age">Saioaren adina</string> + <string name="server_info_packets_sent">Bidalitako paketeak</string> + <string name="server_info_packets_received">Jasotako paketeak</string> + <string name="server_info_connected_accounts">Konektatutako kontuak</string> + <string name="server_info_server_features">Zerbitzariaren ezaugarriak</string> + <string name="server_info_roster_versioning">Roster Versioning</string> + <string name="server_info_carbon_messages">Carbon Messages</string> + <string name="server_info_stream_management">Stream Management</string> + <string name="hours">orduak</string> + <string name="mins">minutuak</string> + <string name="missing_public_keys">Gako publikoen iragarpenak faltan</string> + <string name="last_seen_now">azkenegoz ikusia orain</string> + <string name="last_seen_mins">azkenegoz ikusia %d minutu lehenago</string> + <string name="last_seen_hours">azkenegoz ikusia %d ordu lehenago</string> + <string name="last_seen_days">azkenegoz ikusia %d egun lehenago</string> + <string name="never_seen">inoiz ez ikusia</string> + <string name="last_seen_min">azkenegoz ikusia minutu 1 lehenago</string> + <string name="last_seen_mins">azkenegoz ikusia %d minutu lehenago</string> + <string name="last_seen_hour">azkenegoz ikusia ordu 1 lehenago</string> + <string name="last_seen_hours">azkenegoz ikusia %d ordu lehenago</string> + <string name="last_seen_day">azkenengoz ikusia egun 1 lehenago</string> + <string name="last_seen_days">azkenegoz ikusia %d egun lehenago</string> + <string name="never_seen">inoiz ez ikusia</string> + <string name="install_openkeychain">Mezu enkriptatua. Mesedez instalatu OpenKeychain desenkriptatzeko.</string> + +</resources>
\ No newline at end of file diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 78327d66..3f27220b 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -5,15 +5,18 @@ <string name="action_settings">Paramètres</string> <string name="action_add">Nouvelle conversation</string> <string name="action_accounts">Gérer les comptes</string> + <string name="action_refresh">Rafraichir la liste de contacts</string> <string name="action_end_conversation">Terminer cette conversation</string> <string name="action_contact_details">Détails du contact</string> <string name="action_muc_details">Détails de la conférence</string> <string name="action_secure">Conversation sécurisée</string> + <string name="action_edit_contact">Edit name</string> + <string name="action_delete_contact">Delete from roster</string> <string name="action_add_account">Ajouter un compte</string> <string name="title_activity_contacts">Contacts</string> <string name="just_now">À l\'instant</string> <string name="sending">envoi…</string> - <string name="announce_pgp">Renouveller les annonces PGP</string> + <string name="announce_pgp">Renouveler les annonces PGP</string> <string name="encrypted_message">Déchiffrement du message. Patientez…</string> <string name="conference_details">Détails de la conférence</string> <string name="nick_in_use">Cet identifiant est déjà utilisé.</string> @@ -26,7 +29,7 @@ <string name="account_info">Informations du serveur</string> <string name="register_account">Créer un nouveau compte sur le serveur</string> <string name="share_with">Partager avec</string> - <string name="ask_again"><u>Appuyez pour demander à nouveau</u></string> + <string name="ask_again"><u>Appuyez pour demander à nouveau.</u></string> <string name="show_otr_key">Empreinte OTR</string> <string name="no_otr_fingerprint">Empreinte OTR non générée. Essayez de démarrer une conversation sécurisée.</string> <string name="start_conversation">Démarrer une conversation</string> @@ -41,16 +44,16 @@ <string name="account_offline">Compte hors-ligne</string> <string name="cant_invite_while_offline">Vous devez être en ligne pour inviter des participants à une conférence.</string> <string name="crash_report_title">Conversations s\'est arreté</string> - <string name="crash_report_message">En envoyant des logs vous aidez au développement de Conversations\n\n<b>Attention:</b> Votre compte XMPP sera utilisé pour envoyer les logs aux développeurs.</string> + <string name="crash_report_message">En envoyant des logs vous aidez au développement de Conversations.\n\n<b>Attention:</b> Votre compte XMPP sera utilisé pour envoyer les logs aux développeurs.</string> <string name="send_now">Envoyer</string> <string name="send_never">Ne plus me demander</string> - <string name="problem_connecting_to_account">Impossible de se connecter au compte</string> - <string name="problem_connecting_to_accounts">Impossible de se connecter aux comptes</string> - <string name="touch_to_fix">Appuyez pour gérer vos comptes</string> + <string name="problem_connecting_to_account">Impossible de se connecter au compte.</string> + <string name="problem_connecting_to_accounts">Impossible de se connecter aux comptes.</string> + <string name="touch_to_fix">Appuyez pour gérer vos comptes.</string> <string name="attach_file">Lier un fichier</string> <string name="not_in_roster">Le contact n\'est pas dans votre carnet d\'adresses. Voulez-vous l\'y ajouter?</string> <string name="add_contact">Ajouter un contact</string> - <string name="send_failed">Echec de l\'envoi</string> + <string name="send_failed">Echec de l\'envoi.</string> <string name="send_rejected">Rejeté</string> <string name="receiving_image">Réception d\'une image. Patientez…</string> <string name="preparing_image">Préparation de la transmission de l\'image. Patientez…</string> @@ -63,16 +66,16 @@ <string name="send_message_to_conference">Envoyer un message à la conférence</string> <string name="send_plain_text_message">Envoyer un message</string> <string name="send_otr_message">Envoyer un message sécurisé par OTR</string> - <string name="send_pgp_message">Envoyer un message sécurisé par openPGP</string> + <string name="send_pgp_message">Envoyer un message sécurisé par OpenPGP</string> <string name="your_nick_has_been_changed">Votre identifiant a été changé</string> - <string name="download_image">Télecharger l\'image</string> + <string name="download_image">Télécharger l\'image</string> <string name="error_loading_image">Impossible de télécharger l\'image (Fichier non trouvé)</string> <string name="image_offered_for_download"><i>Image proposée au téléchargement.</i></string> <string name="not_connected">Non connecté</string> <string name="you_are_offline">Vous devez être en ligne pour envoyer %s mais votre compte utilisé dans cette conversation est hors-ligne.</string> <string name="you_are_offline_blank">Vous devez être en ligne pour réaliser cette action.</string> <string name="files">Fichiers</string> - <string name="otr_messages">Message chiffrés par OTR</string> + <string name="otr_messages">Messages chiffrés par OTR</string> <string name="manage_account">Gérer les comptes</string> <string name="contact_offline">Votre correspondant est hors-ligne.</string> <string name="contact_offline_otr">Envoyer un message chiffré via OTR à un correspondant hors-ligne n\'est malheureusement pas possible.\nVoulez-vous envoyer ce message sans chiffrement?</string> @@ -83,4 +86,81 @@ <string name="openkeychain_required_long">Conversations requiert une application tierce nommée <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n<small>(Merci de redémarrer Conversations apres l\'installation du logiciel)</small></string> <string name="restart">Redémarrer</string> <string name="install">Installer</string> + <string name="offering">Proposition…</string> + <string name="no_pgp_key">Aucune clef OpenPGP trouvée.</string> + <string name="contact_has_no_pgp_key">Conversations ne peut chiffrer vos messages car votre correspondant n\'a pas communiqué sa clef publique.\n\n<small>Merci de demander à votre correspondant de configurer OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Message chiffré reçu. Appuyez pour le déchiffrer.</i></string> + <string name="encrypted_image_received"><i>Image chiffrée reçue. Appuyez pour la déchiffrer.</i></string> + <string name="image_file"><i>Image reçue. Appuyez pour visualiser.</i></string> + <string name="otr_file_transfer">Chiffrement OTR non disponible</string> + <string name="otr_file_transfer_msg">Malheureusement le chiffrement OTR n\'est pas disponible pour le transfert de fichiers. Vous pouvez utiliser OpenPGP ou envoyer vos fichiers non chiffrés.</string> + <string name="use_pgp_encryption">Utiliser le chiffrement OpenPGP</string> + <string name="pref_xmpp_resource">Ressource XMPP</string> + <string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client XMPP</string> + <string name="pref_accept_files">Accepter les fichiers</string> + <string name="pref_accept_files_summary">Accepter automatiquement les fichiers plus petits que…</string> + <string name="pref_notification_settings">Paramètres de notification</string> + <string name="pref_notifications">Notifications</string> + <string name="pref_notifications_summary">Notifier l\'arrivée d\'un message</string> + <string name="pref_vibrate">Vibration</string> + <string name="pref_vibrate_summary">Vibrer lors de l\'arrivée d\'un message</string> + <string name="pref_sound">Sonore</string> + <string name="pref_sound_summary">Jouer une sonnerie lors de l\'arrivée d\'un message</string> + <string name="pref_conference_notifications">Notifications lors des conférences</string> + <string name="pref_conference_notifications_summary">Toujours notifier l\'arrivée d\'un message provenant d\'une conférence.</string> + <string name="pref_notification_grace_period">Période sans notification</string> + <string name="pref_notification_grace_period_summary">Désactiver momentanément les notifications après l\'arrivée d\'une copie carbone.</string> + <string name="pref_ui_options">Options d\'affichage</string> + <string name="pref_use_phone_self_picture">Utiliser les images des contacts</string> + <string name="pref_use_phone_self_picture_summary">Vous pourriez ne plus être capable de distinguer quel compte vous utilisez dans une conversation.</string> + <string name="pref_conference_name">Nom de la conférence</string> + <string name="pref_conference_name_summary">Identifier la conférence par son sujet</string> + <string name="pref_advanced_options">Options avancées</string> + <string name="pref_never_send_crash">Ne jamais envoyer de rapports d\'erreurs</string> + <string name="pref_never_send_crash_summary">En envoyant des logs vous aidez au développement de Conversations.</string> + <string name="openpgp_error">Une erreur s\'est produite via OpenKeychain</string> + <string name="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string> + <string name="error_copying_image_file">Erreur lors de la copie du fichier</string> + <string name="accept">Accepter</string> + <string name="error">Une erreur s\'est produite</string> + <string name="pref_grant_presence_updates">Accepter les mises à jour de présence</string> + <string name="pref_grant_presence_updates_summary">Demander et accepter par avance les mises à jour de présence des contacts créés.</string> + <string name="subscriptions">Publications</string> + <string name="your_account">Votre compte</string> + <string name="keys">Clefs</string> + <string name="send_presence_updates">Envoyer les mises à jour de présence</string> + <string name="receive_presence_updates">Recevoir les mises à jour de présence</string> + <string name="ask_for_presence_updates">Demander les mises à jour de présence</string> + <string name="attach_choose_picture">Choisir une image</string> + <string name="attach_take_picture">Prendre une photo</string> + <string name="preemptively_grant">Accepter par avance les demandes de publication.</string> + <string name="error_not_an_image_file">Le fichier choisi n\'est pas une image</string> + <string name="error_compressing_image">Une erreur s\'est produite en convertissant l\'image</string> + <string name="error_file_not_found">Fichier non trouvé</string> + <string name="error_io_exception">Erreur générale d\'E/S. Avez-vous encore de l\'espace libre?</string> + <string name="error_security_exception_during_image_copy">L\'application utilisée empêche la lecture de l\'image.\n\n<small>Choisissez l\'image depuis une autre application.</small></string> + <string name="account_status">Statut :</string> + <string name="account_status_unknown">Inconnu</string> + <string name="account_status_disabled">Temporarily disabled</string> + <string name="account_status_online">En ligne</string> + <string name="account_status_connecting">Connexion\u2026</string> + <string name="account_status_offline">Hors-ligne</string> + <string name="account_status_unauthorized">Non autorisé</string> + <string name="account_status_not_found">Serveur non trouvé</string> + <string name="account_status_no_internet">Aucune connectivité</string> + <string name="account_status_requires_tls">Le serveur requiert TLS</string> + <string name="account_status_error">Certificat non certifié</string> + <string name="account_status_regis_fail">Enregistrement échoué</string> + <string name="account_status_regis_conflict">Identifiant déjà utilisé</string> + <string name="account_status_regis_success">Enregistrement réussi</string> + <string name="account_status_regis_not_sup">Le serveur ne permet pas l\'enregistrement</string> + <string name="certif_no_trust">Annuler</string> + <string name="certif_trust">Croire ce certificat</string> + <string name="encryption_choice_none">Texte clair</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Modifier le compte</string> + <string name="mgmt_account_delete">Supprimer</string> + <string name="mgmt_account_disable">Désactiver temporairement</string> + <string name="mgmt_account_enable">Activer</string> </resources> diff --git a/res/values-gl/arrays.xml b/res/values-gl/arrays.xml new file mode 100644 index 00000000..318b9d5c --- /dev/null +++ b/res/values-gl/arrays.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <array name="resources"> + <item>Móvil</item> + <item>Teléfono</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </array> + <string-array name="filesizes"> + <item>nunca</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> +</resources> diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml new file mode 100644 index 00000000..bbfa6017 --- /dev/null +++ b/res/values-gl/strings.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Axustes</string> + <string name="action_add">Nova conversa</string> + <string name="action_accounts">Xestionar contas</string> + <string name="action_refresh">Actualizar lista de contactos</string> + <string name="action_end_conversation">Terminar conversa</string> + <string name="action_contact_details">Detalles do contacto</string> + <string name="action_muc_details">Detalles da conferencia</string> + <string name="action_secure">Conversa segura</string> + <string name="action_add_account">Engadir conta</string> + <string name="action_edit_contact">Editar contacto</string> + <string name="action_delete_contact">Eliminar contacto da lista</string> + <string name="title_activity_contacts">Contactos</string> + <string name="just_now">agora</string> + <string name="minutes_ago">min</string> + <string name="unread_conversations">conversas sen ler</string> + <string name="sending">enviando…</string> + <string name="announce_pgp">Renovar anuncio PGP</string> + <string name="encrypted_message">Descifrando mensaxe. Agarda uns intres…</string> + <string name="conference_details">Detalles da conferencia</string> + <string name="nick_in_use">O apodo xa está en uso</string> + <string name="moderator">Moderador</string> + <string name="participant">Participante</string> + <string name="visitor">Visitante</string> + <string name="enter_new_name">Introduce un novo nome:</string> + <string name="remove_contact_text">¿Queres eliminar a %s da túa lista?. A conversa asociada a esta conta non se eliminará.</string> + <string name="untrusted_cert_hint">O servidor %s presenta un certificado non confiable, posiblemente auto firmado.</string> + <string name="account_info">Información do servidor</string> + <string name="register_account">Rexistrar nova conta no servidor</string> + <string name="share_with">Compartir con</string> + <string name="ask_again"><u>Pulsa para preguntar outra vez</u></string> + <string name="show_otr_key">Pegada dactilar OTR</string> + <string name="no_otr_fingerprint">Non hai pegadas dactilares OTR. Continúa e comeza unha conversación cifrada</string> + <string name="start_conversation">Comeza conversa</string> + <string name="invite_contacts">Invitar contactos</string> + <string name="invite_contacts_to_existing">Invitar a conferencia existente</string> + <string name="new_conference">Crear nova conferencia</string> + <string name="cancel">Cancelar</string> + <string name="create_invite">Crear \u0026 Invitar</string> + <string name="new_conference_explained">¿Queres crear unha nueva conferencia con unha dirección xerada aleatoriamente e invitar aos contactos seleccionados a ela?</string> + <string name="no_open_mucs">Non hai conferencias existentes</string> + <string name="invitation_sent">Invitación enviada</string> + <string name="account_offline">Conta desconectada</string> + <string name="cant_invite_while_offline">Debes estar conectado para invitar contactos á conferencia</string> + <string name="crash_report_title">Conversations deteuse.</string> + <string name="crash_report_message">Enviando volcados de pilas axudas ao desenrolo de Conversations\n<b>Aviso:</b> Isto empregará a túa conta XMPP para enviar o volcado de pila ao desenrolador.</string> + <string name="send_now">Enviar agora</string> + <string name="send_never">Non preguntar de novo</string> + <string name="problem_connecting_to_account">Erro na conexión á conta</string> + <string name="problem_connecting_to_accounts">Erro na conexión a múltiples contas</string> + <string name="touch_to_fix">Pulsa aquí para xestionar as túas contass</string> + <string name="attach_file">Adxuntar</string> + <string name="not_in_roster">O contacto non está na túa lista. ¿Queres engadilo?</string> + <string name="add_contact">Engadir contacto</string> + <string name="send_failed">Erro ao enviar</string> + <string name="send_rejected">rechazado</string> + <string name="receiving_image">Recibindo arquivo de imaxe. Agarda por favor…</string> + <string name="preparing_image">Preparando imaxe para enviar</string> + <string name="action_clear_history">Limpar historial</string> + <string name="clear_conversation_history">Limpar historial de conversa</string> + <string name="clear_histor_msg">¿Queres borrar todas as mensaxes desta conversa?\n\n<b>Ollo:</b> Isto non afectará ás mensaxes gardadas noutros dispositivos ou servidores.</string> + <string name="delete_messages">Borrar mensaxes</string> + <string name="also_end_conversation">Terminar esta conversa máis tarde</string> + <string name="choose_presence">Selecciona recurso del contacto</string> + <string name="send_message_to_conference">Enviar mensaxe a conferencia</string> + <string name="send_plain_text_message">Enviar mensaxe de texto</string> + <string name="send_otr_message">Enviar mensaxe cifrado con OTR</string> + <string name="send_pgp_message">Enviar mensaxe cifrado con OpenPGP</string> + <string name="your_nick_has_been_changed">Modificouse o teu apodo</string> + <string name="download_image">Descargar imaxe</string> + <string name="error_loading_image">Erro na carga da imaxen (Arquivo non atopado)</string> + <string name="image_offered_for_download"><i>Arquivo de imaxe ofrecido para descarga</i></string> + <string name="not_connected">Non conectado</string> + <string name="you_are_offline">Debes estar conectado para enviar %s pero a túa conta asociada a esta conversa está desconectada.</string> + <string name="you_are_offline_blank">Non podes executar esta acción estando desconectado</string> + <string name="files">arquivos</string> + <string name="otr_messages">Mensaxes cifrados con OTR</string> + <string name="manage_account">Xestionar conta</string> + <string name="contact_offline">O teu contacto está desconectado</string> + <string name="contact_offline_otr">É unha pena, pero non é posible enviar mensaxes encriptados con OTR a un contacto desconectado.\n¿Queres enviar a mensaxe sen cifrar?</string> + <string name="contact_offline_file">É unha pena, pero non se pode enviar arquivos a un contacto desconectado.</string> + <string name="send_unencrypted">Enviar sen cifrar</string> + <string name="decryption_failed">Fallou o descifrado. Quizábeis non teñas a clave privada apropiada.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations emprega unha aplicación de terceiros chamada <b>OpenKeychain</b> para cifrar e descifrar mensaxes e xestionar as túas claves públicas.\n\nOpenKeychain está publicado baixo licencia GPLv3 e disponible en F-Droid e Google Play.\n\n<small>(Por favor, reinicie Conversations despois.)</small></string> + <string name="restart">Reiniciar</string> + <string name="install">Instalar</string> + <string name="offering">ofrecendo…</string> + <string name="no_pgp_key">Clave OpenPGP non atopada</string> + <string name="contact_has_no_pgp_key">Conversations non foi quen de cifrar as túas mensaxes porque o teu contactos non está anunciando a súa clave pública.\n\n<small>Por favor, pídelle ao teu contacto que configure OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Mensaxe cifrado recibido. Pulsa para ver.</i></string> + <string name="encrypted_image_received"><i>Imaxe cifrada recibida. Pulsa para ver.</i></string> + <string name="image_file"><i>Imaxe recibida. Pulsa para ver</i></string> + <string name="otr_file_transfer">Cifrado con OTR non disponible</string> + <string name="otr_file_transfer_msg">É unha pena, pero o cifrado con OTR non está disponible para transferencia de arquivos. Podes selecionar cifrado con OpenPGP ou enviar os datos sen cifrar.</string> + <string name="use_pgp_encryption">Usa cifrado con OpenPGP</string> + <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_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> + <string name="pref_vibrate">Tremer</string> + <string name="pref_vibrate_summary">Treme cando chega unha novo mensaxe</string> + <string name="pref_sound">Son</string> + <string name="pref_sound_summary">Reproduce un ton ca notificación</string> + <string name="pref_conference_notifications">Notificacións de conferencia</string> + <string name="pref_conference_notifications_summary">Siempre notifica cuando chega unha mensaxe de conferencia e non solo cuando chega unha mensaxe destacada</string> + <string name="pref_notification_grace_period">Notificacións Carbons</string> + <string name="pref_notification_grace_period_summary">Deshabilita as notificacións durante un corto periodo de tiempo despois de recibir a copia da mensaxe carbón</string> + <string name="pref_ui_options">Opcións de interfaz</string> + <string name="pref_use_phone_self_picture">Usar a foto do teléfono</string> + <string name="pref_use_phone_self_picture_summary">Poderías non ser capaz de distinguir que conta estase a utlizar nunha conversa</string> + <string name="pref_conference_name">Nome dea conferencia</string> + <string name="pref_conference_name_summary">Usa o nome da sala para identificar Conferencias</string> + <string name="pref_advanced_options">Opcións avanzadas</string> + <string name="pref_never_send_crash">Nunca enviar informe de erros</string> + <string name="pref_never_send_crash_summary">Enviando volcados de pilas axudas al desenrolo de Conversations</string> + <string name="openpgp_error">OpenKeychain reportou un erro</string> + <string name="error_decrypting_file">I/O Erro descifrando arquivo</string> + <string name="error_copying_image_file">Erro copiando arquivo de imaxe.</string> + <string name="accept">Aceptar</string> + <string name="error">Produciuse un erro</string> + <string name="pref_grant_presence_updates">Suscripción de presencia</string> + <string name="pref_grant_presence_updates_summary">Por defecto otorgar e pedir suscripcións de presencia dos contactos que creaches</string> + <string name="subscriptions">Suscripcións</string> + <string name="your_account">A túa conta</string> + <string name="keys">Chaves</string> + <string name="send_presence_updates">Enviar actualizacións de presencia</string> + <string name="receive_presence_updates">Recibir actualizacións de presencia</string> + <string name="ask_for_presence_updates">Solicitar actualizacións de presencia</string> + <string name="attach_choose_picture">Seleccionar imaxe</string> + <string name="attach_take_picture">Facer foto</string> + <string name="preemptively_grant">Por defecto otorgar peticiones de suscripción</string> + <string name="error_not_an_image_file">O arquivo seleccionado non é unha imaxe</string> + <string name="error_compressing_image">Erro convertindo o arquivo de imaxe</string> + <string name="error_file_not_found">Arquivo non atopado</string> + <string name="error_io_exception">Erro xeral de I/O. ¿Quedaches sen espazo no disco?</string> + <string name="error_security_exception_during_image_copy">A aplicación que usas para seleccionar imaxes non proporciona suficientes permisos para leer o arquivo.\n\n<small>Utiliza un explorador de arquivos diferente para seleccionar a imaxe</small></string> + <string name="account_status">Estado:</string> + <string name="account_status_unknown">Descoñecido</string> + <string name="account_status_disabled">Deshabilitado temporalmente</string> + <string name="account_status_online">Conectado</string> + <string name="account_status_connecting">Conectando\u2026</string> + <string name="account_status_offline">Desconectado</string> + <string name="account_status_unauthorized">Non autorizado</string> + <string name="account_status_not_found">Servidor non atopado</string> + <string name="account_status_no_internet">Sen conectividade</string> + <string name="account_status_requires_tls">O servidor require TLS</string> + <string name="account_status_error">Certificado non confiable</string> + <string name="account_status_regis_fail">Erro no rexistro</string> + <string name="account_status_regis_conflict">O identificador xa está en uso</string> + <string name="account_status_regis_success">Rexistro completado</string> + <string name="account_status_regis_not_sup">O servidor non soporta rexistros</string> + <string name="certif_no_trust">Non conectar</string> + <string name="certif_trust">Confiar no certificado</string> + <string name="encryption_choice_none">Texto plano</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Editar conta</string> + <string name="mgmt_account_delete">Eliminar conta</string> + <string name="mgmt_account_disable">Deshabilitar temporalmente</string> + <string name="mgmt_account_enable">Habilitar</string> + <string name="attach_record_voice">Grabar audio</string> + <string name="account_settings">Configuración de conta</string> + <string name="save">Gardar</string> + <string name="passwords_do_not_match">As contrasinais non coinciden</string> + <string name="invalid_jid">O identificador non é un identificador de Jabber válido</string> +</resources> diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 09fd3cc7..f9a198b2 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <array name="resources"> + <string-array name="resources"> <item>Mobile</item> <item>Phone</item> <item>Tablet</item> <item>Conversations</item> <item>Android</item> - </array> + </string-array> <string-array name="filesizes"> <item>never</item> <item>256 KB</item> diff --git a/res/values/strings.xml b/res/values/strings.xml index 10dfb592..021c70e8 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5,23 +5,38 @@ <string name="action_settings">Settings</string> <string name="action_add">New conversation</string> <string name="action_accounts">Manage accounts</string> + <string name="action_refresh">Refresh contact list</string> <string name="action_end_conversation">End this conversation</string> <string name="action_contact_details">Contact details</string> <string name="action_muc_details">Conferenece details</string> <string name="action_secure">Secure conversation</string> <string name="action_add_account">Add account</string> + <string name="action_edit_contact">Edit name</string> + <string name="action_add_phone_book">Add to phone book</string> + <string name="action_delete_contact">Delete from roster</string> <string name="title_activity_contacts">Contacts</string> + <string name="title_activity_manage_accounts">Manage Accounts</string> + <string name="title_activity_settings">Settings</string> + <string name="title_activity_conference_details">Conference Details</string> + <string name="title_activity_contact_details">Contact Details</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Share with Conversation</string> <string name="just_now">just now</string> + <string name="minute_ago">1 min ago</string> + <string name="minutes_ago">%d mins ago</string> + <string name="unread_conversations">unread Conversations</string> <string name="sending">sending…</string> <string name="announce_pgp">Renew PGP announcement</string> <string name="encrypted_message">Decrypting message. Please wait…</string> <string name="conference_details">Conference Details</string> <string name="nick_in_use">Nickname is already in use</string> + <string name="admin">Admin</string> + <string name="owner">Owner</string> <string name="moderator">Moderator</string> <string name="participant">Participant</string> <string name="visitor">Visitor</string> <string name="enter_new_name">Enter a new name:</string> - <string name="remove_contact_text">Do you want to delete %s from your roster. The conversation associated with this account will not be removed.</string> + <string name="remove_contact_text">Do you want to delete %s from your roster? The conversation associated with this account will not be removed.</string> <string name="untrusted_cert_hint">The server %s presented you with an untrusted, possible self signed, certificate.</string> <string name="account_info">Server Info</string> <string name="register_account">Register new account on server</string> @@ -33,7 +48,22 @@ <string name="invite_contacts">Invite Contacts</string> <string name="invite_contacts_to_existing">Invite to existing conference</string> <string name="new_conference">Create new conference</string> + <string name="new_contact">Create new contact</string> + <string name="contacts">Contacts</string> + <string name="search_jabber_id">Search or enter Jabber ID</string> + <string name="choose_account">Choose account</string> + <string name="multi_user_conference">Multi User Conference</string> + <string name="trying_join_conference">Are you trying to join a conference?</string> <string name="cancel">Cancel</string> + <string name="add">Add</string> + <string name="edit">Edit</string> + <string name="delete">Delete</string> + <string name="save">Save</string> + <string name="yes">Yes</string> + <string name="no">No</string> + <string name="ok">OK</string> + <string name="done">Done</string> + <string name="hide">Hide</string> <string name="create_invite">Create \u0026 Invite</string> <string name="new_conference_explained">Do you want to create a new conference with a randomly generated address and invite the selected contacts to it?</string> <string name="no_open_mucs">No existing conferences</string> @@ -50,7 +80,7 @@ <string name="attach_file">Attach file</string> <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string> <string name="add_contact">Add contact</string> - <string name="send_failed">unsuccessful delivery</string> + <string name="send_failed">delivery failed</string> <string name="send_rejected">rejected</string> <string name="receiving_image">Receiving image file. Please wait…</string> <string name="preparing_image">Preparing image for transmission</string> @@ -60,18 +90,14 @@ <string name="delete_messages">Delete messages</string> <string name="also_end_conversation">End this conversations afterwards</string> <string name="choose_presence">Choose presence to contact</string> - <string name="send_message_to_conference">Send message to conference</string> <string name="send_plain_text_message">Send plain text message</string> <string name="send_otr_message">Send OTR encrypted message</string> - <string name="send_pgp_message">Send openPGP encrypted message</string> + <string name="send_pgp_message">Send OpenPGP encrypted message</string> <string name="your_nick_has_been_changed">Your nickname has been changed</string> <string name="download_image">Download Image</string> <string name="error_loading_image">Error loading image (File not found)</string> <string name="image_offered_for_download"><i>Image file offered for download</i></string> <string name="not_connected">Not Connected</string> - <string name="you_are_offline">You have to be online to send %s but your account assoziated with this Conversation is currently offline.</string> - <string name="you_are_offline_blank">You can not perform this action while being offline</string> - <string name="files">files</string> <string name="otr_messages">OTR encrypted messages</string> <string name="manage_account">Manage account</string> <string name="contact_offline">Your Contact is Offline</string> @@ -84,14 +110,17 @@ <string name="restart">Restart</string> <string name="install">Install</string> <string name="offering">offering…</string> - <string name="no_pgp_key">No openPGP Key found</string> - <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup openPGP.</small></string> + <string name="waiting">waiting…</string> + <string name="no_pgp_key">No OpenPGP Key found</string> + <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string> + <string name="no_pgp_keys">No OpenPGP Keys found</string> + <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string> <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string> <string name="encrypted_image_received"><i>Encrypted image received. Touch to view and decrypt.</i></string> <string name="image_file"><i>Image received. Touch to view</i></string> <string name="otr_file_transfer">OTR encryption not available</string> - <string name="otr_file_transfer_msg">Unfortunaly OTR encryption is not available for file transfer. You can choose either openPGP or no encryption.</string> - <string name="use_pgp_encryption">Use openPGP encryption</string> + <string name="otr_file_transfer_msg">Unfortunaly OTR encryption is not available for file transfer. You can choose either OpenPGP or no encryption.</string> + <string name="use_pgp_encryption">Use OpenPGP encryption</string> <string name="pref_xmpp_resource">XMPP resource</string> <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string> <string name="pref_accept_files">Accept files</string> @@ -109,12 +138,16 @@ <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string> <string name="pref_ui_options">UI Options</string> <string name="pref_use_phone_self_picture">Use Phones self contact picture</string> - <string name="pref_use_phone_sefl_picture_summary">You may no longer be able to distinguish which account you are using in a conversation</string> + <string name="pref_use_phone_self_picture_summary">You may no longer be able to distinguish which account you are using in a conversation</string> <string name="pref_conference_name">Conference name</string> <string name="pref_conference_name_summary">Use room’s subject to identify Conferences</string> <string name="pref_advanced_options">Advanced Options</string> <string name="pref_never_send_crash">Never send crash reports</string> <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_show_last_seen">Display last seen</string> + <string name="pref_show_last_seen_summary">Display the latest time a contact has been seen online</string> <string name="openpgp_error">OpenKeychain reporeted an error</string> <string name="error_decrypting_file">I/O Error decrypting file</string> <string name="error_copying_image_file">Error copying image file.</string> @@ -123,9 +156,92 @@ <string name="pref_grant_presence_updates">Grant presence updates</string> <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string> <string name="subscriptions">Subscriptions</string> + <string name="subscription_updated">Subscription updated</string> <string name="your_account">Your account</string> <string name="keys">Keys</string> <string name="send_presence_updates">Send presence updates</string> <string name="receive_presence_updates">Receive presence updates</string> <string name="ask_for_presence_updates">Ask for presence updates</string> -</resources> + <string name="asked_for_presence_updates">Asked for presence updates</string> + <string name="attach_choose_picture">Choose picture</string> + <string name="attach_take_picture">Take picture</string> + <string name="preemptively_grant">Preemptively grant subscription request</string> + <string name="error_not_an_image_file">The file you selected is not an image</string> + <string name="error_compressing_image">Error while converting the image file</string> + <string name="error_file_not_found">File not found</string> + <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string> + <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string> + <string name="account_status">Status:</string> + <string name="account_status_unknown">Unknown</string> + <string name="account_status_disabled">Temporarily disabled</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Connecting\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Unauthorized</string> + <string name="account_status_not_found">Server not found</string> + <string name="account_status_no_internet">No connectivity</string> + <string name="account_status_requires_tls">Server requires TLS</string> + <string name="account_status_error">Untrusted cerficate</string> + <string name="account_status_regis_fail">Registration failed</string> + <string name="account_status_regis_conflict">Username already in use</string> + <string name="account_status_regis_success">Registration completed</string> + <string name="account_status_regis_not_sup">Server does not support registration</string> + <string name="certif_no_trust">Don\'t connect</string> + <string name="certif_trust">Trust certificate</string> + <string name="encryption_choice_none">Plain text</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Edit account</string> + <string name="mgmt_account_delete">Delete</string> + <string name="mgmt_account_disable">Temporarily disable</string> + <string name="mgmt_account_enable">Enable</string> + <string name="mgmt_account_are_you_sure">Are you sure?</string> + <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string> + <string name="mgmt_account_account_offline">Account is offline</string> + <string name="attach_record_voice">Record voice</string> + <string name="account_settings">Account Settings</string> + <string name="account_settings_jabber_id">Jabber ID:</string> + <string name="account_settings_password">Password:</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">Confirm password:</string> + <string name="password">Password</string> + <string name="confirm_password">Confirm password</string> + <string name="passwords_do_not_match">Passwords do not match</string> + <string name="invalid_jid">This is not a valid Jabber ID</string> + <string name="error_out_of_memory">Ouf of memory. Image is to large</string> + <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">free to chat</string> + <string name="contact_status_away">away</string> + <string name="contact_status_extended_away">extended away</string> + <string name="contact_status_do_not_disturb">do not disturb</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Conference</string> + <string name="muc_details_conference_subject">Conference Subject</string> + <string name="muc_details_your_nickname">Your nickname</string> + <string name="muc_details_other_members">Other Members</string> + <string name="subscription_not_updated_offline">Account offline. Could not update subscription</string> + <string name="share_with_active_conversations">Active Conversations</string> + <string name="server_info_statistics">Statistics</string> + <string name="server_info_connection_age">Connection age</string> + <string name="server_info_session_age">Session age</string> + <string name="server_info_packets_sent">Packets sent</string> + <string name="server_info_packets_received">Packets received</string> + <string name="server_info_connected_accounts">Connected accounts</string> + <string name="server_info_server_features">Server Features</string> + <string name="server_info_roster_versioning">Roster Versioning</string> + <string name="server_info_carbon_messages">Carbon Messages</string> + <string name="server_info_stream_management">Stream Management</string> + <string name="hours">hours</string> + <string name="mins">mins</string> + <string name="missing_public_keys">Missing public key announcements</string> + <string name="last_seen_now">last seen just now</string> + <string name="last_seen_min">last seen 1 minute ago</string> + <string name="last_seen_mins">last seen %d minutes ago</string> + <string name="last_seen_hour">last seen 1 hour ago</string> + <string name="last_seen_hours">last seen %d hours ago</string> + <string name="last_seen_day">last seen 1 day ago</string> + <string name="last_seen_days">last seen %d days ago</string> + <string name="never_seen">never seen</string> + <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string> +</resources>
\ No newline at end of file diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index bad2574b..40039cd5 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -22,6 +22,12 @@ android:entries="@array/filesizes" android:entryValues="@array/filesizes_values" android:defaultValue="524288"/> + <CheckBoxPreference + android:key="confirm_messages" + android:title="@string/pref_confirm_messages" + android:summary="@string/pref_confirm_messages_summary" + android:defaultValue="true" + /> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_notification_settings"> @@ -59,7 +65,7 @@ <CheckBoxPreference android:key="show_phone_selfcontact_picture" android:title="@string/pref_use_phone_self_picture" - android:summary="@string/pref_use_phone_sefl_picture_summary" + android:summary="@string/pref_use_phone_self_picture_summary" android:defaultValue="true"/> <CheckBoxPreference android:key="use_subject_in_muc" @@ -68,7 +74,7 @@ android:defaultValue="true"/> </PreferenceCategory> <PreferenceCategory - android:title="Advanced Options"> + android:title="@string/pref_advanced_options"> <CheckBoxPreference android:key="never_send" android:title="@string/pref_never_send_crash" diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java index 48750e24..c0d8ca07 100644 --- a/src/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/eu/siacs/conversations/crypto/PgpEngine.java @@ -17,6 +17,7 @@ import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; @@ -35,17 +36,19 @@ public class PgpEngine { this.mXmppConnectionService = service; } - public void decrypt(final Message message, final UiCallback callback) { - Log.d("xmppService","decrypting message "+message.getUuid()); + public void decrypt(final Message message, + final UiCallback<Message> callback) { + Log.d("xmppService", "decrypting message " + message.getUuid()); Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message .getConversation().getAccount().getJid()); if (message.getType() == Message.TYPE_TEXT) { - InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -53,14 +56,15 @@ public class PgpEngine { case OpenPgpApi.RESULT_CODE_SUCCESS: message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - callback.success(); + callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); return; default: return; @@ -69,13 +73,15 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - final JingleFile inputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, false); - final JingleFile outputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message,true); + final JingleFile inputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, false); + final JingleFile outputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, true); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -83,21 +89,27 @@ public class PgpEngine { case OpenPgpApi.RESULT_CODE_SUCCESS: BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(outputFile.getAbsolutePath(),options); + BitmapFactory.decodeFile( + outputFile.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(""+outputFile.getSize()+","+imageWidth+","+imageHeight); + message.setBody("" + outputFile.getSize() + "," + + imageWidth + "," + imageHeight); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - PgpEngine.this.mXmppConnectionService.updateMessage(message); - PgpEngine.this.mXmppConnectionService.updateUi(message.getConversation(), false); - callback.success(); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + PgpEngine.this.mXmppConnectionService.updateUi( + message.getConversation(), false); + callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); return; default: return; @@ -105,28 +117,38 @@ public class PgpEngine { } }); } catch (FileNotFoundException e) { - callback.error(R.string.error_decrypting_file); + callback.error(R.string.error_decrypting_file, message); } catch (IOException e) { - callback.error(R.string.error_decrypting_file); + callback.error(R.string.error_decrypting_file, message); } - + } } - public void encrypt(final Message message,final UiCallback callback) { - long[] keys = { message.getConversation().getContact().getPgpKeyId() }; + public void encrypt(final Message message, + final UiCallback<Message> callback) { + Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_ENCRYPT); - params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message.getConversation().getAccount().getJid()); - + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + long[] keys = { message.getConversation().getContact() + .getPgpKeyId() }; + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); + } else { + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() + .getMucOptions().getPgpKeyIds()); + } + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + if (message.getType() == Message.TYPE_TEXT) { params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - - InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); + + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, @@ -137,53 +159,59 @@ public class PgpEngine { for (int i = 3; i < lines.length - 1; ++i) { encryptedMessageBody.append(lines[i].trim()); } - message.setEncryptedBody(encryptedMessageBody.toString()); - callback.success(); + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); break; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); break; } } }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - JingleFile inputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, true); - JingleFile outputFile = this.mXmppConnectionService.getFileBackend().getJingleFile(message, false); + JingleFile inputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, true); + JingleFile outputFile = this.mXmppConnectionService + .getFileBackend().getJingleFile(message, false); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(); + callback.success(message); break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); break; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, message); break; } } }); } catch (FileNotFoundException e) { - Log.d("xmppService","file not found: "+e.getMessage()); + Log.d("xmppService", "file not found: " + e.getMessage()); } catch (IOException e) { - Log.d("xmppService","io exception during file encrypt"); + Log.d("xmppService", "io exception during file encrypt"); } } } - + public long fetchKeyId(Account account, String status, String signature) { if ((signature == null) || (api == null)) { return 0; @@ -221,18 +249,20 @@ public class PgpEngine { return 0; } case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - Log.d("xmppService","openpgp user interaction requeried"); return 0; case OpenPgpApi.RESULT_CODE_ERROR: - Log.d("xmppService","openpgp error: "+((OpenPgpError) result - .getParcelableExtra(OpenPgpApi.RESULT_ERROR)).getMessage()); + Log.d("xmppService", + "openpgp error: " + + ((OpenPgpError) result + .getParcelableExtra(OpenPgpApi.RESULT_ERROR)) + .getMessage()); return 0; } return 0; } public void generateSignature(final Account account, String status, - final UiCallback callback) { + final UiCallback<Account> callback) { Intent params = new Intent(); params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); params.setAction(OpenPgpApi.ACTION_SIGN); @@ -251,51 +281,66 @@ public class PgpEngine { signatureBuilder.append(lines[i].trim()); } account.setKey("pgp_signature", signatureBuilder.toString()); - callback.success(); + callback.success(account); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, account); return; } } }); } - - public void hasKey(Contact contact, final UiCallback callback) { + + public void hasKey(final Contact contact, final UiCallback<Contact> callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount().getJid()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); api.executeApiAsync(params, null, null, new IOpenPgpCallback() { - + @Override public void onReturn(Intent result) { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(); + callback.success(contact); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result - .getParcelableExtra(OpenPgpApi.RESULT_INTENT)); + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); return; case OpenPgpApi.RESULT_CODE_ERROR: - callback.error(R.string.openpgp_error); + callback.error(R.string.openpgp_error, contact); return; } } }); } - + public PendingIntent getIntentForKey(Contact contact) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount().getJid()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } + + public PendingIntent getIntentForKey(Account account, long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); Intent result = api.executeApi(params, null, null); - return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); } } diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java index 35870aaa..b9c87eac 100644 --- a/src/eu/siacs/conversations/entities/Account.java +++ b/src/eu/siacs/conversations/entities/Account.java @@ -65,6 +65,8 @@ public class Account extends AbstractEntity{ private String otrFingerprint; + private Roster roster = null; + public Account() { this.uuid = "0"; } @@ -135,7 +137,7 @@ public class Account extends AbstractEntity{ } public boolean hasErrorStatus() { - return getStatus() > STATUS_NO_INTERNET; + return getStatus() > STATUS_NO_INTERNET && (getXmppConnection().getAttempt() >= 2); } public void setResource(String resource) { @@ -287,4 +289,11 @@ public class Account extends AbstractEntity{ return null; } } + + public Roster getRoster() { + if (this.roster==null) { + this.roster = new Roster(this); + } + return this.roster; + } } diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java index 599aa8de..a0047cdf 100644 --- a/src/eu/siacs/conversations/entities/Contact.java +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.entities; -import java.io.Serializable; import java.util.HashSet; -import java.util.Hashtable; import java.util.Set; import org.json.JSONArray; @@ -10,27 +8,24 @@ import org.json.JSONException; import org.json.JSONObject; import eu.siacs.conversations.xml.Element; - import android.content.ContentValues; import android.database.Cursor; -import android.util.Log; - -public class Contact extends AbstractEntity implements Serializable { - private static final long serialVersionUID = -4570817093119419962L; +public class Contact { public static final String TABLENAME = "contacts"; - public static final String DISPLAYNAME = "name"; + public static final String SYSTEMNAME = "systemname"; + public static final String SERVERNAME = "servername"; public static final String JID = "jid"; - public static final String SUBSCRIPTION = "subscription"; + public static final String OPTIONS = "options"; public static final String SYSTEMACCOUNT = "systemaccount"; public static final String PHOTOURI = "photouri"; public static final String KEYS = "pgpkey"; - public static final String PRESENCES = "presences"; public static final String ACCOUNT = "accountUuid"; protected String accountUuid; - protected String displayName; + protected String systemName; + protected String serverName; protected String jid; protected int subscription = 0; protected String systemAccount; @@ -41,27 +36,15 @@ public class Contact extends AbstractEntity implements Serializable { protected Account account; protected boolean inRoster = true; + + public Lastseen lastseen = new Lastseen(); - public Contact(Account account, String displayName, String jid, - String photoUri) { - if (account == null) { - this.accountUuid = null; - } else { - this.accountUuid = account.getUuid(); - } - this.account = account; - this.displayName = displayName; - this.jid = jid; - this.photoUri = photoUri; - this.uuid = java.util.UUID.randomUUID().toString(); - } - - public Contact(String uuid, String account, String displayName, String jid, - int subscription, String photoUri, String systemAccount, - String keys, String presences) { - this.uuid = uuid; + public Contact(String account, String systemName, String serverName, + String jid, int subscription, String photoUri, + String systemAccount, String keys) { this.accountUuid = account; - this.displayName = displayName; + this.systemName = systemName; + this.serverName = serverName; this.jid = jid; this.subscription = subscription; this.photoUri = photoUri; @@ -74,11 +57,20 @@ public class Contact extends AbstractEntity implements Serializable { } catch (JSONException e) { this.keys = new JSONObject(); } - this.presences = Presences.fromJsonString(presences); + } + + public Contact(String jid) { + this.jid = jid; } public String getDisplayName() { - return this.displayName; + if (this.systemName != null) { + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; + } else { + return this.jid.split("@")[0]; + } } public String getProfilePhoto() { @@ -90,35 +82,32 @@ public class Contact extends AbstractEntity implements Serializable { } public boolean match(String needle) { - return (jid.toLowerCase().contains(needle.toLowerCase()) || (displayName + return (jid.toLowerCase().contains(needle.toLowerCase()) || (getDisplayName() .toLowerCase().contains(needle.toLowerCase()))); } - @Override public ContentValues getContentValues() { ContentValues values = new ContentValues(); - values.put(UUID, uuid); values.put(ACCOUNT, accountUuid); - values.put(DISPLAYNAME, displayName); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); values.put(JID, jid); - values.put(SUBSCRIPTION, subscription); + values.put(OPTIONS, subscription); values.put(SYSTEMACCOUNT, systemAccount); values.put(PHOTOURI, photoUri); values.put(KEYS, keys.toString()); - values.put(PRESENCES, presences.toJsonString()); return values; } public static Contact fromCursor(Cursor cursor) { - return new Contact(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - cursor.getString(cursor.getColumnIndex(DISPLAYNAME)), + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), cursor.getString(cursor.getColumnIndex(JID)), - cursor.getInt(cursor.getColumnIndex(SUBSCRIPTION)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(PHOTOURI)), cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), - cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(PRESENCES))); + cursor.getString(cursor.getColumnIndex(KEYS))); } public int getSubscription() { @@ -138,10 +127,6 @@ public class Contact extends AbstractEntity implements Serializable { return this.account; } - public void setUuid(String uuid) { - this.uuid = uuid; - } - public boolean couldBeMuc() { String[] split = this.getJid().split("@"); if (split.length != 2) { @@ -153,15 +138,17 @@ public class Contact extends AbstractEntity implements Serializable { } else { return (domainParts[0].equals("conf") || domainParts[0].equals("conference") + || domainParts[0].equals("room") || domainParts[0].equals("muc") + || domainParts[0].equals("chat") || domainParts[0].equals("sala") || domainParts[0].equals("salas")); } } } - public Hashtable<String, Integer> getPresences() { - return this.presences.getPresences(); + public Presences getPresences() { + return this.presences; } public void updatePresence(String resource, int status) { @@ -188,8 +175,12 @@ public class Contact extends AbstractEntity implements Serializable { this.photoUri = uri; } - public void setDisplayName(String name) { - this.displayName = name; + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSystemName(String systemName) { + this.systemName = systemName; } public String getSystemAccount() { @@ -249,58 +240,76 @@ public class Contact extends AbstractEntity implements Serializable { } } - public void setSubscriptionOption(int option) { + public void setOption(int option) { this.subscription |= 1 << option; } - public void resetSubscriptionOption(int option) { + public void resetOption(int option) { this.subscription &= ~(1 << option); } - public boolean getSubscriptionOption(int option) { + public boolean getOption(int option) { return ((this.subscription & (1 << option)) != 0); } + public boolean showInRoster() { + return (this.getOption(Contact.Options.IN_ROSTER) && (!this + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); + } + public void parseSubscriptionFromElement(Element item) { String ask = item.getAttribute("ask"); String subscription = item.getAttribute("subscription"); if (subscription != null) { if (subscription.equals("to")) { - this.resetSubscriptionOption(Contact.Subscription.FROM); - this.setSubscriptionOption(Contact.Subscription.TO); + this.resetOption(Contact.Options.FROM); + this.setOption(Contact.Options.TO); } else if (subscription.equals("from")) { - this.resetSubscriptionOption(Contact.Subscription.TO); - this.setSubscriptionOption(Contact.Subscription.FROM); + this.resetOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); } else if (subscription.equals("both")) { - this.setSubscriptionOption(Contact.Subscription.TO); - this.setSubscriptionOption(Contact.Subscription.FROM); + this.setOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + } else if (subscription.equals("none")) { + this.resetOption(Contact.Options.FROM); + this.resetOption(Contact.Options.TO); } } - if ((ask != null) && (ask.equals("subscribe"))) { - this.setSubscriptionOption(Contact.Subscription.ASKING); - } else { - this.resetSubscriptionOption(Contact.Subscription.ASKING); + // do NOT override asking if pending push request + if (!this.getOption(Contact.Options.DIRTY_PUSH)) { + if ((ask != null) && (ask.equals("subscribe"))) { + this.setOption(Contact.Options.ASKING); + } else { + this.resetOption(Contact.Options.ASKING); + } + } + } + + public Element asElement() { + Element item = new Element("item"); + item.setAttribute("jid", this.jid); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); } + return item; } - public class Subscription { + public class Options { public static final int TO = 0; public static final int FROM = 1; public static final int ASKING = 2; - public static final int PREEMPTIVE_GRANT = 4; - } - - public void flagAsNotInRoster() { - this.inRoster = false; - } - - public boolean isInRoster() { - return this.inRoster; - } - - public String getAccountUuid() { - return this.accountUuid; + public static final int PREEMPTIVE_GRANT = 3; + public static final int IN_ROSTER = 4; + public static final int PENDING_SUBSCRIPTION_REQUEST = 5; + public static final int DIRTY_PUSH = 6; + public static final int DIRTY_DELETE = 7; + } + + public class Lastseen { + public long time = 0; + public String presence = null; } } diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java index 5674f84a..640d89e9 100644 --- a/src/eu/siacs/conversations/entities/Conversation.java +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -4,13 +4,14 @@ import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; import java.util.List; +import eu.siacs.conversations.services.XmppConnectionService; + import net.java.otr4j.OtrException; import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionStatus; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -44,12 +45,11 @@ public class Conversation extends AbstractEntity { private int status; private long created; private int mode; - + private String nextPresence; private transient List<Message> messages = null; private transient Account account = null; - private transient Contact contact; private transient SessionImpl otrSession; @@ -60,6 +60,10 @@ public class Conversation extends AbstractEntity { private transient MucOptions mucOptions = null; + private transient String latestMarkableMessageId; + + private byte[] symmetricKey; + public Conversation(String name, Account account, String contactJid, int mode) { this(java.util.UUID.randomUUID().toString(), name, null, account @@ -101,15 +105,26 @@ public class Conversation extends AbstractEntity { } public void markRead() { - if (this.messages == null) + if (this.messages == null) { return; + } for (int i = this.messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) - return; + if (messages.get(i).isRead()) { + break; + } this.messages.get(i).markRead(); } } + public void markRead(XmppConnectionService service) { + markRead(); + if (service.confirmMessages() && this.latestMarkableMessageId != null) { + service.sendConfirmMessage(getAccount(), getContactJid(), + this.latestMarkableMessageId); + this.latestMarkableMessageId = null; + } + } + public Message getLatestMessage() { if ((this.messages == null) || (this.messages.size() == 0)) { Message message = new Message(this, "", Message.ENCRYPTION_NONE); @@ -127,21 +142,16 @@ public class Conversation extends AbstractEntity { } public String getName(boolean useSubject) { - if ((getMode() == MODE_MULTI) && (getMucOptions().getSubject() != null) && useSubject) { + if ((getMode() == MODE_MULTI) && (getMucOptions().getSubject() != null) + && useSubject) { return getMucOptions().getSubject(); - } else if (this.contact != null) { - return this.contact.getDisplayName(); } else { - return this.name; + return this.getContact().getDisplayName(); } } public String getProfilePhotoString() { - if (this.contact == null) { - return null; - } else { - return this.contact.getProfilePhoto(); - } + return this.getContact().getProfilePhoto(); } public String getAccountUuid() { @@ -153,14 +163,7 @@ public class Conversation extends AbstractEntity { } public Contact getContact() { - return this.contact; - } - - public void setContact(Contact contact) { - this.contact = contact; - if (contact != null) { - this.contactUuid = contact.getUuid(); - } + return this.account.getRoster().getContact(this.contactJid); } public void setAccount(Account account) { @@ -222,14 +225,15 @@ public class Conversation extends AbstractEntity { this.mode = mode; } - public SessionImpl startOtrSession(Context context, String presence, boolean sendStart) { + public SessionImpl startOtrSession(Context context, String presence, + boolean sendStart) { if (this.otrSession != null) { return this.otrSession; } else { SessionID sessionId = new SessionID(this.getContactJid(), presence, "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine( - context)); + this.otrSession = new SessionImpl(sessionId, getAccount() + .getOtrEngine(context)); try { if (sendStart) { this.otrSession.startSession(); @@ -240,13 +244,13 @@ public class Conversation extends AbstractEntity { return null; } } - + } public SessionImpl getOtrSession() { return this.otrSession; } - + public void resetOtrSession() { this.otrSession = null; } @@ -260,21 +264,14 @@ public class Conversation extends AbstractEntity { } catch (OtrException e) { this.resetOtrSession(); } + } else { + this.resetOtrSession(); } } } public boolean hasValidOtrSession() { - if (this.otrSession == null) { - return false; - } else { - String foreignPresence = this.otrSession.getSessionID().getUserID(); - if (!getContact().getPresences().containsKey(foreignPresence)) { - this.resetOtrSession(); - return false; - } - return true; - } + return this.otrSession != null; } public String getOtrFingerprint() { @@ -298,7 +295,7 @@ public class Conversation extends AbstractEntity { public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { - this.mucOptions = new MucOptions(); + this.mucOptions = new MucOptions(this.getAccount()); } this.mucOptions.setConversation(this); return this.mucOptions; @@ -311,40 +308,59 @@ public class Conversation extends AbstractEntity { public void setContactJid(String jid) { this.contactJid = jid; } - + public void setNextPresence(String presence) { this.nextPresence = presence; } - + public String getNextPresence() { return this.nextPresence; } - + public int getLatestEncryption() { int latestEncryption = this.getLatestMessage().getEncryption(); - if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { return Message.ENCRYPTION_PGP; } else { return latestEncryption; } } - + public int getNextEncryption() { if (this.nextMessageEncryption == -1) { return this.getLatestEncryption(); } return this.nextMessageEncryption; } - + public void setNextEncryption(int encryption) { this.nextMessageEncryption = encryption; } - + public String getNextMessage() { - return this.nextMessage; + if (this.nextMessage == null) { + return ""; + } else { + return this.nextMessage; + } } - + public void setNextMessage(String message) { this.nextMessage = message; } + + public void setLatestMarkableMessageId(String id) { + if (id != null) { + this.latestMarkableMessageId = id; + } + } + + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } } diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java index 33f7a8d4..1e82fe6a 100644 --- a/src/eu/siacs/conversations/entities/Message.java +++ b/src/eu/siacs/conversations/entities/Message.java @@ -19,7 +19,10 @@ public class Message extends AbstractEntity { public static final int STATUS_SEND = 2; public static final int STATUS_SEND_FAILED = 3; public static final int STATUS_SEND_REJECTED = 4; + public static final int STATUS_WAITING = 5; public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; @@ -29,6 +32,8 @@ public class Message extends AbstractEntity { public static final int TYPE_TEXT = 0; public static final int TYPE_IMAGE = 1; + public static final int TYPE_AUDIO = 2; + public static final int TYPE_STATUS = 3; public static String CONVERSATION = "conversationUuid"; public static String COUNTERPART = "counterpart"; @@ -51,6 +56,10 @@ public class Message extends AbstractEntity { protected transient Conversation conversation = null; protected transient JingleConnection jingleConnection = null; + + private Message() { + + } public Message(Conversation conversation, String body, int encryption) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), @@ -192,7 +201,20 @@ public class Message extends AbstractEntity { } public void setPresence(String presence) { - this.counterpart = this.counterpart.split("/")[0] + "/" + presence; + if (presence == null) { + this.counterpart = this.counterpart.split("/")[0]; + } else { + this.counterpart = this.counterpart.split("/")[0] + "/" + presence; + } + } + + public String getPresence() { + String[] counterparts = this.counterpart.split("/"); + if (counterparts.length == 2) { + return counterparts[1]; + } else { + return null; + } } public void setJingleConnection(JingleConnection connection) { @@ -202,4 +224,11 @@ public class Message extends AbstractEntity { public JingleConnection getJingleConnection() { return this.jingleConnection; } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; + } } diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java index fbca8340..0f8e3565 100644 --- a/src/eu/siacs/conversations/entities/MucOptions.java +++ b/src/eu/siacs/conversations/entities/MucOptions.java @@ -3,11 +3,10 @@ package eu.siacs.conversations.entities; import java.util.ArrayList; import java.util.List; -import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import android.annotation.SuppressLint; -import android.util.Log; @SuppressLint("DefaultLocale") public class MucOptions { @@ -31,6 +30,7 @@ public class MucOptions { private int role; private int affiliation; private String name; + private long pgpKeyId = 0; public String getName() { return name; @@ -70,7 +70,15 @@ public class MucOptions { this.affiliation = AFFILIATION_NONE; } } + public void setPgpKeyId(long id) { + this.pgpKeyId = id; + } + + public long getPgpKeyId() { + return this.pgpKeyId; + } } + private Account account; private ArrayList<User> users = new ArrayList<User>(); private Conversation conversation; private boolean isOnline = false; @@ -80,6 +88,9 @@ public class MucOptions { private User self = new User(); private String subject = null; + public MucOptions(Account account) { + this.account = account; + } public void deleteUser(String name) { for(int i = 0; i < users.size(); ++i) { @@ -100,7 +111,7 @@ public class MucOptions { users.add(user); } - public void processPacket(PresencePacket packet) { + public void processPacket(PresencePacket packet, PgpEngine pgp) { String[] fromParts = packet.getFrom().split("/"); if (fromParts.length>=2) { String name = fromParts[1]; @@ -119,6 +130,20 @@ public class MucOptions { } else { addUser(user); } + if (pgp != null) { + Element x = packet.findChild("x", + "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account,msg, x.getContent())); + } + } } else if (type.equals("unavailable")) { if (name.equals(getNick())) { Element item = packet.findChild("x","http://jabber.org/protocol/muc#user").findChild("item"); @@ -211,4 +236,36 @@ public class MucOptions { public void flagAboutToRename() { this.aboutToRename = true; } + + public long[] getPgpKeyIds() { + List<Long> ids = new ArrayList<Long>(); + for(User user : getUsers()) { + if(user.getPgpKeyId()!=0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for(int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for(User user : getUsers()) { + if (user.getPgpKeyId()!=0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for(User user : getUsers()) { + if (user.getPgpKeyId()==0) { + return false; + } + } + return true; + } }
\ No newline at end of file diff --git a/src/eu/siacs/conversations/entities/Presences.java b/src/eu/siacs/conversations/entities/Presences.java index acbaafca..77891648 100644 --- a/src/eu/siacs/conversations/entities/Presences.java +++ b/src/eu/siacs/conversations/entities/Presences.java @@ -4,10 +4,6 @@ import java.util.Hashtable; import java.util.Iterator; import java.util.Map.Entry; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import eu.siacs.conversations.xml.Element; public class Presences { @@ -47,39 +43,6 @@ public class Presences { return status; } - public String toJsonString() { - JSONArray json = new JSONArray(); - Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); - - while (it.hasNext()) { - Entry<String, Integer> entry = it.next(); - JSONObject jObj = new JSONObject(); - try { - jObj.put("resource", entry.getKey()); - jObj.put("status", entry.getValue()); - } catch (JSONException e) { - - } - json.put(jObj); - } - return json.toString(); - } - - public static Presences fromJsonString(String jsonString) { - Presences presences = new Presences(); - try { - JSONArray json = new JSONArray(jsonString); - for (int i = 0; i < json.length(); ++i) { - JSONObject jObj = json.getJSONObject(i); - presences.updatePresence(jObj.getString("resource"), - jObj.getInt("status")); - } - } catch (JSONException e1) { - - } - return presences; - } - public static int parseShow(Element show) { if (show == null) { return Presences.ONLINE; @@ -99,4 +62,14 @@ public class Presences { public int size() { return presences.size(); } + + public String[] asStringArray() { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + + public boolean has(String presence) { + return presences.containsKey(presence); + } } diff --git a/src/eu/siacs/conversations/entities/Roster.java b/src/eu/siacs/conversations/entities/Roster.java new file mode 100644 index 00000000..c1e40dbc --- /dev/null +++ b/src/eu/siacs/conversations/entities/Roster.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public class Roster { + Account account; + ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public boolean hasContact(String jid) { + String cleanJid = jid.split("/")[0]; + return contacts.containsKey(cleanJid); + } + + public Contact getContact(String jid) { + String cleanJid = jid.split("/")[0]; + if (contacts.containsKey(cleanJid)) { + return contacts.get(cleanJid); + } else { + Contact contact = new Contact(cleanJid); + contact.setAccount(account); + contacts.put(cleanJid, contact); + return contact; + } + } + + public void clearPresences() { + for(Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for(Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for(Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List<Contact> getContacts() { + return new ArrayList<Contact>(this.contacts.values()); + } + + public void initContact(Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + contacts.put(contact.getJid(),contact); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/src/eu/siacs/conversations/parser/AbstractParser.java b/src/eu/siacs/conversations/parser/AbstractParser.java new file mode 100644 index 00000000..2bd839f1 --- /dev/null +++ b/src/eu/siacs/conversations/parser/AbstractParser.java @@ -0,0 +1,53 @@ +package eu.siacs.conversations.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected long getTimestamp(Element packet) { + if (packet.hasChild("delay")) { + try { + String stamp = packet.findChild("delay").getAttribute( + "stamp"); + stamp = stamp.replace("Z", "+0000"); + Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + .parse(stamp); + return date.getTime(); + } catch (ParseException e) { + return System.currentTimeMillis(); + } + } else { + return System.currentTimeMillis(); + } + } + + protected void updateLastseen(Element packet, Account account, boolean presenceOverwrite) { + String[] fromParts = packet.getAttribute("from").split("/"); + String from = fromParts[0]; + String presence = null; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } + Contact contact = account.getRoster().getContact(from); + long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if ((presence!=null)&&(presenceOverwrite)) { + contact.lastseen.presence = presence; + } + } + } +} diff --git a/src/eu/siacs/conversations/parser/MessageParser.java b/src/eu/siacs/conversations/parser/MessageParser.java new file mode 100644 index 00000000..a435d055 --- /dev/null +++ b/src/eu/siacs/conversations/parser/MessageParser.java @@ -0,0 +1,223 @@ +package eu.siacs.conversations.parser; + +import android.util.Log; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser { + + public MessageParser(XmppConnectionService service) { + super(service); + } + + public Message parseChat(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + updateLastseen(packet, account,true); + String pgpBody = getPgpBody(packet); + if (pgpBody != null) { + return new Message(conversation, packet.getFrom(), pgpBody, + Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED); + } else { + return new Message(conversation, packet.getFrom(), + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECIEVED); + } + } + + public Message parseOtrChat(MessagePacket packet, Account account) { + boolean properlyAddressed = (packet.getTo().split("/").length == 2) + || (account.countPresences() == 1); + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + updateLastseen(packet, account,true); + String body = packet.getBody(); + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession( + mXmppConnectionService.getApplicationContext(), + fromParts[1], false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(fromParts[1])) { + conversation.resetOtrSession(); + if (properlyAddressed) { + conversation.startOtrSession( + mXmppConnectionService.getApplicationContext(), + fromParts[1], false); + } else { + return null; + } + } + } + try { + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + } + if ((body == null) || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + Log.d("xmppService","new symmetric key: "+CryptoHelper.bytesToHex(conversation.getSymmetricKey())); + return null; + } + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + Message finishedMessage = new Message(conversation, packet.getFrom(), body, + Message.ENCRYPTION_OTR, Message.STATUS_RECIEVED); + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } catch (Exception e) { + conversation.resetOtrSession(); + return null; + } + } + + public Message parseGroupchat(MessagePacket packet, Account account) { + int status; + String[] fromParts = packet.getFrom().split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], true); + if (packet.hasChild("subject")) { + conversation.getMucOptions().setSubject( + packet.findChild("subject").getContent()); + mXmppConnectionService.updateUi(conversation, false); + return null; + } + if ((fromParts.length == 1)) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(conversation.getMucOptions().getNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND)) { + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECIEVED; + } + String pgpBody = getPgpBody(packet); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, counterPart, packet.getBody(), + Message.ENCRYPTION_NONE, status); + } else { + finishedMessage= new Message(conversation, counterPart, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + public Message parseCarbonMessage(MessagePacket packet, Account account) { + int status; + String fullJid; + Element forwarded; + if (packet.hasChild("received")) { + forwarded = packet.findChild("received").findChild("forwarded"); + status = Message.STATUS_RECIEVED; + } else if (packet.hasChild("sent")) { + forwarded = packet.findChild("sent").findChild("forwarded"); + status = Message.STATUS_SEND; + } else { + return null; + } + if (forwarded == null) { + return null; + } + Element message = forwarded.findChild("message"); + if ((message == null) || (!message.hasChild("body"))) + return null; // either malformed or boring + if (status == Message.STATUS_RECIEVED) { + fullJid = message.getAttribute("from"); + updateLastseen(message, account,true); + } else { + fullJid = message.getAttribute("to"); + } + String[] parts = fullJid.split("/"); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, parts[0], false); + conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, fullJid, pgpBody,Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage= new Message(conversation, fullJid, body,Message.ENCRYPTION_NONE, status); + } + finishedMessage.setTime(getTimestamp(message)); + return finishedMessage; + } + + public void parseError(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/"); + mXmppConnectionService.markMessage(account, fromParts[0], + packet.getId(), Message.STATUS_SEND_FAILED); + } + + public void parseNormal(MessagePacket packet, Account account) { + if (packet.hasChild("displayed","urn:xmpp:chat-markers:0")) { + String id = packet.findChild("displayed","urn:xmpp:chat-markers:0").getAttribute("id"); + String[] fromParts = packet.getFrom().split("/"); + updateLastseen(packet, account,true); + mXmppConnectionService.markMessage(account,fromParts[0], id, Message.STATUS_SEND_DISPLAYED); + } else if (packet.hasChild("received","urn:xmpp:chat-markers:0")) { + String id = packet.findChild("received","urn:xmpp:chat-markers:0").getAttribute("id"); + String[] fromParts = packet.getFrom().split("/"); + updateLastseen(packet, account,false); + mXmppConnectionService.markMessage(account,fromParts[0], id, Message.STATUS_SEND_RECEIVED); + } else if (packet.hasChild("x")) { + Element x = packet.findChild("x"); + if (x.hasChild("invite")) { + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, packet.getFrom(), + true); + mXmppConnectionService.updateUi(conversation, false); + } + + } + } + + private String getPgpBody(Element message) { + Element child = message.findChild("x", "jabber:x:encrypted"); + if (child == null) { + return null; + } else { + return child.getContent(); + } + } + + private String getMarkableMessageId(Element message) { + if (message.hasChild("markable", "urn:xmpp:chat-markers:0")) { + return message.getAttribute("id"); + } else { + return null; + } + } + + +} diff --git a/src/eu/siacs/conversations/parser/PresenceParser.java b/src/eu/siacs/conversations/parser/PresenceParser.java new file mode 100644 index 00000000..2003d4cd --- /dev/null +++ b/src/eu/siacs/conversations/parser/PresenceParser.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceParser extends AbstractParser { + + public PresenceParser(XmppConnectionService service) { + super(service); + } + + public void parseConferencePresence(PresencePacket packet, Account account) { + PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Conversation muc = mXmppConnectionService.findMuc(packet + .getAttribute("from").split("/")[0], account); + if (muc != null) { + muc.getMucOptions().processPacket(packet, mPgpEngine); + } + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + Conversation muc = mXmppConnectionService.findMuc(packet + .getAttribute("from").split("/")[0], account); + if (muc != null) { + int error = muc.getMucOptions().getError(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (muc.getMucOptions().getError() != error) { + mXmppConnectionService.updateUi(muc, false); + } + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + String[] fromParts = packet.getAttribute("from").split("/"); + String type = packet.getAttribute("type"); + if (fromParts[0].equals(account.getJid())) { + if (fromParts.length == 2) { + if (type == null) { + account.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + } else if (type.equals("unavailable")) { + account.removePresence(fromParts[1]); + } + } + + } else { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + if (fromParts.length == 2) { + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account,true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact,online); + } + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + contact.clearPresences(); + } else { + contact.removePresence(fromParts[1]); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact,false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresenceUpdatesTo(contact); + contact.setOption(Contact.Options.FROM); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + if ((contact.getOption(Contact.Options.ASKING)) + && (!contact.getOption(Contact.Options.TO))) { + mXmppConnectionService + .requestPresenceUpdatesFrom(contact); + } + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + } + } + } + } + +} diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java index 5a34dac6..fbf45d25 100644 --- a/src/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -2,20 +2,17 @@ package eu.siacs.conversations.persistance; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presences; -import android.content.ContentValues; +import eu.siacs.conversations.entities.Roster; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.os.Bundle; import android.util.Log; public class DatabaseBackend extends SQLiteOpenHelper { @@ -23,7 +20,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 3; + private static final int DATABASE_VERSION = 5; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.SERVERNAME + " TEXT, " + + Contact.SYSTEMNAME + " TEXT," + Contact.JID + " TEXT," + + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," + + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT + + " NUMBER, " + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE("+Contact.ACCOUNT+", "+Contact.JID+") ON CONFLICT REPLACE);"; public DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -36,7 +41,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS - + " NUMBER, "+Account.KEYS+" TEXT)"); + + " NUMBER, " + Account.KEYS + " TEXT)"); db.execSQL("create table " + Conversation.TABLENAME + " (" + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + " TEXT, " + Conversation.CONTACT + " TEXT, " @@ -50,31 +55,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION - + " NUMBER, " + Message.STATUS + " NUMBER," +Message.TYPE +" NUMBER, FOREIGN KEY(" - + Message.CONVERSATION + ") REFERENCES " - + Conversation.TABLENAME + "(" + Conversation.UUID - + ") ON DELETE CASCADE);"); - db.execSQL("create table " + Contact.TABLENAME + "(" + Contact.UUID - + " TEXT PRIMARY KEY, " + Contact.ACCOUNT + " TEXT, " - + Contact.DISPLAYNAME + " TEXT," + Contact.JID + " TEXT," - + Contact.PRESENCES + " TEXT, " + Contact.KEYS - + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.SUBSCRIPTION - + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, " - + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " - + Account.TABLENAME + "(" + Account.UUID - + ") ON DELETE CASCADE);"); + + " NUMBER, " + Message.STATUS + " NUMBER," + Message.TYPE + + " NUMBER, FOREIGN KEY(" + Message.CONVERSATION + + ") REFERENCES " + Conversation.TABLENAME + "(" + + Conversation.UUID + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2 && newVersion >= 2) { - // enable compression by default. - db.execSQL("update " + Account.TABLENAME - + " set " + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); } if (oldVersion < 3 && newVersion >= 3) { - //add field type to message - db.execSQL("ALTER TABLE "+Message.TABLENAME+" ADD COLUMN "+Message.TYPE+" NUMBER");; + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE "+Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE "+Account.TABLENAME+ " SET "+Account.ROSTERVERSION+" = NULL"); } } @@ -99,7 +101,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); db.insert(Account.TABLENAME, null, account.getContentValues()); } - + public void createContact(Contact contact) { SQLiteDatabase db = this.getWritableDatabase(); db.insert(Contact.TABLENAME, null, contact.getContentValues()); @@ -126,14 +128,26 @@ public class DatabaseBackend extends SQLiteOpenHelper { } return list; } + + public List<Message> getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit,-1); + } - public List<Message> getMessages(Conversation conversation, int limit) { + public List<Message> getMessages(Conversation conversation, int limit, long timestamp) { List<Message> list = new CopyOnWriteArrayList<Message>(); SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { conversation.getUuid() }; - Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC", - String.valueOf(limit)); + Cursor cursor; + if (timestamp==-1) { + String[] selectionArgs = { conversation.getUuid() }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } else { + String[] selectionArgs = { conversation.getUuid() , ""+timestamp}; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and "+Message.TIME_SENT+"<?", selectionArgs, null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } if (cursor.getCount() > 0) { cursor.moveToLast(); do { @@ -145,10 +159,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { public Conversation findConversation(Account account, String contactJid) { SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), contactJid+"%" }; + String[] selectionArgs = { account.getUuid(), contactJid + "%" }; Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + " like ?", - selectionArgs, null, null, null); + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + + " like ?", selectionArgs, null, null, null); if (cursor.getCount() == 0) return null; cursor.moveToFirst(); @@ -200,87 +214,32 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args); } - - public void updateContact(Contact contact, boolean updatePresences) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { contact.getUuid() }; - ContentValues values = contact.getContentValues(); - if (!updatePresences) { - values.remove(Contact.PRESENCES); - } else { - values.remove(Contact.DISPLAYNAME); - values.remove(Contact.PHOTOURI); - values.remove(Contact.SYSTEMACCOUNT); - } - db.update(Contact.TABLENAME, contact.getContentValues(), Contact.UUID - + "=?", args); - } - - public void clearPresences(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { account.getUuid() }; - ContentValues values = new ContentValues(); - values.put(Contact.PRESENCES,"[]"); - db.update(Contact.TABLENAME, values, Contact.ACCOUNT - + "=?", args); - } - - public void mergeContacts(List<Contact> contacts) { - SQLiteDatabase db = this.getWritableDatabase(); - for (int i = 0; i < contacts.size(); i++) { - Contact contact = contacts.get(i); - String[] columns = {Contact.UUID, Contact.PRESENCES}; - String[] args = {contact.getAccount().getUuid(), contact.getJid()}; - Cursor cursor = db.query(Contact.TABLENAME, columns,Contact.ACCOUNT+"=? AND "+Contact.JID+"=?", args, null, null, null); - if (cursor.getCount()>=1) { - cursor.moveToFirst(); - contact.setUuid(cursor.getString(0)); - updateContact(contact,false); - } else { - contact.setUuid(UUID.randomUUID().toString()); - createContact(contact); - } - } - } - public List<Contact> getContactsByAccount(Account account) { - List<Contact> list = new ArrayList<Contact>(); + public void readRoster(Roster roster) { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; - if (account==null) { - cursor = db.query(Contact.TABLENAME, null, null, null, null, - null, null); - } else { - String args[] = {account.getUuid()}; - cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT+"=?", args, null, - null, null); - } + String args[] = { roster.getAccount().getUuid() }; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", + args, null, null, null); while (cursor.moveToNext()) { - list.add(Contact.fromCursor(cursor)); + roster.initContact(Contact.fromCursor(cursor)); } - return list; } - public List<Contact> getContacts(String where) { - List<Contact> list = new ArrayList<Contact>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(Contact.TABLENAME, null, where, null, null, null, null); - while (cursor.moveToNext()) { - list.add(Contact.fromCursor(cursor)); + public void writeRoster(Roster roster) { + Account account = roster.getAccount(); + SQLiteDatabase db = this.getWritableDatabase(); + for(Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND "+Contact.JID+"=?"; + String[] whereArgs = {account.getUuid(), contact.getJid()}; + db.delete(Contact.TABLENAME, where, whereArgs); + } } - return list; - } - - public Contact findContact(Account account, String jid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { account.getUuid(), jid }; - Cursor cursor = db.query(Contact.TABLENAME, null, - Contact.ACCOUNT + "=? AND " + Contact.JID + "=?", - selectionArgs, null, null, null); - if (cursor.getCount() == 0) - return null; - cursor.moveToFirst(); - return Contact.fromCursor(cursor); + account.setRosterVersion(roster.getVersion()); + updateAccount(account); } public void deleteMessage(Message message) { @@ -288,34 +247,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] args = { message.getUuid() }; db.delete(Message.TABLENAME, Message.UUID + "=?", args); } - + public void deleteMessagesInConversation(Conversation conversation) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = { conversation.getUuid() }; db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); } - public void deleteContact(Contact contact) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { contact.getUuid() }; - db.delete(Contact.TABLENAME, Contact.UUID + "=?", args); - } - - public Contact getContact(String uuid) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { uuid }; - Cursor cursor = db.query(Contact.TABLENAME, null, Contact.UUID + "=?", args, null, null, null); - if (cursor.getCount() == 0) { - return null; - } - cursor.moveToFirst(); - return Contact.fromCursor(cursor); - } - public Conversation findConversationByUuid(String conversationUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { conversationUuid }; - Cursor cursor = db.query(Conversation.TABLENAME, null, Conversation.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.UUID + "=?", selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } @@ -326,18 +269,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { public Message findMessageByUuid(String messageUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { messageUuid }; - Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", + selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } cursor.moveToFirst(); return Message.fromCursor(cursor); } - + public Account findAccountByUuid(String accountUuid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = { accountUuid }; - Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", selectionArgs, null, null, null); + Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", + selectionArgs, null, null, null); if (cursor.getCount() == 0) { return null; } diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java index becb1ee3..ca6360b9 100644 --- a/src/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/eu/siacs/conversations/persistance/FileBackend.java @@ -1,12 +1,12 @@ package eu.siacs.conversations.persistance; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.ref.WeakReference; import android.content.Context; import android.graphics.Bitmap; @@ -14,14 +14,9 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.util.Log; import android.util.LruCache; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jingle.JingleFile; public class FileBackend { @@ -43,11 +38,11 @@ public class FileBackend { }; } - + public LruCache<String, Bitmap> getThumbnailCache() { return thumbnailCache; } - + public JingleFile getJingleFile(Message message) { return getJingleFile(message, true); } @@ -58,10 +53,14 @@ public class FileBackend { String path = prefix + "/" + conversation.getAccount().getJid() + "/" + conversation.getContactJid(); String filename; - if ((decrypted)||(message.getEncryption() == Message.ENCRYPTION_NONE)) { + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { filename = message.getUuid() + ".webp"; } else { - filename = message.getUuid() + ".webp.pgp"; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename = message.getUuid() + ".webp"; + } else { + filename = message.getUuid() + ".webp.pgp"; + } } return new JingleFile(path + "/" + filename); } @@ -86,37 +85,64 @@ public class FileBackend { return originalBitmap; } } + + public JingleFile copyImageToPrivateStorage(Message message, Uri image) throws ImageCopyException { + return this.copyImageToPrivateStorage(message, image,0); + } - public JingleFile copyImageToPrivateStorage(Message message, Uri image) { + private JingleFile copyImageToPrivateStorage(Message message, Uri image, int sampleSize) + throws ImageCopyException { try { - Log.d("xmppService","copying file: "+image.toString()+ " to internal storage"); - InputStream is = context.getContentResolver() - .openInputStream(image); + InputStream is; + if (image != null) { + is = context.getContentResolver().openInputStream(image); + } else { + is = new FileInputStream(getIncomingFile()); + } JingleFile file = getJingleFile(message); file.getParentFile().mkdirs(); file.createNewFile(); - OutputStream os = new FileOutputStream(file); - Bitmap originalBitmap = BitmapFactory.decodeStream(is); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d("xmppService","reading bitmap with sample size "+inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); is.close(); + if (originalBitmap == null) { + throw new ImageCopyException(R.string.error_not_an_image_file); + } + if (image == null) { + getIncomingFile().delete(); + } Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + OutputStream os = new FileOutputStream(file); boolean success = scalledBitmap.compress( Bitmap.CompressFormat.WEBP, 75, os); if (!success) { - return null; + throw new ImageCopyException(R.string.error_compressing_image); } os.flush(); os.close(); long size = file.getSize(); int width = scalledBitmap.getWidth(); int height = scalledBitmap.getHeight(); - message.setBody(""+size+","+width+","+height); + message.setBody("" + size + "," + width + "," + height); return file; } catch (FileNotFoundException e) { - return null; + throw new ImageCopyException(R.string.error_file_not_found); } catch (IOException e) { - return null; + throw new ImageCopyException(R.string.error_io_exception); } catch (SecurityException e) { - return null; + throw new ImageCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize<=3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new ImageCopyException(R.string.error_out_of_memory); + } } } @@ -128,7 +154,7 @@ public class FileBackend { public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { Bitmap thumbnail = thumbnailCache.get(message.getUuid()); - if ((thumbnail == null)&&(!cacheOnly)) { + if ((thumbnail == null) && (!cacheOnly)) { Bitmap fullsize = BitmapFactory.decodeFile(getJingleFile(message) .getAbsolutePath()); if (fullsize == null) { @@ -160,4 +186,21 @@ public class FileBackend { } f.delete(); } + + public File getIncomingFile() { + return new File(context.getFilesDir().getAbsolutePath() + "/incoming"); + } + + public class ImageCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public ImageCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } } diff --git a/src/eu/siacs/conversations/services/ImageProvider.java b/src/eu/siacs/conversations/services/ImageProvider.java index c1bae661..80cfbd95 100644 --- a/src/eu/siacs/conversations/services/ImageProvider.java +++ b/src/eu/siacs/conversations/services/ImageProvider.java @@ -2,13 +2,13 @@ package eu.siacs.conversations.services; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; - import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; @@ -21,46 +21,62 @@ public class ImageProvider extends ContentProvider { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - DatabaseBackend databaseBackend = DatabaseBackend - .getInstance(getContext()); + ParcelFileDescriptor pfd; FileBackend fileBackend = new FileBackend(getContext()); - String uuids = uri.getPath(); - Log.d("xmppService", "uuids = " + uuids); - if (uuids == null) { - throw new FileNotFoundException(); - } - String[] uuidsSplited = uuids.split("/"); - if (uuidsSplited.length != 3) { + if ("r".equals(mode)) { + DatabaseBackend databaseBackend = DatabaseBackend + .getInstance(getContext()); + String uuids = uri.getPath(); + Log.d("xmppService", "uuids = " + uuids+" mode="+mode); + if (uuids == null) { + throw new FileNotFoundException(); + } + String[] uuidsSplited = uuids.split("/"); + if (uuidsSplited.length != 3) { + throw new FileNotFoundException(); + } + String conversationUuid = uuidsSplited[1]; + String messageUuid = uuidsSplited[2].split("\\.")[0]; + + Log.d("xmppService","messageUuid="+messageUuid); + + Conversation conversation = databaseBackend + .findConversationByUuid(conversationUuid); + if (conversation == null) { + throw new FileNotFoundException("conversation " + conversationUuid + + " could not be found"); + } + Message message = databaseBackend.findMessageByUuid(messageUuid); + if (message == null) { + throw new FileNotFoundException("message " + messageUuid + + " could not be found"); + } + + Account account = databaseBackend.findAccountByUuid(conversation + .getAccountUuid()); + if (account == null) { + throw new FileNotFoundException("account " + + conversation.getAccountUuid() + " cound not be found"); + } + message.setConversation(conversation); + conversation.setAccount(account); + + File file = fileBackend.getJingleFile(message); + pfd = ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_READ_ONLY); + return pfd; + } else if ("w".equals(mode)){ + File file = fileBackend.getIncomingFile(); + try { + file.createNewFile(); + } catch (IOException e) { + throw new FileNotFoundException(); + } + pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE); + return pfd; + } else { throw new FileNotFoundException(); } - String conversationUuid = uuidsSplited[1]; - String messageUuid = uuidsSplited[2]; - - Conversation conversation = databaseBackend - .findConversationByUuid(conversationUuid); - if (conversation == null) { - throw new FileNotFoundException("conversation " + conversationUuid - + " could not be found"); - } - Message message = databaseBackend.findMessageByUuid(messageUuid); - if (message == null) { - throw new FileNotFoundException("message " + messageUuid - + " could not be found"); - } - - Account account = databaseBackend.findAccountByUuid(conversation - .getAccountUuid()); - if (account == null) { - throw new FileNotFoundException("account " - + conversation.getAccountUuid() + " cound not be found"); - } - message.setConversation(conversation); - conversation.setAccount(account); - - File file = fileBackend.getJingleFile(message); - ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, - ParcelFileDescriptor.MODE_READ_ONLY); - return pfd; } @Override @@ -93,5 +109,17 @@ public class ImageProvider extends ContentProvider { public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { return 0; } + + public static Uri getContentUri(Message message) { + return Uri + .parse("content://eu.siacs.conversations.images/" + + message.getConversationUuid() + + "/" + + message.getUuid() + + ".webp"); + } -} + public static Uri getIncomingContentUri() { + return Uri.parse("content://eu.siacs.conversations.images/incoming"); + } +}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index d2742997..505d09e5 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -1,23 +1,18 @@ package eu.siacs.conversations.services; -import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.security.SecureRandom; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Locale; -import java.util.Random; -import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -26,20 +21,22 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.persistance.OnPhoneContactsMerged; import eu.siacs.conversations.ui.OnAccountListChangedListener; import eu.siacs.conversations.ui.OnConversationListChangedListener; -import eu.siacs.conversations.ui.OnRosterFetchedListener; import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.utils.MessageParser; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; +import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; @@ -47,12 +44,12 @@ import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnTLSExceptionReceived; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleFile; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; @@ -60,7 +57,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.ContentObserver; -import android.database.DatabaseUtils; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -88,6 +84,11 @@ public class XmppConnectionService extends Service { private static final int CONNECT_TIMEOUT = 60; private static final long CARBON_GRACE_PERIOD = 60000L; + private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + + private MessageParser mMessageParser = new MessageParser(this); + private PresenceParser mPresenceParser = new PresenceParser(this); + private List<Account> accounts; private List<Conversation> conversations = null; private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( @@ -97,13 +98,26 @@ public class XmppConnectionService extends Service { private int convChangedListenerCount = 0; private OnAccountListChangedListener accountChangedListener = null; private OnTLSExceptionReceived tlsException = null; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = findActiveConversation(contact); + if (conversation != null) { + conversation.endOtrIfNeeded(); + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; public void setOnTLSExceptionReceivedListener( OnTLSExceptionReceived listener) { tlsException = listener; } - private Random mRandom = new Random(System.currentTimeMillis()); + private SecureRandom mRandom; private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD; @@ -111,13 +125,13 @@ public class XmppConnectionService extends Service { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); - Log.d(LOGTAG, "contact list has changed"); - mergePhoneContactsWithRoster(null); + Intent intent = new Intent(getApplicationContext(), + XmppConnectionService.class); + intent.setAction(ACTION_MERGE_PHONE_CONTACTS); + startService(intent); } }; - private XmppConnectionService service = this; - private final IBinder mBinder = new XmppConnectionBinder(); private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() { @@ -132,26 +146,19 @@ public class XmppConnectionService extends Service { } if ((packet.getType() == MessagePacket.TYPE_CHAT)) { - String pgpBody = MessageParser.getPgpBody(packet); - if (pgpBody != null) { - message = MessageParser.parsePgpChat(pgpBody, packet, - account, service); - message.markUnread(); - } else if ((packet.getBody() != null) + if ((packet.getBody() != null) && (packet.getBody().startsWith("?OTR"))) { - message = MessageParser.parseOtrChat(packet, account, - service); + message = mMessageParser.parseOtrChat(packet, account); if (message != null) { message.markUnread(); } } else if (packet.hasChild("body")) { - message = MessageParser.parsePlainTextChat(packet, account, - service); + message = mMessageParser.parseChat(packet, account); message.markUnread(); } else if (packet.hasChild("received") || (packet.hasChild("sent"))) { - message = MessageParser.parseCarbonMessage(packet, account, - service); + message = mMessageParser + .parseCarbonMessage(packet, account); if (message != null) { if (message.getStatus() == Message.STATUS_SEND) { lastCarbonMessageReceived = SystemClock @@ -165,8 +172,7 @@ public class XmppConnectionService extends Service { } } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { - message = MessageParser - .parseGroupchat(packet, account, service); + message = mMessageParser.parseGroupchat(packet, account); if (message != null) { if (message.getStatus() == Message.STATUS_RECIEVED) { message.markUnread(); @@ -176,38 +182,31 @@ public class XmppConnectionService extends Service { } } } else if (packet.getType() == MessagePacket.TYPE_ERROR) { - MessageParser.parseError(packet, account, service); + mMessageParser.parseError(packet, account); return; } else if (packet.getType() == MessagePacket.TYPE_NORMAL) { - if (packet.hasChild("x")) { - Element x = packet.findChild("x"); - if (x.hasChild("invite")) { - findOrCreateConversation(account, packet.getFrom(), - true); - if (convChangedListener != null) { - convChangedListener.onConversationListChanged(); - } - Log.d(LOGTAG, - "invitation received to " + packet.getFrom()); - } - - } else { - // Log.d(LOGTAG, "unparsed message " + packet.toString()); - } + mMessageParser.parseNormal(packet, account); } if ((message == null) || (message.getBody() == null)) { return; } - if (packet.hasChild("delay")) { - try { - String stamp = packet.findChild("delay").getAttribute( - "stamp"); - stamp = stamp.replace("Z", "+0000"); - Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") - .parse(stamp); - message.setTime(date.getTime()); - } catch (ParseException e) { - Log.d(LOGTAG, "error trying to parse date" + e.getMessage()); + if ((confirmMessages()) && ((packet.getId() != null))) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(message.getCounterpart()); + receivedPacket.setFrom(account.getFullJid()); + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + Element received = receivedPacket.addChild("received", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", packet.getId()); + account.getXmppConnection().sendMessagePacket( + receivedPacket); + } else if (packet.hasChild("request", "urn:xmpp:receipts")) { + Element received = receivedPacket.addChild("received", + "urn:xmpp:receipts"); + received.setAttribute("id", packet.getId()); + account.getXmppConnection().sendMessagePacket( + receivedPacket); } } Conversation conversation = message.getConversation(); @@ -234,9 +233,11 @@ public class XmppConnectionService extends Service { List<Conversation> conversations = getConversations(); for (int i = 0; i < conversations.size(); ++i) { if (conversations.get(i).getAccount() == account) { + conversations.get(i).endOtrIfNeeded(); sendUnsendMessages(conversations.get(i)); } } + syncDirtyContacts(account); scheduleWakeupCall(PING_MAX_INTERVAL, true); } else if (account.getStatus() == Account.STATUS_OFFLINE) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { @@ -247,10 +248,18 @@ public class XmppConnectionService extends Service { } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) { databaseBackend.updateAccount(account); reconnectAccount(account, true); - } else { - UIHelper.showErrorNotification(getApplicationContext(), - getAccounts()); + } else if ((account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + int next = account.getXmppConnection().getTimeToNextAttempt(); + Log.d(LOGTAG, account.getJid() + + ": error connecting account. try again in " + next + + "s for the " + + (account.getXmppConnection().getAttempt() + 1) + + " time"); + scheduleWakeupCall(next, false); } + UIHelper.showErrorNotification(getApplicationContext(), + getAccounts()); } }; @@ -260,116 +269,11 @@ public class XmppConnectionService extends Service { public void onPresencePacketReceived(final Account account, PresencePacket packet) { if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { - Conversation muc = findMuc( - packet.getAttribute("from").split("/")[0], account); - if (muc != null) { - muc.getMucOptions().processPacket(packet); - } else { - Log.d(LOGTAG, account.getJid() - + ": could not find muc for received muc package " - + packet.toString()); - } + mPresenceParser.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - Conversation muc = findMuc( - packet.getAttribute("from").split("/")[0], account); - if (muc != null) { - Log.d(LOGTAG, - account.getJid() + ": reading muc status packet " - + packet.toString()); - int error = muc.getMucOptions().getError(); - muc.getMucOptions().processPacket(packet); - if ((muc.getMucOptions().getError() != error) - && (convChangedListener != null)) { - Log.d(LOGTAG, "muc error status changed"); - convChangedListener.onConversationListChanged(); - } - } + mPresenceParser.parseConferencePresence(packet, account); } else { - String[] fromParts = packet.getAttribute("from").split("/"); - String type = packet.getAttribute("type"); - if (fromParts[0].equals(account.getJid())) { - if (fromParts.length == 2) { - if (type == null) { - account.updatePresence(fromParts[1], Presences - .parseShow(packet.findChild("show"))); - } else if (type.equals("unavailable")) { - account.removePresence(fromParts[1]); - } - } - - } else { - Contact contact = findContact(account, fromParts[0]); - if (contact == null) { - if ("subscribe".equals(type)) { - account.getXmppConnection().addPendingSubscription( - fromParts[0]); - } else { - // Log.d(LOGTAG,packet.getFrom()+ - // " could not be found"); - } - return; - } - if (type == null) { - if (fromParts.length == 2) { - contact.updatePresence(fromParts[1], Presences - .parseShow(packet.findChild("show"))); - PgpEngine pgp = getPgpEngine(); - if (pgp != null) { - Element x = packet.findChild("x", - "jabber:x:signed"); - if (x != null) { - Element status = packet.findChild("status"); - String msg; - if (status != null) { - msg = status.getContent(); - } else { - msg = ""; - } - contact.setPgpKeyId(pgp.fetchKeyId(account,msg,x.getContent())); - Log.d("xmppService",account.getJid()+": fetched key id for "+contact.getJid()+" was:"+contact.getPgpKeyId()); - } - } - replaceContactInConversation(account, - contact.getJid(), contact); - databaseBackend.updateContact(contact, true); - } else { - // Log.d(LOGTAG,"presence without resource "+packet.toString()); - } - } else if (type.equals("unavailable")) { - if (fromParts.length != 2) { - contact.clearPresences(); - } else { - contact.removePresence(fromParts[1]); - } - replaceContactInConversation(account, contact.getJid(), - contact); - databaseBackend.updateContact(contact, true); - } else if (type.equals("subscribe")) { - Log.d(LOGTAG, "received subscribe packet from " - + packet.getFrom()); - if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { - Log.d(LOGTAG, "preemptive grant; granting"); - sendPresenceUpdatesTo(contact); - contact.setSubscriptionOption(Contact.Subscription.FROM); - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - replaceContactInConversation(account, - contact.getJid(), contact); - databaseBackend.updateContact(contact, false); - if ((contact - .getSubscriptionOption(Contact.Subscription.ASKING)) - && (!contact - .getSubscriptionOption(Contact.Subscription.TO))) { - requestPresenceUpdatesFrom(contact); - } - } else { - account.getXmppConnection().addPendingSubscription( - fromParts[0]); - } - } else { - // Log.d(LOGTAG, packet.toString()); - } - } + mPresenceParser.parseContactPresence(packet, account); } } }; @@ -378,14 +282,13 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.hasChild("query","jabber:iq:roster")) { + if (packet.hasChild("query", "jabber:iq:roster")) { String from = packet.getFrom(); - if ((from==null)||(from.equals(account.getJid()))) { + if ((from == null) || (from.equals(account.getJid()))) { Element query = packet.findChild("query"); processRosterItems(account, query); - mergePhoneContactsWithRoster(null); } else { - Log.d(LOGTAG,"unauthorized roster push from: "+from); + Log.d(LOGTAG, "unauthorized roster push from: " + from); } } else if (packet .hasChild("open", "http://jabber.org/protocol/ibb") @@ -393,20 +296,34 @@ public class XmppConnectionService extends Service { .hasChild("data", "http://jabber.org/protocol/ibb")) { XmppConnectionService.this.mJingleConnectionManager .deliverIbbPacket(account, packet); - } else if (packet.hasChild("query","http://jabber.org/protocol/disco#info")) { - IqPacket iqResponse = packet.generateRespone(IqPacket.TYPE_RESULT); - Element query = iqResponse.addChild("query", "http://jabber.org/protocol/disco#info"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:1"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:apps:file-transfer:3"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:transports:s5b:1"); - query.addChild("feature").setAttribute("var", "urn:xmpp:jingle:transports:ibb:1"); + } else if (packet.hasChild("query", + "http://jabber.org/protocol/disco#info")) { + IqPacket iqResponse = packet + .generateRespone(IqPacket.TYPE_RESULT); + Element query = iqResponse.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:1"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:apps:file-transfer:3"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:transports:s5b:1"); + query.addChild("feature").setAttribute("var", + "urn:xmpp:jingle:transports:ibb:1"); + if (confirmMessages()) { + query.addChild("feature").setAttribute("var", + "urn:xmpp:receipts"); + } account.getXmppConnection().sendIqPacket(iqResponse, null); } else { - if ((packet.getType() == IqPacket.TYPE_GET)||(packet.getType() == IqPacket.TYPE_SET)) { - IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + IqPacket response = packet + .generateRespone(IqPacket.TYPE_ERROR); Element error = response.addChild("error"); - error.setAttribute("type","cancel"); - error.addChild("feature-not-implemented","urn:ietf:params:xml:ns:xmpp-stanzas"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); account.getXmppConnection().sendIqPacket(response, null); } } @@ -433,7 +350,7 @@ public class XmppConnectionService extends Service { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( getApplicationContext(), - pgpServiceConnection.getService()),this); + pgpServiceConnection.getService()), this); } return mPgpEngine; } else { @@ -446,12 +363,14 @@ public class XmppConnectionService extends Service { return this.fileBackend; } - public Message attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback callback) { + public Message attachImageToConversation(final Conversation conversation, + final Uri uri, final UiCallback<Message> callback) { final Message message; if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "",Message.ENCRYPTION_DECRYPTED); + message = new Message(conversation, "", + Message.ENCRYPTION_DECRYPTED); } else { - message = new Message(conversation, "", Message.ENCRYPTION_NONE); + message = new Message(conversation, "", conversation.getNextEncryption()); } message.setPresence(conversation.getNextPresence()); message.setType(Message.TYPE_IMAGE); @@ -460,22 +379,22 @@ public class XmppConnectionService extends Service { @Override public void run() { - JingleFile file = getFileBackend().copyImageToPrivateStorage(message, uri); - if (file==null) { - callback.error(R.string.error_copying_image_file); - } else { + try { + getFileBackend().copyImageToPrivateStorage(message, uri); if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { - callback.success(); + callback.success(message); } + } catch (FileBackend.ImageCopyException e) { + callback.error(e.getResId(), message); } } }).start(); return message; } - - protected Conversation findMuc(String name, Account account) { + + public Conversation findMuc(String name, Account account) { for (Conversation conversation : this.conversations) { if (conversation.getContactJid().split("/")[0].equals(name) && (conversation.getAccount() == account)) { @@ -488,51 +407,28 @@ public class XmppConnectionService extends Service { private void processRosterItems(Account account, Element elements) { String version = elements.getAttribute("ver"); if (version != null) { - account.setRosterVersion(version); - databaseBackend.updateAccount(account); + account.getRoster().setVersion(version); } for (Element item : elements.getChildren()) { if (item.getName().equals("item")) { String jid = item.getAttribute("jid"); + String name = item.getAttribute("name"); String subscription = item.getAttribute("subscription"); - Contact contact = databaseBackend.findContact(account, jid); - if (contact == null) { - if (!subscription.equals("remove")) { - String name = item.getAttribute("name"); - if (name == null) { - name = jid.split("@")[0]; - } - contact = new Contact(account, name, jid, null); - contact.parseSubscriptionFromElement(item); - databaseBackend.createContact(contact); - } + Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + } + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); } else { - if (subscription.equals("remove")) { - databaseBackend.deleteContact(contact); - replaceContactInConversation(account, contact.getJid(), - null); - } else { - contact.parseSubscriptionFromElement(item); - databaseBackend.updateContact(contact, false); - replaceContactInConversation(account, contact.getJid(), - contact); - } + contact.setOption(Contact.Options.IN_ROSTER); + contact.parseSubscriptionFromElement(item); } } } } - private void replaceContactInConversation(Account account, String jid, - Contact contact) { - List<Conversation> conversations = getConversations(); - for (Conversation c : conversations) { - if (c.getContactJid().equals(jid) && (c.getAccount() == account)) { - c.setContact(contact); - break; - } - } - } - public class XmppConnectionBinder extends Binder { public XmppConnectionService getService() { return XmppConnectionService.this; @@ -541,8 +437,16 @@ public class XmppConnectionService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + if ((intent != null) + && (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) { + mergePhoneContactsWithRoster(); + return START_STICKY; + } else if ((intent != null) + && (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) { + logoutAndSave(); + return START_NOT_STICKY; + } this.wakeLock.acquire(); - // Log.d(LOGTAG,"calling start service. caller was:"+intent.getAction()); ConnectivityManager cm = (ConnectivityManager) getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); @@ -563,8 +467,6 @@ public class XmppConnectionService extends Service { statusListener.onStatusChanged(account); } } - - // TODO 3 remaining cases if (account.getStatus() == Account.STATUS_ONLINE) { long lastReceived = account.getXmppConnection().lastPaketReceived; long lastSent = account.getXmppConnection().lastPingSent; @@ -592,15 +494,9 @@ public class XmppConnectionService extends Service { + ": time out during connect reconnecting"); reconnectAccount(account, true); } else { - Log.d(LOGTAG, - "seconds since last connect:" - + ((SystemClock.elapsedRealtime() - account - .getXmppConnection().lastConnect) / 1000)); - Log.d(LOGTAG, - account.getJid() + ": status=" - + account.getStatus()); - // TODO notify user of ssl cert problem or auth problem - // or what ever + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } } // in any case. reschedule wakup call this.scheduleWakeupCall(PING_MAX_INTERVAL, true); @@ -616,14 +512,21 @@ public class XmppConnectionService extends Service { return START_STICKY; } + @SuppressLint("TrulyRandom") @Override public void onCreate() { ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); this.databaseBackend = DatabaseBackend .getInstance(getApplicationContext()); this.fileBackend = new FileBackend(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); + for (Account account : this.accounts) { + this.databaseBackend.readRoster(account.getRoster()); + } + this.mergePhoneContactsWithRoster(); this.getConversations(); getContentResolver().registerContentObserver( @@ -639,13 +542,30 @@ public class XmppConnectionService extends Service { @Override public void onDestroy() { - Log.d(LOGTAG,"stopping service"); super.onDestroy(); + this.logoutAndSave(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + this.logoutAndSave(); + } + + private void logoutAndSave() { for (Account account : accounts) { + databaseBackend.writeRoster(account.getRoster()); if (account.getXmppConnection() != null) { - disconnect(account, true); + disconnect(account, false); } } + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, EventReceiver.class); + alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); + Log.d(LOGTAG, "good bye"); + stopSelf(); } protected void scheduleWakeupCall(int seconds, boolean ping) { @@ -663,7 +583,6 @@ public class XmppConnectionService extends Service { this.pingIntent, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingPingIntent); - // Log.d(LOGTAG,"schedule ping in "+seconds+" seconds"); } else { long scheduledTime = this.pingIntent.getLongExtra("time", 0); if (scheduledTime < SystemClock.elapsedRealtime() @@ -674,7 +593,6 @@ public class XmppConnectionService extends Service { context, 0, this.pingIntent, 0); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingPingIntent); - // Log.d(LOGTAG,"reschedule old ping to ping in "+seconds+" seconds"); } } } else { @@ -692,7 +610,7 @@ public class XmppConnectionService extends Service { SharedPreferences sharedPref = getPreferences(); account.setResource(sharedPref.getString("resource", "mobile") .toLowerCase(Locale.getDefault())); - XmppConnection connection = new XmppConnection(account, this.pm); + XmppConnection connection = new XmppConnection(account, this); connection.setOnMessagePacketReceivedListener(this.messageListener); connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.presenceListener); @@ -715,12 +633,11 @@ public class XmppConnectionService extends Service { connection.setOnBindListener(new OnBindListener() { @Override - public void onBind(Account account) { - databaseBackend.clearPresences(account); + public void onBind(final Account account) { + account.getRoster().clearPresences(); account.clearPresences(); // self presences - if (account.getXmppConnection().hasFeatureRosterManagment()) { - updateRoster(account, null); - } + fetchRosterFromServer(account); + sendPresence(account); connectMultiModeConversations(account); if (convChangedListener != null) { convChangedListener.onConversationListChanged(); @@ -730,73 +647,86 @@ public class XmppConnectionService extends Service { return connection; } - synchronized public void sendMessage(Message message, String presence) { + synchronized public void sendMessage(Message message) { Account account = message.getConversation().getAccount(); Conversation conv = message.getConversation(); MessagePacket packet = null; - boolean saveInDb = false; - boolean addToConversation = false; + boolean saveInDb = true; boolean send = false; if (account.getStatus() == Account.STATUS_ONLINE) { if (message.getType() == Message.TYPE_IMAGE) { - mJingleConnectionManager.createNewConnection(message); + if (message.getPresence() != null) { + mJingleConnectionManager.createNewConnection(message); + } else { + message.setStatus(Message.STATUS_WAITING); + } } else { if (message.getEncryption() == Message.ENCRYPTION_OTR) { - if (!conv.hasValidOtrSession()) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { // starting otr session. messages will be send later - conv.startOtrSession(getApplicationContext(), presence, - true); - } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + conv.startOtrSession(getApplicationContext(), + message.getPresence(), true); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { // otr session aleary exists, creating message packet // accordingly packet = prepareMessagePacket(account, message, conv.getOtrSession()); send = true; message.setStatus(Message.STATUS_SEND); + } else if (message.getPresence() == null) { + message.setStatus(Message.STATUS_WAITING); } - saveInDb = true; - addToConversation = true; } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { message.getConversation().endOtrIfNeeded(); - packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(message.getConversation().getAccount() - .getFullJid()); - packet.setTo(message.getCounterpart()); + packet = prepareMessagePacket(account, message, null); packet.setBody("This is an XEP-0027 encryted message"); packet.addChild("x", "jabber:x:encrypted").setContent( message.getEncryptedBody()); message.setStatus(Message.STATUS_SEND); message.setEncryption(Message.ENCRYPTION_DECRYPTED); - saveInDb = true; - addToConversation = true; send = true; } else { message.getConversation().endOtrIfNeeded(); // don't encrypt if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { message.setStatus(Message.STATUS_SEND); - saveInDb = true; - addToConversation = true; } packet = prepareMessagePacket(account, message, null); send = true; } } } else { - // account is offline - saveInDb = true; - addToConversation = true; + message.setStatus(Message.STATUS_WAITING); + if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + String pgpBody = message.getEncryptedBody(); + String decryptedBody = message.getBody(); + message.setBody(pgpBody); + databaseBackend.createMessage(message); + saveInDb = false; + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + message.setBody(decryptedBody); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (conv.hasValidOtrSession()) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + } else if (!conv.hasValidOtrSession() + && message.getPresence() != null) { + conv.startOtrSession(getApplicationContext(), + message.getPresence(), false); + } + } + } } if (saveInDb) { databaseBackend.createMessage(message); } - if (addToConversation) { - conv.getMessages().add(message); - if (convChangedListener != null) { - convChangedListener.onConversationListChanged(); - } + conv.getMessages().add(message); + if (convChangedListener != null) { + convChangedListener.onConversationListChanged(); } if ((send) && (packet != null)) { account.getXmppConnection().sendMessagePacket(packet); @@ -806,20 +736,68 @@ public class XmppConnectionService extends Service { private void sendUnsendMessages(Conversation conversation) { for (int i = 0; i < conversation.getMessages().size(); ++i) { - if ((conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) - && (conversation.getMessages().get(i).getEncryption() == Message.ENCRYPTION_NONE)) { - Message message = conversation.getMessages().get(i); - MessagePacket packet = prepareMessagePacket( - conversation.getAccount(), message, null); - conversation.getAccount().getXmppConnection() - .sendMessagePacket(packet); - message.setStatus(Message.STATUS_SEND); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - databaseBackend.updateMessage(message); - } else { - databaseBackend.deleteMessage(message); - conversation.getMessages().remove(i); - i--; + int status = conversation.getMessages().get(i).getStatus(); + if (status == Message.STATUS_WAITING) { + resendMessage(conversation.getMessages().get(i)); + } + } + } + + private void resendMessage(Message message) { + Account account = message.getConversation().getAccount(); + if (message.getType() == Message.TYPE_TEXT) { + MessagePacket packet = null; + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + packet = prepareMessagePacket(account, message, null); + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet = prepareMessagePacket(account, message, null); + packet.setBody("This is an XEP-0027 encryted message"); + if (message.getEncryptedBody() == null) { + markMessage(message, Message.STATUS_SEND_FAILED); + return; + } + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet = prepareMessagePacket(account, message, null); + packet.setBody("This is an XEP-0027 encryted message"); + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if (!message.getConversation().hasValidOtrSession()) { + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + message.getConversation().startOtrSession( + getApplicationContext(), message.getPresence(), + true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession( + getApplicationContext(), presence, true); + } + } + } + } + if (packet != null) { + account.getXmppConnection().sendMessagePacket(packet); + markMessage(message, Message.STATUS_SEND); + } + } else if (message.getType() == Message.TYPE_IMAGE) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.setPresence(presence); + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); } } } @@ -830,6 +808,7 @@ public class XmppConnectionService extends Service { MessagePacket packet = new MessagePacket(); if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); if (otrSession != null) { try { packet.setBody(otrSession.transformSending(message @@ -844,12 +823,11 @@ public class XmppConnectionService extends Service { packet.addChild("no-copy", "urn:xmpp:hints"); packet.setTo(otrSession.getSessionID().getAccountID() + "/" + otrSession.getSessionID().getUserID()); - packet.setFrom(account.getFullJid()); } else { packet.setBody(message.getBody()); packet.setTo(message.getCounterpart()); - packet.setFrom(account.getJid()); } + packet.addChild("markable", "urn:xmpp:chat-markers:0"); } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) { packet.setType(MessagePacket.TYPE_GROUPCHAT); packet.setBody(message.getBody()); @@ -860,27 +838,7 @@ public class XmppConnectionService extends Service { return packet; } - private void getRoster(Account account, - final OnRosterFetchedListener listener) { - List<Contact> contacts = databaseBackend.getContactsByAccount(account); - for (int i = 0; i < contacts.size(); ++i) { - contacts.get(i).setAccount(account); - } - if (listener != null) { - listener.onRosterFetched(contacts); - } - } - - public List<Contact> getRoster(Account account) { - List<Contact> contacts = databaseBackend.getContactsByAccount(account); - for (int i = 0; i < contacts.size(); ++i) { - contacts.get(i).setAccount(account); - } - return contacts; - } - - public void updateRoster(final Account account, - final OnRosterFetchedListener listener) { + public void fetchRosterFromServer(Account account) { IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); if (!"".equals(account.getRosterVersion())) { Log.d(LOGTAG, account.getJid() + ": fetching roster version " @@ -898,62 +856,26 @@ public class XmppConnectionService extends Service { IqPacket packet) { Element roster = packet.findChild("query"); if (roster != null) { - Log.d(LOGTAG, account.getJid() - + ": processing roster"); + account.getRoster().markAllAsNotInRoster(); processRosterItems(account, roster); - StringBuilder mWhere = new StringBuilder(); - mWhere.append("jid NOT IN("); - List<Element> items = roster.getChildren(); - for (int i = 0; i < items.size(); ++i) { - mWhere.append(DatabaseUtils - .sqlEscapeString(items.get(i) - .getAttribute("jid"))); - if (i != items.size() - 1) { - mWhere.append(","); - } - } - mWhere.append(") and accountUuid = \""); - mWhere.append(account.getUuid()); - mWhere.append("\""); - List<Contact> contactsToDelete = databaseBackend - .getContacts(mWhere.toString()); - for (Contact contact : contactsToDelete) { - databaseBackend.deleteContact(contact); - replaceContactInConversation(account, - contact.getJid(), null); - } - - } else { - Log.d(LOGTAG, account.getJid() - + ": empty roster returend"); } - mergePhoneContactsWithRoster(new OnPhoneContactsMerged() { - - @Override - public void phoneContactsMerged() { - if (listener != null) { - getRoster(account, listener); - } - } - }); } }); } - public void mergePhoneContactsWithRoster( - final OnPhoneContactsMerged listener) { + private void mergePhoneContactsWithRoster() { PhoneHelper.loadPhoneContacts(getApplicationContext(), new OnPhoneContactsLoadedListener() { @Override - public void onPhoneContactsLoaded( - Hashtable<String, Bundle> phoneContacts) { - List<Contact> contacts = databaseBackend - .getContactsByAccount(null); - for (int i = 0; i < contacts.size(); ++i) { - Contact contact = contacts.get(i); - if (phoneContacts.containsKey(contact.getJid())) { - Bundle phoneContact = phoneContacts.get(contact - .getJid()); + public void onPhoneContactsLoaded(List<Bundle> phoneContacts) { + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + } + for (Bundle phoneContact : phoneContacts) { + for (Account account : accounts) { + String jid = phoneContact.getString("jid"); + Contact contact = account.getRoster() + .getContact(jid); String systemAccount = phoneContact .getInt("phoneid") + "#" @@ -961,28 +883,10 @@ public class XmppConnectionService extends Service { contact.setSystemAccount(systemAccount); contact.setPhotoUri(phoneContact .getString("photouri")); - contact.setDisplayName(phoneContact + contact.setSystemName(phoneContact .getString("displayname")); - databaseBackend.updateContact(contact, false); - replaceContactInConversation( - contact.getAccount(), contact.getJid(), - contact); - } else { - if ((contact.getSystemAccount() != null) - || (contact.getProfilePhoto() != null)) { - contact.setSystemAccount(null); - contact.setPhotoUri(null); - databaseBackend.updateContact(contact, - false); - replaceContactInConversation( - contact.getAccount(), - contact.getJid(), contact); - } } } - if (listener != null) { - listener.phoneContactsMerged(); - } } }); } @@ -998,30 +902,47 @@ public class XmppConnectionService extends Service { for (Conversation conv : this.conversations) { Account account = accountLookupTable.get(conv.getAccountUuid()); conv.setAccount(account); - conv.setContact(findContact(account, conv.getContactJid())); conv.setMessages(databaseBackend.getMessages(conv, 50)); } } Collections.sort(this.conversations, new Comparator<Conversation>() { @Override public int compare(Conversation lhs, Conversation rhs) { - return (int) (rhs.getLatestMessage().getTimeSent() - lhs - .getLatestMessage().getTimeSent()); + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } } }); return this.conversations; } + public List<Message> getMoreMessages(Conversation conversation, + long timestamp) { + List<Message> messages = databaseBackend.getMessages(conversation, 50, + timestamp); + for (Message message : messages) { + message.setConversation(conversation); + } + return messages; + } + public List<Account> getAccounts() { return this.accounts; } - public Contact findContact(Account account, String jid) { - Contact contact = databaseBackend.findContact(account, jid); - if (contact != null) { - contact.setAccount(account); + public Conversation findActiveConversation(Contact contact) { + for (Conversation conversation : this.getConversations()) { + if (conversation.getContact() == contact) { + return conversation; + } } - return contact; + return null; } public Conversation findOrCreateConversation(Account account, String jid, @@ -1045,11 +966,9 @@ public class XmppConnectionService extends Service { conversation.setMessages(databaseBackend.getMessages(conversation, 50)); this.databaseBackend.updateConversation(conversation); - conversation.setContact(findContact(account, - conversation.getContactJid())); } else { String conversationName; - Contact contact = findContact(account, jid); + Contact contact = account.getRoster().getContact(jid); if (contact != null) { conversationName = contact.getDisplayName(); } else { @@ -1062,7 +981,6 @@ public class XmppConnectionService extends Service { conversation = new Conversation(conversationName, account, jid, Conversation.MODE_SINGLE); } - conversation.setContact(contact); this.databaseBackend.createConversation(conversation); } this.conversations.add(conversation); @@ -1110,23 +1028,14 @@ public class XmppConnectionService extends Service { accountChangedListener.onAccountListChangedListener(); } - public void deleteContact(Contact contact) { - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - Element query = iq.query("jabber:iq:roster"); - query.addChild("item").setAttribute("jid", contact.getJid()) - .setAttribute("subscription", "remove"); - contact.getAccount().getXmppConnection().sendIqPacket(iq, null); - replaceContactInConversation(contact.getAccount(), contact.getJid(), - null); - databaseBackend.deleteContact(contact); - } - public void updateAccount(Account account) { this.statusListener.onStatusChanged(account); databaseBackend.updateAccount(account); reconnectAccount(account, false); - if (accountChangedListener != null) + if (accountChangedListener != null) { accountChangedListener.onAccountListChangedListener(); + } + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); } public void deleteAccount(Account account) { @@ -1135,8 +1044,10 @@ public class XmppConnectionService extends Service { } databaseBackend.deleteAccount(account); this.accounts.remove(account); - if (accountChangedListener != null) + if (accountChangedListener != null) { accountChangedListener.onAccountListChangedListener(); + } + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); } public void setOnConversationListChangedListener( @@ -1173,6 +1084,7 @@ public class XmppConnectionService extends Service { } public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); String[] mucParts = conversation.getContactJid().split("/"); String muc; String nick; @@ -1181,20 +1093,24 @@ public class XmppConnectionService extends Service { nick = mucParts[1]; } else { muc = mucParts[0]; - nick = conversation.getAccount().getUsername(); + nick = account.getUsername(); } PresencePacket packet = new PresencePacket(); packet.setAttribute("to", muc + "/" + nick); Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } if (conversation.getMessages().size() != 0) { long lastMsgTime = conversation.getLatestMessage().getTimeSent(); long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1; x.addChild("history").setAttribute("seconds", diff + ""); } packet.addChild(x); - conversation.getAccount().getXmppConnection() - .sendPresencePacket(packet); + account.getXmppConnection().sendPresencePacket(packet); } private OnRenameListener renameListener = null; @@ -1206,6 +1122,7 @@ public class XmppConnectionService extends Service { public void renameInMuc(final Conversation conversation, final String nick) { final MucOptions options = conversation.getMucOptions(); if (options.online()) { + Account account = conversation.getAccount(); options.setOnRenameListener(new OnRenameListener() { @Override @@ -1214,8 +1131,8 @@ public class XmppConnectionService extends Service { renameListener.onRename(success); } if (success) { - String jid = conversation.getContactJid().split("/")[0] + "/" - + nick; + String jid = conversation.getContactJid().split("/")[0] + + "/" + nick; conversation.setContactJid(jid); databaseBackend.updateConversation(conversation); } @@ -1227,8 +1144,13 @@ public class XmppConnectionService extends Service { conversation.getContactJid().split("/")[0] + "/" + nick); packet.setAttribute("from", conversation.getAccount().getFullJid()); - conversation.getAccount().getXmppConnection() - .sendPresencePacket(packet, null); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + + account.getXmppConnection().sendPresencePacket(packet, null); } else { String jid = conversation.getContactJid().split("/")[0] + "/" + nick; @@ -1277,96 +1199,153 @@ public class XmppConnectionService extends Service { return mBinder; } - public void updateContact(Contact contact) { - databaseBackend.updateContact(contact, false); - replaceContactInConversation(contact.getAccount(), contact.getJid(), - contact); - } - public void updateMessage(Message message) { databaseBackend.updateMessage(message); } + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + Log.d(LOGTAG, "dirty delete"); + deleteContactOnServer(contact); + } + } + } + public void createContact(Contact contact) { SharedPreferences sharedPref = getPreferences(); boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); if (autoGrant) { - contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - contact.setSubscriptionOption(Contact.Subscription.ASKING); + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } + + public void onOtrSessionEstablished(Conversation conversation) { + Account account = conversation.getAccount(); + List<Message> messages = conversation.getMessages(); + Session otrSession = conversation.getOtrSession(); + Log.d(LOGTAG, account.getJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (int i = 0; i < messages.size(); ++i) { + Message msg = messages.get(i); + if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) + && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + MessagePacket outPacket = prepareMessagePacket(account, msg, + otrSession); + msg.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(msg); + account.getXmppConnection().sendMessagePacket(outPacket); + } } - databaseBackend.createContact(contact); - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - Element query = new Element("query"); - query.setAttribute("xmlns", "jabber:iq:roster"); - Element item = new Element("item"); - item.setAttribute("jid", contact.getJid()); - item.setAttribute("name", contact.getJid()); - query.addChild(item); - iq.addChild(query); + updateUi(conversation, false); + } + + public boolean renewSymmetricKey(Conversation conversation) { + Account account = conversation.getAccount(); + byte[] symmetricKey = new byte[32]; + this.mRandom.nextBytes(symmetricKey); + Session otrSession = conversation.getOtrSession(); + if (otrSession!=null) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setTo(otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession.transformSending(CryptoHelper.FILETRANSFER+CryptoHelper.bytesToHex(symmetricKey))); + account.getXmppConnection().sendMessagePacket(packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); Account account = contact.getAccount(); - account.getXmppConnection().sendIqPacket(iq, null); - if (autoGrant) { - requestPresenceUpdatesFrom(contact); - if (account.getXmppConnection().hasPendingSubscription( - contact.getJid())) { + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query("jabber:iq:roster").addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, null); + if (contact.getOption(Contact.Options.ASKING)) { + requestPresenceUpdatesFrom(contact); + } + if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { Log.d("xmppService", "contact had pending subscription"); sendPresenceUpdatesTo(contact); } + contact.resetOption(Contact.Options.DIRTY_PUSH); + } else { + contact.setOption(Contact.Options.DIRTY_PUSH); + } + } + + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_PUSH); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element item = iq.query("jabber:iq:roster").addChild("item"); + item.setAttribute("jid", contact.getJid()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, null); + contact.resetOption(Contact.Options.DIRTY_DELETE); + } else { + contact.setOption(Contact.Options.DIRTY_DELETE); } - replaceContactInConversation(contact.getAccount(), contact.getJid(), - contact); } public void requestPresenceUpdatesFrom(Contact contact) { - // Requesting a Subscription type=subscribe PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "subscribe"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void stopPresenceUpdatesFrom(Contact contact) { - // Unsubscribing type='unsubscribe' PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "unsubscribe"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void stopPresenceUpdatesTo(Contact contact) { - // Canceling a Subscription type=unsubscribed PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "unsubscribed"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } public void sendPresenceUpdatesTo(Contact contact) { - // type='subscribed' PresencePacket packet = new PresencePacket(); packet.setAttribute("type", "subscribed"); packet.setAttribute("to", contact.getJid()); packet.setAttribute("from", contact.getAccount().getJid()); - Log.d(LOGTAG, packet.toString()); contact.getAccount().getXmppConnection().sendPresencePacket(packet); } - public void sendPgpPresence(Account account, String signature) { + public void sendPresence(Account account) { PresencePacket packet = new PresencePacket(); packet.setAttribute("from", account.getFullJid()); - Element status = new Element("status"); - status.setContent("online"); - packet.addChild(status); - Element x = new Element("x"); - x.setAttribute("xmlns", "jabber:x:signed"); - x.setContent(signature); - packet.addChild(x); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } account.getXmppConnection().sendPresencePacket(packet); } @@ -1374,23 +1353,10 @@ public class XmppConnectionService extends Service { this.databaseBackend.updateConversation(conversation); } - public Contact findContact(String uuid) { - Contact contact = this.databaseBackend.getContact(uuid); - if (contact != null) { - for (Account account : getAccounts()) { - if (contact.getAccountUuid().equals(account.getUuid())) { - contact.setAccount(account); - } - } - } - return contact; - } - public void removeOnTLSExceptionReceivedListener() { this.tlsException = null; } - // TODO dont let thread sleep but schedule wake up public void reconnectAccount(final Account account, final boolean force) { new Thread(new Runnable() { @@ -1447,21 +1413,24 @@ public class XmppConnectionService extends Service { public boolean markMessage(Account account, String recipient, String uuid, int status) { - boolean marked = false; for (Conversation conversation : getConversations()) { if (conversation.getContactJid().equals(recipient) && conversation.getAccount().equals(account)) { - for (Message message : conversation.getMessages()) { - if (message.getUuid().equals(uuid)) { - markMessage(message, status); - marked = true; - break; - } - } - break; + return markMessage(conversation, uuid, status); + } + } + return false; + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + for (Message message : conversation.getMessages()) { + if (message.getUuid().equals(uuid)) { + markMessage(message, status); + return true; } } - return marked; + return false; } public void markMessage(Message message, int status) { @@ -1476,12 +1445,49 @@ public class XmppConnectionService extends Service { return PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); } - + + public boolean confirmMessages() { + return getPreferences().getBoolean("confirm_messages", true); + } + public void updateUi(Conversation conversation, boolean notify) { if (convChangedListener != null) { convChangedListener.onConversationListChanged(); } else { - UIHelper.updateNotification(getApplicationContext(), getConversations(), conversation, notify); + UIHelper.updateNotification(getApplicationContext(), + getConversations(), conversation, notify); + } + } + + public Account findAccountByJid(String accountJid) { + for (Account account : this.accounts) { + if (account.getJid().equals(accountJid)) { + return account; + } } + return null; + } + + public void markRead(Conversation conversation) { + conversation.markRead(this); + } + + public void sendConfirmMessage(Account account, String to, String id) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(to); + receivedPacket.setFrom(account.getFullJid()); + Element received = receivedPacket.addChild("displayed", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", id); + account.getXmppConnection().sendMessagePacket(receivedPacket); + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public PowerManager getPowerManager() { + return this.pm; } } diff --git a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java index 06179bc6..d89c35f1 100644 --- a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.ui; -import java.math.BigInteger; import java.util.Iterator; -import java.util.Locale; import org.openintents.openpgp.util.OpenPgpUtils; @@ -17,7 +15,6 @@ import android.os.Bundle; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; -import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -31,6 +28,7 @@ import android.widget.TextView; import android.widget.Toast; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.utils.UIHelper; @@ -40,14 +38,17 @@ public class ContactDetailsActivity extends XmppActivity { protected ContactDetailsActivity activity = this; - private String uuid; private Contact contact; - + + private String accountJid; + private String contactJid; + private EditText name; - private TextView contactJid; - private TextView accountJid; + private TextView contactJidTv; + private TextView accountJidTv; private TextView status; private TextView askAgain; + private TextView lastseen; private CheckBox send; private CheckBox receive; private QuickContactBadge badge; @@ -56,7 +57,7 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - activity.xmppConnectionService.deleteContact(contact); + activity.xmppConnectionService.deleteContactOnServer(contact); activity.finish(); } }; @@ -65,8 +66,8 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - contact.setDisplayName(name.getText().toString()); - activity.xmppConnectionService.updateContact(contact); + contact.setServerName(name.getText().toString()); + activity.xmppConnectionService.pushContactToServer(contact); populateView(); } }; @@ -89,11 +90,10 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Add to phone book"); - builder.setMessage("Do you want to add " + contact.getJid() - + " to your phones contact list?"); - builder.setNegativeButton("Cancel", null); - builder.setPositiveButton("Add", addToPhonebook); + builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid())); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add), addToPhonebook); builder.create().show(); } }; @@ -104,13 +104,15 @@ public class ContactDetailsActivity extends XmppActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { - this.uuid = getIntent().getExtras().getString("uuid"); + this.accountJid = getIntent().getExtras().getString("account"); + this.contactJid = getIntent().getExtras().getString("contact"); } setContentView(R.layout.activity_contact_details); - contactJid = (TextView) findViewById(R.id.details_contactjid); - accountJid = (TextView) findViewById(R.id.details_account); + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); status = (TextView) findViewById(R.id.details_contactstatus); + lastseen = (TextView) findViewById(R.id.details_lastseen); send = (CheckBox) findViewById(R.id.details_send_presence); receive = (CheckBox) findViewById(R.id.details_receive_presence); askAgain = (TextView) findViewById(R.id.ask_again); @@ -124,17 +126,17 @@ public class ContactDetailsActivity extends XmppActivity { @Override public boolean onOptionsItemSelected(MenuItem menuItem) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton("Cancel", null); + builder.setNegativeButton(getString(R.string.cancel), null); switch (menuItem.getItemId()) { case android.R.id.home: finish(); break; case R.id.action_delete_contact: - builder.setTitle("Delete from roster") + builder.setTitle(getString(R.string.action_delete_contact)) .setMessage( getString(R.string.remove_contact_text, contact.getJid())) - .setPositiveButton("Delete", removeFromRoster).create() + .setPositiveButton(getString(R.string.delete), removeFromRoster).create() .show(); break; case R.id.action_edit_contact: @@ -145,7 +147,7 @@ public class ContactDetailsActivity extends XmppActivity { name = (EditText) view.findViewById(R.id.editText1); name.setText(contact.getDisplayName()); builder.setView(view).setTitle(contact.getJid()) - .setPositiveButton("Edit", editContactNameListener) + .setPositiveButton(getString(R.string.edit), editContactNameListener) .create().show(); } else { @@ -170,18 +172,18 @@ public class ContactDetailsActivity extends XmppActivity { private void populateView() { setTitle(contact.getDisplayName()); - if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + if (contact.getOption(Contact.Options.FROM)) { send.setChecked(true); } else { send.setText(R.string.preemptively_grant); if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + .getOption(Contact.Options.PREEMPTIVE_GRANT)) { send.setChecked(true); } else { send.setChecked(false); } } - if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + if (contact.getOption(Contact.Options.TO)) { receive.setChecked(true); } else { receive.setText(R.string.ask_for_presence_updates); @@ -190,54 +192,57 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(View v) { - Toast.makeText(getApplicationContext(), "Asked for presence updates",Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), getString(R.string.asked_for_presence_updates), + Toast.LENGTH_SHORT).show(); xmppConnectionService.requestPresenceUpdatesFrom(contact); } }); - if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + if (contact.getOption(Contact.Options.ASKING)) { receive.setChecked(true); } else { receive.setChecked(false); } } + + lastseen.setText(UIHelper.lastseen(getApplicationContext(),contact.lastseen.time)); switch (contact.getMostAvailableStatus()) { case Presences.CHAT: - status.setText("free to chat"); + status.setText(R.string.contact_status_free_to_chat); status.setTextColor(0xFF83b600); break; case Presences.ONLINE: - status.setText("online"); + status.setText(R.string.contact_status_online); status.setTextColor(0xFF83b600); break; case Presences.AWAY: - status.setText("away"); + status.setText(R.string.contact_status_away); status.setTextColor(0xFFffa713); break; case Presences.XA: - status.setText("extended away"); + status.setText(R.string.contact_status_extended_away); status.setTextColor(0xFFffa713); break; case Presences.DND: - status.setText("do not disturb"); + status.setText(R.string.contact_status_do_not_disturb); status.setTextColor(0xFFe92727); break; case Presences.OFFLINE: - status.setText("offline"); + status.setText(R.string.contact_status_offline); status.setTextColor(0xFFe92727); break; default: - status.setText("offline"); + status.setText(R.string.contact_status_offline); status.setTextColor(0xFFe92727); break; } if (contact.getPresences().size() > 1) { - contactJid.setText(contact.getJid()+" ("+contact.getPresences().size()+")"); + contactJidTv.setText(contact.getJid()+" ("+contact.getPresences().size()+")"); } else { - contactJid.setText(contact.getJid()); + contactJidTv.setText(contact.getJid()); } - accountJid.setText(contact.getAccount().getJid()); + accountJidTv.setText(contact.getAccount().getJid()); UIHelper.prepareContactBadge(this, badge, contact, getApplicationContext()); @@ -286,65 +291,83 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onBackendConnected() { - if (uuid != null) { - this.contact = xmppConnectionService.findContact(uuid); - if (this.contact != null) { - populateView(); + if ((accountJid != null)&&(contactJid != null)) { + Account account = xmppConnectionService.findAccountByJid(accountJid); + if (account==null) { + return; } + this.contact = account.getRoster().getContact(contactJid); + populateView(); } } @Override protected void onStop() { super.onStop(); - boolean needsUpdating = false; - if (contact.getSubscriptionOption(Contact.Subscription.FROM)) { + boolean updated = false; + boolean online = contact.getAccount().getStatus() == Account.STATUS_ONLINE; + if (contact.getOption(Contact.Options.FROM)) { if (!send.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.FROM); - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - activity.xmppConnectionService.stopPresenceUpdatesTo(contact); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.FROM); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + activity.xmppConnectionService.stopPresenceUpdatesTo(contact); + } + updated = true; } } else { if (contact - .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) { + .getOption(Contact.Options.PREEMPTIVE_GRANT)) { if (!send.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } + updated = true; } } else { if (send.isChecked()) { - contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT); - needsUpdating = true; + if (online) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + updated = true; } } } - if (contact.getSubscriptionOption(Contact.Subscription.TO)) { + if (contact.getOption(Contact.Options.TO)) { if (!receive.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.TO); - activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); - needsUpdating = true; + if (online) { + contact.resetOption(Contact.Options.TO); + activity.xmppConnectionService.stopPresenceUpdatesFrom(contact); + } + updated = true; } } else { - if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) { + if (contact.getOption(Contact.Options.ASKING)) { if (!receive.isChecked()) { - contact.resetSubscriptionOption(Contact.Subscription.ASKING); - activity.xmppConnectionService + if (online) { + contact.resetOption(Contact.Options.ASKING); + activity.xmppConnectionService .stopPresenceUpdatesFrom(contact); - needsUpdating = true; + } + updated = true; } } else { if (receive.isChecked()) { - contact.setSubscriptionOption(Contact.Subscription.ASKING); - activity.xmppConnectionService + if (online) { + contact.setOption(Contact.Options.ASKING); + activity.xmppConnectionService .requestPresenceUpdatesFrom(contact); - needsUpdating = true; + } + updated = true; } } } - if (needsUpdating) { - Toast.makeText(getApplicationContext(), "Subscription updated", Toast.LENGTH_SHORT).show(); - activity.xmppConnectionService.updateContact(contact); + if (updated) { + if (online) { + Toast.makeText(getApplicationContext(), getString(R.string.subscription_updated), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getApplicationContext(), getString(R.string.subscription_not_updated_offline), Toast.LENGTH_SHORT).show(); + } } } diff --git a/src/eu/siacs/conversations/ui/ContactsActivity.java b/src/eu/siacs/conversations/ui/ContactsActivity.java index e403450a..4e9c8af6 100644 --- a/src/eu/siacs/conversations/ui/ContactsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactsActivity.java @@ -18,7 +18,6 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.util.SparseBooleanArray; import android.view.ActionMode; import android.view.LayoutInflater; @@ -34,13 +33,11 @@ import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; -import android.widget.ProgressBar; import android.widget.TextView; import android.widget.ImageView; import android.widget.Toast; import android.annotation.SuppressLint; import android.app.AlertDialog; -import android.app.AlertDialog.Builder; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; @@ -130,8 +127,10 @@ public class ContactsActivity extends XmppActivity { Intent intent = new Intent(getApplicationContext(), ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra("uuid", selectedContacts.get(0).getUuid()); + intent.putExtra("account", selectedContacts.get(0).getAccount().getJid()); + intent.putExtra("contact",selectedContacts.get(0).getJid()); startActivity(intent); + finish(); break; case R.id.action_invite: invite(); @@ -187,7 +186,7 @@ public class ContactsActivity extends XmppActivity { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.account_offline)); builder.setMessage(getString(R.string.cant_invite_while_offline)); - builder.setNegativeButton("OK", null); + builder.setNegativeButton(getString(R.string.ok), null); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.create().show(); return false; @@ -238,7 +237,7 @@ public class ContactsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - String mucName = CryptoHelper.randomMucName(); + String mucName = CryptoHelper.randomMucName(xmppConnectionService.getRNG()); String serverName = account.getXmppConnection() .getMucServer(); String jid = mucName + "@" + serverName; @@ -259,7 +258,7 @@ public class ContactsActivity extends XmppActivity { conversation, subject.toString()); xmppConnectionService.inviteToConference(conversation, selectedContacts); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); } }); builder.create().show(); @@ -270,7 +269,7 @@ public class ContactsActivity extends XmppActivity { aggregatedContacts.clear(); for (Contact contact : rosterContacts) { - if (contact.match(searchString)) + if (contact.match(searchString)&&(contact.showInRoster())) aggregatedContacts.add(contact); } @@ -287,16 +286,15 @@ public class ContactsActivity extends XmppActivity { if (aggregatedContacts.size() == 0) { if (Validator.isValidJid(searchString)) { - String name = searchString.split("@")[0]; - Contact newContact = new Contact(null, name, searchString, null); - newContact.flagAsNotInRoster(); + Contact newContact = new Contact(searchString); + newContact.resetOption(Contact.Options.IN_ROSTER); aggregatedContacts.add(newContact); - contactsHeader.setText("Create new contact"); + contactsHeader.setText(getString(R.string.new_contact)); } else { - contactsHeader.setText("Contacts"); + contactsHeader.setText(getString(R.string.contacts)); } } else { - contactsHeader.setText("Contacts"); + contactsHeader.setText(getString(R.string.contacts)); } contactsAdapter.notifyDataSetChanged(); @@ -429,17 +427,19 @@ public class ContactsActivity extends XmppActivity { } AlertDialog.Builder accountChooser = new AlertDialog.Builder(this); - accountChooser.setTitle("Choose account"); + accountChooser.setTitle(getString(R.string.choose_account)); accountChooser.setItems(accountList, listener); return accountChooser.create(); } public void showIsMucDialogIfNeeded(final Contact clickedContact) { - if (clickedContact.couldBeMuc()) { + if (isMuc(clickedContact)) { + startConversation(clickedContact,clickedContact.getAccount(), true); + } else if (clickedContact.couldBeMuc()) { AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle("Multi User Conference"); - dialog.setMessage("Are you trying to join a conference?"); - dialog.setPositiveButton("Yes", new OnClickListener() { + dialog.setTitle(getString(R.string.multi_user_conference)); + dialog.setMessage(getString(R.string.trying_join_conference)); + dialog.setPositiveButton(getString(R.string.yes), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -447,7 +447,7 @@ public class ContactsActivity extends XmppActivity { clickedContact.getAccount(), true); } }); - dialog.setNegativeButton("No", new OnClickListener() { + dialog.setNegativeButton(getString(R.string.no), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -461,15 +461,26 @@ public class ContactsActivity extends XmppActivity { false); } } + + private boolean isMuc(Contact contact) { + ArrayList<String> mucServers = new ArrayList<String>(); + for(Account account : accounts) { + if (account.getXmppConnection()!=null) { + mucServers.add(account.getXmppConnection().getMucServer()); + } + } + String server = contact.getJid().split("@")[1]; + return mucServers.contains(server); + } public void startConversation(Contact contact, Account account, boolean muc) { - if (!contact.isInRoster()&&(!muc)) { + if (!contact.getOption(Contact.Options.IN_ROSTER)&&(!muc)) { xmppConnectionService.createContact(contact); } Conversation conversation = xmppConnectionService .findOrCreateConversation(account, contact.getJid(), muc); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); } @Override @@ -496,7 +507,7 @@ public class ContactsActivity extends XmppActivity { .findOrCreateConversation( accounts.get(which), finalJid, false); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); finish(); } }).show(); @@ -504,7 +515,7 @@ public class ContactsActivity extends XmppActivity { Conversation conversation = xmppConnectionService .findOrCreateConversation(this.accounts.get(0), jid, false); - switchToConversation(conversation, null); + switchToConversation(conversation, null,false); finish(); } } @@ -515,9 +526,10 @@ public class ContactsActivity extends XmppActivity { getActionBar().setHomeButtonEnabled(false); } this.rosterContacts.clear(); - for (int i = 0; i < accounts.size(); ++i) { - rosterContacts.addAll(xmppConnectionService.getRoster(accounts - .get(i))); + for(Account account : accounts) { + if (account.getStatus() != Account.STATUS_DISABLED) { + rosterContacts.addAll(account.getRoster().getContacts()); + } } updateAggregatedContacts(); } @@ -532,52 +544,12 @@ public class ContactsActivity extends XmppActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.action_refresh_contacts: - refreshContacts(); - break; default: break; } return super.onOptionsItemSelected(item); } - private void refreshContacts() { - final ProgressBar progress = (ProgressBar) findViewById(R.id.progressBar1); - final EditText searchBar = (EditText) findViewById(R.id.new_conversation_search); - final TextView contactsHeader = (TextView) findViewById(R.id.contacts_header); - final ListView contactList = (ListView) findViewById(R.id.contactList); - searchBar.setVisibility(View.GONE); - contactsHeader.setVisibility(View.GONE); - contactList.setVisibility(View.GONE); - progress.setVisibility(View.VISIBLE); - this.accounts = xmppConnectionService.getAccounts(); - this.rosterContacts.clear(); - for (int i = 0; i < accounts.size(); ++i) { - if (accounts.get(i).getStatus() == Account.STATUS_ONLINE) { - xmppConnectionService.updateRoster(accounts.get(i), - new OnRosterFetchedListener() { - - @Override - public void onRosterFetched( - final List<Contact> roster) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - rosterContacts.addAll(roster); - progress.setVisibility(View.GONE); - searchBar.setVisibility(View.VISIBLE); - contactList.setVisibility(View.VISIBLE); - contactList.setVisibility(View.VISIBLE); - updateAggregatedContacts(); - } - }); - } - }); - } - } - } - @Override public void onActionModeStarted(ActionMode mode) { super.onActionModeStarted(mode); diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java index 88728245..4e264df7 100644 --- a/src/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -3,22 +3,20 @@ package eu.siacs.conversations.ui; import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Hashtable; import java.util.List; -import org.openintents.openpgp.OpenPgpError; - import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.UIHelper; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; +import android.provider.MediaStore; import android.app.AlertDialog; import android.app.FragmentTransaction; import android.app.PendingIntent; @@ -65,21 +63,25 @@ public class ConversationActivity extends XmppActivity { public static final int REQUEST_SEND_MESSAGE = 0x75441; public static final int REQUEST_DECRYPT_PGP = 0x76783; private static final int REQUEST_ATTACH_FILE_DIALOG = 0x48502; + private static final int REQUEST_IMAGE_CAPTURE = 0x33788; + private static final int REQUEST_RECORD_AUDIO = 0x46189; private static final int REQUEST_SEND_PGP_IMAGE = 0x53883; - private static final int REQUEST_ATTACH_FILE = 0x73824; public static final int REQUEST_ENCRYPT_MESSAGE = 0x378018; + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x92734; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x84123; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x75291; + protected SlidingPaneLayout spl; private List<Conversation> conversationList = new ArrayList<Conversation>(); private Conversation selectedConversation = null; private ListView listView; - + private boolean paneShouldBeOpen = true; private boolean useSubject = true; + private boolean showLastseen = false; private ArrayAdapter<Conversation> listAdapter; - - public Message pendingMessage = null; private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() { @@ -108,7 +110,7 @@ public class ConversationActivity extends XmppActivity { }); } }; - + protected ConversationActivity activity = this; private DisplayMetrics metrics; private Toast prepareImageToast; @@ -121,6 +123,10 @@ public class ConversationActivity extends XmppActivity { return this.selectedConversation; } + public void setSelectedConversation(Conversation conversation) { + this.selectedConversation = conversation; + } + public ListView getConversationListView() { return this.listView; } @@ -137,7 +143,7 @@ public class ConversationActivity extends XmppActivity { protected void onCreate(Bundle savedInstanceState) { metrics = getResources().getDisplayMetrics(); - + super.onCreate(savedInstanceState); setContentView(R.layout.fragment_conversations_overview); @@ -173,15 +179,18 @@ public class ConversationActivity extends XmppActivity { convName.setText(conv.getName(useSubject)); TextView convLastMsg = (TextView) view .findViewById(R.id.conversation_lastmsg); - ImageView imagePreview = (ImageView) view.findViewById(R.id.conversation_lastimage); - + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + Message latestMessage = conv.getLatestMessage(); - + if (latestMessage.getType() == Message.TYPE_TEXT) { - if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)&&(latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP) + && (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { convLastMsg.setText(conv.getLatestMessage().getBody()); } else { - convLastMsg.setText(getText(R.string.encrypted_message_received)); + convLastMsg + .setText(getText(R.string.encrypted_message_received)); } convLastMsg.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); @@ -194,16 +203,16 @@ public class ConversationActivity extends XmppActivity { convLastMsg.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) { - convLastMsg.setText(getText(R.string.image_offered_for_download)); + convLastMsg + .setText(getText(R.string.image_offered_for_download)); } else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) { - convLastMsg.setText(getText(R.string.receiving_image)); + convLastMsg + .setText(getText(R.string.receiving_image)); } else { convLastMsg.setText(""); } } } - - if (!conv.isRead()) { convName.setTypeface(null, Typeface.BOLD); @@ -214,14 +223,14 @@ public class ConversationActivity extends XmppActivity { } ((TextView) view.findViewById(R.id.conversation_lastupdate)) - .setText(UIHelper.readableTimeDifference(conv - .getLatestMessage().getTimeSent())); + .setText(UIHelper.readableTimeDifference(getContext(), + conv.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view .findViewById(R.id.conversation_image); - profilePicture.setImageBitmap(UIHelper.getContactPicture( - conv, 56, activity.getApplicationContext(), false)); - + profilePicture.setImageBitmap(UIHelper.getContactPicture(conv, + 56, activity.getApplicationContext(), false)); + return view; } @@ -235,8 +244,8 @@ public class ConversationActivity extends XmppActivity { public void onItemClick(AdapterView<?> arg0, View clickedView, int position, long arg3) { paneShouldBeOpen = false; - if (selectedConversation != conversationList.get(position)) { - selectedConversation = conversationList.get(position); + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); swapConversationFragment(); // .onBackendConnected(conversationList.get(position)); } else { spl.closePane(); @@ -253,6 +262,7 @@ public class ConversationActivity extends XmppActivity { public void onPanelOpened(View arg0) { paneShouldBeOpen = true; getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); getActionBar().setTitle(R.string.app_name); invalidateOptionsMenu(); hideKeyboard(); @@ -264,11 +274,13 @@ public class ConversationActivity extends XmppActivity { if ((conversationList.size() > 0) && (getSelectedConversation() != null)) { getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); getActionBar().setTitle( getSelectedConversation().getName(useSubject)); invalidateOptionsMenu(); if (!getSelectedConversation().isRead()) { - getSelectedConversation().markRead(); + xmppConnectionService + .markRead(getSelectedConversation()); UIHelper.updateNotification(getApplicationContext(), getConversationList(), null, false); listView.invalidateViews(); @@ -296,7 +308,8 @@ public class ConversationActivity extends XmppActivity { MenuItem menuInviteContacts = (MenuItem) menu .findItem(R.id.action_invite); MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); - MenuItem menuClearHistory = (MenuItem) menu.findItem(R.id.action_clear_history); + MenuItem menuClearHistory = (MenuItem) menu + .findItem(R.id.action_clear_history); if ((spl.isOpen() && (spl.isSlideable()))) { menuArchive.setVisible(false); @@ -310,112 +323,136 @@ public class ConversationActivity extends XmppActivity { ((MenuItem) menu.findItem(R.id.action_add)).setVisible(!spl .isSlideable()); if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); - menuSecure.setVisible(false); menuAttach.setVisible(false); } else { menuMucDetails.setVisible(false); menuInviteContacts.setVisible(false); - if (this.getSelectedConversation().getLatestMessage() - .getEncryption() != Message.ENCRYPTION_NONE) { - menuSecure.setIcon(R.drawable.ic_action_secure); - } } } } return true; } - - private void attachFileDialog() { + + private void selectPresenceToAttachFile(final int attachmentChoice) { selectPresence(getSelectedConversation(), new OnPresenceSelected() { - + @Override - public void onPresenceSelected(boolean success, String presence) { - if (success) { + public void onPresenceSelected() { + if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { + Intent takePictureIntent = new Intent( + MediaStore.ACTION_IMAGE_CAPTURE); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + ImageProvider.getIncomingContentUri()); + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(takePictureIntent, + REQUEST_IMAGE_CAPTURE); + } + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { Intent attachFileIntent = new Intent(); attachFileIntent.setType("image/*"); attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); - Intent chooser = Intent.createChooser(attachFileIntent, getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { + Intent intent = new Intent( + MediaStore.Audio.Media.RECORD_SOUND_ACTION); + startActivityForResult(intent, REQUEST_RECORD_AUDIO); } } - - @Override - public void onSendPlainTextInstead() { - - } - },"file"); + }); } - private void attachFile() { + private void attachFile(final int attachmentChoice) { final Conversation conversation = getSelectedConversation(); if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { if (hasPgp()) { - if (conversation.getContact().getPgpKeyId()!=0) { - xmppConnectionService.getPgpEngine().hasKey(conversation.getContact(), new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - ConversationActivity.this.runIntent(pi, REQUEST_ATTACH_FILE); - } - - @Override - public void success() { - attachFileDialog(); - } - - @Override - public void error(int error) { - // TODO Auto-generated method stub - - } - }); + if (conversation.getContact().getPgpKeyId() != 0) { + xmppConnectionService.getPgpEngine().hasKey( + conversation.getContact(), + new UiCallback<Contact>() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + ConversationActivity.this.runIntent(pi, + attachmentChoice); + } + + @Override + public void success(Contact contact) { + selectPresenceToAttachFile(attachmentChoice); + } + + @Override + public void error(int error, Contact contact) { + displayErrorDialog(error); + } + }); } else { final ConversationFragment fragment = (ConversationFragment) getFragmentManager() .findFragmentByTag("conversation"); if (fragment != null) { - fragment.showNoPGPKeyDialog(new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - attachFileDialog(); - } - }); + fragment.showNoPGPKeyDialog(false, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + selectPresenceToAttachFile(attachmentChoice); + } + }); } } + } else { + showInstallPgpDialog(); } } else if (getSelectedConversation().getNextEncryption() == Message.ENCRYPTION_NONE) { - attachFileDialog(); + selectPresenceToAttachFile(attachmentChoice); } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); + selectPresenceToAttachFile(attachmentChoice); + /*AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.otr_file_transfer)); builder.setMessage(getString(R.string.otr_file_transfer_msg)); builder.setNegativeButton(getString(R.string.cancel), null); - if (conversation.getContact().getPgpKeyId()==0) { - builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - attachFile(); - } - }); + if (conversation.getContact().getPgpKeyId() == 0) { + builder.setPositiveButton(getString(R.string.send_unencrypted), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + attachFile(attachmentChoice); + } + }); } else { - builder.setPositiveButton(getString(R.string.use_pgp_encryption), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - attachFile(); - } - }); + builder.setPositiveButton( + getString(R.string.use_pgp_encryption), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + attachFile(attachmentChoice); + } + }); } - builder.create().show(); + builder.create().show();*/ } } - + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -423,7 +460,29 @@ public class ConversationActivity extends XmppActivity { spl.openPane(); break; case R.id.action_attach_file: - attachFile(); + View menuAttachFile = findViewById(R.id.action_attach_file); + PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); + attachFilePopup.inflate(R.menu.attachment_choices); + attachFilePopup + .setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + } + return false; + } + }); + attachFilePopup.show(); break; case R.id.action_add: startActivity(new Intent(this, ContactsActivity.class)); @@ -433,10 +492,12 @@ public class ConversationActivity extends XmppActivity { break; case R.id.action_contact_details: Contact contact = this.getSelectedConversation().getContact(); - if (contact != null) { + if (contact.showInRoster()) { Intent intent = new Intent(this, ContactDetailsActivity.class); intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra("uuid", contact.getUuid()); + intent.putExtra("account", this.getSelectedConversation() + .getAccount().getJid()); + intent.putExtra("contact", contact.getJid()); startActivity(intent); } else { showAddToRosterDialog(getSelectedConversation()); @@ -452,7 +513,7 @@ public class ConversationActivity extends XmppActivity { Intent inviteIntent = new Intent(getApplicationContext(), ContactsActivity.class); inviteIntent.setAction("invite"); - inviteIntent.putExtra("uuid", selectedConversation.getUuid()); + inviteIntent.putExtra("uuid", getSelectedConversation().getUuid()); startActivity(inviteIntent); break; case R.id.action_security: @@ -468,25 +529,33 @@ public class ConversationActivity extends XmppActivity { public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.encryption_choice_none: - conversation.setNextEncryption(Message.ENCRYPTION_NONE); + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); item.setChecked(true); break; case R.id.encryption_choice_otr: - conversation.setNextEncryption(Message.ENCRYPTION_OTR); + conversation + .setNextEncryption(Message.ENCRYPTION_OTR); item.setChecked(true); break; case R.id.encryption_choice_pgp: if (hasPgp()) { - if (conversation.getAccount().getKeys().has("pgp_signature")) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); + if (conversation.getAccount().getKeys() + .has("pgp_signature")) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); item.setChecked(true); } else { - announcePgp(conversation.getAccount(),conversation); + announcePgp(conversation.getAccount(), + conversation); } + } else { + showInstallPgpDialog(); } break; default: - conversation.setNextEncryption(Message.ENCRYPTION_NONE); + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); break; } fragment.updateChatMsgHint(); @@ -494,14 +563,18 @@ public class ConversationActivity extends XmppActivity { } }); popup.inflate(R.menu.encryption_choices); + MenuItem otr = popup.getMenu().findItem( + R.id.encryption_choice_otr); + if (conversation.getMode() == Conversation.MODE_MULTI) { + otr.setEnabled(false); + } switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: popup.getMenu().findItem(R.id.encryption_choice_none) .setChecked(true); break; case Message.ENCRYPTION_OTR: - popup.getMenu().findItem(R.id.encryption_choice_otr) - .setChecked(true); + otr.setChecked(true); break; case Message.ENCRYPTION_PGP: popup.getMenu().findItem(R.id.encryption_choice_pgp) @@ -524,36 +597,40 @@ public class ConversationActivity extends XmppActivity { } return super.onOptionsItemSelected(item); } - + private void endConversation(Conversation conversation) { conversation.setStatus(Conversation.STATUS_ARCHIVED); paneShouldBeOpen = true; spl.openPane(); xmppConnectionService.archiveConversation(conversation); if (conversationList.size() > 0) { - selectedConversation = conversationList.get(0); + setSelectedConversation(conversationList.get(0)); } else { - selectedConversation = null; + setSelectedConversation(null); } } protected void clearHistoryDialog(final Conversation conversation) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.clear_conversation_history)); - View dialogView = getLayoutInflater().inflate(R.layout.dialog_clear_history, null); - final CheckBox endConversationCheckBox = (CheckBox) dialogView.findViewById(R.id.end_conversation_checkbox); + View dialogView = getLayoutInflater().inflate( + R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = (CheckBox) dialogView + .findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.delete_messages), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - activity.xmppConnectionService.clearConversationHistory(conversation); - if (endConversationCheckBox.isChecked()) { - endConversation(conversation); - } - } - }); + builder.setPositiveButton(getString(R.string.delete_messages), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + activity.xmppConnectionService + .clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + endConversation(conversation); + } + } + }); builder.create().show(); } @@ -580,11 +657,30 @@ public class ConversationActivity extends XmppActivity { } @Override + protected void onNewIntent(Intent intent) { + if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION + .equals(intent.getType())))) { + String convToView = (String) intent.getExtras().get(CONVERSATION); + updateConversationList(); + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + setSelectedConversation(conversationList.get(i)); + break; + } + } + paneShouldBeOpen = false; + String text = intent.getExtras().getString(TEXT, null); + swapConversationFragment().setText(text); + } + } + + @Override public void onStart() { super.onStart(); SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); this.useSubject = preferences.getBoolean("use_subject_in_muc", true); + this.showLastseen = preferences.getBoolean("show_last_seen", false); if (this.xmppConnectionServiceBound) { this.onBackendConnected(); } @@ -619,7 +715,7 @@ public class ConversationActivity extends XmppActivity { for (int i = 0; i < conversationList.size(); ++i) { if (conversationList.get(i).getUuid().equals(convToView)) { - selectedConversation = conversationList.get(i); + setSelectedConversation(conversationList.get(i)); } } paneShouldBeOpen = false; @@ -642,7 +738,7 @@ public class ConversationActivity extends XmppActivity { if (selectedFragment != null) { selectedFragment.onBackendConnected(); } else { - selectedConversation = conversationList.get(0); + setSelectedConversation(conversationList.get(0)); swapConversationFragment(); } ExceptionHelper.checkForCrash(this, this.xmppConnectionService); @@ -658,7 +754,8 @@ public class ConversationActivity extends XmppActivity { } @Override - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == REQUEST_DECRYPT_PGP) { @@ -668,305 +765,213 @@ public class ConversationActivity extends XmppActivity { selectedFragment.hidePgpPassphraseBox(); } } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { - prepareImageToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_image), Toast.LENGTH_LONG); - final Conversation conversation = getSelectedConversation(); - if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE) { - prepareImageToast.show(); - this.pendingMessage = xmppConnectionService.attachImageToConversation(conversation, data.getData(),new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - - } - - @Override - public void success() { - sendPendingImageMessage(); - } - - @Override - public void error(int error) { - pendingMessage = null; - displayErrorDialog(error); - } - }); - } else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - prepareImageToast.show(); - attachPgpFile(conversation,data.getData()); - } else { - Log.d(LOGTAG,"unknown next message encryption: "+conversation.getNextEncryption()); - } + attachImageToConversation(getSelectedConversation(), + data.getData()); } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { - - } else if (requestCode == REQUEST_ATTACH_FILE) { - attachFile(); + + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); } else if (requestCode == REQUEST_ANNOUNCE_PGP) { - announcePgp(getSelectedConversation().getAccount(),getSelectedConversation()); + announcePgp(getSelectedConversation().getAccount(), + getSelectedConversation()); } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { - encryptTextMessage(); + // encryptTextMessage(); + } else if (requestCode == REQUEST_IMAGE_CAPTURE) { + attachImageToConversation(getSelectedConversation(), null); + } else if (requestCode == REQUEST_RECORD_AUDIO) { + Log.d("xmppService", data.getData().toString()); + attachAudioToConversation(getSelectedConversation(), + data.getData()); } else { - Log.d(LOGTAG,"unknown result code:"+requestCode); + Log.d(LOGTAG, "unknown result code:" + requestCode); } } } - - private void attachPgpFile(Conversation conversation, Uri uri) { - pendingMessage = xmppConnectionService.attachImageToConversation(conversation, uri, new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_PGP_IMAGE); - } - - @Override - public void success() { - sendPendingImageMessage(); - } - + + private void attachAudioToConversation(Conversation conversation, Uri uri) { + + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareImageToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareImageToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareImageToast(); + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_PGP_IMAGE); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareImageToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareImageToast() { + if (prepareImageToast != null) { + runOnUiThread(new Runnable() { + @Override - public void error(int error) { - pendingMessage = null; - displayErrorDialog(error); + public void run() { + prepareImageToast.cancel(); } }); + } } - private void sendPendingImageMessage() { - pendingMessage.getConversation().getMessages().add(pendingMessage); - xmppConnectionService.databaseBackend.createMessage(pendingMessage); - xmppConnectionService.sendMessage(pendingMessage, null); - xmppConnectionService.updateUi(pendingMessage.getConversation(), false); - pendingMessage = null; - } - public void updateConversationList() { conversationList.clear(); conversationList.addAll(xmppConnectionService.getConversations()); listView.invalidateViews(); } - - public void selectPresence(final Conversation conversation, final OnPresenceSelected listener, String reason) { - Account account = conversation.getAccount(); - if (account.getStatus() != Account.STATUS_ONLINE) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.not_connected)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - if ("otr".equals(reason)) { - builder.setMessage(getString(R.string.you_are_offline,getString(R.string.otr_messages))); - } else if ("file".equals(reason)) { - builder.setMessage(getString(R.string.you_are_offline,getString(R.string.files))); - } else { - builder.setMessage(getString(R.string.you_are_offline_blank)); - } - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.manage_account), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - startActivity(new Intent(activity, ManageAccountActivity.class)); - } - }); - builder.create().show(); - listener.onPresenceSelected(false, null); + + public boolean showLastseen() { + if (getSelectedConversation() == null) { + return false; } else { - Contact contact = conversation.getContact(); - if (contact==null) { - showAddToRosterDialog(conversation); - listener.onPresenceSelected(false,null); - } else { - Hashtable<String, Integer> presences = contact.getPresences(); - if (presences.size() == 0) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.contact_offline)); - if ("otr".equals(reason)) { - builder.setMessage(getString(R.string.contact_offline_otr)); - builder.setPositiveButton(getString(R.string.send_unencrypted), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - listener.onSendPlainTextInstead(); - } - }); - } else if ("file".equals(reason)) { - builder.setMessage(getString(R.string.contact_offline_file)); - } - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.create().show(); - listener.onPresenceSelected(false, null); - } else if (presences.size() == 1) { - String presence = (String) presences.keySet().toArray()[0]; - conversation.setNextPresence(presence); - listener.onPresenceSelected(true, presence); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.choose_presence)); - final String[] presencesArray = new String[presences.size()]; - presences.keySet().toArray(presencesArray); - builder.setItems(presencesArray, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, - int which) { - String presence = presencesArray[which]; - conversation.setNextPresence(presence); - listener.onPresenceSelected(true,presence); - } - }); - builder.create().show(); - } - } + return this.showLastseen + && getSelectedConversation().getMode() == Conversation.MODE_SINGLE; } } - - private void showAddToRosterDialog(final Conversation conversation) { - String jid = conversation.getContactJid(); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(jid); - builder.setMessage(getString(R.string.not_in_roster)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String jid = conversation.getContactJid(); - Account account = getSelectedConversation().getAccount(); - String name = jid.split("@")[0]; - Contact contact = new Contact(account, name, jid, null); - xmppConnectionService.createContact(contact); - } - }); - builder.create().show(); - } - public void runIntent(PendingIntent pi, int requestCode) { try { - this.startIntentSenderForResult(pi.getIntentSender(),requestCode, null, 0, - 0, 0); + this.startIntentSenderForResult(pi.getIntentSender(), requestCode, + null, 0, 0, 0); } catch (SendIntentException e1) { - Log.d("xmppService","failed to start intent to send message"); + Log.d("xmppService", "failed to start intent to send message"); } } - - + class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { - private final WeakReference<ImageView> imageViewReference; - private Message message = null; - - public BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<ImageView>(imageView); - } - - @Override - protected Bitmap doInBackground(Message... params) { - message = params[0]; - try { - return xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288),false); + private final WeakReference<ImageView> imageViewReference; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<ImageView>(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + return xmppConnectionService.getFileBackend().getThumbnail( + message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { - Log.d("xmppService","file not found!"); + Log.d("xmppService", "file not found!"); return null; } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (imageViewReference != null && bitmap != null) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } - + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + public void loadBitmap(Message message, ImageView imageView) { Bitmap bm; try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true); + bm = xmppConnectionService.getFileBackend().getThumbnail(message, + (int) (metrics.density * 288), true); } catch (FileNotFoundException e) { bm = null; } - if (bm!=null) { + if (bm != null) { imageView.setImageBitmap(bm); imageView.setBackgroundColor(0x00000000); } else { - if (cancelPotentialWork(message, imageView)) { - imageView.setBackgroundColor(0xff333333); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = - new AsyncDrawable(getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - task.execute(message); - } + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + task.execute(message); + } } } - - public static boolean cancelPotentialWork(Message message, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Message oldMessage = bitmapWorkerTask.message; - if (oldMessage == null || message != oldMessage) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + static class AsyncDrawable extends BitmapDrawable { - private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; - public AsyncDrawable(Resources res, Bitmap bitmap, - BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = - new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); - } + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>( + bitmapWorkerTask); + } - public BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } } - public void encryptTextMessage() { - xmppConnectionService.getPgpEngine().encrypt(this.pendingMessage, new UiCallback() { + public void encryptTextMessage(Message message) { + xmppConnectionService.getPgpEngine().encrypt(message, + new UiCallback<Message>() { @Override - public void userInputRequried( - PendingIntent pi) { - activity.runIntent( - pi, + public void userInputRequried(PendingIntent pi, + Message message) { + activity.runIntent(pi, ConversationActivity.REQUEST_SEND_MESSAGE); } @Override - public void success() { - xmppConnectionService.sendMessage(pendingMessage, null); - pendingMessage = null; - ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); - if (selectedFragment != null) { - selectedFragment.clearInputField(); - } + public void success(Message message) { + xmppConnectionService.sendMessage(message); } @Override - public void error(int error) { + public void error(int error, Message message) { } }); diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java index 91a39ecc..2eac7ad0 100644 --- a/src/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -5,17 +5,15 @@ import java.util.HashMap; import java.util.List; import java.util.Set; -import org.openintents.openpgp.OpenPgpError; - import net.java.otr4j.session.SessionStatus; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jingle.JingleConnection; @@ -30,17 +28,20 @@ import android.content.SharedPreferences; import android.content.IntentSender.SendIntentException; import android.graphics.Bitmap; import android.graphics.Typeface; -import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.Selection; import android.util.DisplayMetrics; import android.util.Log; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AbsListView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; @@ -69,6 +70,7 @@ public class ConversationFragment extends Fragment { protected Bitmap selfBitmap; private boolean useSubject = true; + private boolean messagesLoaded = false; private IntentSender askForPassphraseIntent = null; @@ -93,7 +95,7 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - if (askForPassphraseIntent != null) { + if (activity.hasPgp() && askForPassphraseIntent != null) { try { getActivity().startIntentSenderForResult( askForPassphraseIntent, @@ -119,6 +121,33 @@ public class ConversationFragment extends Fragment { startActivity(intent); } }; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem == 0 && messagesLoaded) { + long timestamp = messageList.get(0).getTimeSent(); + messagesLoaded = false; + List<Message> messages = activity.xmppConnectionService + .getMoreMessages(conversation, timestamp); + messageList.addAll(0, messages); + messageListAdapter.notifyDataSetChanged(); + if (messages.size() != 0) { + messagesLoaded = true; + } + messagesView.setSelectionFromTop(messages.size() + 1, 0); + } + } + }; + private ConversationActivity activity; public void hidePgpPassphraseBox() { @@ -126,22 +155,18 @@ public class ConversationFragment extends Fragment { } public void updateChatMsgHint() { - if (conversation.getMode() == Conversation.MODE_MULTI) { - chatMsg.setHint(getString(R.string.send_message_to_conference)); - } else { - switch (conversation.getNextEncryption()) { - case Message.ENCRYPTION_NONE: - chatMsg.setHint(getString(R.string.send_plain_text_message)); - break; - case Message.ENCRYPTION_OTR: - chatMsg.setHint(getString(R.string.send_otr_message)); - break; - case Message.ENCRYPTION_PGP: - chatMsg.setHint(getString(R.string.send_pgp_message)); - break; - default: - break; - } + switch (conversation.getNextEncryption()) { + case Message.ENCRYPTION_NONE: + chatMsg.setHint(getString(R.string.send_plain_text_message)); + break; + case Message.ENCRYPTION_OTR: + chatMsg.setHint(getString(R.string.send_otr_message)); + break; + case Message.ENCRYPTION_PGP: + chatMsg.setHint(getString(R.string.send_pgp_message)); + break; + default: + break; } } @@ -168,6 +193,8 @@ public class ConversationFragment extends Fragment { mucErrorText = (TextView) view.findViewById(R.id.muc_error_msg); messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); + messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); messageListAdapter = new ArrayAdapter<Message>(this.getActivity() .getApplicationContext(), R.layout.message_sent, @@ -175,15 +202,18 @@ public class ConversationFragment extends Fragment { private static final int SENT = 0; private static final int RECIEVED = 1; + private static final int STATUS = 2; @Override public int getViewTypeCount() { - return 2; + return 3; } @Override public int getItemViewType(int position) { - if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) { + if (getItem(position).getType() == Message.TYPE_STATUS) { + return STATUS; + } else if (getItem(position).getStatus() <= Message.STATUS_RECIEVED) { return RECIEVED; } else { return SENT; @@ -194,6 +224,8 @@ public class ConversationFragment extends Fragment { String filesize = null; String info = null; boolean error = false; + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getStatus() <= Message.STATUS_RECIEVED; if (message.getType() == Message.TYPE_IMAGE) { String[] fileParams = message.getBody().split(","); try { @@ -204,6 +236,9 @@ public class ConversationFragment extends Fragment { } } switch (message.getStatus()) { + case Message.STATUS_WAITING: + info = getString(R.string.waiting); + break; case Message.STATUS_UNSEND: info = getString(R.string.sending); break; @@ -219,8 +254,7 @@ public class ConversationFragment extends Fragment { error = true; break; default: - if ((message.getConversation().getMode() == Conversation.MODE_MULTI) - && (message.getStatus() <= Message.STATUS_RECIEVED)) { + if (multiReceived) { info = message.getCounterpart(); } break; @@ -236,8 +270,8 @@ public class ConversationFragment extends Fragment { viewHolder.indicator.setVisibility(View.VISIBLE); } - String formatedTime = UIHelper.readableTimeDifference(message - .getTimeSent()); + String formatedTime = UIHelper.readableTimeDifference( + getContext(), message.getTimeSent()); if (message.getStatus() <= Message.STATUS_RECIEVED) { if ((filesize != null) && (info != null)) { viewHolder.time.setText(filesize + " \u00B7 " + info); @@ -254,8 +288,12 @@ public class ConversationFragment extends Fragment { if ((filesize != null) && (info != null)) { viewHolder.time.setText(filesize + " \u00B7 " + info); } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(info + " \u00B7 " - + formatedTime); + if (error) { + viewHolder.time.setText(info + " \u00B7 " + + formatedTime); + } else { + viewHolder.time.setText(info); + } } else if ((filesize != null) && (info == null)) { viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); @@ -266,22 +304,28 @@ public class ConversationFragment extends Fragment { } private void displayInfoMessage(ViewHolder viewHolder, int r) { - viewHolder.download_button.setVisibility(View.GONE); + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getString(r)); viewHolder.messageBody.setTextColor(0xff33B5E5); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayDecryptionFailed(ViewHolder viewHolder) { - viewHolder.download_button.setVisibility(View.GONE); + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody .setText(getString(R.string.decryption_failed)); viewHolder.messageBody.setTextColor(0xFFe92727); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayTextMessage(ViewHolder viewHolder, String text) { @@ -297,6 +341,7 @@ public class ConversationFragment extends Fragment { } viewHolder.messageBody.setTextColor(0xff333333); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(true); } private void displayImageMessage(ViewHolder viewHolder, @@ -329,18 +374,27 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - Uri uri = Uri - .parse("content://eu.siacs.conversations.images/" - + message.getConversationUuid() - + "/" - + message.getUuid()); - Log.d("xmppService", - "staring intent with uri:" + uri.toString()); Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(uri, "image/*"); + intent.setDataAndType( + ImageProvider.getContentUri(message), "image/*"); startActivity(intent); } }); + viewHolder.image + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + ImageProvider.getContentUri(message)); + shareIntent.setType("image/webp"); + startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_with))); + return true; + } + }); } @Override @@ -354,13 +408,26 @@ public class ConversationFragment extends Fragment { case SENT: view = (View) inflater.inflate(R.layout.message_sent, null); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); viewHolder.contact_picture.setImageBitmap(selfBitmap); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); break; case RECIEVED: view = (View) inflater.inflate( R.layout.message_recieved, null); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); @@ -379,40 +446,61 @@ public class ConversationFragment extends Fragment { .getApplicationContext())); } + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); + break; + case STATUS: + view = (View) inflater.inflate(R.layout.message_status, + null); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + + viewHolder.contact_picture + .setImageBitmap(mBitmapCache.get( + item.getConversation().getName( + useSubject), item + .getConversation() + .getContact(), + getActivity() + .getApplicationContext())); + viewHolder.contact_picture.setAlpha(128); + + } break; default: viewHolder = null; break; } - viewHolder.indicator = (ImageView) view - .findViewById(R.id.security_indicator); - viewHolder.image = (ImageView) view - .findViewById(R.id.message_image); - viewHolder.messageBody = (TextView) view - .findViewById(R.id.message_body); - viewHolder.time = (TextView) view - .findViewById(R.id.message_time); - view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); } + if (type == STATUS) { + return view; + } + if (type == RECIEVED) { if (item.getConversation().getMode() == Conversation.MODE_MULTI) { - if (item.getCounterpart() != null) { - viewHolder.contact_picture - .setImageBitmap(mBitmapCache.get(item - .getCounterpart(), null, - getActivity() - .getApplicationContext())); - } else { - viewHolder.contact_picture - .setImageBitmap(mBitmapCache.get( - item.getConversation().getName( - useSubject), null, - getActivity() - .getApplicationContext())); - } + viewHolder.contact_picture.setImageBitmap(mBitmapCache + .get(item.getCounterpart(), null, getActivity() + .getApplicationContext())); + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + highlightInConference(item + .getCounterpart()); + } + }); } } @@ -439,7 +527,8 @@ public class ConversationFragment extends Fragment { } }); } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) - || (item.getEncryption() == Message.ENCRYPTION_NONE)) { + || (item.getEncryption() == Message.ENCRYPTION_NONE) + || (item.getEncryption() == Message.ENCRYPTION_OTR)) { displayImageMessage(viewHolder, item); } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { displayInfoMessage(viewHolder, @@ -449,8 +538,21 @@ public class ConversationFragment extends Fragment { } } else { if (item.getEncryption() == Message.ENCRYPTION_PGP) { - displayInfoMessage(viewHolder, - R.string.encrypted_message); + if (activity.hasPgp()) { + displayInfoMessage(viewHolder, + R.string.encrypted_message); + } else { + displayInfoMessage(viewHolder, + R.string.install_openkeychain); + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayDecryptionFailed(viewHolder); } else { @@ -468,6 +570,18 @@ public class ConversationFragment extends Fragment { return view; } + protected void highlightInConference(String nick) { + String oldString = chatMsg.getText().toString().trim(); + if (oldString.isEmpty()) { + chatMsg.setText(nick + ": "); + } else { + chatMsg.setText(oldString + " " + nick + " "); + } + int position = chatMsg.length(); + Editable etext = chatMsg.getText(); + Selection.setSelection(etext, position); + } + protected Bitmap findSelfPicture() { SharedPreferences sharedPref = PreferenceManager .getDefaultSharedPreferences(getActivity() @@ -494,7 +608,7 @@ public class ConversationFragment extends Fragment { @Override public void onStop() { super.onStop(); - if (this.conversation!=null) { + if (this.conversation != null) { this.conversation.setNextMessage(chatMsg.getText().toString()); } } @@ -504,10 +618,16 @@ public class ConversationFragment extends Fragment { if (this.conversation == null) { return; } + String oldString = conversation.getNextMessage().trim(); if (this.pastedText == null) { - this.chatMsg.setText(conversation.getNextMessage()); + this.chatMsg.setText(oldString); } else { - chatMsg.setText(conversation.getNextMessage() + " " + pastedText); + + if (oldString.isEmpty()) { + chatMsg.setText(pastedText); + } else { + chatMsg.setText(oldString + " " + pastedText); + } pastedText = null; } int position = chatMsg.length(); @@ -519,10 +639,10 @@ public class ConversationFragment extends Fragment { if (!activity.shouldPaneBeOpen()) { activity.getSlidingPaneLayout().closePane(); activity.getActionBar().setDisplayHomeAsUpEnabled(true); + activity.getActionBar().setHomeButtonEnabled(true); activity.getActionBar().setTitle( conversation.getName(useSubject)); activity.invalidateOptionsMenu(); - } } if (conversation.getMode() == Conversation.MODE_MULTI) { @@ -555,50 +675,61 @@ public class ConversationFragment extends Fragment { } } - private void decryptMessage(final Message message) { + private void decryptMessage(Message message) { PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); if (engine != null) { - engine.decrypt(message, new UiCallback() { + engine.decrypt(message, new UiCallback<Message>() { @Override - public void userInputRequried(PendingIntent pi) { + public void userInputRequried(PendingIntent pi, Message message) { askForPassphraseIntent = pi.getIntentSender(); pgpInfo.setVisibility(View.VISIBLE); } @Override - public void success() { + public void success(Message message) { activity.xmppConnectionService.databaseBackend .updateMessage(message); updateMessages(); } @Override - public void error(int error) { + public void error(int error, Message message) { message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); // updateMessages(); } }); } else { - pgpInfo.setVisibility(View.VISIBLE); + pgpInfo.setVisibility(View.GONE); } } public void updateMessages() { - if (getView()==null) { + if (getView() == null) { return; } ConversationActivity activity = (ConversationActivity) getActivity(); if (this.conversation != null) { for (Message message : this.conversation.getMessages()) { if ((message.getEncryption() == Message.ENCRYPTION_PGP) - && (message.getStatus() == Message.STATUS_RECIEVED)) { + && ((message.getStatus() == Message.STATUS_RECIEVED) || (message + .getStatus() == Message.STATUS_SEND))) { decryptMessage(message); break; } } - this.messageList.clear(); - this.messageList.addAll(this.conversation.getMessages()); + if (this.conversation.getMessages().size() == 0) { + this.messageList.clear(); + messagesLoaded = false; + } else { + for (Message message : this.conversation.getMessages()) { + if (!this.messageList.contains(message)) { + this.messageList.add(message); + } + } + messagesLoaded = true; + updateStatusMessages(); + } this.messageListAdapter.notifyDataSetChanged(); if (conversation.getMode() == Conversation.MODE_SINGLE) { if (messageList.size() >= 1) { @@ -616,11 +747,8 @@ public class ConversationFragment extends Fragment { } getActivity().invalidateOptionsMenu(); updateChatMsgHint(); - int size = this.messageList.size(); - if (size >= 1) - messagesView.setSelection(size - 1); if (!activity.shouldPaneBeOpen()) { - conversation.markRead(); + activity.xmppConnectionService.markRead(conversation); // TODO update notifications UIHelper.updateNotification(getActivity(), activity.getConversationList(), null, false); @@ -629,34 +757,61 @@ public class ConversationFragment extends Fragment { } } + private void messageSent() { + int size = this.messageList.size(); + if (size >= 1) { + messagesView.setSelection(size - 1); + } + chatMsg.setText(""); + } + + protected void updateStatusMessages() { + boolean addedStatusMsg = false; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (addedStatusMsg) { + if (this.messageList.get(i).getType() == Message.TYPE_STATUS) { + this.messageList.remove(i); + --i; + } + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECIEVED) { + addedStatusMsg = true; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation)); + addedStatusMsg = true; + } + } + } + } + } + } + protected void makeFingerprintWarning(int latestEncryption) { final LinearLayout fingerprintWarning = (LinearLayout) getView() .findViewById(R.id.new_fingerprint); - if (conversation.getContact() != null) { - Set<String> knownFingerprints = conversation.getContact() - .getOtrFingerprints(); - if ((latestEncryption == Message.ENCRYPTION_OTR) - && (conversation.hasValidOtrSession() - && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints - .contains(conversation.getOtrFingerprint())))) { - fingerprintWarning.setVisibility(View.VISIBLE); - TextView fingerprint = (TextView) getView().findViewById( - R.id.otr_fingerprint); - fingerprint.setText(conversation.getOtrFingerprint()); - fingerprintWarning.setOnClickListener(new OnClickListener() { + Set<String> knownFingerprints = conversation.getContact() + .getOtrFingerprints(); + if ((latestEncryption == Message.ENCRYPTION_OTR) + && (conversation.hasValidOtrSession() + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints + .contains(conversation.getOtrFingerprint())))) { + fingerprintWarning.setVisibility(View.VISIBLE); + TextView fingerprint = (TextView) getView().findViewById( + R.id.otr_fingerprint); + fingerprint.setText(conversation.getOtrFingerprint()); + fingerprintWarning.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog dialog = UIHelper - .getVerifyFingerprintDialog( - (ConversationActivity) getActivity(), - conversation, fingerprintWarning); - dialog.show(); - } - }); - } else { - fingerprintWarning.setVisibility(View.GONE); - } + @Override + public void onClick(View v) { + AlertDialog dialog = UIHelper.getVerifyFingerprintDialog( + (ConversationActivity) getActivity(), conversation, + fingerprintWarning); + dialog.show(); + } + }); } else { fingerprintWarning.setVisibility(View.GONE); } @@ -664,97 +819,128 @@ public class ConversationFragment extends Fragment { protected void sendPlainTextMessage(Message message) { ConversationActivity activity = (ConversationActivity) getActivity(); - activity.xmppConnectionService.sendMessage(message, null); - chatMsg.setText(""); + activity.xmppConnectionService.sendMessage(message); + messageSent(); } protected void sendPgpMessage(final Message message) { - activity.pendingMessage = message; final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; final Contact contact = message.getConversation().getContact(); if (activity.hasPgp()) { - if (contact.getPgpKeyId() != 0) { - xmppService.getPgpEngine().hasKey(contact, - new UiCallback() { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (contact.getPgpKeyId() != 0) { + xmppService.getPgpEngine().hasKey(contact, + new UiCallback<Contact>() { - @Override - public void userInputRequried(PendingIntent pi) { - activity.runIntent( - pi, - ConversationActivity.REQUEST_ENCRYPT_MESSAGE); - } + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + activity.runIntent( + pi, + ConversationActivity.REQUEST_ENCRYPT_MESSAGE); + } - @Override - public void success() { - activity.encryptTextMessage(); - } + @Override + public void success(Contact contact) { + messageSent(); + activity.encryptTextMessage(message); + } - @Override - public void error(int error) { - - } - }); + @Override + public void error(int error, Contact contact) { - } else { - showNoPGPKeyDialog(new DialogInterface.OnClickListener() { + } + }); - @Override - public void onClick(DialogInterface dialog, - int which) { - conversation - .setNextEncryption(Message.ENCRYPTION_NONE); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message, null); - chatMsg.setText(""); + } else { + showNoPGPKeyDialog(false, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } else { + if (conversation.getMucOptions().pgpKeysInUse()) { + if (!conversation.getMucOptions().everybodyHasKeys()) { + Toast warning = Toast + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); + warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); + warning.show(); } - }); + activity.encryptTextMessage(message); + messageSent(); + } else { + showNoPGPKeyDialog(true, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } } + } else { + activity.showInstallPgpDialog(); } } - - public void showNoPGPKeyDialog(DialogInterface.OnClickListener listener) { - AlertDialog.Builder builder = new AlertDialog.Builder( - getActivity()); - builder.setTitle(getString(R.string.no_pgp_key)); + + public void showNoPGPKeyDialog(boolean plural, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + if (plural) { + builder.setTitle(getString(R.string.no_pgp_keys)); + builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); + } else { + builder.setTitle(getString(R.string.no_pgp_key)); + builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + } builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.send_unencrypted),listener); + builder.setPositiveButton(getString(R.string.send_unencrypted), + listener); builder.create().show(); } protected void sendOtrMessage(final Message message) { - ConversationActivity activity = (ConversationActivity) getActivity(); + final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; if (conversation.hasValidOtrSession()) { - activity.xmppConnectionService.sendMessage(message, null); - chatMsg.setText(""); + activity.xmppConnectionService.sendMessage(message); + messageSent(); } else { activity.selectPresence(message.getConversation(), new OnPresenceSelected() { @Override - public void onPresenceSelected(boolean success, - String presence) { - if (success) { - xmppService.sendMessage(message, presence); - chatMsg.setText(""); - } + public void onPresenceSelected() { + message.setPresence(conversation.getNextPresence()); + xmppService.sendMessage(message); + messageSent(); } - - @Override - public void onSendPlainTextInstead() { - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message, null); - chatMsg.setText(""); - } - }, "otr"); + }); } } private static class ViewHolder { + protected LinearLayout message_box; protected Button download_button; protected ImageView image; protected ImageView indicator; @@ -766,7 +952,6 @@ public class ConversationFragment extends Fragment { private class BitmapCache { private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>(); - private Bitmap error = null; public Bitmap get(String name, Contact contact, Context context) { if (bitmaps.containsKey(name)) { diff --git a/src/eu/siacs/conversations/ui/EditAccount.java b/src/eu/siacs/conversations/ui/EditAccount.java index 8b1c0fa6..e1bcaeb5 100644 --- a/src/eu/siacs/conversations/ui/EditAccount.java +++ b/src/eu/siacs/conversations/ui/EditAccount.java @@ -7,7 +7,6 @@ import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; @@ -51,27 +50,19 @@ public class EditAccount extends DialogFragment { final CheckBox registerAccount = (CheckBox) view .findViewById(R.id.edit_account_register_new); - final String okButtonDesc; - if (account != null) { jidText.setText(account.getJid()); password.setText(account.getPassword()); - Log.d("xmppService","mein debugger. account != null"); if (account.isOptionSet(Account.OPTION_REGISTER)) { registerAccount.setChecked(true); - builder.setTitle(getString(R.string.register_account)); - okButtonDesc = "Register"; passwordConfirm.setVisibility(View.VISIBLE); passwordConfirm.setText(account.getPassword()); } else { registerAccount.setVisibility(View.GONE); - builder.setTitle("Edit account"); - okButtonDesc = "Edit"; } - } else { - builder.setTitle("Add account"); - okButtonDesc = "Add"; } + builder.setTitle(R.string.account_settings); + registerAccount .setOnCheckedChangeListener(new OnCheckedChangeListener() { @@ -79,26 +70,19 @@ public class EditAccount extends DialogFragment { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - AlertDialog d = (AlertDialog) getDialog(); - Button positiveButton = (Button) d - .getButton(Dialog.BUTTON_POSITIVE); if (isChecked) { - d.setTitle(getString(R.string.register_account)); - positiveButton.setText("Register"); passwordConfirm.setVisibility(View.VISIBLE); confirmPwDesc.setVisibility(View.VISIBLE); } else { - d.setTitle("Add account"); passwordConfirm.setVisibility(View.GONE); - positiveButton.setText("Add"); confirmPwDesc.setVisibility(View.GONE); } } }); builder.setView(view); - builder.setNeutralButton("Cancel", null); - builder.setPositiveButton(okButtonDesc, null); + builder.setNeutralButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.save), null); return builder.create(); } @@ -114,7 +98,9 @@ public class EditAccount extends DialogFragment { String jid = jidEdit.getText().toString(); EditText passwordEdit = (EditText) d .findViewById(R.id.account_password); + EditText passwordConfirmEdit = (EditText) d.findViewById(R.id.account_password_confirm2); String password = passwordEdit.getText().toString(); + String passwordConfirm = passwordConfirmEdit.getText().toString(); CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new); String username; String server; @@ -123,9 +109,15 @@ public class EditAccount extends DialogFragment { username = parts[0]; server = parts[1]; } else { - jidEdit.setError("Invalid Jabber ID"); + jidEdit.setError(getString(R.string.invalid_jid)); return; } + if (register.isChecked()) { + if (!passwordConfirm.equals(password)) { + passwordConfirmEdit.setError(getString(R.string.passwords_do_not_match)); + return; + } + } if (account != null) { account.setPassword(password); account.setUsername(username); diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java index 0b7dac58..c52916a2 100644 --- a/src/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -3,25 +3,19 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; import java.util.List; -import org.openintents.openpgp.OpenPgpError; - import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.ui.EditAccount.EditAccountListener; import eu.siacs.conversations.xmpp.OnTLSExceptionReceived; import eu.siacs.conversations.xmpp.XmppConnection; import android.app.Activity; import android.app.AlertDialog; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.SystemClock; -import android.util.Log; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; @@ -73,7 +67,7 @@ public class ManageAccountActivity extends XmppActivity { @Override public void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Untrusted Certificate"); + builder.setTitle(getString(R.string.account_status_error)); builder.setIconAttribute(android.R.attr.alertDialogIcon); View view = (View) getLayoutInflater().inflate(R.layout.cert_warning, null); TextView sha = (TextView) view.findViewById(R.id.sha); @@ -91,8 +85,8 @@ public class ManageAccountActivity extends XmppActivity { hint.setText(getString(R.string.untrusted_cert_hint,account.getServer())); sha.setText(humanReadableSha.toString()); builder.setView(view); - builder.setNegativeButton("Don't connect", null); - builder.setPositiveButton("Trust certificate", new OnClickListener() { + builder.setNegativeButton(getString(R.string.certif_no_trust), null); + builder.setPositiveButton(getString(R.string.certif_trust), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -130,55 +124,55 @@ public class ManageAccountActivity extends XmppActivity { .findViewById(R.id.account_status); switch (account.getStatus()) { case Account.STATUS_DISABLED: - statusView.setText("temporarily disabled"); + statusView.setText(getString(R.string.account_status_disabled)); statusView.setTextColor(0xFF1da9da); break; case Account.STATUS_ONLINE: - statusView.setText("online"); + statusView.setText(getString(R.string.account_status_online)); statusView.setTextColor(0xFF83b600); break; case Account.STATUS_CONNECTING: - statusView.setText("connecting\u2026"); + statusView.setText(getString(R.string.account_status_connecting)); statusView.setTextColor(0xFF1da9da); break; case Account.STATUS_OFFLINE: - statusView.setText("offline"); + statusView.setText(getString(R.string.account_status_offline)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_UNAUTHORIZED: - statusView.setText("unauthorized"); + statusView.setText(getString(R.string.account_status_unauthorized)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_SERVER_NOT_FOUND: - statusView.setText("server not found"); + statusView.setText(getString(R.string.account_status_not_found)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_NO_INTERNET: - statusView.setText("no internet"); + statusView.setText(getString(R.string.account_status_no_internet)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_SERVER_REQUIRES_TLS: - statusView.setText("server requires TLS"); + statusView.setText(getString(R.string.account_status_requires_tls)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_TLS_ERROR: - statusView.setText("untrusted cerficate"); + statusView.setText(getString(R.string.account_status_error)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_FAILED: - statusView.setText("registration failed"); + statusView.setText(getString(R.string.account_status_regis_fail)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_CONFLICT: - statusView.setText("username already in use"); + statusView.setText(getString(R.string.account_status_regis_conflict)); statusView.setTextColor(0xFFe92727); break; case Account.STATUS_REGISTRATION_SUCCESSFULL: - statusView.setText("registration completed"); + statusView.setText(getString(R.string.account_status_regis_success)); statusView.setTextColor(0xFF83b600); break; case Account.STATUS_REGISTRATION_NOT_SUPPORTED: - statusView.setText("server does not support registration"); + statusView.setText(getString(R.string.account_status_regis_not_sup)); statusView.setTextColor(0xFFe92727); break; default: @@ -261,10 +255,10 @@ public class ManageAccountActivity extends XmppActivity { mode.finish(); } else if (item.getItemId()==R.id.mgmt_account_delete) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle("Are you sure?"); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage("If you delete your account your entire conversation history will be lost"); - builder.setPositiveButton("Delete", new OnClickListener() { + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -273,12 +267,14 @@ public class ManageAccountActivity extends XmppActivity { mode.finish(); } }); - builder.setNegativeButton("Cancel",null); + builder.setNegativeButton(getString(R.string.cancel),null); builder.create().show(); } else if (item.getItemId()==R.id.mgmt_account_announce_pgp) { if (activity.hasPgp()) { mode.finish(); announcePgp(selectedAccountForActionMode,null); + } else { + activity.showInstallPgpDialog(); } } else if (item.getItemId() == R.id.mgmt_otr_key) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -293,7 +289,7 @@ public class ManageAccountActivity extends XmppActivity { noFingerprintView.setVisibility(View.GONE); } builder.setView(view); - builder.setPositiveButton("Done", null); + builder.setPositiveButton(getString(R.string.done), null); builder.create().show(); } else if (item.getItemId() == R.id.mgmt_account_info) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); @@ -317,36 +313,36 @@ public class ManageAccountActivity extends XmppActivity { pcks_received.setText(""+xmpp.getReceivedStanzas()); pcks_sent.setText(""+xmpp.getSentStanzas()); if (connectionAgeHours >= 2) { - connection.setText(connectionAgeHours+" hours"); + connection.setText(connectionAgeHours+" " + getString(R.string.hours)); } else { - connection.setText(connectionAge+" mins"); + connection.setText(connectionAge+" " + getString(R.string.mins)); } if (xmpp.hasFeatureStreamManagment()) { if (sessionAgeHours >= 2) { - session.setText(sessionAgeHours+" hours"); + session.setText(sessionAgeHours+" " + getString(R.string.hours)); } else { - session.setText(sessionAge+" mins"); + session.setText(sessionAge+" " + getString(R.string.mins)); } - stream.setText("Yes"); + stream.setText(getString(R.string.yes)); } else { - stream.setText("No"); + stream.setText(getString(R.string.no)); session.setText(connection.getText()); } if (xmpp.hasFeaturesCarbon()) { - carbon.setText("Yes"); + carbon.setText(getString(R.string.yes)); } else { - carbon.setText("No"); + carbon.setText(getString(R.string.no)); } if (xmpp.hasFeatureRosterManagment()) { - roster.setText("Yes"); + roster.setText(getString(R.string.yes)); } else { - roster.setText("No"); + roster.setText(getString(R.string.no)); } builder.setView(view); } else { - builder.setMessage("Account is offline"); + builder.setMessage(getString(R.string.mgmt_account_account_offline)); } - builder.setPositiveButton("Hide", null); + builder.setPositiveButton(getString(R.string.hide), null); builder.create().show(); } return true; @@ -380,6 +376,7 @@ public class ManageAccountActivity extends XmppActivity { accountListViewAdapter.notifyDataSetChanged(); if ((this.accountList.size() == 0)&&(this.firstrun)) { getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); addAccount(); this.firstrun = false; } @@ -403,6 +400,25 @@ public class ManageAccountActivity extends XmppActivity { return super.onOptionsItemSelected(item); } + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, ContactsActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + private void editAccount(Account account) { EditAccount dialog = new EditAccount(); dialog.setAccount(account); @@ -429,6 +445,7 @@ public class ManageAccountActivity extends XmppActivity { public void onAccountEdited(Account account) { xmppConnectionService.createAccount(account); activity.getActionBar().setDisplayHomeAsUpEnabled(true); + activity.getActionBar().setHomeButtonEnabled(true); } }); dialog.show(getFragmentManager(), "add_account"); diff --git a/src/eu/siacs/conversations/ui/MucDetailsActivity.java b/src/eu/siacs/conversations/ui/MucDetailsActivity.java index c6807c61..ee6709b7 100644 --- a/src/eu/siacs/conversations/ui/MucDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/MucDetailsActivity.java @@ -3,14 +3,19 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; import java.util.List; +import org.openintents.openpgp.util.OpenPgpUtils; + import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.utils.UIHelper; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -19,8 +24,6 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -153,11 +156,11 @@ public class MucDetailsActivity extends XmppActivity { switch (self.getAffiliation()) { case User.AFFILIATION_ADMIN: mRoleAffiliaton.setText(getReadableRole(self.getRole()) - + " (Admin)"); + + " (" + getString(R.string.admin) + ")"); break; case User.AFFILIATION_OWNER: mRoleAffiliaton.setText(getReadableRole(self.getRole()) - + " (Owner)"); + + " (" + getString(R.string.owner) + ")"); break; default: mRoleAffiliaton @@ -167,16 +170,36 @@ public class MucDetailsActivity extends XmppActivity { } this.users.clear(); this.users.addAll(conversation.getMucOptions().getUsers()); - //contactsAdapter.notifyDataSetChanged(); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); - for(User contact : conversation.getMucOptions().getUsers()) { + for(final User contact : conversation.getMucOptions().getUsers()) { View view = (View) inflater.inflate(R.layout.contact, null); - - ((TextView) view.findViewById(R.id.contact_display_name)) - .setText(contact.getName()); + TextView displayName = (TextView) view.findViewById(R.id.contact_display_name); + TextView key = (TextView) view.findViewById(R.id.key); + displayName.setText(contact.getName()); TextView role = (TextView) view.findViewById(R.id.contact_jid); role.setText(getReadableRole(contact.getRole())); + if (contact.getPgpKeyId()!=0) { + key.setVisibility(View.VISIBLE); + key.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp!=null) { + PendingIntent intent = pgp.getIntentForKey(conversation.getAccount(), contact.getPgpKeyId()); + if (intent!=null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0); + } catch (SendIntentException e) { + + } + } + } + } + }); + key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); + } ImageView imageView = (ImageView) view .findViewById(R.id.contact_photo); imageView.setImageBitmap(UIHelper.getContactPicture(contact.getName(), 48,this.getApplicationContext(), false)); diff --git a/src/eu/siacs/conversations/ui/OnPresenceSelected.java b/src/eu/siacs/conversations/ui/OnPresenceSelected.java index b3a995dc..1c967224 100644 --- a/src/eu/siacs/conversations/ui/OnPresenceSelected.java +++ b/src/eu/siacs/conversations/ui/OnPresenceSelected.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; public interface OnPresenceSelected { - public void onPresenceSelected(boolean success, String presence); - public void onSendPlainTextInstead(); + public void onPresenceSelected(); } diff --git a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java deleted file mode 100644 index d69ce35b..00000000 --- a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.siacs.conversations.ui; - -import java.util.List; - -import eu.siacs.conversations.entities.Contact; - -public interface OnRosterFetchedListener { - public void onRosterFetched(List<Contact> roster); -} diff --git a/src/eu/siacs/conversations/ui/ShareWithActivity.java b/src/eu/siacs/conversations/ui/ShareWithActivity.java index 1bc9fc46..d4d23ddf 100644 --- a/src/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/eu/siacs/conversations/ui/ShareWithActivity.java @@ -11,10 +11,13 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.utils.UIHelper; +import android.app.PendingIntent; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; @@ -25,37 +28,49 @@ import android.widget.LinearLayout; import android.widget.TextView; public class ShareWithActivity extends XmppActivity { - + private LinearLayout conversations; private LinearLayout contacts; + private boolean isImage = false; - private OnClickListener click = new OnClickListener() { + private UiCallback<Message> attachImageCallback = new UiCallback<Message>() { @Override - public void onClick(View v) { + public void userInputRequried(PendingIntent pi, Message object) { + // TODO Auto-generated method stub + + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { // TODO Auto-generated method stub } }; - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.share_with); - setTitle("Share with Conversation"); - + setTitle(getString(R.string.title_activity_sharewith)); + contacts = (LinearLayout) findViewById(R.id.contacts); conversations = (LinearLayout) findViewById(R.id.conversations); - + } - - + public View createContactView(String name, String msgTxt, Bitmap bm) { View view = (View) getLayoutInflater().inflate(R.layout.contact, null); view.setBackgroundResource(R.drawable.greybackground); - TextView contactName =(TextView) view.findViewById(R.id.contact_display_name); + TextView contactName = (TextView) view + .findViewById(R.id.contact_display_name); contactName.setText(name); TextView msg = (TextView) view.findViewById(R.id.contact_jid); msg.setText(msgTxt); @@ -63,75 +78,100 @@ public class ShareWithActivity extends XmppActivity { imageView.setImageBitmap(bm); return view; } - - - + @Override void onBackendConnected() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + this.isImage = (getIntent().getType() != null && getIntent() + .getType().startsWith("image/")); + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); boolean useSubject = preferences.getBoolean("use_subject_in_muc", true); - - Set<String> displayedContacts = new HashSet<String>(); + + Set<Contact> displayedContacts = new HashSet<Contact>(); conversations.removeAllViews(); List<Conversation> convList = xmppConnectionService.getConversations(); Collections.sort(convList, new Comparator<Conversation>() { @Override public int compare(Conversation lhs, Conversation rhs) { - return (int) (rhs.getLatestMessage().getTimeSent() - lhs.getLatestMessage().getTimeSent()); + return (int) (rhs.getLatestMessage().getTimeSent() - lhs + .getLatestMessage().getTimeSent()); } }); - for(final Conversation conversation : convList) { - View view = createContactView(conversation.getName(useSubject), - conversation.getLatestMessage().getBody().trim(), - UIHelper.getContactPicture(conversation, 48, - this.getApplicationContext(), false)); - view.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - String sharedText = getIntent().getStringExtra(Intent.EXTRA_TEXT); - switchToConversation(conversation, sharedText); - finish(); - } - }); - conversations.addView(view); - if (conversation.getContact() != null) { - displayedContacts.add(conversation.getContact().getUuid()); + for (final Conversation conversation : convList) { + if (!isImage || conversation.getMode() == Conversation.MODE_SINGLE) { + View view = createContactView( + conversation.getName(useSubject), + conversation.getLatestMessage().getBody().trim(), + UIHelper.getContactPicture(conversation, 48, + this.getApplicationContext(), false)); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + share(conversation); + } + }); + conversations.addView(view); + displayedContacts.add(conversation.getContact()); } } contacts.removeAllViews(); - final List<Contact> contactsList = new ArrayList<Contact>(); - for(Account account : xmppConnectionService.getAccounts()) { - for(final Contact contact : xmppConnectionService.getRoster(account)) { - if (!displayedContacts.contains(contact.getUuid())) { + List<Contact> contactsList = new ArrayList<Contact>(); + for (Account account : xmppConnectionService.getAccounts()) { + for (Contact contact : account.getRoster().getContacts()) { + if (!displayedContacts.contains(contact) + && (contact.showInRoster())) { contactsList.add(contact); } } } - + Collections.sort(contactsList, new Comparator<Contact>() { @Override public int compare(Contact lhs, Contact rhs) { - return lhs.getDisplayName().compareToIgnoreCase(rhs.getDisplayName()); + return lhs.getDisplayName().compareToIgnoreCase( + rhs.getDisplayName()); } }); - - for(int i = 0; i < contactsList.size(); ++i) { + + for (int i = 0; i < contactsList.size(); ++i) { final Contact con = contactsList.get(i); - View view = createContactView(con.getDisplayName(), con.getJid(), - UIHelper.getContactPicture(con, 48, this.getApplicationContext(), false)); + View view = createContactView( + con.getDisplayName(), + con.getJid(), + UIHelper.getContactPicture(con, 48, + this.getApplicationContext(), false)); view.setOnClickListener(new OnClickListener() { - + @Override public void onClick(View v) { - String sharedText = getIntent().getStringExtra(Intent.EXTRA_TEXT); - Conversation conversation = xmppConnectionService.findOrCreateConversation(con.getAccount(), con.getJid(), false); - switchToConversation(conversation, sharedText); - finish(); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(con.getAccount(), + con.getJid(), false); + share(conversation); } }); contacts.addView(view); } } + + private void share(final Conversation conversation) { + String sharedText = null; + if (isImage) { + final Uri uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + ShareWithActivity.this.xmppConnectionService.attachImageToConversation(conversation, uri,attachImageCallback); + } + }); + + } else { + sharedText = getIntent().getStringExtra( + Intent.EXTRA_TEXT); + } + switchToConversation(conversation, sharedText, true); + finish(); + } } diff --git a/src/eu/siacs/conversations/ui/UiCallback.java b/src/eu/siacs/conversations/ui/UiCallback.java index f9273b96..05a869f7 100644 --- a/src/eu/siacs/conversations/ui/UiCallback.java +++ b/src/eu/siacs/conversations/ui/UiCallback.java @@ -2,8 +2,8 @@ package eu.siacs.conversations.ui; import android.app.PendingIntent; -public interface UiCallback { - public void success(); - public void error(int errorCode); - public void userInputRequried(PendingIntent pi); +public interface UiCallback<T> { + public void success(T object); + public void error(int errorCode, T object); + public void userInputRequried(PendingIntent pi, T object); } diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java index dc894ad5..c95cbfec 100644 --- a/src/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/eu/siacs/conversations/ui/XmppActivity.java @@ -1,11 +1,11 @@ package eu.siacs.conversations.ui; -import java.nio.channels.AlreadyConnectedException; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.ExceptionHelper; @@ -29,15 +29,15 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; public abstract class XmppActivity extends Activity { - + public static final int REQUEST_ANNOUNCE_PGP = 0x73731; - + protected final static String LOGTAG = "xmppService"; - + public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; protected boolean handledViewIntent = false; - + protected ServiceConnection mConnection = new ServiceConnection() { @Override @@ -53,7 +53,7 @@ public abstract class XmppActivity extends Activity { xmppConnectionServiceBound = false; } }; - + @Override protected void onStart() { super.onStart(); @@ -61,14 +61,14 @@ public abstract class XmppActivity extends Activity { connectToBackend(); } } - + public void connectToBackend() { Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("ui"); startService(intent); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } - + @Override protected void onStop() { super.onStop(); @@ -77,7 +77,7 @@ public abstract class XmppActivity extends Activity { xmppConnectionServiceBound = false; } } - + protected void hideKeyboard() { InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @@ -85,50 +85,52 @@ public abstract class XmppActivity extends Activity { if (focus != null) { - inputManager.hideSoftInputFromWindow( - focus.getWindowToken(), + inputManager.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } - + public boolean hasPgp() { - if (xmppConnectionService.getPgpEngine()!=null) { - return true; - } else { - Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.openkeychain_required)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getText(R.string.openkeychain_required_long)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton(getString(R.string.restart), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - if (xmppConnectionServiceBound) { - unbindService(mConnection); - xmppConnectionServiceBound = false; + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); } - stopService(new Intent(XmppActivity.this, XmppConnectionService.class)); - finish(); - } - }); - builder.setPositiveButton(getString(R.string.install), new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse("market://details?id=org.sufficientlysecure.keychain"); - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(intent); - finish(); - } - }); - builder.create().show(); - return false; - } + }); + builder.setPositiveButton(getString(R.string.install), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + finish(); + } + }); + builder.create().show(); } - + abstract void onBackendConnected(); - + public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: @@ -140,62 +142,76 @@ public abstract class XmppActivity extends Activity { } return super.onOptionsItemSelected(item); } - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ExceptionHelper.init(getApplicationContext()); } - - public void switchToConversation(Conversation conversation, String text) { + + public void switchToConversation(Conversation conversation, String text, + boolean newTask) { Intent viewConversationIntent = new Intent(this, ConversationActivity.class); viewConversationIntent.setAction(Intent.ACTION_VIEW); viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversation.getUuid()); - if (text!=null) { + if (text != null) { viewConversationIntent.putExtra(ConversationActivity.TEXT, text); } viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); - viewConversationIntent.setFlags(viewConversationIntent.getFlags() - | Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (newTask) { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } startActivity(viewConversationIntent); } - - protected void announcePgp(final Account account, final Conversation conversation) { - xmppConnectionService.getPgpEngine().generateSignature(account, "online", new UiCallback() { - - @Override - public void userInputRequried(PendingIntent pi) { - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (SendIntentException e) { - Log.d("xmppService","coulnd start intent for pgp anncouncment"); - } - } - - @Override - public void success() { - xmppConnectionService.databaseBackend.updateAccount(account); - xmppConnectionService.sendPgpPresence(account, account.getPgpSignature()); - if (conversation!=null) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - } - } - - @Override - public void error(int error) { - displayErrorDialog(error); - } - }); + + protected void announcePgp(Account account, final Conversation conversation) { + xmppConnectionService.getPgpEngine().generateSignature(account, + "online", new UiCallback<Account>() { + + @Override + public void userInputRequried(PendingIntent pi, + Account account) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + Log.d("xmppService", + "coulnd start intent for pgp anncouncment"); + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.sendPresence(account); + if (conversation != null) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + } + } + + @Override + public void error(int error, Account account) { + displayErrorDialog(error); + } + }); } - + protected void displayErrorDialog(final int errorCode) { runOnUiThread(new Runnable() { - + @Override public void run() { - AlertDialog.Builder builder = new AlertDialog.Builder(XmppActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder( + XmppActivity.this); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setTitle(getString(R.string.error)); builder.setMessage(errorCode); @@ -203,6 +219,78 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } }); - + + } + + protected void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = conversation.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + } + }); + builder.create().show(); + } + + public void selectPresence(final Conversation conversation, + final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (contact == null) { + showAddToRosterDialog(conversation); + } else { + Presences presences = contact.getPresences(); + if (presences.size() == 0) { + conversation.setNextPresence(null); + listener.onPresenceSelected(); + } else if (presences.size() == 1) { + String presence = (String) presences.asStringArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(); + } else { + final StringBuilder presence = new StringBuilder(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = presences.asStringArray(); + int preselectedPresence = 0; + for (int i = 0; i < presencesArray.length; ++i) { + if (presencesArray[i].equals(contact.lastseen.presence)) { + preselectedPresence = i; + break; + } + } + presence.append(presencesArray[preselectedPresence]); + builder.setSingleChoiceItems(presencesArray, + preselectedPresence, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + presence.delete(0, presence.length()); + presence.append(presencesArray[which]); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(presence.toString()); + listener.onPresenceSelected(); + } + }); + builder.create().show(); + } + } } } diff --git a/src/eu/siacs/conversations/utils/CryptoHelper.java b/src/eu/siacs/conversations/utils/CryptoHelper.java index 98dbea4d..a70b419e 100644 --- a/src/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/eu/siacs/conversations/utils/CryptoHelper.java @@ -5,15 +5,14 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Random; +import java.util.Arrays; import eu.siacs.conversations.entities.Account; - import android.util.Base64; -import android.util.Log; public class CryptoHelper { - final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + public static final String FILETRANSFER = "?FILETRANSFERv1:"; + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); final protected static char[] vowels = "aeiou".toCharArray(); final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz" .toCharArray(); @@ -25,7 +24,15 @@ public class CryptoHelper { hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } - return new String(hexChars).toLowerCase(); + return new String(hexChars); + } + + public static byte[] hexToBytes(String hexString) { + byte[] array = new BigInteger(hexString, 16).toByteArray(); + if (array[0] == 0) { + array = Arrays.copyOfRange(array, 1, array.length); + } + return array; } public static String saslPlain(String username, String password) { @@ -41,9 +48,8 @@ public class CryptoHelper { return result; } - public static String saslDigestMd5(Account account, String challenge) { + public static String saslDigestMd5(Account account, String challenge, SecureRandom random) { try { - Random random = new SecureRandom(); String[] challengeParts = new String(Base64.decode(challenge, Base64.DEFAULT)).split(","); String nonce = ""; @@ -85,12 +91,11 @@ public class CryptoHelper { } } - public static String randomMucName() { - Random random = new SecureRandom(); + public static String randomMucName(SecureRandom random) { return randomWord(3, random) + "." + randomWord(7, random); } - protected static String randomWord(int lenght, Random random) { + protected static String randomWord(int lenght, SecureRandom random) { StringBuilder builder = new StringBuilder(lenght); for (int i = 0; i < lenght; ++i) { if (i % 2 == 0) { diff --git a/src/eu/siacs/conversations/utils/DNSHelper.java b/src/eu/siacs/conversations/utils/DNSHelper.java index 94dd3b51..002e124f 100644 --- a/src/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/eu/siacs/conversations/utils/DNSHelper.java @@ -52,7 +52,7 @@ public class DNSHelper { DNSMessage message = client.query( qname, - TYPE.ANY, + TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); diff --git a/src/eu/siacs/conversations/utils/ExceptionHelper.java b/src/eu/siacs/conversations/utils/ExceptionHelper.java index f9cd375e..05b16240 100644 --- a/src/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/eu/siacs/conversations/utils/ExceptionHelper.java @@ -70,7 +70,7 @@ public class ExceptionHelper { Log.d("xmppService","using account="+finalAccount.getJid()+" to send in stack trace"); Conversation conversation = service.findOrCreateConversation(finalAccount, "bugs@siacs.eu", false); Message message = new Message(conversation, stacktrace.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message, null); + service.sendMessage(message); } }); builder.setNegativeButton(context.getText(R.string.send_never),new OnClickListener() { diff --git a/src/eu/siacs/conversations/utils/MessageParser.java b/src/eu/siacs/conversations/utils/MessageParser.java deleted file mode 100644 index 52b18f66..00000000 --- a/src/eu/siacs/conversations/utils/MessageParser.java +++ /dev/null @@ -1,162 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.List; - -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; -import android.util.Log; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class MessageParser { - - protected static final String LOGTAG = "xmppService"; - - public static Message parsePlainTextChat(MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - String body = packet.getBody(); - return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED); - } - - public static Message parsePgpChat(String pgpBody, MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - return new Message(conversation, packet.getFrom(), pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED); - } - - public static Message parseOtrChat(MessagePacket packet, Account account, XmppConnectionService service) { - boolean properlyAddressed = (packet.getTo().split("/").length == 2) || (account.countPresences() == 1); - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false); - String body = packet.getBody(); - if (!conversation.hasValidOtrSession()) { - if (properlyAddressed) { - Log.d("xmppService","starting new otr session with "+packet.getFrom()+" because no valid otr session has been found"); - conversation.startOtrSession(service.getApplicationContext(), fromParts[1],false); - } else { - Log.d("xmppService",account.getJid()+": ignoring otr session with "+fromParts[0]); - return null; - } - } else { - String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); - if (!foreignPresence.equals(fromParts[1])) { - conversation.resetOtrSession(); - if (properlyAddressed) { - Log.d("xmppService","replacing otr session with "+packet.getFrom()); - conversation.startOtrSession(service.getApplicationContext(), fromParts[1],false); - } else { - return null; - } - } - } - try { - Session otrSession = conversation.getOtrSession(); - SessionStatus before = otrSession - .getSessionStatus(); - body = otrSession.transformReceiving(body); - SessionStatus after = otrSession.getSessionStatus(); - if ((before != after) - && (after == SessionStatus.ENCRYPTED)) { - Log.d(LOGTAG, "otr session etablished"); - List<Message> messages = conversation - .getMessages(); - for (int i = 0; i < messages.size(); ++i) { - Message msg = messages.get(i); - if ((msg.getStatus() == Message.STATUS_UNSEND) - && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { - MessagePacket outPacket = service.prepareMessagePacket( - account, msg, otrSession); - msg.setStatus(Message.STATUS_SEND); - service.databaseBackend.updateMessage(msg); - account.getXmppConnection() - .sendMessagePacket(outPacket); - } - } - service.updateUi(conversation, false); - } else if ((before != after) && (after == SessionStatus.FINISHED)) { - conversation.resetOtrSession(); - Log.d(LOGTAG,"otr session stoped"); - } - //isEmpty is a work around for some weird clients which send emtpty strings over otr - if ((body == null)||(body.isEmpty())) { - return null; - } - return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_OTR,Message.STATUS_RECIEVED); - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - - public static Message parseGroupchat(MessagePacket packet, Account account, XmppConnectionService service) { - int status; - String[] fromParts = packet.getFrom().split("/"); - Conversation conversation = service.findOrCreateConversation(account, fromParts[0],true); - if (packet.hasChild("subject")) { - conversation.getMucOptions().setSubject(packet.findChild("subject").getContent()); - service.updateUi(conversation, false); - return null; - } - if ((fromParts.length == 1)) { - return null; - } - String counterPart = fromParts[1]; - if (counterPart.equals(conversation.getMucOptions().getNick())) { - status = Message.STATUS_SEND; - } else { - status = Message.STATUS_RECIEVED; - } - return new Message(conversation, counterPart, packet.getBody(), Message.ENCRYPTION_NONE, status); - } - - public static Message parseCarbonMessage(MessagePacket packet, - Account account, XmppConnectionService service) { - int status; - String fullJid; - Element forwarded; - if (packet.hasChild("received")) { - forwarded = packet.findChild("received").findChild( - "forwarded"); - status = Message.STATUS_RECIEVED; - } else if (packet.hasChild("sent")) { - forwarded = packet.findChild("sent").findChild( - "forwarded"); - status = Message.STATUS_SEND; - } else { - return null; - } - if (forwarded==null) { - return null; - } - Element message = forwarded.findChild("message"); - if ((message == null) || (!message.hasChild("body"))) - return null; // either malformed or boring - if (status == Message.STATUS_RECIEVED) { - fullJid = message.getAttribute("from"); - } else { - fullJid = message.getAttribute("to"); - } - String[] parts = fullJid.split("/"); - Conversation conversation = service.findOrCreateConversation(account, parts[0],false); - return new Message(conversation,fullJid, message.findChild("body").getContent(), Message.ENCRYPTION_NONE,status); - } - - public static void parseError(MessagePacket packet, Account account, XmppConnectionService service) { - String[] fromParts = packet.getFrom().split("/"); - service.markMessage(account, fromParts[0], packet.getId(), Message.STATUS_SEND_FAILED); - } - - public static String getPgpBody(MessagePacket packet) { - for(Element child : packet.getChildren()) { - if (child.getName().equals("x")&&child.getAttribute("xmlns").equals("jabber:x:encrypted")) { - return child.getContent(); - } - } - return null; - } -} diff --git a/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java index fa8cea04..9a689768 100644 --- a/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java +++ b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -1,9 +1,9 @@ package eu.siacs.conversations.utils; -import java.util.Hashtable; +import java.util.List; import android.os.Bundle; public interface OnPhoneContactsLoadedListener { - public void onPhoneContactsLoaded(Hashtable<String, Bundle> phoneContacts); + public void onPhoneContactsLoaded(List<Bundle> phoneContacts); } diff --git a/src/eu/siacs/conversations/utils/PRNGFixes.java b/src/eu/siacs/conversations/utils/PRNGFixes.java new file mode 100644 index 00000000..cf28ff30 --- /dev/null +++ b/src/eu/siacs/conversations/utils/PRNGFixes.java @@ -0,0 +1,326 @@ +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; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = + getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() {} + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = + Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals( + secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng1.getProvider().getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", + 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java index 6355e378..773312bb 100644 --- a/src/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -1,8 +1,8 @@ package eu.siacs.conversations.utils; -import java.util.Hashtable; +import java.util.ArrayList; +import java.util.List; -import android.app.Activity; import android.content.Context; import android.content.CursorLoader; import android.content.Loader; @@ -10,21 +10,15 @@ import android.content.Loader.OnLoadCompleteListener; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.os.Looper; import android.provider.ContactsContract; import android.provider.ContactsContract.Profile; -import android.provider.MediaStore; public class PhoneHelper { public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - final Looper mLooper = Looper.myLooper(); - final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>(); - + final List<Bundle> phoneContacts = new ArrayList<Bundle>(); + final String[] PROJECTION = new String[] { ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.PHOTO_THUMBNAIL_URI, @@ -58,15 +52,14 @@ public class PhoneHelper { .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI))); contact.putString("lookup", cursor.getString(cursor .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); - phoneContacts.put( - cursor.getString(cursor - .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)), - contact); + + contact.putString("jid",cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); + phoneContacts.add(contact); } if (listener != null) { listener.onPhoneContactsLoaded(phoneContacts); } - mLooper.quit(); } }); mCursorLoader.startLoading(); diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java index 8baa3c25..183d94f3 100644 --- a/src/eu/siacs/conversations/utils/UIHelper.java +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -1,18 +1,19 @@ package eu.siacs.conversations.utils; import java.io.FileNotFoundException; -import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.regex.Pattern; +import java.util.regex.Matcher; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; @@ -27,7 +28,6 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -38,11 +38,11 @@ import android.net.Uri; import android.preference.PreferenceManager; import android.provider.ContactsContract.Contacts; import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.InboxStyle; import android.support.v4.app.TaskStackBuilder; +import android.text.format.DateFormat; +import android.text.format.DateUtils; import android.text.Html; import android.util.DisplayMetrics; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; @@ -53,23 +53,56 @@ public class UIHelper { private static final int BG_COLOR = 0xFF181818; private static final int FG_COLOR = 0xFFE5E5E5; private static final int TRANSPARENT = 0x00000000; + private static final int DATE_NO_YEAR_FLAGS = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; - public static String readableTimeDifference(long time) { + public static String readableTimeDifference(Context context, long time) { if (time == 0) { - return "just now"; + return context.getString(R.string.just_now); } Date date = new Date(time); long difference = (System.currentTimeMillis() - time) / 1000; if (difference < 60) { - return "just now"; - } else if (difference < 60 * 10) { - return difference / 60 + " min ago"; - } else if (difference < 60 * 60 * 24) { - SimpleDateFormat sdf = new SimpleDateFormat("HH:mm",Locale.US); - return sdf.format(date); + return context.getString(R.string.just_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.minute_ago); + } else if (difference < 60 * 15) { + return context.getString(R.string.minutes_ago,Math.round(difference/60.0)); + } else if (today(date)) { + java.text.DateFormat df = DateFormat.getTimeFormat(context); + return df.format(date); } else { - SimpleDateFormat sdf = new SimpleDateFormat("MM/dd",Locale.US); - return sdf.format(date); + return DateUtils.formatDateTime(context, date.getTime(), DATE_NO_YEAR_FLAGS); + } + } + + private static boolean today(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date); + cal2.setTimeInMillis(System.currentTimeMillis()); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); + } + + public static String lastseen(Context context, long time) { + if (time==0) { + return context.getString(R.string.never_seen); + } + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.last_seen_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.last_seen_min); + } else if (difference < 60 * 60) { + return context.getString(R.string.last_seen_mins,Math.round(difference/60.0)); + } else if (difference < 60 * 60 * 2) { + return context.getString(R.string.last_seen_hour); + } else if (difference < 60 * 60 * 24) { + return context.getString(R.string.last_seen_hours,Math.round(difference/(60.0*60.0))); + } else if (difference < 60 * 60 * 48) { + return context.getString(R.string.last_seen_day); + } else { + return context.getString(R.string.last_seen_days,Math.round(difference/(60.0*60.0*24.0))); } } @@ -81,8 +114,24 @@ public class UIHelper { private static int getNameColor(String name) { int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713, 0xFFe92727 }; - int color = holoColors[Math.abs(name.toLowerCase(Locale.getDefault()).hashCode()) % holoColors.length]; - return color; + return holoColors[Math.abs(name.toLowerCase(Locale.getDefault()).hashCode()) % holoColors.length]; + } + + private static void drawTile(Canvas canvas, String letter, int tileColor, int textColor, int left, int top, int right, int bottom) { + Paint tilePaint = new Paint(), textPaint = new Paint(); + tilePaint.setColor(tileColor); + textPaint.setColor(textColor); + textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); + textPaint.setTextSize((float) ((right - left) * 0.8)); + Rect rect = new Rect(); + + canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); + textPaint.getTextBounds(letter, 0, 1, rect); + float width = textPaint.measureText(letter); + canvas.drawText(letter, + (right+left)/2 - width/2, + (top+bottom)/2 + rect.height()/2, + textPaint); } private static Bitmap getUnknownContactPicture(String[] names, int size, int bgColor, int fgColor) { @@ -110,135 +159,46 @@ public class UIHelper { colors[3] = 0xFF444444; } } - Paint textPaint = new Paint(), tilePaint = new Paint(); - textPaint.setColor(fgColor); - textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); - Rect rect, left, right, topLeft, bottomLeft, topRight, bottomRight; - float width; + + bitmap.eraseColor(bgColor); switch(tiles) { case 1: - bitmap.eraseColor(colors[0]); - - textPaint.setTextSize((float) (size * 0.8)); - textPaint.setAntiAlias(true); - rect = new Rect(); - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 2) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size, size); break; case 2: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - left = new Rect(0, 0, (size/2)-1, size); - canvas.drawRect(left, tilePaint); - - tilePaint.setColor(colors[1]); - right = new Rect((size/2)+1, 0, size, size); - canvas.drawRect(right, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size); + drawTile(canvas, letters[1], colors[1], fgColor, + size/2 + 1, 0, size, size); break; case 3: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - left = new Rect(0, 0, (size/2)-1, size); - canvas.drawRect(left, tilePaint); - - tilePaint.setColor(colors[1]); - topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); - canvas.drawRect(topRight, tilePaint); - - tilePaint.setColor(colors[2]); - bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); - canvas.drawRect(bottomRight, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 2) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (3 * size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[2], 0, 1, rect); - width = textPaint.measureText(letters[2]); - canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size); + drawTile(canvas, letters[1], colors[1], fgColor, + size/2 + 1, 0, size, size/2 - 1); + drawTile(canvas, letters[2], colors[2], fgColor, + size/2 + 1, size/2 + 1, size, size); break; case 4: - bitmap.eraseColor(bgColor); - - tilePaint.setColor(colors[0]); - topLeft = new Rect(0, 0, (size/2)-1, (size/2)-1); - canvas.drawRect(topLeft, tilePaint); - - tilePaint.setColor(colors[1]); - bottomLeft = new Rect(0, (size/2)+1, (size/2)-1, size); - canvas.drawRect(bottomLeft, tilePaint); - - tilePaint.setColor(colors[2]); - topRight = new Rect((size/2)+1, 0, size, (size/2 - 1)); - canvas.drawRect(topRight, tilePaint); - - tilePaint.setColor(colors[3]); - bottomRight = new Rect((size/2)+1, (size/2 + 1), size, size); - canvas.drawRect(bottomRight, tilePaint); - - textPaint.setTextSize((float) (size * 0.8*0.5)); - textPaint.setAntiAlias(true); - rect = new Rect(); - - textPaint.getTextBounds(letters[0], 0, 1, rect); - width = textPaint.measureText(letters[0]); - canvas.drawText(letters[0], (size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[1], 0, 1, rect); - width = textPaint.measureText(letters[1]); - canvas.drawText(letters[1], (size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[2], 0, 1, rect); - width = textPaint.measureText(letters[2]); - canvas.drawText(letters[2], (3 * size / 4) - (width / 2), (size / 4) - + (rect.height() / 2), textPaint); - - textPaint.getTextBounds(letters[3], 0, 1, rect); - width = textPaint.measureText(letters[3]); - canvas.drawText(letters[3], (3 * size / 4) - (width / 2), (3* size / 4) - + (rect.height() / 2), textPaint); + drawTile(canvas, letters[0], colors[0], fgColor, + 0, 0, size/2 - 1, size/2 - 1); + drawTile(canvas, letters[1], colors[1], fgColor, + 0, size/2 + 1, size/2 - 1, size); + drawTile(canvas, letters[2], colors[2], fgColor, + size/2 + 1, 0, size, size/2 - 1); + drawTile(canvas, letters[3], colors[3], fgColor, + size/2 + 1, size/2 + 1, size, size); break; } - return bitmap; - } - private static Bitmap getUnknownContactPicture(String[] names, int size) { - return getUnknownContactPicture(names, size, UIHelper.BG_COLOR, UIHelper.FG_COLOR); + return bitmap; } - + private static Bitmap getMucContactPicture(Conversation conversation, int size, int bgColor, int fgColor) { List<User> members = conversation.getMucOptions().getUsers(); if (members.size() == 0) { @@ -255,13 +215,8 @@ public class UIHelper { public static Bitmap getContactPicture(Conversation conversation, int dpSize, Context context, boolean notification) { if(conversation.getMode() == Conversation.MODE_SINGLE) { - if (conversation.getContact() != null){ return getContactPicture(conversation.getContact(), dpSize, context, notification); - } else { - return getContactPicture(conversation.getName(false), dpSize, - context, notification); - } } else{ int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? @@ -273,10 +228,6 @@ public class UIHelper { } public static Bitmap getContactPicture(Contact contact, int dpSize, Context context, boolean notification) { - int fgColor = UIHelper.FG_COLOR, - bgColor = (notification) ? - UIHelper.BG_COLOR : UIHelper.TRANSPARENT; - String uri = contact.getProfilePhoto(); if (uri==null) { return getContactPicture(contact.getDisplayName(), dpSize, @@ -340,6 +291,15 @@ public class UIHelper { mNotificationManager.notify(1111, notification); } + private static Pattern generateNickHighlightPattern(String nick) { + // We expect a word boundary, i.e. space or start of string, followed by the + // nick (matched in case-insensitive manner), followed by optional + // punctuation (for example "bob: i disagree" or "how are you alice?"), + // followed by another word boundary. + return Pattern.compile("\\b"+nick+"\\p{Punct}?\\b", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + public static void updateNotification(Context context, List<Conversation> conversations, Conversation currentCon, boolean notify) { NotificationManager mNotificationManager = (NotificationManager) context @@ -360,7 +320,9 @@ public class UIHelper { if ((currentCon != null) &&(currentCon.getMode() == Conversation.MODE_MULTI)&&(!alwaysNotify)) { String nick = currentCon.getMucOptions().getNick(); - notify = currentCon.getLatestMessage().getBody().contains(nick); + Pattern highlight = generateNickHighlightPattern(nick); + Matcher m = highlight.matcher(currentCon.getLatestMessage().getBody()); + notify = m.find(); } List<Conversation> unread = new ArrayList<Conversation>(); @@ -412,7 +374,7 @@ public class UIHelper { .bigText(bigText.toString())); } else { NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(unread.size() + " unread Conversations"); + style.setBigContentTitle(unread.size() + " " + context.getString(R.string.unread_conversations)); StringBuilder names = new StringBuilder(); for (int i = 0; i < unread.size(); ++i) { targetUuid = unread.get(i).getUuid(); @@ -424,7 +386,7 @@ public class UIHelper { style.addLine(Html.fromHtml("<b>" + unread.get(i).getName(useSubject) + "</b> " + unread.get(i).getLatestMessage().getReadableBody(context))); } - mBuilder.setContentTitle(unread.size() + " unread Conversations"); + mBuilder.setContentTitle(unread.size() + " " + context.getString(R.string.unread_conversations)); mBuilder.setContentText(names.toString()); mBuilder.setStyle(style); } @@ -470,11 +432,13 @@ public class UIHelper { private static boolean wasHighlighted(Conversation conversation) { List<Message> messages = conversation.getMessages(); String nick = conversation.getMucOptions().getNick(); + Pattern highlight = generateNickHighlightPattern(nick); for(int i = messages.size() - 1; i >= 0; --i) { if (messages.get(i).isRead()) { break; } else { - if (messages.get(i).getBody().contains(nick)) { + Matcher m = highlight.matcher(messages.get(i).getBody()); + if (m.find()) { return true; } } @@ -518,7 +482,7 @@ public class UIHelper { public void onClick(DialogInterface dialog, int which) { contact.addOtrFingerprint(conversation.getOtrFingerprint()); msg.setVisibility(View.GONE); - activity.xmppConnectionService.updateContact(contact); + //activity.xmppConnectionService.updateContact(contact); } }); builder.setView(view); diff --git a/src/eu/siacs/conversations/utils/Validator.java b/src/eu/siacs/conversations/utils/Validator.java index fce953ae..51d8b25c 100644 --- a/src/eu/siacs/conversations/utils/Validator.java +++ b/src/eu/siacs/conversations/utils/Validator.java @@ -5,7 +5,7 @@ import java.util.regex.Pattern; public class Validator { public static final Pattern VALID_JID = - Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); + Pattern.compile("\\b^[A-Z0-9._%+-]+@([A-Z0-9.-]+\\.)?\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}\\b$|^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); public static boolean isValidJid(String jid) { Matcher matcher = VALID_JID.matcher(jid); diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java index ce1d10ce..f8e070f7 100644 --- a/src/eu/siacs/conversations/xml/Element.java +++ b/src/eu/siacs/conversations/xml/Element.java @@ -9,24 +9,24 @@ public class Element { protected Hashtable<String, String> attributes = new Hashtable<String, String>(); protected String content; protected List<Element> children = new ArrayList<Element>(); - + public Element(String name) { this.name = name; } - + public Element addChild(Element child) { this.content = null; children.add(child); return child; } - + public Element addChild(String name) { this.content = null; Element child = new Element(name); children.add(child); return child; } - + public Element addChild(String name, String xmlns) { this.content = null; Element child = new Element(name); @@ -34,64 +34,65 @@ public class Element { children.add(child); return child; } - + public Element setContent(String content) { this.content = content; this.children.clear(); return this; } - + public Element findChild(String name) { - for(Element child : this.children) { + for (Element child : this.children) { if (child.getName().equals(name)) { return child; } } return null; } - + public Element findChild(String name, String xmlns) { - for(Element child : this.children) { - if (child.getName().equals(name)&&(child.getAttribute("xmlns").equals(xmlns))) { + for (Element child : this.children) { + if (child.getName().equals(name) + && (child.getAttribute("xmlns").equals(xmlns))) { return child; } } return null; } - + public boolean hasChild(String name) { return findChild(name) != null; } - + public boolean hasChild(String name, String xmlns) { return findChild(name, xmlns) != null; } - - - + public List<Element> getChildren() { return this.children; } - + public Element setChildren(List<Element> children) { this.children = children; return this; } - + public String getContent() { return content; } - + public Element setAttribute(String name, String value) { - this.attributes.put(name, value); + if (name != null && value != null) { + this.attributes.put(name, value); + } return this; } - + public Element setAttributes(Hashtable<String, String> attributes) { this.attributes = attributes; return this; } - + public String getAttribute(String name) { if (this.attributes.containsKey(name)) { return this.attributes.get(name); @@ -99,14 +100,14 @@ public class Element { return null; } } - + public Hashtable<String, String> getAttributes() { return this.attributes; } - + public String toString() { StringBuilder elementOutput = new StringBuilder(); - if ((content==null)&&(children.size() == 0)) { + if ((content == null) && (children.size() == 0)) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); elementOutput.append(emptyTag.toString()); @@ -114,10 +115,10 @@ public class Element { Tag startTag = Tag.start(name); startTag.setAtttributes(this.attributes); elementOutput.append(startTag); - if (content!=null) { + if (content != null) { elementOutput.append(encodeEntities(content)); } else { - for(Element child : children) { + for (Element child : children) { elementOutput.append(child.toString()); } } @@ -130,13 +131,13 @@ public class Element { public String getName() { return name; } - + private String encodeEntities(String content) { - content = content.replace("&","&"); - content = content.replace("<","<"); - content = content.replace(">",">"); - content = content.replace("\"","""); - content = content.replace("'","'"); + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); return content; } diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java index 0a82a5d8..b4b1647e 100644 --- a/src/eu/siacs/conversations/xml/XmlReader.java +++ b/src/eu/siacs/conversations/xml/XmlReader.java @@ -85,10 +85,8 @@ public class XmlReader { public Element readElement(Tag currentTag) throws XmlPullParserException, IOException { Element element = new Element(currentTag.getName()); - //Log.d(LOGTAG,"trying to read element "+element.getName()); element.setAttributes(currentTag.getAttributes()); Tag nextTag = this.readTag(); - //Log.d(LOGTAG,"next Tag is: "+nextTag.toString()); if(nextTag.isNo()) { element.setContent(nextTag.getName()); nextTag = this.readTag(); @@ -96,15 +94,16 @@ public class XmlReader { if (nextTag == null) { throw new IOException("unterupted mid tag"); } - //Log.d(LOGTAG,"reading till the end of "+element.getName()); while(!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { Element child = this.readElement(nextTag); element.addChild(child); } nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } } - //Log.d(LOGTAG,"return with element"+element); return element; } } diff --git a/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..849e8e76 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(Contact contact, boolean online); +} diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index adb27ec8..2447b49b 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -17,11 +17,9 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.SSLContext; @@ -31,7 +29,6 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.json.JSONException; import org.xmlpull.v1.XmlPullParserException; import android.os.Bundle; @@ -40,6 +37,7 @@ import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.util.Log; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.DNSHelper; import eu.siacs.conversations.utils.zlib.ZLibOutputStream; @@ -66,7 +64,7 @@ public class XmppConnection implements Runnable { private WakeLock wakeLock; - private SecureRandom random = new SecureRandom(); + private SecureRandom mRandom; private Socket socket; private XmlReader tagReader; @@ -76,20 +74,20 @@ public class XmppConnection implements Runnable { private boolean shouldAuthenticate = true; private Element streamFeatures; private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); - - private HashSet<String> pendingSubscriptions = new HashSet<String>(); - + private String streamId = null; private int smVersion = 3; - + private int stanzasReceived = 0; private int stanzasSent = 0; - + public long lastPaketReceived = 0; public long lastPingSent = 0; public long lastConnect = 0; public long lastSessionStarted = 0; + private int attempt = 0; + private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; @@ -103,17 +101,25 @@ public class XmppConnection implements Runnable { private OnTLSExceptionReceived tlsListener = null; private OnBindListener bindListener = null; - public XmppConnection(Account account, PowerManager pm) { + public XmppConnection(Account account, XmppConnectionService service) { + this.mRandom = service.getRNG(); this.account = account; - this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,account.getJid()); + this.wakeLock = service.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + account.getJid()); tagWriter = new TagWriter(); } protected void changeStatus(int nextStatus) { if (account.getStatus() != nextStatus) { - if ((nextStatus == Account.STATUS_OFFLINE)&&(account.getStatus() != Account.STATUS_CONNECTING)&&(account.getStatus() != Account.STATUS_ONLINE)) { + if ((nextStatus == Account.STATUS_OFFLINE) + && (account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_ONLINE) + && (account.getStatus() != Account.STATUS_DISABLED)) { return; } + if (nextStatus == Account.STATUS_ONLINE) { + this.attempt = 0; + } account.setStatus(nextStatus); if (statusListener != null) { statusListener.onStatusChanged(account); @@ -122,17 +128,19 @@ public class XmppConnection implements Runnable { } protected void connect() { - Log.d(LOGTAG,account.getJid()+ ": connecting"); + Log.d(LOGTAG, account.getJid() + ": connecting"); lastConnect = SystemClock.elapsedRealtime(); + this.attempt++; try { - shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER); + shouldAuthenticate = shouldBind = !account + .isOptionSet(Account.OPTION_REGISTER); tagReader = new XmlReader(wakeLock); tagWriter = new TagWriter(); packetCallbacks.clear(); this.changeStatus(Account.STATUS_CONNECTING); Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); if ("timeout".equals(namePort.getString("error"))) { - Log.d(LOGTAG,account.getJid()+": dns timeout"); + Log.d(LOGTAG, account.getJid() + ": dns timeout"); this.changeStatus(Account.STATUS_OFFLINE); return; } @@ -142,12 +150,12 @@ public class XmppConnection implements Runnable { if (srvRecordServer != null) { if (srvIpServer != null) { Log.d(LOGTAG, account.getJid() + ": using values from dns " - + srvRecordServer + "[" + srvIpServer + "]:" - + srvRecordPort); + + srvRecordServer + "[" + srvIpServer + "]:" + + srvRecordPort); socket = new Socket(srvIpServer, srvRecordPort); } else { Log.d(LOGTAG, account.getJid() + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); + + srvRecordServer + ":" + srvRecordPort); socket = new Socket(srvRecordServer, srvRecordPort); } } else { @@ -227,56 +235,60 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("compressed")) { switchOverToZLib(nextTag); } else if (nextTag.isStart("success")) { - Log.d(LOGTAG, account.getJid() - + ": logged in"); + Log.d(LOGTAG, account.getJid() + ": logged in"); tagReader.readTag(); tagReader.reset(); sendStartStream(); processStream(tagReader.readTag()); break; } else if (nextTag.isStart("failure")) { - Element failure = tagReader.readElement(nextTag); + tagReader.readElement(nextTag); changeStatus(Account.STATUS_UNAUTHORIZED); } else if (nextTag.isStart("challenge")) { String challange = tagReader.readElement(nextTag).getContent(); Element response = new Element("response"); - response.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); - response.setContent(CryptoHelper.saslDigestMd5(account, challange)); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + response.setContent(CryptoHelper.saslDigestMd5(account, + challange,mRandom)); tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { this.stanzasSent = 0; Element enabled = tagReader.readElement(nextTag); if ("true".equals(enabled.getAttribute("resume"))) { this.streamId = enabled.getAttribute("id"); - Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled (resumable)"); + Log.d(LOGTAG, account.getJid() + ": stream managment(" + + smVersion + ") enabled (resumable)"); } else { - Log.d(LOGTAG,account.getJid()+": stream managment("+smVersion+") enabled"); + Log.d(LOGTAG, account.getJid() + ": stream managment(" + + smVersion + ") enabled"); } this.lastSessionStarted = SystemClock.elapsedRealtime(); this.stanzasReceived = 0; RequestPacket r = new RequestPacket(smVersion); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { + lastPaketReceived = SystemClock.elapsedRealtime(); + Log.d(LOGTAG, account.getJid() + ": session resumed"); tagReader.readElement(nextTag); sendPing(); changeStatus(Account.STATUS_ONLINE); - Log.d(LOGTAG,account.getJid()+": session resumed"); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); - AckPacket ack = new AckPacket(this.stanzasReceived,smVersion); - //Log.d(LOGTAG,ack.toString()); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + // Log.d(LOGTAG,ack.toString()); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { Element ack = tagReader.readElement(nextTag); lastPaketReceived = SystemClock.elapsedRealtime(); int serverSequence = Integer.parseInt(ack.getAttribute("h")); - if (serverSequence>this.stanzasSent) { + if (serverSequence > this.stanzasSent) { this.stanzasSent = serverSequence; } - //Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")"); + // Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")"); } else if (nextTag.isStart("failed")) { tagReader.readElement(nextTag); - Log.d(LOGTAG,account.getJid()+": resumption failed"); + Log.d(LOGTAG, account.getJid() + ": resumption failed"); streamId = null; if (account.getStatus() != Account.STATUS_ONLINE) { sendBindRequest(); @@ -319,16 +331,23 @@ public class XmppConnection implements Runnable { } element.setAttributes(currentTag.getAttributes()); Tag nextTag = tagReader.readTag(); + if (nextTag==null) { + throw new IOException("interrupted mid tag"); + } while (!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { Element child = tagReader.readElement(nextTag); - if ((packetType == PACKET_IQ)&&("jingle".equals(child.getName()))) { + if ((packetType == PACKET_IQ) + && ("jingle".equals(child.getName()))) { element = new JinglePacket(); element.setAttributes(currentTag.getAttributes()); } element.addChild(child); } nextTag = tagReader.readTag(); + if (nextTag==null) { + throw new IOException("interrupted mid tag"); + } } ++stanzasReceived; lastPaketReceived = SystemClock.elapsedRealtime(); @@ -338,14 +357,15 @@ public class XmppConnection implements Runnable { private void processIq(Tag currentTag) throws XmlPullParserException, IOException { IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); - + if (packet.getId() == null) { - return; //an iq packet without id is definitely invalid + return; // an iq packet without id is definitely invalid } - + if (packet instanceof JinglePacket) { - if (this.jingleListener !=null) { - this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet); + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); } } else { if (packetCallbacks.containsKey(packet.getId())) { @@ -353,7 +373,7 @@ public class XmppConnection implements Runnable { ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) .onIqPacketReceived(account, packet); } - + packetCallbacks.remove(packet.getId()); } else if (this.unregisteredIqListener != null) { this.unregisteredIqListener.onIqPacketReceived(account, packet); @@ -400,15 +420,18 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(compress); } - private void switchOverToZLib(Tag currentTag) throws XmlPullParserException, - IOException, NoSuchAlgorithmException { + private void switchOverToZLib(Tag currentTag) + throws XmlPullParserException, IOException, + NoSuchAlgorithmException { tagReader.readTag(); // read tag close - tagWriter.setOutputStream(new ZLibOutputStream(tagWriter.getOutputStream())); - tagReader.setInputStream(new ZLibInputStream(tagReader.getInputStream())); + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); sendStartStream(); - Log.d(LOGTAG,account.getJid()+": compression enabled"); + Log.d(LOGTAG, account.getJid() + ": compression enabled"); processStream(tagReader.readTag()); } @@ -420,17 +443,14 @@ public class XmppConnection implements Runnable { private void switchOverToTls(Tag currentTag) throws XmlPullParserException, IOException { - Tag nextTag = tagReader.readTag(); // should be proceed end tag + tagReader.readTag(); try { SSLContext sc = SSLContext.getInstance("TLS"); TrustManagerFactory tmf = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - // Initialise the TMF as you normally would, for example: - // tmf.in try { tmf.init((KeyStore) null); } catch (KeyStoreException e1) { - // TODO Auto-generated catch block e1.printStackTrace(); } @@ -454,13 +474,15 @@ public class XmppConnection implements Runnable { if (e.getCause() instanceof CertPathValidatorException) { String sha; try { - MessageDigest sha1 = MessageDigest.getInstance("SHA1"); + MessageDigest sha1 = MessageDigest + .getInstance("SHA1"); sha1.update(chain[0].getEncoded()); sha = CryptoHelper.bytesToHex(sha1.digest()); if (!sha.equals(account.getSSLFingerprint())) { changeStatus(Account.STATUS_TLS_ERROR); - if (tlsListener!=null) { - tlsListener.onTLSExceptionReceived(sha,account); + if (tlsListener != null) { + tlsListener.onTLSExceptionReceived(sha, + account); } throw new CertificateException(); } @@ -483,12 +505,12 @@ public class XmppConnection implements Runnable { sc.init(null, wrappedTrustManagers, null); SSLSocketFactory factory = sc.getSocketFactory(); SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), - true); + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); sendStartStream(); - Log.d(LOGTAG,account.getJid()+": TLS connection established"); + Log.d(LOGTAG, account.getJid() + ": TLS connection established"); processStream(tagReader.readTag()); sslSocket.close(); } catch (NoSuchAlgorithmException e1) { @@ -509,7 +531,7 @@ public class XmppConnection implements Runnable { auth.setContent(saslString); tagWriter.writeElement(auth); } - + private void sendSaslAuthDigestMd5() throws IOException { Element auth = new Element("auth"); auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); @@ -525,41 +547,47 @@ public class XmppConnection implements Runnable { sendStartTLS(); } else if (compressionAvailable()) { sendCompressionZlib(); - } else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { - sendRegistryRequest(); - } else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) { + } else if (this.streamFeatures.hasChild("register") + && (account.isOptionSet(Account.OPTION_REGISTER))) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && (account.isOptionSet(Account.OPTION_REGISTER))) { changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); disconnect(true); } else if (this.streamFeatures.hasChild("mechanisms") && shouldAuthenticate) { - List<String> mechanisms = extractMechanisms( streamFeatures.findChild("mechanisms")); + List<String> mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); if (mechanisms.contains("PLAIN")) { sendSaslAuthPlain(); } else if (mechanisms.contains("DIGEST-MD5")) { sendSaslAuthDigestMd5(); } - } else if (this.streamFeatures.hasChild("sm","urn:xmpp:sm:"+smVersion) && streamId != null) { - ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived,smVersion); + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); this.tagWriter.writeStanzaAsync(resume); } else if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); - if (this.streamFeatures.hasChild("session")) { - Log.d(LOGTAG,account.getJid()+": sending deprecated session"); - IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); - startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("") - this.sendIqPacket(startSession, null); - } } } private boolean compressionAvailable() { - if (!this.streamFeatures.hasChild("compression", "http://jabber.org/features/compress")) return false; - if (!ZLibOutputStream.SUPPORTED) return false; - if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) return false; + if (!this.streamFeatures.hasChild("compression", + "http://jabber.org/features/compress")) + return false; + if (!ZLibOutputStream.SUPPORTED) + return false; + if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) + return false; - Element compression = this.streamFeatures.findChild("compression", "http://jabber.org/features/compress"); + Element compression = this.streamFeatures.findChild("compression", + "http://jabber.org/features/compress"); for (Element child : compression.getChildren()) { - if (!"method".equals(child.getName())) continue; + if (!"method".equals(child.getName())) + continue; if ("zlib".equalsIgnoreCase(child.getContent())) { return true; @@ -569,8 +597,9 @@ public class XmppConnection implements Runnable { } private List<String> extractMechanisms(Element stream) { - ArrayList<String> mechanisms = new ArrayList<String>(stream.getChildren().size()); - for(Element child : stream.getChildren()) { + ArrayList<String> mechanisms = new ArrayList<String>(stream + .getChildren().size()); + for (Element child : stream.getChildren()) { mechanisms.add(child.getContent()); } return mechanisms; @@ -581,28 +610,35 @@ public class XmppConnection implements Runnable { register.query("jabber:iq:register"); register.setTo(account.getServer()); sendIqPacket(register, new OnIqPacketReceived() { - + @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element instructions = packet.query().findChild("instructions"); - if (packet.query().hasChild("username")&&(packet.query().hasChild("password"))) { + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { IqPacket register = new IqPacket(IqPacket.TYPE_SET); - Element username = new Element("username").setContent(account.getUsername()); - Element password = new Element("password").setContent(account.getPassword()); + Element username = new Element("username") + .setContent(account.getUsername()); + Element password = new Element("password") + .setContent(account.getPassword()); register.query("jabber:iq:register").addChild(username); register.query().addChild(password); sendIqPacket(register, new OnIqPacketReceived() { - + @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType()==IqPacket.TYPE_RESULT) { - account.setOption(Account.OPTION_REGISTER, false); + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); - } else if (packet.hasChild("error")&&(packet.findChild("error").hasChild("conflict"))){ + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { changeStatus(Account.STATUS_REGISTRATION_CONFLICT); } else { changeStatus(Account.STATUS_REGISTRATION_FAILED); - Log.d(LOGTAG,packet.toString()); + Log.d(LOGTAG, packet.toString()); } disconnect(true); } @@ -610,54 +646,49 @@ public class XmppConnection implements Runnable { } else { changeStatus(Account.STATUS_REGISTRATION_FAILED); disconnect(true); - Log.d(LOGTAG,account.getJid()+": could not register. instructions are"+instructions.getContent()); + Log.d(LOGTAG, account.getJid() + + ": could not register. instructions are" + + instructions.getContent()); } } }); } - private void sendInitialPresence() { - PresencePacket packet = new PresencePacket(); - packet.setAttribute("from", account.getFullJid()); - if (account.getKeys().has("pgp_signature")) { - try { - String signature = account.getKeys().getString("pgp_signature"); - packet.addChild("status").setContent("online"); - packet.addChild("x","jabber:x:signed").setContent(signature); - } catch (JSONException e) { - // - } - } - this.sendPresencePacket(packet); - } - private void sendBindRequest() throws IOException { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource()); - this.sendIqPacket(iq, new OnIqPacketReceived() { + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { String resource = packet.findChild("bind").findChild("jid") .getContent().split("/")[1]; account.setResource(resource); - if (streamFeatures.hasChild("sm","urn:xmpp:sm:3")) { + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { smVersion = 3; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); - } else if (streamFeatures.hasChild("sm","urn:xmpp:sm:2")) { + } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { smVersion = 2; EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); } - sendInitialPresence(); sendServiceDiscoveryInfo(account.getServer()); sendServiceDiscoveryItems(account.getServer()); - if (bindListener !=null) { + if (bindListener != null) { bindListener.onBind(account); } + changeStatus(Account.STATUS_ONLINE); } }); + if (this.streamFeatures.hasChild("session")) { + Log.d(LOGTAG, account.getJid() + ": sending deprecated session"); + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + startSession.addChild("session", + "urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnboundIqPacket(startSession, null); + } } private void sendServiceDiscoveryInfo(final String server) { @@ -672,25 +703,24 @@ public class XmppConnection implements Runnable { List<String> features = new ArrayList<String>(); for (int i = 0; i < elements.size(); ++i) { if (elements.get(i).getName().equals("feature")) { - features.add(elements.get(i).getAttribute( - "var")); + features.add(elements.get(i).getAttribute("var")); } } disco.put(server, features); - + if (account.getServer().equals(server)) { enableAdvancedStreamFeatures(); } } }); } - + private void enableAdvancedStreamFeatures() { if (hasFeaturesCarbon()) { sendEnableCarbons(); } } - + private void sendServiceDiscoveryItems(final String server) { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setTo(server); @@ -702,8 +732,7 @@ public class XmppConnection implements Runnable { List<Element> elements = packet.query().getChildren(); for (int i = 0; i < elements.size(); ++i) { if (elements.get(i).getName().equals("item")) { - String jid = elements.get(i).getAttribute( - "jid"); + String jid = elements.get(i).getAttribute("jid"); sendServiceDiscoveryInfo(jid); } } @@ -713,7 +742,7 @@ public class XmppConnection implements Runnable { private void sendEnableCarbons() { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - iq.addChild("enable","urn:xmpp:carbons:2"); + iq.addChild("enable", "urn:xmpp:carbons:2"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override @@ -745,11 +774,11 @@ public class XmppConnection implements Runnable { } private String nextRandomId() { - return new BigInteger(50, random).toString(32); + return new BigInteger(50, mRandom).toString(32); } public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { - if (packet.getId()==null) { + if (packet.getId() == null) { String id = nextRandomId(); packet.setAttribute("id", id); } @@ -757,6 +786,14 @@ public class XmppConnection implements Runnable { this.sendPacket(packet, callback); } + public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + this.sendPacket(packet, callback); + } + public void sendMessagePacket(MessagePacket packet) { this.sendPacket(packet, null); } @@ -774,26 +811,27 @@ public class XmppConnection implements Runnable { OnPresencePacketReceived callback) { this.sendPacket(packet, callback); } - - private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) { + + private synchronized void sendPacket(final AbstractStanza packet, + PacketReceived callback) { // TODO dont increment stanza count if packet = request packet or ack; ++stanzasSent; tagWriter.writeStanzaAsync(packet); if (callback != null) { - if (packet.getId()==null) { + if (packet.getId() == null) { packet.setId(nextRandomId()); } packetCallbacks.put(packet.getId(), callback); } } - + public void sendPing() { if (streamFeatures.hasChild("sm")) { tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); } else { IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setFrom(account.getFullJid()); - iq.addChild("ping","urn:xmpp:ping"); + iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, null); } } @@ -812,82 +850,95 @@ public class XmppConnection implements Runnable { OnPresencePacketReceived listener) { this.presenceListener = listener; } - - public void setOnJinglePacketReceivedListener(OnJinglePacketReceived listener) { + + public void setOnJinglePacketReceivedListener( + OnJinglePacketReceived listener) { this.jingleListener = listener; } public void setOnStatusChangedListener(OnStatusChanged listener) { this.statusListener = listener; } - - public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) { + + public void setOnTLSExceptionReceivedListener( + OnTLSExceptionReceived listener) { this.tlsListener = listener; } - + public void setOnBindListener(OnBindListener listener) { this.bindListener = listener; } public void disconnect(boolean force) { changeStatus(Account.STATUS_OFFLINE); - Log.d(LOGTAG,"disconnecting"); + Log.d(LOGTAG, "disconnecting"); try { - if (force) { + if (force) { socket.close(); return; - } - if (tagWriter.isActive()) { - tagWriter.finish(); - while(!tagWriter.finished()) { - //Log.d(LOGTAG,"not yet finished"); - Thread.sleep(100); } - tagWriter.writeTag(Tag.end("stream:stream")); - } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + } catch (IOException e) { + Log.d(LOGTAG, "io exception during disconnect"); + } catch (InterruptedException e) { + Log.d(LOGTAG, "interrupted"); + } + } + } + }).start(); } catch (IOException e) { - Log.d(LOGTAG,"io exception during disconnect"); - } catch (InterruptedException e) { - Log.d(LOGTAG,"interupted while waiting for disconnect"); + Log.d(LOGTAG, "io exception during disconnect"); } } - + public boolean hasFeatureRosterManagment() { - if (this.streamFeatures==null) { + if (this.streamFeatures == null) { return false; } else { return this.streamFeatures.hasChild("ver"); } } - + public boolean hasFeatureStreamManagment() { - if (this.streamFeatures==null) { + if (this.streamFeatures == null) { return false; } else { return this.streamFeatures.hasChild("sm"); } } - + public boolean hasFeaturesCarbon() { return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); } - + public boolean hasDiscoFeature(String server, String feature) { if (!disco.containsKey(server)) { return false; } return disco.get(server).contains(feature); } - + public String findDiscoItemByFeature(String feature) { - Iterator<Entry<String, List<String>>> it = this.disco.entrySet().iterator(); - while (it.hasNext()) { - Entry<String, List<String>> pairs = it.next(); - if (pairs.getValue().contains(feature)) { - return pairs.getKey(); - } - it.remove(); - } + Iterator<Entry<String, List<String>>> it = this.disco.entrySet() + .iterator(); + while (it.hasNext()) { + Entry<String, List<String>> pairs = it.next(); + if (pairs.getValue().contains(feature)&&pairs.getValue().size()==1) { + return pairs.getKey(); + } + it.remove(); + } return null; } @@ -898,7 +949,7 @@ public class XmppConnection implements Runnable { public int getReceivedStanzas() { return this.stanzasReceived; } - + public int getSentStanzas() { return this.stanzasSent; } @@ -906,13 +957,14 @@ public class XmppConnection implements Runnable { public String getMucServer() { return findDiscoItemByFeature("http://jabber.org/protocol/muc"); } - - public boolean hasPendingSubscription(String jid) { - return this.pendingSubscriptions.contains(jid); + + public int getTimeToNextAttempt() { + int interval = (int) (25 * Math.pow(1.5, attempt)); + int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; } - - public void addPendingSubscription(String jid) { - Log.d(LOGTAG,"adding "+jid+" to pending subscriptions"); - this.pendingSubscriptions.add(jid); + + public int getAttempt() { + return this.attempt; } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 8f88688a..85110c38 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -9,11 +9,11 @@ import java.util.Map.Entry; import android.graphics.BitmapFactory; import android.util.Log; - import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; @@ -24,7 +24,7 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleConnection { private final String[] extensions = {"webp","jpeg","jpg","png"}; - private final String[] cryptoExtensions = {"pgp","gpg"}; + private final String[] cryptoExtensions = {"pgp","gpg","otr"}; private JingleConnectionManager mJingleConnectionManager; private XmppConnectionService mXmppConnectionService; @@ -244,6 +244,7 @@ public class JingleConnection { Element fileNameElement = fileOffer.findChild("name"); if (fileNameElement!=null) { boolean supportedFile = false; + Log.d("xmppService","file offer: "+fileNameElement.getContent()); String[] filename = fileNameElement.getContent().toLowerCase().split("\\."); if (Arrays.asList(this.extensions).contains(filename[filename.length - 1])) { supportedFile = true; @@ -251,7 +252,12 @@ public class JingleConnection { if (filename.length == 3) { if (Arrays.asList(this.extensions).contains(filename[filename.length -2])) { supportedFile = true; - this.message.setEncryption(Message.ENCRYPTION_PGP); + if (filename[filename.length - 1].equals("otr")) { + Log.d("xmppService","receiving otr file"); + this.message.setEncryption(Message.ENCRYPTION_OTR); + } else { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } } } } @@ -269,6 +275,9 @@ public class JingleConnection { this.mXmppConnectionService.updateUi(conversation, true); } this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + this.file.setKey(conversation.getSymmetricKey()); + } this.file.setExpectedSize(size); } else { this.sendCancel(); @@ -287,7 +296,14 @@ public class JingleConnection { if (message.getType() == Message.TYPE_IMAGE) { content.setTransportId(this.transportId); this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); - content.setFileOffer(this.file); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file,false); + } this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.socks5transport().setChildren(getCandidatesAsElements()); diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java index 21cbd716..3672351b 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java @@ -1,6 +1,12 @@ package eu.siacs.conversations.xmpp.jingle; import java.io.File; +import java.security.Key; + +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.utils.CryptoHelper; +import android.util.Log; public class JingleFile extends File { @@ -8,6 +14,7 @@ public class JingleFile extends File { private long expectedSize = 0; private String sha1sum; + private Key aeskey; public JingleFile(String path) { super(path); @@ -18,7 +25,11 @@ public class JingleFile extends File { } public long getExpectedSize() { - return this.expectedSize; + if (this.aeskey!=null) { + return (this.expectedSize/16 + 1) * 16; + } else { + return this.expectedSize; + } } public void setExpectedSize(long size) { @@ -32,4 +43,23 @@ public class JingleFile extends File { public void setSha1Sum(String sum) { this.sha1sum = sum; } + + public void setKey(byte[] key) { + if (key.length>=32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length>=16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else { + Log.d("xmppService","weird key"); + } + Log.d("xmppService","using aes key "+CryptoHelper.bytesToHex(this.aeskey.getEncoded())); + } + + public Key getKey() { + return this.aeskey; + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 8326fa39..a46e4a47 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -8,12 +8,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import android.util.Base64; -import android.util.Log; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.PacketReceived; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleInbandTransport extends JingleTransport { @@ -39,7 +37,6 @@ public class JingleInbandTransport extends JingleTransport { private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d("xmppService", "on ack received"); if (packet.getType() == IqPacket.TYPE_RESULT) { sendNextBlock(); } @@ -82,7 +79,6 @@ public class JingleInbandTransport extends JingleTransport { public void receive(JingleFile file, OnFileTransmitted callback) { this.onFileTransmitted = callback; this.file = file; - Log.d("xmppService", "receiving file over ibb"); try { this.digest = MessageDigest.getInstance("SHA-1"); digest.reset(); @@ -101,7 +97,6 @@ public class JingleInbandTransport extends JingleTransport { public void send(JingleFile file, OnFileTransmitted callback) { this.onFileTransmitted = callback; this.file = file; - Log.d("xmppService", "sending file over ibb"); try { this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); @@ -151,10 +146,8 @@ public class JingleInbandTransport extends JingleTransport { this.fileOutputStream.write(buffer); this.digest.update(buffer); - Log.d("xmppService", "remaining file size:" + this.remainingSize); if (this.remainingSize <= 0) { file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); - Log.d("xmppService","file name: "+file.getAbsolutePath()); fileOutputStream.flush(); fileOutputStream.close(); this.onFileTransmitted.onFileTransmitted(file); @@ -179,7 +172,7 @@ public class JingleInbandTransport extends JingleTransport { this.account.getXmppConnection().sendIqPacket( packet.generateRespone(IqPacket.TYPE_RESULT), null); } else { - Log.d("xmppServic","couldnt deliver payload "+packet.toString()); + //TODO some sort of exception } } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index ea5d7285..4fef0388 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -6,17 +6,15 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.ObjectInputStream.GetField; import java.net.Socket; import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.Element; - import android.util.Log; -import android.widget.Button; +import eu.siacs.conversations.utils.CryptoHelper; public class JingleSocks5Transport extends JingleTransport { private JingleCandidate candidate; @@ -94,17 +92,20 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { - FileInputStream fileInputStream = null; + InputStream fileInputStream = null; try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); - fileInputStream = new FileInputStream(file); + fileInputStream = getInputStream(file); int count; + long txBytes = 0; byte[] buffer = new byte[8192]; while ((count = fileInputStream.read(buffer)) > 0) { + txBytes += count; outputStream.write(buffer, 0, count); digest.update(buffer, 0, count); } + Log.d("xmppService","txBytes="+txBytes); outputStream.flush(); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); if (callback!=null) { @@ -145,28 +146,26 @@ public class JingleSocks5Transport extends JingleTransport { inputStream.skip(45); file.getParentFile().mkdirs(); file.createNewFile(); - FileOutputStream fileOutputStream = new FileOutputStream(file); + OutputStream fileOutputStream = getOutputStream(file); long remainingSize = file.getExpectedSize(); byte[] buffer = new byte[8192]; int count = buffer.length; + long rxBytes = 0; while(remainingSize > 0) { - Log.d("xmppService","remaning size:"+remainingSize); - if (remainingSize<=count) { - count = (int) remainingSize; - } - count = inputStream.read(buffer, 0, count); + count = inputStream.read(buffer); if (count==-1) { - Log.d("xmppService","end of stream"); + Log.d("xmppService","read end"); } else { + rxBytes += count; fileOutputStream.write(buffer, 0, count); digest.update(buffer, 0, count); remainingSize-=count; } } + Log.d("xmppService","rx bytes="+rxBytes); fileOutputStream.flush(); fileOutputStream.close(); file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); - Log.d("xmppService","transmitted filename was: "+file.getAbsolutePath()); callback.onFileTransmitted(file); } catch (FileNotFoundException e) { // TODO Auto-generated catch block @@ -194,9 +193,8 @@ public class JingleSocks5Transport extends JingleTransport { if (this.socket!=null) { try { this.socket.close(); - Log.d("xmppService","cloesd socket with "+candidate.getHost()+":"+candidate.getPort()); } catch (IOException e) { - Log.d("xmppService","error closing socket with "+candidate.getHost()+":"+candidate.getPort()); + } } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index 6e9482a9..5db4b715 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -1,9 +1,77 @@ package eu.siacs.conversations.xmpp.jingle; -import eu.siacs.conversations.xml.Element; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; + +import android.util.Log; public abstract class JingleTransport { public abstract void connect(final OnTransportConnected callback); public abstract void receive(final JingleFile file, final OnFileTransmitted callback); public abstract void send(final JingleFile file, final OnFileTransmitted callback); + private byte[] iv = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0xf}; + + protected InputStream getInputStream(JingleFile file) throws FileNotFoundException { + if (file.getKey() == null) { + return new FileInputStream(file); + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, file.getKey(),ips); + Log.d("xmppService","opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(file), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d("xmppService","no such algo: "+e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d("xmppService","no such padding: "+e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d("xmppService","invalid key: "+e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d("xmppService","invavid iv:"+e.getMessage()); + return null; + } + } + } + + protected OutputStream getOutputStream(JingleFile file) throws FileNotFoundException { + if (file.getKey() == null) { + return new FileOutputStream(file); + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, file.getKey(),ips); + Log.d("xmppService","opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(file), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d("xmppService","no such algo: "+e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d("xmppService","no such padding: "+e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d("xmppService","invalid key: "+e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d("xmppService","invavid iv:"+e.getMessage()); + return null; + } + } + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index abede91a..494ff0d6 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -25,12 +25,16 @@ public class Content extends Element { this.transportId = sid; } - public void setFileOffer(JingleFile actualFile) { + public void setFileOffer(JingleFile actualFile, boolean otr) { Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element offer = description.addChild("offer"); Element file = offer.addChild("file"); file.addChild("size").setContent(""+actualFile.getSize()); - file.addChild("name").setContent(actualFile.getName()); + if (otr) { + file.addChild("name").setContent(actualFile.getName()+".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } } public Element getFileOffer() { diff --git a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 48e5c0ec..1d4e44d1 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -69,7 +69,6 @@ public class IqPacket extends AbstractStanza { public IqPacket generateRespone(int type) { IqPacket packet = new IqPacket(type); - packet.setFrom(this.getTo()); packet.setTo(this.getFrom()); packet.setId(this.getId()); return packet; diff --git a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 941bda4f..64a9edc3 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -37,6 +37,10 @@ public class MessagePacket extends AbstractStanza { case TYPE_GROUPCHAT: this.setAttribute("type", "groupchat"); break; + case TYPE_UNKNOWN: + break; + case TYPE_NORMAL: + break; default: this.setAttribute("type","chat"); break; |