Support for per-recipient muting, blocking, and ringtones.

Fixes #757
Fixes #354
Fixes #222
Closes #1815
Closes #3378

// FREEBIE
master
Moxie Marlinspike 2015-06-09 07:37:20 -07:00
parent cb3cf7789f
commit 40af2a81db
80 changed files with 1858 additions and 321 deletions

View File

@ -226,6 +226,14 @@
</intent-filter>
</activity>
<activity android:name=".RecipientPreferenceActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".BlockedContactsActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.preferences.BlockedContactListItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:paddingRight="25dip">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/contact_photo_image"
android:layout_width="@dimen/contact_selection_photo_size"
android:layout_height="@dimen/contact_selection_photo_size"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:cropToPadding="true"
android:scaleType="centerCrop"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" />
<TextView android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_marginLeft="14dip"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_toRightOf="@id/contact_photo_image"
android:gravity="center_vertical|left"
android:textAppearance="?android:attr/textAppearanceMedium" />
</org.thoughtcrime.securesms.preferences.BlockedContactListItem>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="16dip"
android:paddingRight="16dip">
<ListView android:id="@id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawSelectorOnTop="false"/>
<TextView android:id="@id/android:empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center|center_vertical"
android:gravity="center|center_vertical"
android:textSize="20sp"
android:text="@string/blocked_contacts_fragment__no_blocked_contacts"/>
</LinearLayout>

View File

@ -131,6 +131,13 @@
</LinearLayout>
<Button android:id="@+id/unblock_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="Unblock"
android:visibility="gone"/>
<TextView android:id="@+id/space_left"
android:paddingLeft="5dip"
android:layout_width="fill_parent"

View File

@ -17,7 +17,6 @@
android:layout_marginTop="3dp"
android:layout_marginBottom="3dp"
android:cropToPadding="true"
android:visibility="gone"
tools:src="@drawable/ic_contact_picture"
android:contentDescription="@string/conversation_list_item_view__contact_photo_image"
android:layout_marginRight="10dp"
@ -50,7 +49,8 @@
android:textColor="?attr/conversation_list_item_contact_color"
android:singleLine="true"
tools:text="Jules Bonnot"
android:ellipsize="marquee" />
android:ellipsize="end"
android:drawablePadding="5dp"/>
<ImageView android:id="@+id/error"
android:layout_marginLeft="3dip"

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationTitleView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:drawablePadding="5dp"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
style="@style/TextSecure.TitleTextStyle"/>
<TextView android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
style="@style/TextSecure.SubtitleTextStyle"/>
</org.thoughtcrime.securesms.ConversationTitleView>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_height="130dp"
android:layout_width="match_parent"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/TextSecure.LightActionBar">
<RelativeLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/avatar"
android:foreground="@drawable/contact_photo_background"
android:layout_width="50dp"
android:layout_height="50dp"
android:cropToPadding="true"
android:layout_marginLeft="0dp"
android:layout_alignParentLeft="true"/>
<TextView android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
style="@style/TextSecure.TitleTextStyle"
android:layout_toRightOf="@id/avatar"
android:layout_marginLeft="10dip"/>
<TextView android:id="@+id/blocked_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/avatar"
android:layout_below="@id/name"
android:layout_alignLeft="@id/name"
android:text="@string/recipient_preference_activity__blocked"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?recipient_preference_blocked"
android:textAllCaps="true" />
</RelativeLayout>
</android.support.v7.widget.Toolbar>
<FrameLayout android:id="@+id/preference_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/conversation_muted__unmute"
android:id="@+id/menu_unmute_notifications" />
</menu>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/conversation_unmuted__mute_notifications"
android:id="@+id/menu_mute_notifications" />
</menu>

View File

@ -171,4 +171,23 @@
<item>@string/arrays__use_custom</item>
</string-array>
<string-array name="mute_durations">
<item>@string/arrays__mute_for_one_hour</item>
<item>@string/arrays__mute_for_two_hours</item>
<item>@string/arrays__mute_for_one_day</item>
<item>@string/arrays__mute_for_seven_days</item>
</string-array>
<string-array name="recipient_vibrate_entries">
<item>@string/arrays__settings_default</item>
<item>@string/arrays__enabled</item>
<item>@string/arrays__disabled</item>
</string-array>
<string-array name="recipient_vibrate_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
</resources>

View File

@ -116,4 +116,6 @@
<attr name="group_members_dialog_icon" format="reference"/>
<attr name="lockscreen_watermark" format="reference" />
<attr name="recipient_preference_blocked" format="color"/>
</resources>

View File

@ -56,6 +56,9 @@
<string name="AttachmentTypeSelectorAdapter_audio">Audio</string>
<string name="AttachmentTypeSelectorAdapter_contact">Contact info</string>
<!-- BlockedContactsActivity -->
<string name="BlockedContactsActivity_blocked_contacts">Blocked contacts</string>
<!-- ConfirmIdentityDialog -->
<string name="ConfirmIdentityDialog_the_signature_on_this_key_exchange_is_different">The
identifying key material for %1$s has changed. This could either mean that someone is trying to
@ -118,6 +121,9 @@
<string name="ConversationActivity_mms_not_supported_title">MMS not supported</string>
<string name="ConversationActivity_mms_not_supported_message">This message cannot be sent since your carrier doesn\'t support MMS.</string>
<string name="ConversationActivity_specify_recipient">Please choose a contact</string>
<string name="ConversationActivity_unblock_question">Unblock?</string>
<string name="ConversationActivity_are_you_sure_you_want_to_unblock_this_contact">Are you sure you want to unblock this contact?</string>
<string name="ConversationActivity_unblock">Unblock</string>
<!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string>
@ -294,6 +300,14 @@
<string name="RatingManager_no_thanks">No thanks</string>
<string name="RatingManager_later">Later</string>
<!-- RecipientPreferencesActivity -->
<string name="RecipientPreferenceActivity_block_this_contact_question">Block this contact?</string>
<string name="RecipientPreferenceActivity_you_will_no_longer_see_messages_from_this_user">You will no longer see messages sent from this user.</string>
<string name="RecipientPreferenceActivity_block">Block</string>
<string name="RecipientPreferenceActivity_unblock_this_contact_question">Unblock this contact?</string>
<string name="RecipientPreferenceActivity_are_you_sure_you_want_to_unblock_this_contact">Are you sure you want to unblock this contact?</string>
<string name="RecipientPreferenceActivity_unblock">Unblock</string>
<!-- RegistrationActivity -->
<string name="RegistrationActivity_connect_with_textsecure">Connect With TextSecure</string>
<string name="RegistrationActivity_select_your_country">Select your country</string>
@ -465,9 +479,15 @@
<!-- ContactSelectionListFragment-->
<string name="ContactSelectionlistFragment_select_for">Select for</string>
<!-- blocked_contacts_fragment -->
<string name="blocked_contacts_fragment__no_blocked_contacts">No blocked contacts...</string>
<!-- contact_selection_recent_activity -->
<string name="contact_selection_recent_activity__no_recent_calls">No recent calls.</string>
<!-- conversation_title_view -->
<string name="conversation_title_view__conversation_muted">Conversation muted</string>
<!-- conversation_activity -->
<string name="conversation_activity__type_message_push">Send TextSecure message</string>
<string name="conversation_activity__type_message_sms_insecure">Send unsecured SMS</string>
@ -563,6 +583,16 @@
<string name="prompt_mms_activity__textsecure_requires_mms_settings_to_deliver_media_and_group_messages">TextSecure requires MMS settings to deliver media and group messages through your wireless carrier. Your device does not make this information available, which is occasionally true for locked devices and other restrictive configurations.</string>
<string name="prompt_mms_activity__to_send_media_and_group_messages_click_ok">To send media and group messages, click \'OK\' and complete the requested settings. The MMS settings for your carrier can generally be located by searching for \'your carrier APN\'. You will only need to do this once.</string>
<!-- recipient_preferences_activity -->
<string name="recipient_preference_activity__blocked">BLOCKED</string>
<!-- recipient_preferences -->
<string name="recipient_preferences__mute_conversation">Mute conversation</string>
<string name="recipient_preferences__disable_notifications_for_this_conversation">Disable notifications for this conversation</string>
<string name="recipient_preferences__ringtone">Ringtone</string>
<string name="recipient_preferences__vibrate">Vibrate</string>
<string name="recipient_preferences__block">Block</string>
<!-- registration_activity -->
<string name="registration_activity__textsecure_can_use_instant_messages_to_avoid_sms_charges_when_communicating_with_other_textsecure_users">
Verify your phone number to connect with TextSecure.
@ -684,6 +714,15 @@
<string name="arrays__use_default">Use default</string>
<string name="arrays__use_custom">Use custom</string>
<string name="arrays__mute_for_one_hour">Mute for 1 hour</string>
<string name="arrays__mute_for_two_hours">Mute for 2 hours</string>
<string name="arrays__mute_for_one_day">Mute for 1 day</string>
<string name="arrays__mute_for_seven_days">Mute for 7 days</string>
<string name="arrays__settings_default">Settings default</string>
<string name="arrays__enabled">Enabled</string>
<string name="arrays__disabled">Disabled</string>
<!-- plurals.xml -->
<plurals name="hours_ago">
<item quantity="one">%d hour</item>
@ -787,6 +826,8 @@
<string name="preferences__submit_debug_log">Submit debug log</string>
<string name="preferences__support_wifi_calling">\'WiFi Calling\' compatibility mode</string>
<string name="preferences__enable_if_your_device_supports_sms_mms_delivery_over_wifi">Enable if your device uses SMS/MMS delivery over WiFi (only enable when \'WiFi Calling\' is enabled on your device)</string>
<string name="preferences__privacy">Privacy</string>
<string name="preferences_app_protection__blocked_contacts">Blocked contacts</string>
<!-- **************************************** -->
<!-- menus -->
@ -844,6 +885,12 @@
<string name="conversation_secure_verified__menu_verify_identity">Verify identity</string>
<string name="conversation_secure_verified__menu_abort_secure_session">End secure session</string>
<!-- conversation_muted -->
<string name="conversation_muted__unmute">Unmute</string>
<!-- conversation_unmuted -->
<string name="conversation_unmuted__mute_notifications">Mute notifications</string>
<!-- conversation -->
<string name="conversation__menu_add_attachment">Add attachment</string>
<string name="conversation__menu_update_group">Update group</string>
@ -900,6 +947,8 @@
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
<string name="MuteDialog_mute_notifications">Mute notifications</string>
<!-- EOF -->

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AppCompat.NoActionBar" parent="@style/Theme.AppCompat">
<item name="android:windowNoTitle">true</item>
</style>
<style name="NoAnimation.Theme.BlackScreen" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowBackground">@android:color/black</item>
@ -32,7 +28,8 @@
<item name="logo">@drawable/actionbar_icon_holo_dark</item>
<item name="android:popupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="popupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="titleTextStyle">@style/TextSecure.TitleTextStyle</item>
<item name="subtitleTextStyle">@style/TextSecure.SubtitleTextStyle</item>
</style>
<style name="TextSecure.LightActionBar"
@ -42,6 +39,9 @@
<item name="logo">@drawable/actionbar_icon_holo_dark</item>
<item name="icon">@drawable/actionbar_icon_holo_dark</item>
<item name="titleTextStyle">@style/TextSecure.TitleTextStyle</item>
<item name="subtitleTextStyle">@style/TextSecure.SubtitleTextStyle</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">#99ffffff</item>
</style>
<style name="TextSecure.DarkActionBar.TabBar"
@ -58,10 +58,11 @@
<style name="TextSecure.TitleTextStyle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
<item name="android:textStyle">bold</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="TextSecure.SubtitleTextStyle" parent="TextAppearance.AppCompat.Widget.ActionBar.Subtitle">
<item name="android:textColor">#ff555555</item>
<item name="android:textColor">#99ffffff</item>
</style>
<style name="TextSecure.IntroActionBar" parent="Widget.AppCompat.Light.ActionBar.Solid.Inverse">

View File

@ -1,5 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="TextSecure.LightNoActionBar" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="actionBarStyle">@style/TextSecure.LightActionBar</item>
<item name="actionBarTabBarStyle">@style/TextSecure.LightActionBar.TabBar</item>
<item name="colorPrimary">@color/textsecure_primary</item>
<item name="colorPrimaryDark">@color/textsecure_primary_dark</item>
<item name="colorAccent">@color/textsecure_primary_dark</item>
<item name="recipient_preference_blocked">#8e0000</item>
</style>
<style name="TextSecure.DarkNoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="actionBarStyle">@style/TextSecure.DarkActionBar</item>
<item name="actionBarTabBarStyle">@style/TextSecure.DarkActionBar.TabBar</item>
<item name="actionBarPopupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
<item name="recipient_preference_blocked">#d00000</item>
</style>
<style name="TextSecure.LightIntroTheme" parent="@style/Theme.AppCompat.Light">
<!--<item name="colorPrimary">@android:color/transparent</item>-->
<item name="actionBarStyle">@style/TextSecure.IntroActionBar</item>

View File

@ -10,7 +10,7 @@
android:icon="?pref_ic_notifications"/>
<Preference android:key="preference_category_app_protection"
android:title="@string/preferences__app_protection"
android:title="@string/preferences__privacy"
android:icon="?pref_ic_app_protection"/>
<Preference android:key="preference_category_appearance"
@ -24,4 +24,5 @@
<Preference android:key="preference_category_advanced"
android:title="@string/preferences__advanced"
android:icon="?pref_ic_advanced"/>
</PreferenceScreen>

View File

@ -27,4 +27,7 @@
android:key="pref_screen_security"
android:title="@string/preferences__screen_security"
android:summary="@string/preferences__disable_screen_security_to_allow_screen_shots" />
<Preference android:key="preference_category_blocked"
android:title="@string/preferences_app_protection__blocked_contacts" />
</PreferenceScreen>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:key="pref_key_recipient_mute"
android:title="@string/recipient_preferences__mute_conversation"
android:summary="@string/recipient_preferences__disable_notifications_for_this_conversation"
android:defaultValue="false"
android:disableDependentsState="true"
android:persistent="false" />
<RingtonePreference android:dependency="pref_key_recipient_mute"
android:key="pref_key_recipient_ringtone"
android:title="@string/recipient_preferences__ringtone"
android:ringtoneType="notification"
android:showSilent="false"
android:showDefault="true"
android:persistent="false"/>
<ListPreference android:dependency="pref_key_recipient_mute"
android:key="pref_key_recipient_vibrate"
android:title="@string/recipient_preferences__vibrate"
android:entries="@array/recipient_vibrate_entries"
android:entryValues="@array/recipient_vibrate_values"
android:defaultValue="0"
android:persistent="false"/>
<Preference android:key="pref_key_recipient_block"
android:title="@string/recipient_preferences__block" />
</PreferenceScreen>

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.view.ViewConfiguration;
@ -10,7 +10,7 @@ import android.view.ViewConfiguration;
import java.lang.reflect.Field;
public abstract class BaseActionBarActivity extends ActionBarActivity {
public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
@Override
@ -46,7 +46,9 @@ public abstract class BaseActionBarActivity extends ActionBarActivity {
menuKeyField.setAccessible(true);
menuKeyField.setBoolean(config, false);
}
} catch (IllegalAccessException | NoSuchFieldException e) {
} catch (IllegalAccessException e) {
Log.w(TAG, "Failed to force overflow menu.");
} catch (NoSuchFieldException e) {
Log.w(TAG, "Failed to force overflow menu.");
}
}

