refactor and improve contact selection

* unify single and multi contact selection activities
* follow android listview design recommendations more closely
* add contact photos to selection
* change indicator for push to be more obvious
* cache circle-cropped bitmaps
* dedupe numbers when contact has multiple of same phone number

// FREEBIE
This commit is contained in:
Jake McGinty 2014-03-17 23:25:09 -07:00
parent c414334059
commit ca6d8a8a0d
42 changed files with 1173 additions and 876 deletions

View file

@ -90,7 +90,7 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MmsPreferencesActivity"
<activity android:name=".MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ConversationListActivity"
@ -135,15 +135,15 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SingleContactSelectionActivity"
android:label="@string/AndroidManifest__select_contact"
<activity android:name=".NewConversationActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".AutoInitiateActivity"
android:theme="@style/TextSecure.Light.Dialog"

View file

@ -23,6 +23,7 @@ dependencies {
compile 'com.actionbarsherlock:actionbarsherlock:4.4.0@aar'
compile 'com.android.support:support-v4:19.0.1'
compile 'com.google.android.gcm:gcm-client:1.0.2'
compile 'se.emilsjolander:stickylistheaders:2.2.0'
compile project(':library')
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<scale
android:duration="150"
android:fromXScale="0.85"
android:fromYScale="0.85"
android:toXScale="1.0"
android:toYScale="1.0"
android:pivotX="50%"
android:pivotY="50%" />
<alpha
android:duration="150"
android:fromAlpha="0.6"
android:toAlpha="1.0" />
</set>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<scale
android:duration="150"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:toXScale="0.85"
android:toYScale="0.85"
android:pivotX="50%"
android:pivotY="50%" />
<alpha
android:duration="150"
android:fromAlpha="1.0"
android:toAlpha="0.6" />
</set>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:duration="150"
android:fromXDelta="0%"
android:toXDelta="100%" />
</set>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle">
<solid android:color="#33000000" />
</shape>
</item>
<item android:bottom="1dp">
<shape
android:shape="rectangle">
<solid android:color="@color/white" />
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle">
<solid android:color="#33ffffff" />
</shape>
</item>
<item android:bottom="1dp">
<shape
android:shape="rectangle">
<solid android:color="@color/black" />
</shape>
</item>
</layer-list>

View file

@ -7,7 +7,7 @@
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:drawSelectorOnTop="false"
android:scrollbarStyle="insideOverlay"
android:fadingEdgeLength="16dip"

View file

@ -5,12 +5,16 @@
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<org.thoughtcrime.securesms.components.SingleRecipientPanel android:id="@+id/recipients"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:id="@+id/contact_selection_list_fragment"
android:name="org.thoughtcrime.securesms.SingleContactSelectionListFragment">
android:name="org.thoughtcrime.securesms.PushContactSelectionListFragment">
</fragment>
</LinearLayout>

View file

@ -8,7 +8,7 @@
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:id="@+id/contact_selection_list_fragment"
android:name="org.thoughtcrime.securesms.PushContactSelectionListFragment">
</fragment>

View file

@ -1,13 +1,12 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fastScrollEnabled="true" />
<se.emilsjolander.stickylistheaders.StickyListHeadersListView android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView android:id="@android:id/empty"
android:layout_width="match_parent"

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="32sp"
android:paddingLeft="10dp"
android:paddingRight="25dp">
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:textSize="15sp"
android:textColor="?conversation_sent_text_secondary_color"
android:textStyle="bold" />
<View android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_alignParentBottom="true"
android:layout_marginTop="2dp"
android:background="?conversation_received_text_secondary_color" />
</RelativeLayout>

View file

@ -1,65 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:paddingRight="25dip">
<View android:id="@+id/push_support_label"
android:layout_height="fill_parent"
android:layout_width="3dip"
android:layout_alignParentLeft="true"
android:background="#ff64a926"
android:visibility="visible"
/>
<TextView android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<ImageView 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_alignParentBottom="true"
android:layout_marginBottom="8dip"
android:layout_marginTop="-8dip"
android:layout_marginLeft="14dip"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textStyle="bold"
android:visibility = "gone"
/>
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:cropToPadding="true"
android:scaleType="centerCrop"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" />
<TextView android:id="@+id/number"
android:visibility = "visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dip"
android:layout_marginTop="-8dip"
android:layout_marginLeft="14dip"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_toRightOf="@id/contact_photo_image"
android:textAppearance="?android:attr/textAppearanceSmall"
android:fontFamily="sans-serif-light"
/>
android:fontFamily="sans-serif-light" />
<TextView android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/number"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="1dip"
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"
/>
android:textAppearance="?android:attr/textAppearanceMedium" />
<CheckBox
android:id="@+id/check_box"
<CheckBox android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"

View file

@ -1,26 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.SingleRecipientPanel android:id="@+id/recipients"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="15dp"
android:paddingRight="15dp" />
<ListView android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:fastScrollEnabled="true" />
<TextView android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center|center_vertical"
android:layout_marginTop="15dp"
android:text="@string/contact_selection_group_activity__finding_contacts"
android:textSize="20sp" />
</LinearLayout>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:paddingRight="25dip">
<View android:id="@+id/push_support_label"
android:layout_height="fill_parent"
android:layout_width="3dip"
android:layout_alignParentLeft="true"
android:background="#ff64a926"
android:visibility="visible"
/>
<TextView android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:layout_marginBottom="8dip"
android:layout_marginTop="-8dip"
android:layout_marginLeft="14dip"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textStyle="bold"
android:visibility = "gone"
/>
<TextView android:id="@+id/number"
android:visibility = "visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dip"
android:layout_marginTop="-8dip"
android:layout_marginLeft="14dip"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:singleLine="true"
android:ellipsize="marquee"
android:textAppearance="?android:attr/textAppearanceSmall"
android:fontFamily="sans-serif-light"
/>
<TextView android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/number"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_marginBottom="1dip"
android:layout_marginLeft="14dip"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:singleLine="true"
android:ellipsize="marquee"
android:gravity="center_vertical|left"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
</RelativeLayout>

View file

@ -13,7 +13,11 @@
android:singleLine="true"
android:hint="@string/recipients_panel__to"
android:paddingRight="45dip"
android:paddingLeft="15dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:textColor="?conversation_editor_text_color"
android:background="?conversation_editor_background"
android:layout_width="fill_parent"/>
</RelativeLayout>

View file

@ -21,7 +21,7 @@
<attr name="lower_right_divet" format="reference" />
<attr name="conversation_background" format="reference|color"/>
<attr name="conversation_editor_background" format="reference|color"/>
<attr name="conversation_editor_background" format="reference"/>
<attr name="conversation_editor_text_color" format="reference|color"/>
<attr name="conversation_send_button" format="reference"/>
<attr name="conversation_send_secure_button" format="reference"/>
@ -45,8 +45,7 @@
<attr name="contact_selection_push_user" format="reference|color" />
<attr name="contact_selection_lay_user" format="reference|color" />
<attr name="contact_selection_push_label" format="reference|color" />
<attr name="contact_selection_lay_label" format="reference|color" />
<attr name="contact_selection_label_text" format="reference|color" />
<attr name="navigation_drawer_background" format="reference|color"/>
<attr name="navigation_drawer_text_color" format="color"/>

View file

@ -3,4 +3,5 @@
<dimen name="emoji_drawer_size">40dip</dimen>
<dimen name="conversation_item_corner_radius">3dp</dimen>
<dimen name="conversation_item_drop_shadow_dist">2dp</dimen>
</resources>
<dimen name="contact_selection_photo_size">50dp</dimen>
</resources>

5
res/values/ids.xml Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="holder_tag"/>
<item type="id" name="contact_info_tag"/>
</resources>

View file

@ -413,13 +413,14 @@
<!-- contact_selection_group_activity -->
<!-- contact_selection_list_activity -->
<string name="contact_selection_group_activity__no_contacts">No contacts.</string>
<string name="contact_selection_group_activity__finding_contacts">Finding contacts&#8230;</string>
<string name="contact_selection_group_activity__finding_contacts">Loading contacts&#8230;</string>
<!-- single_contact_selection_activity -->
<string name="single_contact_selection_group_activity__filter">Type a name to filter&#8230;</string>
<string name="SingleContactSelectionActivity_you_are_not_registered_with_the_push_service">You are not registered with the push service...</string>
<string name="SingleContactSelectionActivity_you_are_not_registered_with_the_push_service">You are not registered with the push service&#8230;</string>
<string name="SingleContactSelectionActivity_updating_directory">Updating directory</string>
<string name="SingleContactSelectionActivity_updating_push_directory">Updating push directory...</string>
<string name="SingleContactSelectionActivity_updating_push_directory">Updating push directory&#8230;</string>
<string name="SingleContactSelectionActivity_contact_photo">Contact Photo</string>
<!-- ContactSelectionListFragment-->
<string name="ContactSelectionlistFragment_select_for">Select for</string>
@ -468,6 +469,8 @@
<string name="log_submit_activity__copied_to_clipboard">Copied to clipboard</string>
<string name="log_submit_activity__loading_logcat">Loading logcat&#8230;</string>
<string name="log_submit_activity__thanks">Thanks for your help!</string>
<string name="log_submit_activity__submitting">Submitting</string>
<string name="log_submit_activity__posting_logs">Posting logs to pastebin&#8230;</string>
<!-- database_migration_activity -->
<string name="database_migration_activity__would_you_like_to_import_your_existing_text_messages">Would you like to import your existing text messages into TextSecure\'s encrypted database?</string>
@ -602,7 +605,7 @@
<string name="registration_progress_activity__generating_keys">Generating keys...</string>
<!-- recipients_panel -->
<string name="recipients_panel__to">To</string>
<string name="recipients_panel__to"><small>Enter a name or number</small></string>
<string name="recipients_panel__add_member">Add member</string>
<!-- review_identities -->
@ -748,6 +751,8 @@
<!-- contact_selection_list -->
<string name="contact_selection_list__menu_select_all">Select All</string>
<string name="contact_selection_list__menu_unselect_all">Unselect All</string>
<string name="contact_selection_list__header_textsecure_users">TEXTSECURE USERS</string>
<string name="contact_selection_list__header_other">ALL CONTACTS</string>
<!-- contact_selection -->
<string name="contact_selection__menu_finished">Finished</string>

View file

@ -23,6 +23,22 @@
<item name="background">#ff111111</item>
</style>
<style name="TextSecure.TitleTextStyle" parent="TextAppearance.Sherlock.Widget.ActionBar.Title">
<item name="android:textColor">#ff555555</item>
<item name="android:textSize">19sp</item>
</style>
<style name="TextSecure.SubtitleTextStyle" parent="TextAppearance.Sherlock.Widget.ActionBar.Subtitle">
<item name="android:textColor">#ff555555</item>
</style>
<style name="TextSecure.LightActionBar" parent="Widget.Sherlock.Light.ActionBar.Solid">
<item name="android:titleTextStyle">@style/TextSecure.TitleTextStyle</item>
<item name="titleTextStyle">@style/TextSecure.TitleTextStyle</item>
<item name="android:subtitleTextStyle">@style/TextSecure.SubtitleTextStyle</item>
<item name="subtitleTextStyle">@style/TextSecure.SubtitleTextStyle</item>
</style>
<style name="transparent_progress">
<item name="android:windowFrame">@null</item>
<item name="android:windowBackground">@android:color/transparent</item>

View file

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="TextSecure.LightTheme" parent="@style/Theme.Sherlock.Light">
<item name="android:actionBarStyle">@style/TextSecure.LightActionBar</item>
<item name="actionBarStyle">@style/TextSecure.LightActionBar</item>
<item name="android:windowContentOverlay">@null</item>
<item name="windowContentOverlay">@null</item>
<item name="conversation_list_item_background_read">@drawable/conversation_list_item_background_read_light</item>
<item name="conversation_list_item_background_unread">@drawable/conversation_list_item_background_unread_light</item>
<item name="conversation_list_item_background_selected">@drawable/list_selected_holo_light</item>
@ -21,11 +25,10 @@
<item name="contact_selection_push_user">#ff000000</item>
<item name="contact_selection_lay_user">#a0000000</item>
<item name="contact_selection_push_label">#ff64a926</item>
<item name="contact_selection_lay_label">#99000000</item>
<item name="contact_selection_label_text">#66000000</item>
<item name="conversation_background">#ffdddddd</item>
<item name="conversation_editor_background">#eeeeee</item>
<item name="conversation_editor_background">@drawable/textlines</item>
<item name="conversation_editor_text_color">#ff111111</item>
<item name="conversation_send_button">@drawable/ic_send_holo_light</item>
<item name="conversation_send_secure_button">@drawable/ic_send_encrypted_holo_light</item>
@ -89,10 +92,9 @@
<item name="conversation_received_text_primary_color">#ffeeeeee</item>
<item name="conversation_received_text_secondary_color">#44eeeeee</item>
<item name="contact_selection_push_user">#ffdddddd</item>
<item name="contact_selection_lay_user">#ffcccccc</item>
<item name="contact_selection_push_label">#ff64a926</item>
<item name="contact_selection_lay_label">#11ffffff</item>
<item name="contact_selection_push_user">#ffeeeeee</item>
<item name="contact_selection_lay_user">#afeeeeee</item>
<item name="contact_selection_label_text">#66eeeeee</item>
<item name="conversation_item_received_background">@drawable/conversation_item_received_shape_dark</item>
<item name="conversation_item_received_triangle_background">@drawable/conversation_item_received_triangle_shape_dark</item>
@ -112,7 +114,7 @@
<item name="lower_right_divet">@drawable/divet_lower_right_light</item>
<item name="conversation_background">@color/black</item>
<item name="conversation_editor_background">#ff222222</item>
<item name="conversation_editor_background">@drawable/textlines_dark</item>
<item name="conversation_editor_text_color">#ffeeeeee</item>
<item name="conversation_send_button">@drawable/ic_send_holo_dark</item>
<item name="conversation_send_secure_button">@drawable/ic_send_holo_dark_encrypted</item>

View file

@ -544,39 +544,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredSherlockPr
private class DirectoryUpdateListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
final Context context = ApplicationPreferencesActivity.this;
if (!TextSecurePreferences.isPushRegistered(context)) {
Toast.makeText(context,
getString(R.string.ApplicationPreferencesActivity_you_are_not_registered_with_the_push_service),
Toast.LENGTH_LONG).show();
return true;
}
new AsyncTask<Void, Void, Void>() {
private ProgressDialog progress;
@Override
protected void onPreExecute() {
progress = ProgressDialog.show(context,
getString(R.string.ApplicationPreferencesActivity_updating_directory),
getString(R.string.ApplicationPreferencesActivity_updating_push_directory),
true);
}
@Override
protected Void doInBackground(Void... params) {
DirectoryHelper.refreshDirectory(context);
return null;
}
@Override
protected void onPostExecute(Void result) {
if (progress != null)
progress.dismiss();
}
}.execute();
DirectoryHelper.refreshDirectoryWithProgressDialog(ApplicationPreferencesActivity.this);
return true;
}
}