View File

@ -0,0 +1,136 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.BlockedContactsActivity_blocked_contacts);
initFragment(android.R.id.content, new BlockedContactsFragment(), masterSecret);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
public static class BlockedContactsFragment
extends ListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, ListView.OnItemClickListener
{
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.blocked_contacts_fragment, container, false);
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setListAdapter(new BlockedContactAdapter(getActivity(), null));
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
getListView().setOnItemClickListener(this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new BlockedContactsLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (getListAdapter() != null) {
((CursorAdapter) getListAdapter()).changeCursor(data);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (getListAdapter() != null) {
((CursorAdapter) getListAdapter()).changeCursor(null);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Recipients recipients = ((BlockedContactListItem)view).getRecipients();
Intent intent = new Intent(getActivity(), RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENTS_EXTRA, recipients.getIds());
startActivity(intent);
}
private static class BlockedContactAdapter extends CursorAdapter {
public BlockedContactAdapter(Context context, Cursor c) {
super(context, c);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return LayoutInflater.from(context)
.inflate(R.layout.blocked_contact_list_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
String recipientIds = cursor.getString(1);
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientIds, true);
((BlockedContactListItem) view).set(recipients);
}
}
}
}

View File

@ -27,13 +27,13 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.telephony.PhoneNumberUtils;
import android.support.v7.app.ActionBar.LayoutParams;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -44,6 +44,7 @@ import android.view.View.OnKeyListener;
import android.view.ViewStub;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
@ -54,9 +55,9 @@ import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.MasterCipher;
@ -83,6 +84,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.recipients.Recipients.RecipientsModifiedListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
@ -107,7 +109,6 @@ import java.util.List;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import static org.whispersystems.textsecure.internal.push.PushMessageProtos.PushMessageContent.GroupContext;
/**
@ -120,7 +121,7 @@ import static org.whispersystems.textsecure.internal.push.PushMessageProtos.Push
public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
RecipientModifiedListener
RecipientsModifiedListener
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@ -139,13 +140,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int GROUP_EDIT = 5;
private static final int CAPTURE_PHOTO = 6;
private MasterSecret masterSecret;
private ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
private TextView charactersLeft;
private ConversationFragment fragment;
private MasterSecret masterSecret;
private ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
private ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private View composePanel;
private AttachmentTypeSelectorAdapter attachmentAdapter;
private AttachmentManager attachmentManager;
@ -175,10 +179,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
this.masterSecret = masterSecret;
setContentView(R.layout.conversation_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
fragment = initFragment(R.id.fragment_content, new ConversationFragment(), masterSecret, dynamicLanguage.getCurrentLocale());
fragment = initFragment(R.id.fragment_content, new ConversationFragment(),
masterSecret, dynamicLanguage.getCurrentLocale());
initializeReceivers();
initializeActionBar();
initializeViews();
initializeResources();
initializeDraft();
@ -210,10 +216,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
dynamicLanguage.onResume(this);
initializeSecurity();
initializeTitleBar();
initializeEnabledCheck();
initializeMmsEnabledCheck();
initializeIme();
titleView.setTitle(recipients);
setBlockedUserState(recipients);
calculateCharactersRemaining();
MessageNotifier.setVisibleThread(threadId);
@ -263,7 +271,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
break;
case GROUP_EDIT:
this.recipients = RecipientFactory.getRecipientsForIds(this, data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true);
initializeTitleBar();
titleView.setTitle(recipients);
setBlockedUserState(recipients);
supportInvalidateOptionsMenu();
break;
}
}
@ -299,6 +309,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inflater.inflate(R.menu.conversation, menu);
if (recipients != null && recipients.isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
else inflater.inflate(R.menu.conversation_unmuted, menu);
if (isSingleConversation() && getRecipients().getPrimaryRecipient().getContactUri() == null) {
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
}
@ -324,6 +337,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_edit_group: handleEditPushGroup(); return true;
case R.id.menu_leave: handleLeavePushGroup(); return true;
case R.id.menu_invite: handleInviteLink(); return true;
case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
case R.id.menu_unmute_notifications: handleUnmuteNotifications(); return true;
case android.R.id.home: handleReturnToConversationList(); return true;
}
@ -349,6 +364,61 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
finish();
}
private void handleMuteNotifications() {
MuteDialog.show(this, new MuteDialog.MuteSelectionListener() {
@Override
public void onMuted(final long until) {
recipients.setMuted(until);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
.setMuted(recipients, until);
return null;
}
}.execute();
}
});
}
private void handleUnmuteNotifications() {
recipients.setMuted(0);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
.setMuted(recipients, 0);
return null;
}
}.execute();
}
private void handleUnblock() {
new AlertDialogWrapper.Builder(this)
.setTitle(R.string.ConversationActivity_unblock_question)
.setMessage(R.string.ConversationActivity_are_you_sure_you_want_to_unblock_this_contact)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.ConversationActivity_unblock, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
recipients.setBlocked(false);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this)
.setBlocked(recipients, false);
return null;
}
}.execute();
}
}).show();
}
private void handleInviteLink() {
try {
boolean a = SecureRandom.getInstance("SHA1PRNG").nextBoolean();
@ -551,45 +621,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
///// Initializers
private void initializeTitleBar() {
final String title;
final String subtitle;
final Recipient recipient = getRecipients().getPrimaryRecipient();
if (isSingleConversation()) {
if (TextUtils.isEmpty(recipient.getName())) {
title = recipient.getNumber();
subtitle = null;
} else {
title = recipient.getName();
subtitle = PhoneNumberUtils.formatNumber(recipient.getNumber());
}
} else if (isGroupConversation()) {
if (isPushGroupConversation()) {
final String groupName = recipient.getName();
title = (!TextUtils.isEmpty(groupName)) ? groupName : getString(R.string.ConversationActivity_unnamed_group);
subtitle = null;
} else {
final int size = getRecipients().getRecipientsList().size();
title = getString(R.string.ConversationActivity_group_conversation);
subtitle = (size == 1) ? getString(R.string.ConversationActivity_d_recipients_in_group_singular)
: String.format(getString(R.string.ConversationActivity_d_recipients_in_group), size);
}
} else {
title = getString(R.string.ConversationActivity_compose_message);
subtitle = null;
}
getSupportActionBar().setTitle(title);
getSupportActionBar().setSubtitle(subtitle);
getWindow().getDecorView().setContentDescription(getString(R.string.conversation_activity__window_description, title));
this.supportInvalidateOptionsMenu();
}
private void initializeDraft() {
String draftText = getIntent().getStringExtra(DRAFT_TEXT_EXTRA);
Uri draftImage = getIntent().getParcelableExtra(DRAFT_IMAGE_EXTRA);
@ -659,6 +690,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
else sendButton.setDefaultTransport(Type.SMS);
calculateCharactersRemaining();
supportInvalidateOptionsMenu();
}
private void initializeMmsEnabledCheck() {
@ -692,6 +724,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeText = (ComposeText) findViewById(R.id.embedded_text_editor);
charactersLeft = (TextView) findViewById(R.id.space_left);
emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle);
titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
unblockButton = (Button) findViewById(R.id.unblock_button);
composePanel = findViewById(R.id.bottom_panel);
attachmentAdapter = new AttachmentTypeSelectorAdapter(this);
attachmentManager = new AttachmentManager(this, this);
@ -710,6 +745,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
titleView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(ConversationActivity.this, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENTS_EXTRA, recipients.getIds());
startActivity(intent);
}
});
unblockButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
handleUnblock();
}
});
composeText.setOnKeyListener(composeKeyPressedListener);
composeText.addTextChangedListener(composeKeyPressedListener);
composeText.setOnEditorActionListener(sendButtonListener);
@ -718,6 +769,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
emojiToggle.setOnClickListener(new EmojiToggleListener());
}
private void initializeActionBar() {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setCustomView(R.layout.conversation_title_view);
getSupportActionBar().setDisplayShowCustomEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(false);
}
private EmojiDrawer getEmojiDrawer() {
if (emojiDrawer.isPresent()) return emojiDrawer.get();
EmojiDrawer emojiDrawer = (EmojiDrawer)((ViewStub)findViewById(R.id.emoji_drawer_stub)).inflate();
@ -739,8 +797,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
@Override
public void onModified(Recipient recipient) {
initializeTitleBar();
public void onModified(Recipients recipients) {
titleView.setTitle(recipients);
setBlockedUserState(recipients);
}
private void initializeReceivers() {
@ -751,7 +810,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (eventThreadId == threadId || eventThreadId == -2) {
initializeSecurity();
initializeTitleBar();
calculateCharactersRemaining();
}
}
@ -765,7 +823,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long[] ids = recipients.getIds();
Log.w("ConversationActivity", "Looking up new recipients...");
recipients = RecipientFactory.getRecipientsForIds(context, ids, false);
initializeTitleBar();
titleView.setTitle(recipients);
}
}
};
@ -917,6 +975,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(thisThreadId);
}
private void setBlockedUserState(Recipients recipients) {
if (recipients.isBlocked()) {
unblockButton.setVisibility(View.VISIBLE);
composePanel.setVisibility(View.GONE);
} else {
composePanel.setVisibility(View.VISIBLE);
unblockButton.setVisibility(View.GONE);
}
}
private void calculateCharactersRemaining() {
int charactersSpent = composeText.getText().toString().length();
TransportOption transportOption = sendButton.getSelectedTransport();
@ -994,7 +1062,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (refreshFragment) {
fragment.reload(recipients, threadId);
initializeTitleBar();
initializeSecurity();
}

View File

@ -11,7 +11,7 @@ import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.text.ClipboardManager;
import android.util.Log;
@ -335,7 +335,7 @@ public class ConversationFragment extends ListFragment
((ConversationAdapter) getListAdapter()).toggleBatchSelected(messageRecord);
((ConversationAdapter) getListAdapter()).notifyDataSetChanged();
actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(actionModeCallback);
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
return true;
}

View File

@ -25,7 +25,7 @@ import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@ -248,7 +248,7 @@ public class ConversationListFragment extends Fragment
@Override
public void onItemLongClick(ConversationListItem item) {
actionMode = ((ActionBarActivity)getActivity()).startSupportActionMode(ConversationListFragment.this);
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(ConversationListFragment.this);
getListAdapter().initializeBatchMode(true);
getListAdapter().toggleThreadInBatchSet(item.getThreadId());

View File

@ -27,7 +27,6 @@ import android.widget.TextView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
@ -44,7 +43,7 @@ import static org.thoughtcrime.securesms.util.SpanUtil.color;
*/
public class ConversationListItem extends RelativeLayout
implements Recipient.RecipientModifiedListener
implements Recipients.RecipientsModifiedListener
{
private final static String TAG = ConversationListItem.class.getSimpleName();
@ -76,12 +75,11 @@ public class ConversationListItem extends RelativeLayout
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.subjectView = (TextView) findViewById(R.id.subject);
this.fromView = (FromTextView) findViewById(R.id.from);
this.dateView = (TextView) findViewById(R.id.date);
this.contactPhotoImage = (AvatarImageView) findViewById(R.id.contact_photo_image);
initializeContactWidgetVisibility();
}
public void set(ThreadRecord thread, Locale locale, Set<Long> selectedThreads, boolean batchMode) {
@ -112,10 +110,6 @@ public class ConversationListItem extends RelativeLayout
this.recipients.removeListener(this);
}
private void initializeContactWidgetVisibility() {
contactPhotoImage.setVisibility(View.VISIBLE);
}
private void setBatchState(boolean batch) {
setSelected(batch && selectedThreads.contains(threadId));
}
@ -133,7 +127,7 @@ public class ConversationListItem extends RelativeLayout
}
@Override
public void onModified(Recipient recipient) {
public void onModified(final Recipients recipients) {
handler.post(new Runnable() {
@Override
public void run() {

View File

@ -0,0 +1,94 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class ConversationTitleView extends LinearLayout {
private static final String TAG = ConversationTitleView.class.getSimpleName();
private TextView title;
private TextView subtitle;
public ConversationTitleView(Context context) {
this(context, null);
}
public ConversationTitleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.title = (TextView) findViewById(R.id.title);
this.subtitle = (TextView) findViewById(R.id.subtitle);
}
public void setTitle(@Nullable Recipients recipients) {
if (recipients == null) setComposeTitle();
else if (recipients.isSingleRecipient()) setRecipientTitle(recipients.getPrimaryRecipient());
else setRecipientsTitle(recipients);
if (recipients != null && recipients.isBlocked()) {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_white_18dp, 0, 0, 0);
} else if (recipients != null && recipients.isMuted()) {
title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_volume_off_white_18dp, 0, 0, 0);
} else {
title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
private void setComposeTitle() {
this.title.setText(R.string.ConversationActivity_compose_message);
this.subtitle.setText(null);
this.subtitle.setVisibility(View.GONE);
}
private void setRecipientTitle(Recipient recipient) {
if (!recipient.isGroupRecipient()) {
if (TextUtils.isEmpty(recipient.getName())) {
this.title.setText(recipient.getNumber());
this.subtitle.setText(null);
this.subtitle.setVisibility(View.GONE);
} else {
this.title.setText(recipient.getName());
this.subtitle.setText(recipient.getNumber());
this.subtitle.setVisibility(View.VISIBLE);
}
} else {
String groupName = (!TextUtils.isEmpty(recipient.getName())) ?
recipient.getName() :
getContext().getString(R.string.ConversationActivity_unnamed_group);
this.title.setText(groupName);
this.subtitle.setText(null);
this.subtitle.setVisibility(View.GONE);
}
}
private void setRecipientsTitle(Recipients recipients) {
int size = recipients.getRecipientsList().size();
title.setText(getContext().getString(R.string.ConversationActivity_group_conversation));
subtitle.setText((size == 1) ? getContext().getString(R.string.ConversationActivity_d_recipients_in_group_singular) :
String.format(getContext().getString(R.string.ConversationActivity_d_recipients_in_group), size));
subtitle.setVisibility(View.VISIBLE);
}
}

View File

@ -465,7 +465,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
}
private long handleCreateMmsGroup(Set<Recipient> members) {
Recipients recipients = new Recipients(new LinkedList<Recipient>(members));
Recipients recipients = RecipientFactory.getRecipientsFor(this, new LinkedList<>(members), false);
return DatabaseFactory.getThreadDatabase(this)
.getThreadIdFor(recipients,
ThreadDatabase.DistributionTypes.CONVERSATION);
@ -532,7 +532,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
ArrayList<Recipient> selectedContactsList = setToArrayList(selectedContacts);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, new Recipients(selectedContactsList).getIds());
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, RecipientFactory.getRecipientsFor(GroupCreateActivity.this, selectedContactsList, true).getIds());
startActivity(intent);
finish();
} else {

View File

@ -12,6 +12,7 @@ import com.afollestad.materialdialogs.AlertDialogWrapper;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -45,7 +46,7 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, Recipients> {
.getGroupMembers(GroupUtil.getDecodedId(groupId), true);
} catch (IOException e) {
Log.w(TAG, e);
return new Recipients(new LinkedList<Recipient>());
return RecipientFactory.getRecipientsFor(context, new LinkedList<Recipient>(), true);
}
}

View File

@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DirectoryHelper;
@ -276,7 +277,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
.getGroupMembers(GroupUtil.getDecodedId(groupId), false);
} catch (IOException e) {
Log.w(TAG, e);
recipients = new Recipients(new LinkedList<Recipient>());
recipients = RecipientFactory.getRecipientsFor(MessageDetailsActivity.this, new LinkedList<Recipient>(), false);
}
}