View file

@ -176,6 +176,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
@Override
protected void onCreate(Bundle state) {
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
super.onCreate(state);
@ -219,6 +220,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
protected void onPause() {
super.onPause();
MessageNotifier.setVisibleThread(-1L);
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
}
@Override

View file

@ -170,8 +170,8 @@ public class ConversationListActivity extends PassphraseRequiredSherlockFragment
}
private void openSingleContactSelection() {
Intent intent = new Intent(this, SingleContactSelectionActivity.class);
intent.putExtra(SingleContactSelectionActivity.MASTER_SECRET_EXTRA, masterSecret);
Intent intent = new Intent(this, NewConversationActivity.class);
intent.putExtra(NewConversationActivity.MASTER_SECRET_EXTRA, masterSecret);
startActivity(intent);
}

View file

@ -35,6 +35,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.Util;
import java.io.BufferedInputStream;
@ -159,11 +160,11 @@ public class LogSubmitActivity extends SherlockActivity {
}
}
private class SubmitToPastebinAsyncTask extends AsyncTask<Void,Void,String> {
private ProgressDialog progressDialog;
private class SubmitToPastebinAsyncTask extends ProgressDialogAsyncTask<Void,Void,String> {
private final String paste;
public SubmitToPastebinAsyncTask(String paste) {
super(LogSubmitActivity.this, R.string.log_submit_activity__submitting, R.string.log_submit_activity__posting_logs);
this.paste = paste;
}
@ -174,7 +175,6 @@ public class LogSubmitActivity extends SherlockActivity {
URL url = new URL(HASTEBIN_ENDPOINT);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setDoOutput(true);
urlConnection.setFixedLengthStreamingMode(paste.length());
urlConnection.setReadTimeout(10000);
urlConnection.connect();
@ -201,22 +201,9 @@ public class LogSubmitActivity extends SherlockActivity {
return null;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
progressDialog = new ProgressDialog(LogSubmitActivity.this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setTitle("Submitting");
progressDialog.setMessage("Posting logs to pastebin...");
progressDialog.show();
}
@Override
protected void onPostExecute(final String response) {
super.onPostExecute(response);
progressDialog.dismiss();
if (response != null && !response.startsWith("Bad API request")) {
TextView showText = new TextView(LogSubmitActivity.this);

View file

@ -16,12 +16,8 @@
*/
package org.thoughtcrime.securesms;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.Preference;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
@ -43,28 +39,32 @@ import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
/**
* Activity container for selecting a list of contacts. Provides a tab frame for
* contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity
* when selecting a list of contacts to address a message to.
* Activity container for selecting a list of contacts.
*
* @author Moxie Marlinspike
*
*/
public class SingleContactSelectionActivity extends PassphraseRequiredSherlockFragmentActivity {
private final static String TAG = "SingleContactSelectionActivity";
public final static String MASTER_SECRET_EXTRA = "master_secret";
public class NewConversationActivity extends PassphraseRequiredSherlockFragmentActivity {
private final static String TAG = "ContactSelectActivity";
public final static String MASTER_SECRET_EXTRA = "master_secret";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private MasterSecret masterSecret;
private MasterSecret masterSecret;
private SingleRecipientPanel recipientsPanel;
private PushContactSelectionListFragment contactsFragment;
@Override
protected void onCreate(Bundle icicle) {
dynamicTheme.onCreate(this);
@ -74,16 +74,41 @@ public class SingleContactSelectionActivity extends PassphraseRequiredSherlockFr
ActionBarUtil.initializeDefaultActionBar(this, actionBar);
actionBar.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.single_contact_selection_activity);
setContentView(R.layout.new_conversation_activity);
initializeResources();
}
private void initializeResources() {
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA);
final SingleRecipientPanel recipientsPanel = (SingleRecipientPanel) findViewById(R.id.recipients);
}
final SingleContactSelectionListFragment listFragment = (SingleContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
listFragment.setOnContactSelectedListener(new SingleContactSelectionListFragment.OnContactSelectedListener() {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getSupportMenuInflater();
menu.clear();
if (TextSecurePreferences.isPushRegistered(this)) inflater.inflate(R.menu.push_directory, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_refresh_directory: handleDirectoryRefresh(); return true;
case R.id.menu_selection_finished: handleSelectionFinished(); return true;
case android.R.id.home: finish(); return true;
}
return false;
}
private void initializeResources() {
recipientsPanel = (SingleRecipientPanel) findViewById(R.id.recipients);
contactsFragment = (PushContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
contactsFragment.setOnContactSelectedListener(new PushContactSelectionListFragment.OnContactSelectedListener() {
@Override
public void onContactSelected(ContactData contactData) {
Log.i(TAG, "Choosing contact from list.");
@ -99,17 +124,37 @@ public class SingleContactSelectionActivity extends PassphraseRequiredSherlockFr
openNewConversation(recipients);
}
});
}
private void handleSelectionFinished() {
final Intent resultIntent = getIntent();
final List<ContactData> selectedContacts = contactsFragment.getSelectedContacts();
if (selectedContacts != null) {
resultIntent.putParcelableArrayListExtra("contacts", new ArrayList<ContactData>(contactsFragment.getSelectedContacts()));
}
setResult(RESULT_OK, resultIntent);
finish();
}
private void handleDirectoryRefresh() {
DirectoryHelper.refreshDirectoryWithProgressDialog(this, new DirectoryHelper.DirectoryUpdateFinishedListener() {
@Override
public void onUpdateFinished() {
contactsFragment.update();
}
});
}
private Recipients contactDataToRecipients(ContactData contactData) {
if (contactData == null || contactData.numbers == null) return null;
Recipients recipients = new Recipients(new ArrayList<Recipient>());
Recipients recipients = new Recipients(new LinkedList<Recipient>());
for (ContactAccessor.NumberData numberData : contactData.numbers) {
if (NumberUtil.isValidSmsOrEmailOrGroup(numberData.number)) {
try {
Recipients recipientsForNumber = RecipientFactory.getRecipientsFromString(SingleContactSelectionActivity.this,
numberData.number,
false);
Recipients recipientsForNumber = RecipientFactory.getRecipientsFromString(NewConversationActivity.this,
numberData.number,
false);
recipients.getRecipientsList().addAll(recipientsForNumber.getRecipientsList());
} catch (RecipientFormattingException rfe) {
Log.w(TAG, "Caught RecipientFormattingException when trying to convert a selected number to a Recipient.", rfe);
@ -121,83 +166,14 @@ public class SingleContactSelectionActivity extends PassphraseRequiredSherlockFr
private void openNewConversation(Recipients recipients) {
if (recipients != null) {
Intent intent = new Intent(SingleContactSelectionActivity.this, ConversationActivity.class);
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.toIdString());
intent.putExtra(ConversationActivity.MASTER_SECRET_EXTRA, masterSecret);
long existingThread = DatabaseFactory.getThreadDatabase(SingleContactSelectionActivity.this).getThreadIdIfExistsFor(recipients);
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipients);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
startActivity(intent);
finish();
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getSupportMenuInflater();
menu.clear();
if (TextSecurePreferences.isPushRegistered(this)) {
inflater.inflate(R.menu.push_directory, menu);
}
return true;
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh_directory:
handleDirectoryRefresh();
return true;
case android.R.id.home:
setResult(RESULT_CANCELED);
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void handleDirectoryRefresh() {
if (!TextSecurePreferences.isPushRegistered(SingleContactSelectionActivity.this)) {
Toast.makeText(getApplicationContext(),
getString(R.string.SingleContactSelectionActivity_you_are_not_registered_with_the_push_service),
Toast.LENGTH_LONG).show();
return;
}
new AsyncTask<Void, Void, Void>() {
private ProgressDialog progress;
@Override
protected void onPreExecute() {
progress = ProgressDialog.show(SingleContactSelectionActivity.this,
getString(R.string.SingleContactSelectionActivity_updating_directory),
getString(R.string.SingleContactSelectionActivity_updating_push_directory),
true);
}
@Override
protected Void doInBackground(Void... params) {
DirectoryHelper.refreshDirectory(getApplicationContext());
return null;
}
@Override
protected void onPostExecute(Void result) {
final SingleContactSelectionListFragment listFragment = (SingleContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
listFragment.update();
if (progress != null)
progress.dismiss();
}
}.execute();
}
}

View file

@ -19,11 +19,24 @@ package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import org.thoughtcrime.securesms.components.SingleRecipientPanel;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
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.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.NumberUtil;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.view.Menu;
@ -31,22 +44,24 @@ import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
/**
* Activity container for selecting a list of contacts. Provides a tab frame for
* contact, group, and "recent contact" activity tabs. Used by ComposeMessageActivity
* when selecting a list of contacts to address a message to.
* Activity container for selecting a list of contacts.
*
* @author Moxie Marlinspike
*
*/
public class PushContactSelectionActivity extends PassphraseRequiredSherlockFragmentActivity {
private final static String TAG = "ContactSelectActivity";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private PushContactSelectionListFragment contactsFragment;
@Override
protected void onCreate(Bundle icicle) {
dynamicTheme.onCreate(this);
@ -57,6 +72,7 @@ public class PushContactSelectionActivity extends PassphraseRequiredSherlockFrag
actionBar.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.push_contact_selection_activity);
initializeResources();
}
@Override
@ -66,34 +82,55 @@ public class PushContactSelectionActivity extends PassphraseRequiredSherlockFrag
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getSupportMenuInflater();
inflater.inflate(R.menu.contact_selection, menu);
menu.clear();
if (TextSecurePreferences.isPushRegistered(this)) inflater.inflate(R.menu.push_directory, menu);
inflater.inflate(R.menu.contact_selection, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_selection_finished:
case android.R.id.home:
handleSelectionFinished(); return true;
case R.id.menu_refresh_directory: handleDirectoryRefresh(); return true;
case R.id.menu_selection_finished: handleSelectionFinished(); return true;
case android.R.id.home: finish(); return true;
}
return false;
}
private void initializeResources() {
contactsFragment = (PushContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
contactsFragment.setMultiSelect(true);
contactsFragment.setOnContactSelectedListener(new PushContactSelectionListFragment.OnContactSelectedListener() {
@Override
public void onContactSelected(ContactData contactData) {
Log.i(TAG, "Choosing contact from list.");
}
});
}
private void handleSelectionFinished() {
PushContactSelectionListFragment contactsFragment = (PushContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
List<ContactData> contacts = contactsFragment.getSelectedContacts();
Intent resultIntent = getIntent();
resultIntent.putParcelableArrayListExtra("contacts", new ArrayList<ContactData>(contacts));
final Intent resultIntent = getIntent();
final List<ContactData> selectedContacts = contactsFragment.getSelectedContacts();
if (selectedContacts != null) {
resultIntent.putParcelableArrayListExtra("contacts", new ArrayList<ContactData>(contactsFragment.getSelectedContacts()));
}
setResult(RESULT_OK, resultIntent);
finish();
}
private void handleDirectoryRefresh() {
DirectoryHelper.refreshDirectoryWithProgressDialog(this, new DirectoryHelper.DirectoryUpdateFinishedListener() {
@Override
public void onUpdateFinished() {
contactsFragment.update();
}
});
}
}

View file

@ -17,345 +17,167 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.database.MergeCursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.AdapterView;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockListFragment;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.DataHolder;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
/**
* Activity for selecting a list of contacts. Displayed inside
* a PushContactSelectionActivity tab frame, and ultimately called by
* ComposeMessageActivity for selecting a list of destination contacts.
* Fragment for selecting a one or more contacts from a list.
*
* @author Moxie Marlinspike
*
*/
public class PushContactSelectionListFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
public class PushContactSelectionListFragment extends Fragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private final int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
R.attr.contact_selection_lay_user,
R.attr.contact_selection_push_label,
R.attr.contact_selection_lay_label};
private static final String TAG = "ContactSelectFragment";
private final HashMap<Long, ContactData> selectedContacts = new HashMap<Long, ContactData>();
private static LayoutInflater li;
private TypedArray drawables;
private TextView emptyText;
private Map<Long, ContactData> selectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private boolean multi = false;
private StickyListHeadersListView listView;
@Override
public void onActivityCreated(Bundle icicle) {
super.onCreate(icicle);
li = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
initializeResources();
initializeCursor();
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.push_contact_selection_list_activity, container, false);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.contact_selection_list, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_select_all:
handleSelectAll();
return true;
case R.id.menu_unselect_all:
handleUnselectAll();
return true;
}
super.onOptionsItemSelected(item);
return false;
}
public List<ContactData> getSelectedContacts() {
if (selectedContacts == null) return null;
List<ContactData> selected = new LinkedList<ContactData>();
selected.addAll(selectedContacts.values());
return selected;
}
private void handleUnselectAll() {
selectedContacts.clear();
((CursorAdapter) getListView().getAdapter()).notifyDataSetChanged();
public void setMultiSelect(boolean multi) {
this.multi = multi;
}
private void handleSelectAll() {
selectedContacts.clear();
Cursor cursor = null;
try {
cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(getActivity());
while (cursor != null && cursor.moveToNext()) {
ContactData contactData = ContactAccessor.getInstance().getContactData(getActivity(), cursor);
if (contactData.numbers.isEmpty()) continue;
else if (contactData.numbers.size() == 1) addSingleNumberContact(contactData);
else addMultipleNumberContact(contactData, null, null);
}
} finally {
if (cursor != null)
cursor.close();
private void addContact(DataHolder data) {
final ContactData contactData = new ContactData(data.id, data.name);
final CharSequence label = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getResources(),
data.numberType, "");
contactData.numbers.add(new ContactAccessor.NumberData(label.toString(), data.number));
if (multi) {
selectedContacts.put(contactData.id, contactData);
}
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contactData);
}
((CursorAdapter) getListView().getAdapter()).notifyDataSetChanged();
}
private void addSingleNumberContact(ContactData contactData) {
selectedContacts.put(contactData.id, contactData);
}
private void removeContact(ContactData contactData) {
private void removeContact(DataHolder contactData) {
selectedContacts.remove(contactData.id);
}
private void addMultipleNumberContact(ContactData contactData, TextView textView, CheckBox checkBox) {
String[] options = new String[contactData.numbers.size()];
int i = 0;
for (NumberData option : contactData.numbers) {
options[i++] = option.type + " " + option.number;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getString(R.string.ContactSelectionlistFragment_select_for) + " " + contactData.name);
builder.setMultiChoiceItems(options, null, new DiscriminatorClickedListener(contactData));
builder.setPositiveButton(android.R.string.ok, new DiscriminatorFinishedListener(contactData, textView, checkBox));
builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView, checkBox));
builder.show();
}
private void initializeCursor() {
setListAdapter(new ContactSelectionListAdapter(getActivity(), null));
ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), null, multi);
selectedContacts = adapter.getSelectedContacts();
listView.setAdapter(adapter);
this.getLoaderManager().initLoader(0, null, this);
}
private void initializeResources() {
this.getListView().setFocusable(true);
this.drawables = getActivity().obtainStyledAttributes(STYLE_ATTRIBUTES);
emptyText = (TextView) getView().findViewById(android.R.id.empty);
listView = (StickyListHeadersListView) getView().findViewById(android.R.id.list);
listView.setFocusable(true);
listView.setFastScrollEnabled(true);
listView.setFastScrollAlwaysVisible(true);
listView.setDrawingListUnderStickyHeader(false);
listView.setOnItemClickListener(new ListClickListener());
}
public void update() {
this.getLoaderManager().restartLoader(0, null, this);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
((ContactItemView)v).selected();
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return ContactAccessor.getInstance().getCursorLoaderForContacts(getActivity());
}
private class ContactSelectionListAdapter extends CursorAdapter {
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
((CursorAdapter) listView.getAdapter()).changeCursor(data);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
}
public ContactSelectionListAdapter(Context context, Cursor c) {
super(context, c);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((CursorAdapter) listView.getAdapter()).changeCursor(null);
}
private class ListClickListener implements AdapterView.OnItemClickListener {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ContactItemView view = new ContactItemView(context);
bindView(view, context, cursor);
public void onItemClick(AdapterView<?> l, View v, int position, long id) {
final DataHolder contactData = (DataHolder) v.getTag(R.id.contact_info_tag);
final ViewHolder holder = (ViewHolder) v.getTag(R.id.holder_tag);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
boolean isPushUser;
try {
isPushUser = (cursor.getInt(cursor.getColumnIndexOrThrow(ContactAccessor.PUSH_COLUMN)) > 0);
} catch (IllegalArgumentException iae) {
isPushUser = false;
if (holder == null) {
Log.w(TAG, "ViewHolder was null, can't proceed with click logic.");
return;
}
ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor);
PushContactData pushContactData = new PushContactData(contactData, isPushUser);
((ContactItemView)view).set(pushContactData);
}
}
private class PushContactData {
private final ContactData contactData;
private final boolean pushSupport;
public PushContactData(ContactData contactData, boolean pushSupport) {
this.contactData = contactData;
this.pushSupport = pushSupport;
}
}
if (multi) holder.checkBox.toggle();
private class ContactItemView extends RelativeLayout {
private ContactData contactData;
private boolean pushSupport;
private CheckBox checkBox;
private TextView name;
private TextView number;
private TextView label;
private View pushLabel;
public ContactItemView(Context context) {
super(context);
li.inflate(R.layout.push_contact_selection_list_item, this, true);
this.name = (TextView) findViewById(R.id.name);
this.number = (TextView) findViewById(R.id.number);
this.label = (TextView) findViewById(R.id.label);
this.checkBox = (CheckBox) findViewById(R.id.check_box);
this.pushLabel = findViewById(R.id.push_support_label);
}
public void selected() {
checkBox.toggle();
if (checkBox.isChecked()) {
if (contactData.numbers.size() == 1) addSingleNumberContact(contactData);
else addMultipleNumberContact(contactData, name, checkBox);
} else {
if (!multi || holder.checkBox.isChecked()) {
addContact(contactData);
} else if (multi) {
removeContact(contactData);
}
}
public void set(PushContactData pushContactData) {
this.contactData = pushContactData.contactData;
this.pushSupport = pushContactData.pushSupport;
if (!pushSupport) {
this.name.setTextColor(drawables.getColor(1, 0xff000000));
this.number.setTextColor(drawables.getColor(1, 0xff000000));
this.pushLabel.setBackgroundColor(drawables.getColor(3, 0x99000000));
} else {
this.name.setTextColor(drawables.getColor(0, 0xa0000000));
this.number.setTextColor(drawables.getColor(0, 0xa0000000));
this.pushLabel.setBackgroundColor(drawables.getColor(2, 0xff64a926));
}
if (selectedContacts.containsKey(contactData.id))
this.checkBox.setChecked(true);
else
this.checkBox.setChecked(false);
this.name.setText(contactData.name);
if (contactData.numbers.isEmpty()) {
this.name.setEnabled(false);
this.number.setText("");
this.label.setText("");
} else {
this.number.setText(contactData.numbers.get(0).number);
this.label.setText(contactData.numbers.get(0).type);
}
}
}
private class DiscriminatorFinishedListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private final ContactData contactData;
private final TextView textView;
private final CheckBox checkBox;
public DiscriminatorFinishedListener(ContactData contactData, TextView textView, CheckBox checkBox) {
this.contactData = contactData;
this.textView = textView;
this.checkBox = checkBox;
}
public void onClick(DialogInterface dialog, int which) {
ContactData selected = selectedContacts.get(contactData.id);
if (selected == null && textView != null) {
if (textView != null) checkBox.setChecked(false);
} else if (selected.numbers.size() == 0) {
selectedContacts.remove(selected.id);
if (textView != null) checkBox.setChecked(false);
}
if (textView == null)
((CursorAdapter) getListView().getAdapter()).notifyDataSetChanged();
}
public void onCancel(DialogInterface dialog) {
onClick(dialog, 0);
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
private class DiscriminatorClickedListener implements DialogInterface.OnMultiChoiceClickListener {
private final ContactData contactData;
public DiscriminatorClickedListener(ContactData contactData) {
this.contactData = contactData;
}
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
Log.w("ContactSelectionListActivity", "Got checked: " + isChecked);
ContactData existing = selectedContacts.get(contactData.id);
if (existing == null) {
Log.w("ContactSelectionListActivity", "No existing contact data, creating...");
if (!isChecked)
throw new AssertionError("We shouldn't be unchecking data that doesn't exist.");
existing = new ContactData(contactData.id, contactData.name);
selectedContacts.put(existing.id, existing);
}
NumberData selectedData = contactData.numbers.get(which);
if (!isChecked) existing.numbers.remove(selectedData);
else existing.numbers.add(selectedData);
}
}
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContactAccessor.getInstance().getCursorLoaderForContactsWithNumbers(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
Cursor pushCursor = ContactAccessor.getInstance().getCursorForContactsWithPush(getActivity());
((CursorAdapter) getListAdapter()).changeCursor(new MergeCursor(new Cursor[]{pushCursor,cursor}));
((TextView)getView().findViewById(android.R.id.empty)).setText(R.string.contact_selection_group_activity__no_contacts);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter) getListAdapter()).changeCursor(null);
public interface OnContactSelectedListener {
public void onContactSelected(ContactData contactData);
}
}

View file

@ -1,269 +0,0 @@
/**
* Copyright (C) 2011 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.database.MergeCursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.actionbarsherlock.app.SherlockListFragment;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import java.util.Collections;
import java.util.HashMap;
/**
* Activity for selecting a list of contacts. Displayed inside
* a PushContactSelectionActivity tab frame, and ultimately called by
* ComposeMessageActivity for selecting a list of destination contacts.
*
* @author Moxie Marlinspike
*
*/
public class SingleContactSelectionListFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private final String TAG = SingleContactSelectionListFragment.class.getSimpleName();
private final int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
R.attr.contact_selection_lay_user,
R.attr.contact_selection_push_label,
R.attr.contact_selection_lay_label};
private static LayoutInflater li;
private OnContactSelectedListener onContactSelectedListener;
private TypedArray drawables;
@Override
public void onActivityCreated(Bundle icicle) {
super.onCreate(icicle);
li = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
initializeResources();
initializeCursor();
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.single_contact_selection_list_activity, container, false);
}
private void addSingleNumberContact(ContactData contactData) {
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contactData);
}
}
public void update() {
this.getLoaderManager().restartLoader(0, null, this);
}
private void addMultipleNumberContact(ContactData contactData, TextView textView) {
String[] options = new String[contactData.numbers.size()];
int i = 0;
for (NumberData option : contactData.numbers) {
options[i++] = option.type + " " + option.number;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.ContactSelectionlistFragment_select_for + " " + contactData.name);
builder.setSingleChoiceItems(options, -1, new DiscriminatorClickedListener(contactData));
//builder.setPositiveButton(android.R.string.ok, new DiscriminatorFinishedListener(contactData, textView));
builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView));
builder.show();
}
private void initializeCursor() {
final ContactSelectionListAdapter listAdapter = new ContactSelectionListAdapter(getActivity(), null);
setListAdapter(listAdapter);
this.getLoaderManager().initLoader(0, null, this);
}
private void initializeResources() {
this.getListView().setFocusable(true);
this.drawables = getActivity().obtainStyledAttributes(STYLE_ATTRIBUTES);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
((ContactItemView)v).selected();
}
private class ContactSelectionListAdapter extends CursorAdapter {
public ContactSelectionListAdapter(Context context, Cursor c) {
super(context, c);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ContactItemView view = new ContactItemView(context);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
boolean isPushUser;
try {
isPushUser = (cursor.getInt(cursor.getColumnIndexOrThrow(ContactAccessor.PUSH_COLUMN)) > 0);
} catch (IllegalArgumentException iae) {
isPushUser = false;
}
ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor);
PushContactData pushContactData = new PushContactData(contactData, isPushUser);
((ContactItemView)view).set(pushContactData);
}
}
private class PushContactData {
private final ContactData contactData;
private final boolean pushSupport;
public PushContactData(ContactData contactData, boolean pushSupport) {
this.contactData = contactData;
this.pushSupport = pushSupport;
}
}
private class ContactItemView extends RelativeLayout {
private ContactData contactData;
private boolean pushSupport;
private TextView name;
private TextView number;
private TextView label;
private View pushLabel;
public ContactItemView(Context context) {
super(context);
li.inflate(R.layout.single_contact_selection_list_item, this, true);
this.name = (TextView) findViewById(R.id.name);
this.number = (TextView) findViewById(R.id.number);
this.label = (TextView) findViewById(R.id.label);
this.pushLabel = findViewById(R.id.push_support_label);
}
public void selected() {
if (contactData.numbers.size() == 1) addSingleNumberContact(contactData);
else addMultipleNumberContact(contactData, name);
}
public void set(PushContactData pushContactData) {
this.contactData = pushContactData.contactData;
this.pushSupport = pushContactData.pushSupport;
if (!pushSupport) {
this.name.setTextColor(drawables.getColor(1, 0xff000000));
this.number.setTextColor(drawables.getColor(1, 0xff000000));
this.pushLabel.setBackgroundColor(drawables.getColor(3, 0x99000000));
} else {
this.name.setTextColor(drawables.getColor(0, 0xa0000000));
this.number.setTextColor(drawables.getColor(0, 0xa0000000));
this.pushLabel.setBackgroundColor(drawables.getColor(2, 0xff64a926));
}
this.name.setText(contactData.name);
if (contactData.numbers.isEmpty()) {
this.name.setEnabled(false);
this.number.setText("");
this.label.setText("");
} else {
this.number.setText(contactData.numbers.get(0).number);
this.label.setText(contactData.numbers.get(0).type);
}
}
}
private class DiscriminatorFinishedListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private final ContactData contactData;
private final TextView textView;
public DiscriminatorFinishedListener(ContactData contactData, TextView textView) {
this.contactData = contactData;
this.textView = textView;
}
public void onClick(DialogInterface dialog, int which) {
// ignore
}
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
}
}
private class DiscriminatorClickedListener implements DialogInterface.OnClickListener {
private final ContactData contactData;
public DiscriminatorClickedListener(ContactData contactData) {
this.contactData = contactData;
}
public void onClick(DialogInterface dialog, int which) {
ContactData singlePhoneContact = new ContactData(contactData.id,
contactData.name,
Collections.singletonList(contactData.numbers.get(which)));
addSingleNumberContact(singlePhoneContact);
}
}
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContactAccessor.getInstance().getCursorLoaderForContactsWithNumbers(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
Cursor pushCursor = ContactAccessor.getInstance().getCursorForContactsWithPush(getActivity());
((CursorAdapter) getListAdapter()).changeCursor(new MergeCursor(new Cursor[]{pushCursor,cursor}));
((TextView)getView().findViewById(android.R.id.empty)).setText(R.string.contact_selection_group_activity__no_contacts);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter) getListAdapter()).changeCursor(null);
}
public interface OnContactSelectedListener {
public void onContactSelected(ContactData contactData);
}
}