View File

@ -0,0 +1,43 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import com.afollestad.materialdialogs.AlertDialogWrapper;
import java.util.concurrent.TimeUnit;
public class MuteDialog extends AlertDialogWrapper {
private MuteDialog() {}
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, final int which) {
final long muteUntil;
switch (which) {
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
}
listener.onMuted(muteUntil);
}
});
builder.show();
}
public interface MuteSelectionListener {
public void onMuted(long until);
}
}

View File

@ -80,14 +80,35 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
else finish();
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@NonNull T fragment,
@NonNull MasterSecret masterSecret)
{
return initFragment(target, fragment, masterSecret, null);
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@NonNull T fragment,
@NonNull MasterSecret masterSecret,
@Nullable Locale locale) {
@Nullable Locale locale)
{
return initFragment(target, fragment, masterSecret, locale, null);
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@NonNull T fragment,
@NonNull MasterSecret masterSecret,
@Nullable Locale locale,
@Nullable Bundle extras)
{
Bundle args = new Bundle();
args.putParcelable("master_secret", masterSecret);
args.putSerializable(LOCALE_EXTRA, locale);
if (extras != null) {
args.putAll(extras);
}
fragment.setArguments(args);
getSupportFragmentManager().beginTransaction()
.replace(target, fragment)
@ -95,12 +116,6 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return fragment;
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@NonNull T fragment,
@NonNull MasterSecret masterSecret) {
return initFragment(target, fragment, masterSecret, null);
}
private void routeApplicationState(MasterSecret masterSecret) {
Intent intent = getIntentForState(masterSecret, getApplicationState(masterSecret));
if (intent != null) {

View File

@ -0,0 +1,348 @@
package org.thoughtcrime.securesms;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.preference.PreferenceFragment;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.afollestad.materialdialogs.AlertDialogWrapper;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActivity implements Recipients.RecipientsModifiedListener
{
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
public static final String RECIPIENTS_EXTRA = "recipient_ids";
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
private static final String PREFERENCE_TONE = "pref_key_recipient_ringtone";
private static final String PREFERENCE_VIBRATE = "pref_key_recipient_vibrate";
private static final String PREFERENCE_BLOCK = "pref_key_recipient_block";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private AvatarImageView avatar;
private TextView title;
private TextView blockedIndicator;
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle instanceState, @NonNull MasterSecret masterSecret) {
setContentView(R.layout.recipient_preference_activity);
long[] recipientIds = getIntent().getLongArrayExtra(RECIPIENTS_EXTRA);
Recipients recipients = RecipientFactory.getRecipientsForIds(this, recipientIds, true);
initializeToolbar();
setHeader(recipients);
recipients.addListener(this);
Bundle bundle = new Bundle();
bundle.putLongArray(RECIPIENTS_EXTRA, recipientIds);
initFragment(R.id.preference_fragment, new RecipientPreferenceFragment(), masterSecret, null, bundle);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preference_fragment);
fragment.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
private void initializeToolbar() {
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowTitleEnabled(false);
this.avatar = (AvatarImageView) toolbar.findViewById(R.id.avatar);
this.title = (TextView) toolbar.findViewById(R.id.name);
this.blockedIndicator = (TextView) toolbar.findViewById(R.id.blocked_indicator);
}
private void setHeader(Recipients recipients) {
this.avatar.setAvatar(recipients.getPrimaryRecipient(), true);
this.title.setText(recipients.toShortString());
if (recipients.isBlocked()) this.blockedIndicator.setVisibility(View.VISIBLE);
else this.blockedIndicator.setVisibility(View.GONE);
}
@Override
public void onModified(final Recipients recipients) {
title.post(new Runnable() {
@Override
public void run() {
setHeader(recipients);
}
});
}
public static class RecipientPreferenceFragment
extends PreferenceFragment
implements Recipients.RecipientsModifiedListener
{
private final Handler handler = new Handler();
private Recipients recipients;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.recipient_preferences);
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(),
getArguments().getLongArray(RECIPIENTS_EXTRA),
true);
this.recipients.addListener(this);
this.findPreference(PREFERENCE_TONE)
.setOnPreferenceChangeListener(new RingtoneChangeListener());
this.findPreference(PREFERENCE_VIBRATE)
.setOnPreferenceChangeListener(new VibrateChangeListener());
this.findPreference(PREFERENCE_MUTED)
.setOnPreferenceClickListener(new MuteClickedListener());
this.findPreference(PREFERENCE_BLOCK)
.setOnPreferenceClickListener(new BlockClickedListener());
}
@Override
public void onResume() {
super.onResume();
setSummaries(recipients);
}
private void setSummaries(Recipients recipients) {
CheckBoxPreference mutePreference = (CheckBoxPreference) this.findPreference(PREFERENCE_MUTED);
Preference ringtonePreference = this.findPreference(PREFERENCE_TONE);
Preference vibratePreference = this.findPreference(PREFERENCE_VIBRATE);
Preference blockPreference = this.findPreference(PREFERENCE_BLOCK);
mutePreference.setChecked(recipients.isMuted());
if (recipients.getRingtone() != null) {
Ringtone tone = RingtoneManager.getRingtone(getActivity(), recipients.getRingtone());
if (tone != null) {
ringtonePreference.setSummary(tone.getTitle(getActivity()));
}
} else {
ringtonePreference.setSummary(R.string.preferences__default);
}
if (recipients.getVibrate() == VibrateState.DEFAULT) {
vibratePreference.setSummary(R.string.preferences__default);
} else if (recipients.getVibrate() == VibrateState.ENABLED) {
vibratePreference.setSummary("Enabled");
} else {
vibratePreference.setSummary("Disabled");
}
if (!recipients.isSingleRecipient() || recipients.isGroupRecipient()) {
blockPreference.setEnabled(false);
} else {
blockPreference.setEnabled(true);
if (recipients.isBlocked()) blockPreference.setTitle("Unblock");
else blockPreference.setTitle("Block");
}
}
@Override
public void onModified(final Recipients recipients) {
handler.post(new Runnable() {
@Override
public void run() {
setSummaries(recipients);
}
});
}
private class RingtoneChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String value = (String)newValue;
final Uri uri;
if (TextUtils.isEmpty(value) || Settings.System.DEFAULT_NOTIFICATION_URI.toString().equals(value)) {
uri = null;
} else {
uri = Uri.parse(value);
}
recipients.setRingtone(uri);
new AsyncTask<Uri, Void, Void>() {
@Override
protected Void doInBackground(Uri... params) {
DatabaseFactory.getRecipientPreferenceDatabase(getActivity())
.setRingtone(recipients, params[0]);
return null;
}
}.execute(uri);
return false;
}
}
private class VibrateChangeListener implements Preference.OnPreferenceChangeListener {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
int value = Integer.parseInt((String) newValue);
final VibrateState vibrateState = VibrateState.fromId(value);
recipients.setVibrate(vibrateState);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(getActivity())
.setVibrate(recipients, vibrateState);
return null;
}
}.execute();
return false;
}
}
private class MuteClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (recipients.isMuted()) handleUnmute();
else handleMute();
return true;
}
private void handleMute() {
MuteDialog.show(getActivity(), new MuteDialog.MuteSelectionListener() {
@Override
public void onMuted(long until) {
setMuted(recipients, until);
}
});
setSummaries(recipients);
}
private void handleUnmute() {
setMuted(recipients, 0);
}
private void setMuted(final Recipients recipients, final long until) {
recipients.setMuted(until);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(getActivity())
.setMuted(recipients, until);
return null;
}
}.execute();
}
}
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (recipients.isBlocked()) handleUnblock();
else handleBlock();
return true;
}
private void handleBlock() {
new AlertDialogWrapper.Builder(getActivity())
.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_see_messages_from_this_user)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
setBlocked(recipients, true);
}
}).show();
}
private void handleUnblock() {
new AlertDialogWrapper.Builder(getActivity())
.setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_are_you_sure_you_want_to_unblock_this_contact)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
setBlocked(recipients, false);
}
}).show();
}
private void setBlocked(final Recipients recipients, final boolean blocked) {
recipients.setBlocked(blocked);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientPreferenceDatabase(getActivity())
.setBlocked(recipients, blocked);
return null;
}
}.execute();
}
}
}
}

View File

@ -25,7 +25,6 @@ import android.widget.RelativeLayout;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
/**
@ -34,7 +33,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
* @author Jake McGinty
*/
public class ShareListItem extends RelativeLayout
implements Recipient.RecipientModifiedListener
implements Recipients.RecipientsModifiedListener
{
private final static String TAG = ShareListItem.class.getSimpleName();
@ -102,7 +101,7 @@ public class ShareListItem extends RelativeLayout
}
@Override
public void onModified(Recipient recipient) {
public void onModified(final Recipients recipients) {
handler.post(new Runnable() {
@Override
public void run() {

View File

@ -3,19 +3,24 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
public class FromTextView extends EmojiTextView {
private static final String TAG = FromTextView.class.getSimpleName();
public FromTextView(Context context) {
super(context);
}
@ -25,7 +30,7 @@ public class FromTextView extends EmojiTextView {
}
public void setText(Recipient recipient) {
setText(new Recipients(recipient));
setText(RecipientFactory.getRecipientsFor(getContext(), recipient, true));
}
public void setText(Recipients recipients) {
@ -63,6 +68,10 @@ public class FromTextView extends EmojiTextView {
colors.recycle();
setText(builder);
if (recipients.isBlocked()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
else if (recipients.isMuted()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_volume_off_grey600_18dp, 0, 0, 0);
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}

View File

@ -127,7 +127,7 @@ public class PushRecipientsPanel extends RelativeLayout {
try {
recipients = getRecipients();
} catch (RecipientFormattingException e) {
recipients = new Recipients( new LinkedList<Recipient>() );
recipients = RecipientFactory.getRecipientsFor(getContext(), new LinkedList<Recipient>(), true);
}
recipientsText.setAdapter(new RecipientsAdapter(this.getContext()));

View File

@ -130,7 +130,7 @@ public class SingleRecipientPanel extends RelativeLayout {
try {
recipients = getRecipients();
} catch (RecipientFormattingException e) {
recipients = new Recipients( new LinkedList<Recipient>() );
recipients = RecipientFactory.getRecipientsFor(getContext(), new LinkedList<Recipient>(), true);
}
recipientsText.setAdapter(new RecipientsAdapter(this.getContext()));

View File

@ -96,11 +96,6 @@ public class ContactPhotoFactory {
localUserContactPhotoCache.clear();
}
public static void clearCache(Recipient recipient) {
if (localUserContactPhotoCache.containsKey(recipient.getContactUri()))
localUserContactPhotoCache.remove(recipient.getContactUri());
}
public static Drawable getContactPhoto(Context context, Uri uri, String name) {
final InputStream inputStream = getContactPhotoStream(context, uri);
final int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);

View File

@ -61,7 +61,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_THUMBNAILS_VERSION = 15;
private static final int INTRODUCED_IDENTITY_COLUMN_VERSION = 16;
private static final int INTRODUCED_UNIQUE_PART_IDS_VERSION = 17;
private static final int DATABASE_VERSION = 17;
private static final int INTRODUCED_RECIPIENT_PREFS_DB = 18;
private static final int DATABASE_VERSION = 18;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -82,6 +83,7 @@ public class DatabaseFactory {
private final DraftDatabase draftDatabase;
private final PushDatabase pushDatabase;
private final GroupDatabase groupDatabase;
private final RecipientPreferenceDatabase recipientPreferenceDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@ -140,20 +142,25 @@ public class DatabaseFactory {
return getInstance(context).groupDatabase;
}
public static RecipientPreferenceDatabase getRecipientPreferenceDatabase(Context context) {
return getInstance(context).recipientPreferenceDatabase;
}
private DatabaseFactory(Context context) {
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.part = new PartDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.address = CanonicalAddressDatabase.getInstance(context);
this.mmsAddress = new MmsAddressDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
this.sms = new SmsDatabase(context, databaseHelper);
this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper);
this.mms = new MmsDatabase(context, databaseHelper);
this.part = new PartDatabase(context, databaseHelper);
this.thread = new ThreadDatabase(context, databaseHelper);
this.address = CanonicalAddressDatabase.getInstance(context);
this.mmsAddress = new MmsAddressDatabase(context, databaseHelper);
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
this.draftDatabase = new DraftDatabase(context, databaseHelper);
this.pushDatabase = new PushDatabase(context, databaseHelper);
this.groupDatabase = new GroupDatabase(context, databaseHelper);
this.recipientPreferenceDatabase = new RecipientPreferenceDatabase(context, databaseHelper);
}
public void reset(Context context) {
@ -171,6 +178,7 @@ public class DatabaseFactory {
this.draftDatabase.reset(databaseHelper);
this.pushDatabase.reset(databaseHelper);
this.groupDatabase.reset(databaseHelper);
this.recipientPreferenceDatabase.reset(databaseHelper);
old.close();
this.address.reset(context);
@ -480,6 +488,7 @@ public class DatabaseFactory {
db.execSQL(DraftDatabase.CREATE_TABLE);
db.execSQL(PushDatabase.CREATE_TABLE);
db.execSQL(GroupDatabase.CREATE_TABLE);
db.execSQL(RecipientPreferenceDatabase.CREATE_TABLE);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
executeStatements(db, MmsDatabase.CREATE_INDEXS);
@ -722,6 +731,12 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE part ADD COLUMN unique_id INTEGER NOT NULL DEFAULT 0");
}
if (oldVersion < INTRODUCED_RECIPIENT_PREFS_DB) {
db.execSQL("CREATE TABLE recipient_preferences " +
"(_id INTEGER PRIMARY KEY, recipient_ids TEXT UNIQUE, block INTEGER DEFAULT 0, " +
"notification TEXT DEFAULT NULL, vibrate INTEGER DEFAULT 0, mute_until INTEGER DEFAULT 0)");
}
db.setTransactionSuccessful();
db.endTransaction();
}

View File

@ -90,7 +90,7 @@ public class GroupDatabase extends Database {
public Recipients getGroupMembers(byte[] groupId, boolean includeSelf) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
List<String> members = getCurrentMembers(groupId);
List<Recipient> recipients = new LinkedList<Recipient>();
List<Recipient> recipients = new LinkedList<>();
for (String member : members) {
if (!includeSelf && member.equals(localNumber))
@ -100,7 +100,7 @@ public class GroupDatabase extends Database {
.getRecipientsList());
}
return new Recipients(recipients);
return RecipientFactory.getRecipientsFor(context, recipients, false);
}
public void create(byte[] groupId, String title, List<String> members,

View File

@ -145,7 +145,7 @@ public class MmsAddressDatabase extends Database {
}
}
return new Recipients(results);
return RecipientFactory.getRecipientsFor(context, results, false);
}

View File

@ -345,7 +345,7 @@ public class MmsDatabase extends MessagingDatabase {
? Util.toIsoString(notification.getFrom().getTextString())
: "";
Recipients recipients = RecipientFactory.getRecipientsFromString(context, fromString, false);
if (recipients.isEmpty()) recipients = new Recipients(Recipient.getUnknownRecipient(context));
if (recipients.isEmpty()) recipients = RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
}
@ -1054,13 +1054,13 @@ public class MmsDatabase extends MessagingDatabase {
private Recipients getRecipientsFor(String address) {
if (TextUtils.isEmpty(address) || address.equals("insert-address-token")) {
return new Recipients(Recipient.getUnknownRecipient(context));
return RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
}
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false);
if (recipients == null || recipients.isEmpty()) {
return new Recipients(Recipient.getUnknownRecipient(context));
return RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
}
return recipients;

View File

@ -0,0 +1,172 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.Arrays;
public class RecipientPreferenceDatabase extends Database {
private static final String TAG = RecipientPreferenceDatabase.class.getSimpleName();
private static final String TABLE_NAME = "recipient_preferences";
private static final String ID = "_id";
private static final String RECIPIENT_IDS = "recipient_ids";
private static final String BLOCK = "block";
private static final String NOTIFICATION = "notification";
private static final String VIBRATE = "vibrate";
private static final String MUTE_UNTIL = "mute_until";
public enum VibrateState {
DEFAULT(0), ENABLED(1), DISABLED(2);
private final int id;
VibrateState(int id) {
this.id = id;
}
public int getId() {
return id;
}
public static VibrateState fromId(int id) {
return values()[id];
}
}
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
RECIPIENT_IDS + " TEXT UNIQUE, " +
BLOCK + " INTEGER DEFAULT 0," +
NOTIFICATION + " TEXT DEFAULT NULL, " +
VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " +
MUTE_UNTIL + " INTEGER DEFAULT 0);";
public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Cursor getBlocked() {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
return database.query(TABLE_NAME, new String[] {ID, RECIPIENT_IDS}, BLOCK + " = 1",
null, null, null, null, null);
}
public Optional<RecipientsPreferences> getRecipientsPreferences(@NonNull long[] recipients) {
Arrays.sort(recipients);
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = database.query(TABLE_NAME, null, RECIPIENT_IDS + " = ?",
new String[] {Util.join(recipients, " ")},
null, null, null);
if (cursor != null && cursor.moveToNext()) {
boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1;
String notification = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION));
int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE));
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
Uri notificationUri = notification == null ? null : Uri.parse(notification);
Log.w(TAG, "Muted until: " + muteUntil);
return Optional.of(new RecipientsPreferences(blocked, muteUntil,
VibrateState.fromId(vibrateState),
notificationUri));
}
return Optional.absent();
} finally {
if (cursor != null) cursor.close();
}
}
public void setBlocked(Recipients recipients, boolean blocked) {
ContentValues values = new ContentValues();
values.put(BLOCK, blocked ? 1 : 0);
updateOrInsert(recipients, values);
}
public void setRingtone(Recipients recipients, @Nullable Uri notification) {
ContentValues values = new ContentValues();
values.put(NOTIFICATION, notification == null ? null : notification.toString());
updateOrInsert(recipients, values);
}
public void setVibrate(Recipients recipients, @NonNull VibrateState enabled) {
ContentValues values = new ContentValues();
values.put(VIBRATE, enabled.getId());
updateOrInsert(recipients, values);
}
public void setMuted(Recipients recipients, long until) {
Log.w(TAG, "Setting muted until: " + until);
ContentValues values = new ContentValues();
values.put(MUTE_UNTIL, until);
updateOrInsert(recipients, values);
}
private void updateOrInsert(Recipients recipients, ContentValues contentValues) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
int updated = database.update(TABLE_NAME, contentValues, RECIPIENT_IDS + " = ?",
new String[] {String.valueOf(recipients.getSortedIdsString())});
if (updated < 1) {
contentValues.put(RECIPIENT_IDS, recipients.getSortedIdsString());
database.insert(TABLE_NAME, null, contentValues);
}
database.setTransactionSuccessful();
database.endTransaction();
}
public static class RecipientsPreferences {
private final boolean blocked;
private final long muteUntil;
private final VibrateState vibrateState;
private final Uri notification;
public RecipientsPreferences(boolean blocked, long muteUntil, VibrateState vibrateState, Uri notification) {
this.blocked = blocked;
this.muteUntil = muteUntil;
this.vibrateState = vibrateState;
this.notification = notification;
}
public boolean isBlocked() {
return blocked;
}
public long getMuteUntil() {
return muteUntil;
}
public @NonNull VibrateState getVibrateState() {
return vibrateState;
}
public @Nullable Uri getRingtone() {
return notification;
}
}
}

View File

@ -368,7 +368,7 @@ public class SmsDatabase extends MessagingDatabase {
recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), true);
} else {
Log.w(TAG, "Sender is null, returning unknown recipient");
recipients = new Recipients(Recipient.getUnknownRecipient(context));
recipients = RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
}
Recipients groupRecipients;
@ -615,13 +615,13 @@ public class SmsDatabase extends MessagingDatabase {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, address, false);
if (recipients == null || recipients.isEmpty()) {
return new Recipients(Recipient.getUnknownRecipient(context));
return RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
}
return recipients;
} else {
Log.w(TAG, "getRecipientsFor() address is null");
return new Recipients(Recipient.getUnknownRecipient(context));
return RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(context), false);
}
}