View file

@ -20,9 +20,7 @@ import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
@ -33,6 +31,7 @@ import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.PhoneLookup;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
@ -44,6 +43,7 @@ import org.whispersystems.textsecure.util.Base64;
import java.io.IOException;
import java.lang.Long;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@ -82,6 +82,10 @@ public class ContactAccessor {
null, null, null, ContactsContract.Groups.TITLE + " ASC");
}
public Loader<Cursor> getCursorLoaderForContacts(Context context) {
return new ContactsCursorLoader(context);
}
public Cursor getCursorForContactsWithNumbers(Context context) {
Uri uri = ContactsContract.Contacts.CONTENT_URI;
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1";
@ -90,25 +94,28 @@ public class ContactAccessor {
ContactsContract.Contacts.DISPLAY_NAME + " ASC");
}
public Cursor getCursorForContactsWithPush(Context context) {
public Collection<ContactData> getContactsWithPush(Context context) {
final ContentResolver resolver = context.getContentResolver();
final String[] inProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME};
final String[] outProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME, PUSH_COLUMN};
MatrixCursor cursor = new MatrixCursor(outProjection);
final String[] inProjection = new String[]{PhoneLookup._ID, PhoneLookup.DISPLAY_NAME};
List<String> pushNumbers = Directory.getInstance(context).getActiveNumbers();
final Collection<ContactData> lookupData = new ArrayList<ContactData>(pushNumbers.size());
for (String pushNumber : pushNumbers) {
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(pushNumber));
Cursor lookupCursor = resolver.query(uri, inProjection, null, null, null);
try {
if (lookupCursor != null && lookupCursor.moveToFirst()) {
cursor.addRow(new Object[]{lookupCursor.getLong(0), lookupCursor.getString(1), 1});
final ContactData contactData = new ContactData(lookupCursor.getLong(0), lookupCursor.getString(1));
contactData.numbers.add(new NumberData("TextSecure", pushNumber));
lookupData.add(contactData);
}
} finally {
if (lookupCursor != null)
lookupCursor.close();
}
}
return cursor;
return lookupData;
}
public String getNameFromContact(Context context, Uri uri) {

View file

@ -10,6 +10,7 @@ import android.provider.ContactsContract.Contacts;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.LRUCache;
import java.io.InputStream;
@ -82,10 +83,13 @@ public class ContactPhotoFactory {
localUserContactPhotoCache.remove(recipient.getContactUri());
}
private static Bitmap getContactPhoto(Context context, Uri uri) {
public static Bitmap getContactPhoto(Context context, Uri uri) {
InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
if (inputStream == null) return ContactPhotoFactory.getDefaultContactPhoto(context);
else return BitmapFactory.decodeStream(inputStream);
final Bitmap contactPhoto;
if (inputStream == null) contactPhoto = ContactPhotoFactory.getDefaultContactPhoto(context);
else contactPhoto = BitmapFactory.decodeStream(inputStream);
return contactPhoto;
}
}

View file

@ -0,0 +1,254 @@
/**
* Copyright (C) 2014 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.provider.ContactsContract;
import android.support.v4.widget.CursorAdapter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.BitmapWorkerRunnable;
import org.thoughtcrime.securesms.util.BitmapWorkerRunnable.AsyncDrawable;
import org.thoughtcrime.securesms.util.TaggedFutureTask;
import org.thoughtcrime.securesms.util.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
/**
* List adapter to display all contacts and their related information
*
* @author Jake McGinty
*/
public class ContactSelectionListAdapter extends CursorAdapter
implements StickyListHeadersAdapter
{
private final static String TAG = "ContactListAdapter";
private final static ExecutorService photoResolver = Util.newSingleThreadedLifoExecutor();
private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user,
R.attr.contact_selection_lay_user,
R.attr.contact_selection_label_text};
private int TYPE_COLUMN = -1;
private int NAME_COLUMN = -1;
private int NUMBER_COLUMN = -1;
private int NUMBER_TYPE_COLUMN = -1;
private int ID_COLUMN = -1;
private final Context context;
private final boolean multiSelect;
private final LayoutInflater li;
private final TypedArray drawables;
private final Bitmap defaultPhoto;
private final Bitmap defaultCroppedPhoto;
private final int scaledPhotoSize;
private final HashMap<Long, ContactAccessor.ContactData> selectedContacts = new HashMap<Long, ContactAccessor.ContactData>();
public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) {
super(context, cursor, 0);
this.context = context;
this.li = LayoutInflater.from(context);
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
this.multiSelect = multiSelect;
this.defaultPhoto = ContactPhotoFactory.getDefaultContactPhoto(context);
this.scaledPhotoSize = context.getResources().getDimensionPixelSize(R.dimen.contact_selection_photo_size);
this.defaultCroppedPhoto = BitmapUtil.getScaledCircleCroppedBitmap(defaultPhoto, scaledPhotoSize);
}
public static class ViewHolder {
public CheckBox checkBox;
public TextView name;
public TextView number;
public ImageView contactPhoto;
public int position;
}
public static class DataHolder {
public int type;
public String name;
public String number;
public int numberType;
public long id;
}
public static class HeaderViewHolder {
TextView text;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final View v = li.inflate(R.layout.push_contact_selection_list_item, parent, false);
final ViewHolder holder = new ViewHolder();
if (v != null) {
holder.name = (TextView) v.findViewById(R.id.name);
holder.number = (TextView) v.findViewById(R.id.number);
holder.checkBox = (CheckBox) v.findViewById(R.id.check_box);
holder.contactPhoto = (ImageView) v.findViewById(R.id.contact_photo_image);
if (!multiSelect) holder.checkBox.setVisibility(View.GONE);
v.setTag(R.id.holder_tag, holder);
v.setTag(R.id.contact_info_tag, new DataHolder());
}
return v;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
final DataHolder contactData = (DataHolder) view.getTag(R.id.contact_info_tag);
final ViewHolder holder = (ViewHolder) view.getTag(R.id.holder_tag);
if (holder == null) {
Log.w(TAG, "ViewHolder was null. This should not happen.");
return;
}
if (contactData == null) {
Log.w(TAG, "DataHolder was null. This should not happen.");
return;
}
if (ID_COLUMN < 0) {
populateColumnIndices(cursor);
}
contactData.type = cursor.getInt(TYPE_COLUMN);
contactData.name = cursor.getString(NAME_COLUMN);
contactData.number = cursor.getString(NUMBER_COLUMN);
contactData.numberType = cursor.getInt(NUMBER_TYPE_COLUMN);
contactData.id = cursor.getLong(ID_COLUMN);
if (contactData.type != ContactsDatabase.PUSH_TYPE) {
holder.name.setTextColor(drawables.getColor(1, 0xff000000));
holder.number.setTextColor(drawables.getColor(1, 0xff000000));
} else {
holder.name.setTextColor(drawables.getColor(0, 0xa0000000));
holder.number.setTextColor(drawables.getColor(0, 0xa0000000));
}
if (selectedContacts.containsKey(contactData.id)) {
holder.checkBox.setChecked(true);
} else {
holder.checkBox.setChecked(false);
}
holder.name.setText(contactData.name);
if (contactData.number == null || contactData.number.isEmpty()) {
holder.name.setEnabled(false);
holder.number.setText("");
} else if (contactData.type == ContactsDatabase.PUSH_TYPE) {
holder.number.setText(contactData.number);
} else {
final CharSequence label = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(),
contactData.numberType, "");
final CharSequence numberWithLabel = contactData.number + " " + label;
final Spannable numberLabelSpan = new SpannableString(numberWithLabel);
numberLabelSpan.setSpan(new ForegroundColorSpan(drawables.getColor(2, 0xff444444)), contactData.number.length(), numberWithLabel.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
holder.number.setText(numberLabelSpan);
}
holder.contactPhoto.setImageBitmap(defaultCroppedPhoto);
loadBitmap(contactData.number, holder.contactPhoto);
}
@Override
public View getHeaderView(int i, View convertView, ViewGroup viewGroup) {
final Cursor c = getCursor();
final HeaderViewHolder holder;
if (convertView == null) {
holder = new HeaderViewHolder();
convertView = li.inflate(R.layout.push_contact_selection_list_header, viewGroup, false);
holder.text = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = (HeaderViewHolder) convertView.getTag();
}
c.moveToPosition(i);
final int type = c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN));
final int headerTextRes;
switch (type) {
case 1: headerTextRes = R.string.contact_selection_list__header_textsecure_users; break;
default: headerTextRes = R.string.contact_selection_list__header_other; break;
}
holder.text.setText(headerTextRes);
return convertView;
}
@Override
public long getHeaderId(int i) {
final Cursor c = getCursor();
c.moveToPosition(i);
return c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN));
}
public boolean cancelPotentialWork(String number, ImageView imageView) {
final TaggedFutureTask<?> bitmapWorkerTask = AsyncDrawable.getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final Object tag = bitmapWorkerTask.getTag();
if (tag != null && !tag.equals(number)) {
bitmapWorkerTask.cancel(true);
} else {
return false;
}
}
return true;
}
public void loadBitmap(String number, ImageView imageView) {
if (cancelPotentialWork(number, imageView)) {
final BitmapWorkerRunnable runnable = new BitmapWorkerRunnable(context, imageView, defaultPhoto, number, scaledPhotoSize);
final TaggedFutureTask<?> task = new TaggedFutureTask<Void>(runnable, null, number);
final AsyncDrawable asyncDrawable = new AsyncDrawable(context.getResources(), defaultCroppedPhoto, task);
imageView.setImageDrawable(asyncDrawable);
if (!task.isCancelled()) photoResolver.execute(new FutureTask<Void>(task, null));
}
}
public Map<Long,ContactAccessor.ContactData> getSelectedContacts() {
return selectedContacts;
}
private void populateColumnIndices(final Cursor cursor) {
this.TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN);
this.NAME_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN);
this.NUMBER_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN);
this.NUMBER_TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN);
this.ID_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN);
}
}

View file

@ -0,0 +1,49 @@
/**
* Copyright (C) 2013 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.CursorLoader;
/**
* CursorLoader that initializes a ContactsDatabase instance
*
* @author Jake McGinty
*/
public class ContactsCursorLoader extends CursorLoader {
private final Context context;
private ContactsDatabase db;
public ContactsCursorLoader(Context context) {
super(context);
this.context = context;
}
@Override
public Cursor loadInBackground() {
db = new ContactsDatabase(context);
return db.getAllContacts();
}
@Override
public void onReset() {
super.onReset();
db.close();
}
}

View file

@ -0,0 +1,211 @@
/**
* Copyright (C) 2013 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.contacts;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.ContactsContract;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.util.Collection;
/**
* Database to supply all types of contacts that TextSecure needs to know about
*
* @author Jake McGinty
*/
public class ContactsDatabase {
private static final String TAG = ContactsDatabase.class.getSimpleName();
private final DatabaseOpenHelper dbHelper;
private final Context context;
public static final String TABLE_NAME = "CONTACTS";
public static final String ID_COLUMN = ContactsContract.CommonDataKinds.Phone._ID;
public static final String NAME_COLUMN = ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME;
public static final String NUMBER_TYPE_COLUMN = ContactsContract.CommonDataKinds.Phone.TYPE;
public static final String NUMBER_COLUMN = ContactsContract.CommonDataKinds.Phone.NUMBER;
public static final String TYPE_COLUMN = "type";
private static final String CONTACT_LIST_SORT = NAME_COLUMN + " ASC";
private static final String[] ANDROID_PROJECTION = new String[]{ID_COLUMN,
NAME_COLUMN,
NUMBER_TYPE_COLUMN,
NUMBER_COLUMN};
public static final int NORMAL_TYPE = 0;
public static final int PUSH_TYPE = 1;
public static final int GROUP_TYPE = 2;
public ContactsDatabase(Context context) {
this.dbHelper = new DatabaseOpenHelper(context);
this.context = context;
}
public void close() {
dbHelper.close();
}
public Cursor getAllContacts() {
return query(null, null, null);
}
private Cursor query(String selection, String[] selectionArgs, String[] columns) {
final Cursor localCursor = queryLocalDb(selection, selectionArgs, columns);
final Cursor androidCursor;
if (TextSecurePreferences.isSmsNonDataOutEnabled(context)) {
androidCursor = queryAndroidDb();
} else{
return localCursor;
}
if (localCursor != null && androidCursor != null) return new MergeCursor(new Cursor[]{localCursor,androidCursor});
else if (localCursor != null) return localCursor;
else if (androidCursor != null) return androidCursor;
else return null;
}
private Cursor queryAndroidDb() {
Cursor cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, ANDROID_PROJECTION, null, null, CONTACT_LIST_SORT);
return new TypedCursorWrapper(cursor);
}
private Cursor queryLocalDb(String selection, String[] selectionArgs, String[] columns) {
SQLiteDatabase localDb = dbHelper.getReadableDatabase();
final Cursor localCursor;
if (localDb != null) localCursor = localDb.query(TABLE_NAME, columns, selection, selectionArgs, null, null, CONTACT_LIST_SORT);
else localCursor = null;
if (localCursor != null && !localCursor.moveToFirst()) {
localCursor.close();
return null;
}
return localCursor;
}
private static class DatabaseOpenHelper extends SQLiteOpenHelper {
private final Context context;
private SQLiteDatabase mDatabase;
private static final String TABLE_CREATE =
"CREATE TABLE " + TABLE_NAME + " (" +
ID_COLUMN + " INTEGER PRIMARY KEY, " +
NAME_COLUMN + " TEXT, " +
NUMBER_TYPE_COLUMN + " INTEGER, " +
NUMBER_COLUMN + " TEXT, " +
TYPE_COLUMN + " INTEGER);";
DatabaseOpenHelper(Context context) {
super(context, null, null, 1);
this.context = context;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.d(TAG, "onCreate called for contacts database.");
mDatabase = db;
mDatabase.execSQL(TABLE_CREATE);
if (TextSecurePreferences.isPushRegistered(context)) {
try {
loadPushUsers();
} catch (IOException ioe) {
Log.e(TAG, "Issue when trying to load push users into memory db.", ioe);
}
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
private void loadPushUsers() throws IOException {
Log.d(TAG, "populating push users into virtual db.");
Collection<ContactAccessor.ContactData> pushUsers = ContactAccessor.getInstance().getContactsWithPush(context);
for (ContactAccessor.ContactData user : pushUsers) {
ContentValues values = new ContentValues();
values.put(ID_COLUMN, user.id);
values.put(NAME_COLUMN, user.name);
values.put(NUMBER_TYPE_COLUMN, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM);
values.put(NUMBER_COLUMN, user.numbers.get(0).number);
values.put(TYPE_COLUMN, PUSH_TYPE);
mDatabase.insert(TABLE_NAME, null, values);
}
Log.d(TAG, "finished populating push users.");
}
}
private static class TypedCursorWrapper extends CursorWrapper {
private final int pushColumnIndex;
public TypedCursorWrapper(Cursor cursor) {
super(cursor);
pushColumnIndex = cursor.getColumnCount();
}
@Override
public int getColumnCount() {
return super.getColumnCount() + 1;
}
@Override
public int getColumnIndex(String columnName) {
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount();
else return super.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
if (TYPE_COLUMN.equals(columnName)) return super.getColumnCount();
else return super.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
if (columnIndex == pushColumnIndex) return TYPE_COLUMN;
else return super.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
final String[] columns = new String[super.getColumnCount() + 1];
System.arraycopy(super.getColumnNames(), 0, columns, 0, super.getColumnCount());
columns[pushColumnIndex] = TYPE_COLUMN;
return columns;
}
@Override
public int getInt(int columnIndex) {
if (columnIndex == pushColumnIndex) return NORMAL_TYPE;
else return super.getInt(columnIndex);
}
}
}

View file

@ -100,8 +100,8 @@ public class Recipient implements Parcelable, CanonicalRecipient {
this.number = in.readString();
this.name = in.readString();
this.recipientId = in.readLong();
this.contactUri = (Uri)in.readParcelable(null);
this.contactPhoto = (Bitmap)in.readParcelable(null);
this.contactUri = in.readParcelable(null);
this.contactPhoto = in.readParcelable(null);
}
public synchronized Uri getContactUri() {
@ -138,19 +138,6 @@ public class Recipient implements Parcelable, CanonicalRecipient {
return GroupUtil.isEncodedGroup(number);
}
// public void updateAsynchronousContent(RecipientDetails result) {
// if (result != null) {
// Recipient.this.name.set(result.name);
// Recipient.this.contactUri.set(result.contactUri);
// Recipient.this.contactPhoto.set(result.avatar);
//
// synchronized(this) {
// if (listener == null) asynchronousUpdateComplete = true;
// else listener.onModified(Recipient.this);
// }
// }
// }
public synchronized void addListener(RecipientModifiedListener listener) {
listeners.add(listener);
}

View file

@ -129,8 +129,8 @@ public class RecipientProvider {
try {
if (cursor != null && cursor.moveToFirst()) {
Uri contactUri = Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1));
Bitmap contactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
cursor.getLong(2)+""));
Bitmap contactPhoto = ContactPhotoFactory.getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
cursor.getLong(2)+""));
return new RecipientDetails(cursor.getString(0), contactUri, contactPhoto);
}
@ -164,15 +164,6 @@ public class RecipientProvider {
}
}
private Bitmap getContactPhoto(Context context, Uri uri) {
InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
if (inputStream == null)
return ContactPhotoFactory.getDefaultContactPhoto(context);
else
return BitmapFactory.decodeStream(inputStream);
}
public static class RecipientDetails {
public final String name;
public final Bitmap avatar;

View file

@ -0,0 +1,116 @@
/**
* Copyright (C) 2014 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import java.lang.ref.WeakReference;
/**
* Runnable to load contact photos if they have them
*
* @author Jake McGinty
*/
public class BitmapWorkerRunnable implements Runnable {
private final static String TAG = BitmapWorkerRunnable.class.getSimpleName();
private final Bitmap defaultPhoto;
private final WeakReference<ImageView> imageViewReference;
private final Context context;
private final int size;
public final String number;
public BitmapWorkerRunnable(Context context, ImageView imageView, Bitmap defaultPhoto, String number, int size) {
this.imageViewReference = new WeakReference<ImageView>(imageView);
this.context = context;
this.defaultPhoto = defaultPhoto;
this.size = size;
this.number = number;
}
@Override
public void run() {
final Bitmap bitmap;
try {
final Recipient recipient = RecipientFactory.getRecipientsFromString(context, number, false).getPrimaryRecipient();
final Bitmap contactPhoto = recipient.getContactPhoto();
if (defaultPhoto == contactPhoto) {
return;
}
bitmap = BitmapUtil.getScaledCircleCroppedBitmap(contactPhoto, size);
} catch (RecipientFormattingException rfe) {
Log.w(TAG, "Couldn't get recipient from string", rfe);
return;
}
if (bitmap != null) {
final ImageView imageView = imageViewReference.get();
final TaggedFutureTask<?> bitmapWorkerTask = AsyncDrawable.getBitmapWorkerTask(imageView);
if (bitmapWorkerTask.getTag().equals(number) && imageView != null) {
final BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);
imageView.post(new Runnable() {
@Override
public void run() {
imageView.setImageDrawable(drawable);
}
});
}
}
}
public static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<TaggedFutureTask<?>> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
TaggedFutureTask<?> bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<TaggedFutureTask<?>>(bitmapWorkerTask);
}
public TaggedFutureTask<?> getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
public static TaggedFutureTask<?> getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
}
}