View File

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
public class BlockedContactsLoader extends AbstractCursorLoader {
public BlockedContactsLoader(Context context) {
super(context);
}
@Override
public Cursor getCursor() {
return DatabaseFactory.getRecipientPreferenceDatabase(getContext())
.getBlocked();
}
}

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import org.thoughtcrime.securesms.BuildConfig;
@ -11,19 +10,15 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.push.TextSecurePushTrustStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.textsecure.internal.push.PushServiceSocket;
import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider;
import java.io.File;
@ -72,10 +67,6 @@ public class AvatarDownloadJob extends MasterSecretJob {
Bitmap avatar = BitmapUtil.createScaledBitmap(measureInputStream, scaleInputStream, 500, 500);
database.updateAvatar(groupId, avatar);
Recipient groupRecipient = RecipientFactory.getRecipientsFromString(context, GroupUtil.getEncodedId(groupId), true)
.getPrimaryRecipient();
groupRecipient.setContactPhoto(new BitmapDrawable(avatar));
}
} catch (InvalidMessageException | BitmapDecodingException | NonSuccessfulResponseCodeException e) {
Log.w(TAG, e);

View File

@ -7,6 +7,9 @@ import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobParameters;
import ws.com.google.android.mms.pdu.GenericPdu;
@ -49,7 +52,7 @@ public class MmsReceiveJob extends ContextJob {
Log.w(TAG, e);
}
if (pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
if (isNotification(pdu) && !isBlocked(pdu)) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu);
@ -61,6 +64,8 @@ public class MmsReceiveJob extends ContextJob {
messageAndThreadId.first,
messageAndThreadId.second,
true));
} else if (isNotification(pdu)) {
Log.w(TAG, "*** Received blocked MMS, ignoring...");
}
}
@ -73,4 +78,17 @@ public class MmsReceiveJob extends ContextJob {
public boolean onShouldRetry(Exception exception) {
return false;
}
private boolean isBlocked(GenericPdu pdu) {
if (pdu.getFrom() != null && pdu.getFrom().getTextString() != null) {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, Util.toIsoString(pdu.getFrom().getTextString()), false);
return recipients.isBlocked();
}
return false;
}
private boolean isNotification(GenericPdu pdu) {
return pdu != null && pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
}
}

View File

@ -7,6 +7,8 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NotInDirectoryException;
import org.thoughtcrime.securesms.database.TextSecureDirectory;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
@ -34,16 +36,21 @@ public abstract class PushReceivedJob extends ContextJob {
}
private void handleMessage(TextSecureEnvelope envelope, boolean sendExplicitReceipt) {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
long messageId = DatabaseFactory.getPushDatabase(context).insert(envelope);
if (!recipients.isBlocked()) {
long messageId = DatabaseFactory.getPushDatabase(context).insert(envelope);
jobManager.add(new PushDecryptJob(context, messageId, envelope.getSource()));
} else {
Log.w(TAG, "*** Received blocked push message, ignoring...");
}
if (sendExplicitReceipt) {
jobManager.add(new DeliveryReceiptJob(context, envelope.getSource(),
envelope.getTimestamp(),
envelope.getRelay()));
}
jobManager.add(new PushDecryptJob(context, messageId, envelope.getSource()));
}
private void handleReceipt(TextSecureEnvelope envelope) {

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.telephony.SmsMessage;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
@ -11,6 +12,8 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
@ -42,9 +45,11 @@ public class SmsReceiveJob extends ContextJob {
public void onRun() {
Optional<IncomingTextMessage> message = assembleMessageFragments(pdus);
if (message.isPresent()) {
if (message.isPresent() && !isBlocked(message.get())) {
Pair<Long, Long> messageAndThreadId = storeMessage(message.get());
MessageNotifier.updateNotification(context, KeyCachingService.getMasterSecret(context), messageAndThreadId.second);
} else if (message.isPresent()) {
Log.w(TAG, "*** Received blocked SMS, ignoring...");
}
}
@ -58,6 +63,15 @@ public class SmsReceiveJob extends ContextJob {
return false;
}
private boolean isBlocked(IncomingTextMessage message) {
if (message.getSender() != null) {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), false);
return recipients.isBlocked();
}
return false;
}
private Pair<Long, Long> storeMessage(IncomingTextMessage message) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);

View File