View file

@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.textsecure.directory.Directory;
@ -19,6 +21,37 @@ import java.util.Set;
public class DirectoryHelper {
private static final String TAG = DirectoryHelper.class.getSimpleName();
public static void refreshDirectoryWithProgressDialog(final Context context) {
refreshDirectoryWithProgressDialog(context, null);
}
public static void refreshDirectoryWithProgressDialog(final Context context, final DirectoryUpdateFinishedListener listener) {
if (!TextSecurePreferences.isPushRegistered(context)) {
Toast.makeText(context.getApplicationContext(),
context.getString(R.string.SingleContactSelectionActivity_you_are_not_registered_with_the_push_service),
Toast.LENGTH_LONG).show();
return;
}
new ProgressDialogAsyncTask<Void,Void,Void>(context,
R.string.SingleContactSelectionActivity_updating_directory,
R.string.SingleContactSelectionActivity_updating_push_directory)
{
@Override
protected Void doInBackground(Void... voids) {
DirectoryHelper.refreshDirectory(context.getApplicationContext());
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
if (listener != null) listener.onUpdateFinished();
}
}.execute();
}
public static void refreshDirectory(final Context context) {
refreshDirectory(context, PushServiceSocketFactory.create(context));
}
@ -63,4 +96,8 @@ public class DirectoryHelper {
return false;
}
}
public static interface DirectoryUpdateFinishedListener {
public void onUpdateFinished();
}
}

View file

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.util;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
private final Context context;
private ProgressDialog progress;
private final String title;
private final String message;
public ProgressDialogAsyncTask(Context context, String title, String message) {
super();
this.context = context;
this.title = title;
this.message = message;
}
public ProgressDialogAsyncTask(Context context, int title, int message) {
this(context, context.getString(title), context.getString(message));
}
@Override
protected void onPreExecute() {
progress = ProgressDialog.show(context, title, message, true);
}
@Override
protected void onPostExecute(Result result) {
if (progress != null) progress.dismiss();
}
}

View file

@ -0,0 +1,43 @@
/**
* Copyright (C) 2014 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.util;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* FutureTask with a reference identifier tag.
*
* @author Jake McGinty
*/
public class TaggedFutureTask<V> extends FutureTask<V> {
private final Object tag;
public TaggedFutureTask(Runnable runnable, V result, Object tag) {
super(runnable, result);
this.tag = tag;
}
public TaggedFutureTask(Callable<V> callable, Object tag) {
super(callable);
this.tag = tag;
}
public Object getTag() {
return tag;
}
}