@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Action;
import android.support.v4.app.NotificationCompat.BigTextStyle;
@ -49,7 +50,9 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
@ -88,7 +91,7 @@ public class MessageNotifier {
public static void notifyMessageDeliveryFailed(Context context, Recipients recipients, long threadId) {
if (visibleThread == threadId) {
sendInThreadNotification(context);
sendInThreadNotification(context, recipients);
} else {
Intent intent = new Intent(context, ConversationActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
@ -105,7 +108,7 @@ public class MessageNotifier {
builder.setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
builder.setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
builder.setAutoCancel(true);
setNotificationAlarms(context, builder, true);
setNotificationAlarms(context, builder, true, null, VibrateState.DEFAULT);
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
.notify((int)threadId, builder.build());
@ -126,8 +129,9 @@ public class MessageNotifier {
}
if (visibleThread == threadId) {
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
sendInThreadNotification(context);
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
threads.setRead(threadId);
sendInThreadNotification(context, threads.getRecipientsForThreadId(threadId));
} else {
updateNotification(context, masterSecret, true, 0);
}
@ -225,7 +229,9 @@ public class MessageNotifier {
builder.setStyle(new BigTextStyle().bigText(content));
setNotificationAlarms(context, builder, signal);
setNotificationAlarms(context, builder, signal,
notificationState.getRingtone(),
notificationState.getVibrate());
if (signal) {
builder.setTicker(notifications.get(0).getTickerText());
@ -283,7 +289,9 @@ public class MessageNotifier {
builder.setStyle(style);
setNotificationAlarms(context, builder, signal);
setNotificationAlarms(context, builder, signal,
notificationState.getRingtone(),
notificationState.getVibrate());
if (signal) {
builder.setTicker(notifications.get(0).getTickerText());
@ -293,23 +301,27 @@ public class MessageNotifier {
.notify(NOTIFICATION_ID, builder.build());
}
private static void sendInThreadNotification(Context context) {
private static void sendInThreadNotification(Context context, Recipients recipients) {
try {
if (!TextSecurePreferences.isInThreadNotifications(context)) {
return;
}
String ringtone = TextSecurePreferences.getNotificationRingtone(context);
if (ringtone == null) {
Log.w(TAG, "ringtone preference was null.");
return;
}
Uri uri = Uri.parse(ringtone);
Uri uri = recipients.getRingtone();
if (uri == null) {
Log.w(TAG, "couldn't parse ringtone uri " + ringtone);
String ringtone = TextSecurePreferences.getNotificationRingtone(context);
if (ringtone == null) {
Log.w(TAG, "ringtone preference was null.");
return;
} else {
uri = Uri.parse(ringtone);
}
}
if (uri == null) {
Log.w(TAG, "couldn't parse ringtone uri " + TextSecurePreferences.getNotificationRingtone(context));
return;
}
@ -358,7 +370,9 @@ public class MessageNotifier {
SpannableString body = new SpannableString(context.getString(R.string.MessageNotifier_encrypted_message));
body.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
notificationState.addNotification(new NotificationItem(recipient, recipients, null, threadId, body, null, 0));
if (!recipients.isMuted()) {
notificationState.addNotification(new NotificationItem(recipient, recipients, null, threadId, body, null, 0));
}
}
} finally {
if (reader != null)
@ -403,7 +417,9 @@ public class MessageNotifier {
body = SpanUtil.italic(message, italicLength);
}
notificationState.addNotification(new NotificationItem(recipient, recipients, threadRecipients, threadId, body, image, timestamp));
if (threadRecipients == null || !threadRecipients.isMuted()) {
notificationState.addNotification(new NotificationItem(recipient, recipients, threadRecipients, threadId, body, image, timestamp));
}
}
reader.close();
@ -412,18 +428,23 @@ public class MessageNotifier {
private static void setNotificationAlarms(Context context,
NotificationCompat.Builder builder,
boolean signal)
boolean signal,
@Nullable Uri ringtone,
VibrateState vibrate)
{
String ringtone = TextSecurePreferences.getNotificationRingtone(context);
boolean vibrate = TextSecurePreferences.isNotificationVibrateEnabled(context);
String defaultRingtoneName = TextSecurePreferences.getNotificationRingtone(context);
boolean defaultVibrate = TextSecurePreferences.isNotificationVibrateEnabled(context);
String ledColor = TextSecurePreferences.getNotificationLedColor(context);
String ledBlinkPattern = TextSecurePreferences.getNotificationLedPattern(context);
String ledBlinkPatternCustom = TextSecurePreferences.getNotificationLedPatternCustom(context);
String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom);
builder.setSound(TextUtils.isEmpty(ringtone) || !signal ? null : Uri.parse(ringtone));
if (signal && ringtone != null) builder.setSound(ringtone);
else if (signal && !TextUtils.isEmpty(defaultRingtoneName)) builder.setSound(Uri.parse(defaultRingtoneName));
else builder.setSound(null);
if (signal && vibrate) {
if (signal && (vibrate == VibrateState.ENABLED || (vibrate == VibrateState.DEFAULT && defaultVibrate))) {
builder.setDefaults(Notification.DEFAULT_VIBRATE);
}

View File

@ -4,6 +4,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import org.thoughtcrime.securesms.ConversationActivity;
@ -34,6 +35,10 @@ public class NotificationItem {
this.timestamp = timestamp;
}
public @Nullable Recipients getRecipients() {
return threadRecipients;
}
public Recipient getIndividualRecipient() {
return individualRecipient;
}

View File

@ -3,9 +3,14 @@ package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.HashSet;
import java.util.LinkedList;
@ -14,8 +19,8 @@ import java.util.Set;
public class NotificationState {
private final LinkedList<NotificationItem> notifications = new LinkedList<NotificationItem>();
private final Set<Long> threads = new HashSet<Long>();
private final LinkedList<NotificationItem> notifications = new LinkedList<>();
private final Set<Long> threads = new HashSet<>();
private int notificationCount = 0;
@ -25,6 +30,30 @@ public class NotificationState {
notificationCount++;
}
public @Nullable Uri getRingtone() {
if (!notifications.isEmpty()) {
Recipients recipients = notifications.getFirst().getRecipients();
if (recipients != null) {
return recipients.getRingtone();
}
}
return null;
}
public VibrateState getVibrate() {
if (!notifications.isEmpty()) {
Recipients recipients = notifications.getFirst().getRecipients();
if (recipients != null) {
return recipients.getVibrate();
}
}
return VibrateState.DEFAULT;
}
public boolean hasMultipleThreads() {
return threads.size() > 1;
}

View File

@ -17,6 +17,7 @@ import com.doomonafireball.betterpickers.hmspicker.HmsPickerBuilder;
import com.doomonafireball.betterpickers.hmspicker.HmsPickerDialogFragment;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.BlockedContactsActivity;
import org.thoughtcrime.securesms.PassphraseChangeActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -27,6 +28,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.concurrent.TimeUnit;
public class AppProtectionPreferenceFragment extends PreferenceFragment {
private static final String PREFERENCE_CATEGORY_BLOCKED = "preference_category_blocked";
private MasterSecret masterSecret;
private CheckBoxPreference disablePassphrase;
@ -42,6 +46,8 @@ public class AppProtectionPreferenceFragment extends PreferenceFragment {
.setOnPreferenceClickListener(new ChangePassphraseClickListener());
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF)
.setOnPreferenceClickListener(new PassphraseIntervalClickListener());
this.findPreference(PREFERENCE_CATEGORY_BLOCKED)
.setOnPreferenceClickListener(new BlockedContactsClickListener());
disablePassphrase
.setOnPreferenceChangeListener(new DisablePassphraseClickListener());
}
@ -73,6 +79,15 @@ public class AppProtectionPreferenceFragment extends PreferenceFragment {
.setSummary(getString(R.string.AppProtectionPreferenceFragment_minutes, timeoutMinutes));
}
private class BlockedContactsClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), BlockedContactsActivity.class);
startActivity(intent);
return true;
}
}
private class ChangePassphraseClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {

View File

@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.preferences;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.recipients.Recipients;
public class BlockedContactListItem extends RelativeLayout implements Recipients.RecipientsModifiedListener {
private AvatarImageView contactPhotoImage;
private TextView nameView;
private Recipients recipients;
public BlockedContactListItem(Context context) {
super(context);
}
public BlockedContactListItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BlockedContactListItem(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.contactPhotoImage = (AvatarImageView)findViewById(R.id.contact_photo_image);
this.nameView = (TextView) findViewById(R.id.name);
}
public void set(Recipients recipients) {
this.recipients = recipients;
onModified(recipients);
recipients.addListener(this);
}
@Override
public void onModified(Recipients recipients) {
this.contactPhotoImage.setAvatar(recipients.getPrimaryRecipient(), false);
this.nameView.setText(recipients.toShortString());
}
public Recipients getRecipients() {
return recipients;
}
}

View File

@ -91,16 +91,6 @@ public class Recipient {
return this.contactUri;
}
public synchronized void setContactPhoto(Drawable bitmap) {
this.contactPhoto = bitmap;
notifyListeners();
}
public synchronized void setName(String name) {
this.name = name;
notifyListeners();
}
public synchronized String getName() {
return this.name;
}

View File

@ -19,10 +19,11 @@ package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
@ -34,59 +35,73 @@ public class RecipientFactory {
public static Recipients getRecipientsForIds(Context context, String recipientIds, boolean asynchronous) {
if (TextUtils.isEmpty(recipientIds))
return new Recipients(new LinkedList<Recipient>());
return new Recipients();
List<Recipient> results = new LinkedList<>();
StringTokenizer tokenizer = new StringTokenizer(recipientIds.trim(), " ");
return getRecipientsForIds(context, Util.split(recipientIds, " "), asynchronous);
}
while (tokenizer.hasMoreTokens()) {
String recipientId = tokenizer.nextToken();
Recipient recipient = getRecipientFromProviderId(context, Long.parseLong(recipientId), asynchronous);
public static Recipients getRecipientsFor(Context context, List<Recipient> recipients, boolean asynchronous) {
long[] ids = new long[recipients.size()];
int i = 0;
results.add(recipient);
for (Recipient recipient : recipients) {
ids[i++] = recipient.getRecipientId();
}
return new Recipients(results);
return provider.getRecipients(context, ids, asynchronous);
}
public static Recipients getRecipientsFor(Context context, Recipient recipient, boolean asynchronous) {
long[] ids = new long[1];
ids[0] = recipient.getRecipientId();
return provider.getRecipients(context, ids, asynchronous);
}
public static Recipient getRecipientForId(Context context, long recipientId, boolean asynchronous) {
return getRecipientFromProviderId(context, recipientId, asynchronous);
}
public static Recipients getRecipientsForIds(Context context, long[] recipientIds, boolean asynchronous) {
List<Recipient> results = new LinkedList<>();
if (recipientIds == null) return new Recipients(results);
for (long recipientId : recipientIds) {
results.add(getRecipientFromProviderId(context, recipientId, asynchronous));
}
return new Recipients(results);
}
private static Recipient getRecipientForNumber(Context context, String number, boolean asynchronous) {
long recipientId = CanonicalAddressDatabase.getInstance(context).getCanonicalAddressId(number);
return provider.getRecipient(context, recipientId, asynchronous);
}
public static Recipients getRecipientsFromString(Context context, @NonNull String rawText, boolean asynchronous) {
List<Recipient> results = new LinkedList<>();
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
while (tokenizer.hasMoreTokens()) {
Recipient recipient = parseRecipient(context, tokenizer.nextToken(), asynchronous);
if( recipient != null )
results.add(recipient);
}
return new Recipients(results);
public static Recipients getRecipientsForIds(Context context, long[] recipientIds, boolean asynchronous) {
return provider.getRecipients(context, recipientIds, asynchronous);
}
private static Recipient getRecipientFromProviderId(Context context, long recipientId, boolean asynchronous) {
try {
return provider.getRecipient(context, recipientId, asynchronous);
} catch (NumberFormatException e) {
Log.w("RecipientFactory", e);
return Recipient.getUnknownRecipient(context);
public static Recipients getRecipientsFromString(Context context, @NonNull String rawText, boolean asynchronous) {
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
List<String> ids = new LinkedList<>();
while (tokenizer.hasMoreTokens()) {
Optional<Long> id = getRecipientIdFromNumber(context, tokenizer.nextToken());
if (id.isPresent()) {
ids.add(String.valueOf(id.get()));
}
}
return getRecipientsForIds(context, ids, asynchronous);
}
private static Recipients getRecipientsForIds(Context context, List<String> idStrings, boolean asynchronous) {
long[] ids = new long[idStrings.size()];
int i = 0;
for (String id : idStrings) {
ids[i++] = Long.parseLong(id);
}
return provider.getRecipients(context, ids, asynchronous);
}
private static Optional<Long> getRecipientIdFromNumber(Context context, String number) {
number = number.trim();
if (number.isEmpty()) return Optional.absent();
if (hasBracketedNumber(number)) {
number = parseBracketedNumber(number);
}
return Optional.of(CanonicalAddressDatabase.getInstance(context).getCanonicalAddressId(number));
}
private static boolean hasBracketedNumber(String recipient) {
@ -104,26 +119,9 @@ public class RecipientFactory {
return value;
}
private static Recipient parseRecipient(Context context, String recipient, boolean asynchronous) {
recipient = recipient.trim();
if( recipient.length() == 0 )
return null;
if (hasBracketedNumber(recipient))
return getRecipientForNumber(context, parseBracketedNumber(recipient), asynchronous);
return getRecipientForNumber(context, recipient, asynchronous);
}
public static void clearCache() {
ContactPhotoFactory.clearCache();
provider.clearCache();
}
public static void clearCache(Recipient recipient) {
ContactPhotoFactory.clearCache(recipient);
provider.clearCache(recipient);
}
}

View File

@ -22,27 +22,34 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.database.CanonicalAddressDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
public class RecipientProvider {
private static final Map<Long,Recipient> recipientCache = Collections.synchronizedMap(new LRUCache<Long,Recipient>(1000));
private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor();
private static final Map<Long,Recipient> recipientCache = Collections.synchronizedMap(new LRUCache<Long,Recipient>(1000));
private static final Map<RecipientIds,Recipients> recipientsCache = Collections.synchronizedMap(new LRUCache<RecipientIds, Recipients>(1000));
private static final ExecutorService asyncRecipientResolver = Util.newSingleThreadedLifoExecutor();
private static final String[] CALLER_ID_PROJECTION = new String[] {
PhoneLookup.DISPLAY_NAME,
@ -51,12 +58,29 @@ public class RecipientProvider {
PhoneLookup.NUMBER
};
public Recipient getRecipient(Context context, long recipientId, boolean asynchronous) {
Recipient getRecipient(Context context, long recipientId, boolean asynchronous) {
Recipient cachedRecipient = recipientCache.get(recipientId);
if (cachedRecipient != null) return cachedRecipient;
else if (asynchronous) return getAsynchronousRecipient(context, recipientId);
else return getSynchronousRecipient(context, recipientId);
if (cachedRecipient != null) return cachedRecipient;
else if (asynchronous) return getAsynchronousRecipient(context, recipientId);
else return getSynchronousRecipient(context, recipientId);
}
Recipients getRecipients(Context context, long[] recipientIds, boolean asynchronous) {
Recipients cachedRecipients = recipientsCache.get(new RecipientIds(recipientIds));
if (cachedRecipients != null) return cachedRecipients;
List<Recipient> recipientList = new LinkedList<>();
for (long recipientId : recipientIds) {
recipientList.add(getRecipient(context, recipientId, false));
}
if (asynchronous) cachedRecipients = new Recipients(recipientList, getRecipientsPreferencesAsync(context, recipientIds));
else cachedRecipients = new Recipients(recipientList, getRecipientsPreferencesSync(context, recipientIds));
recipientsCache.put(new RecipientIds(recipientIds), cachedRecipients);
return cachedRecipients;
}
private Recipient getSynchronousRecipient(final Context context, final long recipientId) {
@ -116,13 +140,9 @@ public class RecipientProvider {
return recipient;
}
public void clearCache() {
void clearCache() {
recipientCache.clear();
}
public void clearCache(Recipient recipient) {
if (recipientCache.containsKey(recipient.getRecipientId()))
recipientCache.remove(recipient.getRecipientId());
recipientsCache.clear();
}
private RecipientDetails getRecipientDetails(Context context, String number) {
@ -164,6 +184,25 @@ public class RecipientProvider {
}
}
private @Nullable RecipientsPreferences getRecipientsPreferencesSync(Context context, long[] recipientIds) {
return DatabaseFactory.getRecipientPreferenceDatabase(context)
.getRecipientsPreferences(recipientIds)
.orNull();
}
private ListenableFutureTask<RecipientsPreferences> getRecipientsPreferencesAsync(final Context context, final long[] recipientIds) {
ListenableFutureTask<RecipientsPreferences> task = new ListenableFutureTask<>(new Callable<RecipientsPreferences>() {
@Override
public RecipientsPreferences call() throws Exception {
return getRecipientsPreferencesSync(context, recipientIds);
}
});
asyncRecipientResolver.execute(task);
return task;
}
public static class RecipientDetails {
public final String name;
public final String number;
@ -178,5 +217,23 @@ public class RecipientProvider {
}
}
private static class RecipientIds {
private final long[] ids;
private RecipientIds(long[] ids) {
this.ids = ids;
}
public boolean equals(Object other) {
if (other == null || !(other instanceof RecipientIds)) return false;
return Arrays.equals(this.ids, ((RecipientIds) other).ids);
}
public int hashCode() {
return Arrays.hashCode(ids);
}
}
}

View File

@ -1,5 +1,5 @@
/**
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,52 +16,155 @@
*/
package org.thoughtcrime.securesms.recipients;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Patterns;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences;
import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
public class Recipients implements Iterable<Recipient> {
public class Recipients implements Iterable<Recipient>, RecipientModifiedListener {
private List<Recipient> recipients;
private static final String TAG = Recipients.class.getSimpleName();
public Recipients(List<Recipient> recipients) {
private final Set<RecipientsModifiedListener> listeners = Collections.newSetFromMap(new WeakHashMap<RecipientsModifiedListener, Boolean>());
private final List<Recipient> recipients;
private Uri ringtone = null;
private long mutedUntil = 0;
private boolean blocked = false;
private VibrateState vibrate = VibrateState.DEFAULT;
Recipients() {
this(new LinkedList<Recipient>(), (RecipientsPreferences)null);
}
Recipients(List<Recipient> recipients, @Nullable RecipientsPreferences preferences) {
this.recipients = recipients;
}
public Recipients(final Recipient recipient) {
this.recipients = new LinkedList<Recipient>() {{
add(recipient);
}};
}
public void append(Recipients recipients) {
this.recipients.addAll(recipients.getRecipientsList());
}
// public Recipients truncateToSingleRecipient() {
// assert(!this.recipients.isEmpty());
// this.recipients = this.recipients.subList(0, 1);
// return this;
// }
public void addListener(RecipientModifiedListener listener) {
for (Recipient recipient : recipients) {
recipient.addListener(listener);
if (preferences != null) {
ringtone = preferences.getRingtone();
mutedUntil = preferences.getMuteUntil();
vibrate = preferences.getVibrateState();
blocked = preferences.isBlocked();
}
}
public void removeListener(RecipientModifiedListener listener) {
for (Recipient recipient : recipients) {
recipient.removeListener(listener);
Recipients(List<Recipient> recipients, ListenableFutureTask<RecipientsPreferences> preferences) {
this.recipients = recipients;
preferences.addListener(new FutureTaskListener<RecipientsPreferences>() {
@Override
public void onSuccess(RecipientsPreferences result) {
if (result != null) {
Set<RecipientsModifiedListener> localListeners;
synchronized (Recipients.this) {
ringtone = result.getRingtone();
mutedUntil = result.getMuteUntil();
vibrate = result.getVibrateState();
blocked = result.isBlocked();
localListeners = new HashSet<>(listeners);
}
for (RecipientsModifiedListener listener : localListeners) {
listener.onModified(Recipients.this);
}
}
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
}
});
}
public synchronized @Nullable Uri getRingtone() {
return ringtone;
}
public void setRingtone(Uri ringtone) {
synchronized (this) {
this.ringtone = ringtone;
}
notifyListeners();
}
public synchronized boolean isMuted() {
return System.currentTimeMillis() <= mutedUntil;
}
public void setMuted(long mutedUntil) {
synchronized (this) {
this.mutedUntil = mutedUntil;
}
notifyListeners();
}
public synchronized boolean isBlocked() {
return blocked;
}
public void setBlocked(boolean blocked) {
synchronized (this) {
this.blocked = blocked;
}
notifyListeners();
}
public synchronized VibrateState getVibrate() {
return vibrate;
}
public void setVibrate(VibrateState vibrate) {
synchronized (this) {
this.vibrate = vibrate;
}
notifyListeners();
}
public synchronized void addListener(RecipientsModifiedListener listener) {
if (listeners.isEmpty()) {
for (Recipient recipient : recipients) {
recipient.addListener(this);
}
}
synchronized (this) {
listeners.add(listener);
}
}
public synchronized void removeListener(RecipientsModifiedListener listener) {
listeners.remove(listener);
if (listeners.isEmpty()) {
for (Recipient recipient : recipients) {
recipient.removeListener(this);
}
}
}
@ -78,30 +181,6 @@ public class Recipients implements Iterable<Recipient> {
return isSingleRecipient() && GroupUtil.isEncodedGroup(recipients.get(0).getNumber());
}
// public Recipients getSecureSessionRecipients(Context context) {
// List<Recipient> secureRecipients = new LinkedList<Recipient>();
//
// for (Recipient recipient : recipients) {
// if (KeyUtil.isSessionFor(context, recipient)) {
// secureRecipients.add(recipient);
// }
// }
//
// return new Recipients(secureRecipients);
// }
//
// public Recipients getInsecureSessionRecipients(Context context) {
// List<Recipient> insecureRecipients = new LinkedList<Recipient>();
//
// for (Recipient recipient : recipients) {
// if (!KeyUtil.isSessionFor(context, recipient)) {
// insecureRecipients.add(recipient);
// }
// }
//
// return new Recipients(insecureRecipients);
// }
public boolean isEmpty() {
return this.recipients.isEmpty();
}
@ -129,6 +208,25 @@ public class Recipients implements Iterable<Recipient> {
return ids;
}
public String getSortedIdsString() {
Set<Long> recipientSet = new HashSet<>();
for (Recipient recipient : this.recipients) {
recipientSet.add(recipient.getRecipientId());
}
long[] recipientArray = new long[recipientSet.size()];
int i = 0;
for (Long recipientId : recipientSet) {
recipientArray[i++] = recipientId;
}
Arrays.sort(recipientArray);
return Util.join(recipientArray, " ");
}
public String[] toNumberStringArray(boolean scrub) {
String[] recipientsArray = new String[recipients.size()];
Iterator<Recipient> iterator = recipients.iterator();
@ -163,12 +261,31 @@ public class Recipients implements Iterable<Recipient> {
return fromString;
}
public int describeContents() {
return 0;
}
@Override
public Iterator<Recipient> iterator() {
return recipients.iterator();
}
@Override
public void onModified(Recipient recipient) {
notifyListeners();
}
private void notifyListeners() {
Set<RecipientsModifiedListener> localListeners;
synchronized (this) {
localListeners = new HashSet<>(listeners);
}
for (RecipientsModifiedListener listener : localListeners) {
listener.onModified(this);
}
}
public interface RecipientsModifiedListener {
public void onModified(Recipients recipient);
}
}

View File

@ -9,10 +9,6 @@ public class OutgoingEncryptedMessage extends OutgoingTextMessage {
super(recipients, body);
}
public OutgoingEncryptedMessage(Recipient recipient, String body) {
super(recipient, body);
}
private OutgoingEncryptedMessage(OutgoingEncryptedMessage base, String body) {
super(base, body);
}

View File

@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingKeyExchangeMessage extends OutgoingTextMessage {
public OutgoingKeyExchangeMessage(Recipient recipient, String message) {
super(recipient, message);
public OutgoingKeyExchangeMessage(Recipients recipients, String message) {
super(recipients, message);
}
private OutgoingKeyExchangeMessage(OutgoingKeyExchangeMessage base, String body) {

View File

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.sms;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class OutgoingTextMessage {
@ -9,10 +8,6 @@ public class OutgoingTextMessage {
private final Recipients recipients;
private final String message;
public OutgoingTextMessage(Recipient recipient, String message) {
this(new Recipients(recipient), message);
}
public OutgoingTextMessage(Recipients recipients, String message) {
this.recipients = recipients;
this.message = message;
@ -49,13 +44,13 @@ public class OutgoingTextMessage {
public static OutgoingTextMessage from(SmsMessageRecord record) {
if (record.isSecure()) {
return new OutgoingEncryptedMessage(record.getIndividualRecipient(), record.getBody().getBody());
return new OutgoingEncryptedMessage(record.getRecipients(), record.getBody().getBody());
} else if (record.isKeyExchange()) {
return new OutgoingKeyExchangeMessage(record.getIndividualRecipient(), record.getBody().getBody());
return new OutgoingKeyExchangeMessage(record.getRecipients(), record.getBody().getBody());
} else if (record.isEndSession()) {
return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody().getBody()));
return new OutgoingEndSessionMessage(new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody()));
} else {
return new OutgoingTextMessage(record.getIndividualRecipient(), record.getBody().getBody());
return new OutgoingTextMessage(record.getRecipients(), record.getBody().getBody());
}
}

View File

@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import org.thoughtcrime.securesms.R;
public class DynamicNoActionBarTheme extends DynamicTheme {
@Override
protected int getSelectedTheme(Activity activity) {
String theme = TextSecurePreferences.getTheme(activity);
if (theme.equals("dark")) return R.style.TextSecure_DarkNoActionBar;
return R.style.TextSecure_LightNoActionBar;
}
}

View File

@ -77,6 +77,17 @@ public class Util {
return result.toString();
}
public static String join(long[] list, String delimeter) {
StringBuilder sb = new StringBuilder();
for (int j=0;j<list.length;j++) {
if (j != 0) sb.append(delimeter);
sb.append(list[j]);
}
return sb.toString();
}
public static ExecutorService newSingleThreadedLifoExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingLifoQueue<Runnable>());