Fix the look of tabbed multi-contact selector.

1) Updated to ActionBar style.
2) Split out into fragments.
3) Switch to cursor loaders.
This commit is contained in:
Moxie Marlinspike
2012-07-20 22:23:25 -07:00
parent c7e891eda4
commit 863e1c6508
31 changed files with 743 additions and 1040 deletions

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,54 +10,66 @@
* 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 org.thoughtcrime.securesms.service.KeyCachingService;
import android.app.TabActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Window;
import android.widget.TabHost;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.ActionBar.Tab;
import com.actionbarsherlock.app.ActionBar.TabListener;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
/**
* 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.
*
*
* @author Moxie Marlinspike
*
*/
public class ContactSelectionActivity extends SherlockFragmentActivity {
public class ContactSelectionActivity extends TabActivity implements TabHost.OnTabChangeListener {
private ContactSelectionListFragment contactsFragment;
private ContactSelectionGroupsFragment groupsFragment;
private ContactSelectionRecentFragment recentFragment;
private TabHost tabHost;
private Recipients recipients;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.contact_selection_activity);
ActionBar actionBar = this.getSupportActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
actionBar.setDisplayHomeAsUpEnabled(true);
tabHost = getTabHost();
tabHost.setOnTabChangedListener(this);
setContentView(R.layout.contact_selection_activity);
setupContactsTab();
setupGroupsTab();
setupRecentTab();
}
@Override
protected void onStart() {
super.onStart();
registerPassphraseActivityStarted();
}
@Override
protected void onStop() {
super.onStop();
@@ -65,48 +77,102 @@ public class ContactSelectionActivity extends TabActivity implements TabHost.OnT
}
@Override
public boolean onSearchRequested() {
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = this.getSupportMenuInflater();
inflater.inflate(R.menu.contact_selection, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_selection_finished:
case android.R.id.home:
handleSelectionFinished(); return true;
}
return false;
}
private void handleSelectionFinished() {
recipients = contactsFragment.getSelectedContacts();
recipients.append(recentFragment.getSelectedContacts());
recipients.append(groupsFragment.getSelectedContacts());
Intent resultIntent = getIntent();
resultIntent.putExtra("recipients", this.recipients);
setResult(RESULT_OK, resultIntent);
finish();
}
private ActionBar.Tab constructTab(final Fragment fragment) {
ActionBar actionBar = this.getSupportActionBar();
ActionBar.Tab tab = actionBar.newTab();
tab.setTabListener(new TabListener(){
@Override
public void onTabSelected(Tab tab, FragmentTransaction ignore) {
FragmentManager manager = ContactSelectionActivity.this.getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.add(R.id.fragment_container, fragment);
ft.commit();
}
@Override
public void onTabUnselected(Tab tab, FragmentTransaction ignore) {
FragmentManager manager = ContactSelectionActivity.this.getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.remove(fragment);
ft.commit();
}
@Override
public void onTabReselected(Tab tab, FragmentTransaction ft) {}
});
return tab;
}
private void setupContactsTab() {
contactsFragment = (ContactSelectionListFragment)Fragment.instantiate(this,
ContactSelectionListFragment.class.getName());
ActionBar.Tab contactsTab = constructTab(contactsFragment);
contactsTab.setIcon(R.drawable.ic_tab_contacts);
this.getSupportActionBar().addTab(contactsTab);
}
private void setupGroupsTab() {
groupsFragment = (ContactSelectionGroupsFragment)Fragment.instantiate(this,
ContactSelectionGroupsFragment.class.getName());
ActionBar.Tab groupsTab = constructTab(groupsFragment);
groupsTab.setIcon(R.drawable.ic_tab_groups);
this.getSupportActionBar().addTab(groupsTab);
}
private void setupRecentTab() {
recentFragment = (ContactSelectionRecentFragment)Fragment.instantiate(this,
ContactSelectionRecentFragment.class.getName());
ActionBar.Tab recentTab = constructTab(recentFragment);
recentTab.setIcon(R.drawable.ic_tab_recent);
this.getSupportActionBar().addTab(recentTab);
}
private void registerPassphraseActivityStarted() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_START_EVENT);
startService(intent);
startService(intent);
}
private void registerPassphraseActivityStopped() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.ACTIVITY_STOP_EVENT);
startService(intent);
}
private void setupGroupsTab() {
Intent intent = new Intent(this, GroupSelectionListActivity.class);
tabHost.addTab(tabHost.newTabSpec("groups")
.setIndicator("Groups", getResources().getDrawable(android.R.drawable.ic_menu_share))
.setContent(intent));
}
private void setupRecentTab() {
Intent intent = new Intent(this, ContactSelectionRecentActivity.class);
tabHost.addTab(tabHost.newTabSpec("recent")
.setIndicator("Recent", getResources().getDrawable(android.R.drawable.ic_menu_call))
.setContent(intent));
}
private void setupContactsTab() {
Intent intent = new Intent(this, ContactSelectionListActivity.class);
tabHost.addTab(tabHost.newTabSpec("contacts")
.setIndicator("Contacts", getResources().getDrawable(android.R.drawable.ic_menu_agenda))
.setContent(intent));
}
public void onTabChanged(String tabId) {
}
}

View File

@@ -0,0 +1,231 @@
/**
* 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.content.Context;
import android.database.Cursor;
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.CheckedTextView;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import com.actionbarsherlock.app.SherlockListFragment;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.GroupData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* An activity for selecting a list of "contact groups." Displayed
* by ContactSelectionActivity in a tabbed frame, and ultimately called
* by ComposeMessageActivity for selecting a list of recipients.
*
* @author Moxie Marlinspike
*
*/
public class ContactSelectionGroupsFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private final HashMap<Long, GroupData> selectedGroups = new HashMap<Long, GroupData>();
@Override
public void onActivityCreated(Bundle icicle) {
super.onActivityCreated(icicle);
initializeResources();
initializeCursor();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.contact_selection_group_activity, container, false);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
((GroupItemView)v).selected();
}
private void initializeCursor() {
setListAdapter(new GroupSelectionListAdapter(getActivity(), null));
this.getLoaderManager().initLoader(0, null, this);
}
private void initializeResources() {
this.getListView().setFocusable(true);
}
public Recipients getSelectedContacts() {
List<Recipient> recipientList = new LinkedList<Recipient>();
for (GroupData groupData : selectedGroups.values()) {
List<ContactData> contactDataList = ContactAccessor.getInstance()
.getGroupMembership(getActivity(), groupData.id);
Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size());
for (ContactData contactData : contactDataList) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
}
}
}
return new Recipients(recipientList);
}
private void addGroup(GroupData groupData) {
selectedGroups.put(groupData.id, groupData);
}
private void removeGroup(GroupData groupData) {
selectedGroups.remove(groupData.id);
}
private class GroupSelectionListAdapter extends CursorAdapter {
public GroupSelectionListAdapter(Context context, Cursor cursor) {
super(context, cursor);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
GroupItemView view = new GroupItemView(context);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
GroupData groupData = ContactAccessor.getInstance().getGroupData(getActivity(), cursor);
((GroupItemView)view).set(groupData);
}
}
private class GroupItemView extends LinearLayout {
private GroupData groupData;
private CheckedTextView name;
public GroupItemView(Context context) {
super(context);
LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
li.inflate(R.layout.contact_selection_group_item, this, true);
this.name = (CheckedTextView)findViewById(R.id.name);
}
public void selected() {
name.toggle();
if (name.isChecked()) {
addGroup(groupData);
} else {
removeGroup(groupData);
}
}
public void set(GroupData groupData) {
this.groupData = groupData;
if (selectedGroups.containsKey(groupData.id))
this.name.setChecked(true);
else
this.name.setChecked(false);
this.name.setText(groupData.name);
}
}
// private class GroupAggregationHandler extends Handler implements Runnable {
// private List<Recipient> recipientList = new LinkedList<Recipient>();
// private ProgressDialog progressDialog;
// private final Context context;
//
// public GroupAggregationHandler(Context context) {
// this.context = context;
// }
//
// public void run() {
// recipientList.clear();
//
// for (GroupData groupData : selectedGroups.values()) {
// List<ContactData> contactDataList = ContactAccessor.getInstance()
// .getGroupMembership(getActivity(), groupData.id);
//
// Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size());
//
// for (ContactData contactData : contactDataList) {
// for (NumberData numberData : contactData.numbers) {
// recipientList.add(new Recipient(contactData.name, numberData.number, null));
// }
// }
// }
//
// this.obtainMessage().sendToTarget();
// }
//
// public void aggregateContacts() {
// progressDialog = new ProgressDialog(context);
// progressDialog.setTitle("Aggregating Contacts");
// progressDialog.setMessage("Aggregating group contacts...");
// progressDialog.setCancelable(false);
// progressDialog.setIndeterminate(true);
// progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
// progressDialog.show();
// Log.w("GroupSelectionListActivity", "Showing group spinner...");
// new Thread(this).start();
// }
//
// @Override
// public void handleMessage(Message message) {
// progressDialog.dismiss();
//
// listener.groupAggregationComplete(new Recipients(recipientList));
// }
// }
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContactAccessor.getInstance().getCursorLoaderForContactGroups(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
((CursorAdapter)getListAdapter()).changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter)getListAdapter()).changeCursor(null);
}
}

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,35 +10,22 @@
* 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 java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
@@ -47,85 +34,97 @@ import android.widget.ListView;
import android.widget.RelativeLayout;
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.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Activity for selecting a list of contacts. Displayed inside
* a ContactSelectionActivity tab frame, and ultimately called by
* ComposeMessageActivity for selecting a list of destination contacts.
*
*
* @author Moxie Marlinspike
*
*/
public class ContactSelectionListActivity extends ListActivity {
private final HashMap<Long, ContactData> selectedContacts = new HashMap<Long, ContactData>();
public class ContactSelectionListFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private final HashMap<Long, ContactData> selectedContacts = new HashMap<Long, ContactData>();
private static final int MENU_OPTION_EXIT = 1;
private static final int MENU_OPTION_SELECT_ALL = 2;
private static final int MENU_OPTION_UNSELECT_ALL = 3;
@Override
protected void onCreate(Bundle icicle) {
public void onActivityCreated(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.contact_selection_list_activity);
initializeResources();
displayContacts();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as);
menu.add(0, MENU_OPTION_SELECT_ALL, Menu.NONE, "Select all").setIcon(android.R.drawable.ic_menu_add);
menu.add(0, MENU_OPTION_UNSELECT_ALL, Menu.NONE, "Unselect all").setIcon(android.R.drawable.ic_menu_revert);
return true;
initializeResources();
initializeCursor();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.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) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case MENU_OPTION_EXIT: saveAndExit(); return true;
case MENU_OPTION_SELECT_ALL: selectAll(); return true;
case MENU_OPTION_UNSELECT_ALL: unselectAll(); return true;
case R.id.menu_select_all: handleSelectAll(); return true;
case R.id.menu_unselect_all: handleUnselectAll(); return true;
}
super.onOptionsItemSelected(item);
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
saveAndExit();
return true;
public Recipients getSelectedContacts() {
List<Recipient> recipientList = new LinkedList<Recipient>();
for (ContactData contactData : selectedContacts.values()) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
}
}
return super.onKeyDown(keyCode, event);
return new Recipients(recipientList);
}
private void unselectAll() {
private void handleUnselectAll() {
selectedContacts.clear();
((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged();
}
private void selectAll() {
Log.w("ContactSelectionListActivity", "Selecting all...");
private void handleSelectAll() {
selectedContacts.clear();
Cursor cursor = null;
try {
cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(this);
cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(getActivity());
while (cursor != null && cursor.moveToNext()) {
ContactData contactData = ContactAccessor.getInstance().getContactData(this, cursor);
ContactData contactData = ContactAccessor.getInstance().getContactData(getActivity(), cursor);
if (contactData.numbers.isEmpty()) continue;
else if (contactData.numbers.size() == 1) addSingleNumberContact(contactData);
else addMultipleNumberContact(contactData, null);
@@ -137,66 +136,47 @@ public class ContactSelectionListActivity extends ListActivity {
((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged();
}
private void saveAndExit() {
List<Recipient> recipientList = new LinkedList<Recipient>();
for (ContactData contactData : selectedContacts.values()) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
}
}
Intent resultIntent = getIntent();
resultIntent.putExtra("recipients", new Recipients(recipientList));
if (getParent() == null) setResult(RESULT_OK, resultIntent);
else getParent().setResult(RESULT_OK, resultIntent);
finish();
}
private void addSingleNumberContact(ContactData contactData) {
selectedContacts.put(contactData.id, contactData);
}
private void removeContact(ContactData contactData) {
selectedContacts.remove(contactData.id);
}
private void addMultipleNumberContact(ContactData contactData, CheckedTextView 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(this);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle("Select for " + contactData.name);
builder.setMultiChoiceItems(options, null, new DiscriminatorClickedListener(contactData));
builder.setPositiveButton("Ok", new DiscriminatorFinishedListener(contactData, textView));
builder.setOnCancelListener(new DiscriminatorFinishedListener(contactData, textView));
builder.show();
}
private void displayContacts() {
Cursor cursor = ContactAccessor.getInstance().getCursorForContactsWithNumbers(this);
startManagingCursor(cursor);
setListAdapter(new ContactSelectionListAdapter(this, cursor));
private void initializeCursor() {
setListAdapter(new ContactSelectionListAdapter(getActivity(), null));
this.getLoaderManager().initLoader(0, null, this);
}
private void initializeResources() {
this.getListView().setFocusable(true);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
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);
}
@@ -213,9 +193,9 @@ public class ContactSelectionListActivity extends ListActivity {
public void bindView(View view, Context context, Cursor cursor) {
ContactData contactData = ContactAccessor.getInstance().getContactData(context, cursor);
((ContactItemView)view).set(contactData);
}
}
}
}
private class ContactItemView extends RelativeLayout {
private ContactData contactData;
private CheckedTextView name;
@@ -236,40 +216,40 @@ public class ContactSelectionListActivity extends ListActivity {
public void selected() {
name.toggle();
if (name.isChecked()) {
if (contactData.numbers.size() == 1) addSingleNumberContact(contactData);
else addMultipleNumberContact(contactData, name);
} else {
removeContact(contactData);
}
}
}
public void set(ContactData contactData) {
this.contactData = contactData;
if (selectedContacts.containsKey(contactData.id))
this.name.setChecked(true);
else
this.name.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);
this.label.setText(contactData.numbers.get(0).type);
}
}
}
private class DiscriminatorFinishedListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private final ContactData contactData;
private final CheckedTextView textView;
public DiscriminatorFinishedListener(ContactData contactData, CheckedTextView textView) {
this.contactData = contactData;
this.textView = textView;
@@ -277,14 +257,14 @@ public class ContactSelectionListActivity extends ListActivity {
public void onClick(DialogInterface dialog, int which) {
ContactData selected = selectedContacts.get(contactData.id);
if (selected == null && textView != null) {
if (textView != null) textView.setChecked(false);
} else if (selected.numbers.size() == 0) {
selectedContacts.remove(selected.id);
if (textView != null) textView.setChecked(false);
}
if (textView == null)
((CursorAdapter)getListView().getAdapter()).notifyDataSetChanged();
}
@@ -293,38 +273,52 @@ public class ContactSelectionListActivity extends ListActivity {
onClick(dialog, 0);
}
}
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();
existing.id = contactData.id;
existing.name = contactData.name;
existing.numbers = new LinkedList<NumberData>();
selectedContacts.put(existing.id, existing);
}
NumberData selectedData = contactData.numbers.get(which);
if (!isChecked) existing.numbers.remove(selectedData);
else existing.numbers.add(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) {
((CursorAdapter)getListAdapter()).changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter)getListAdapter()).changeCursor(null);
}
}

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,35 +10,21 @@
* 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 java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.RedPhoneCallTypes;
import android.app.ListActivity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.CallLog.Calls;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
@@ -48,102 +34,82 @@ 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 org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.RedPhoneCallTypes;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Displays a list of recently used contacts for multi-select. Displayed
* by the ContactSelectionActivity in a tab frame, and ultimately used by
* ComposeMessageActivity for selecting destination message contacts.
*
*
* @author Moxie Marlinspike
*
*/
public class ContactSelectionRecentActivity extends ListActivity {
private final HashMap<Long, ContactData> selectedContacts = new HashMap<Long, ContactData>();
public class ContactSelectionRecentFragment extends SherlockListFragment
implements LoaderManager.LoaderCallbacks<Cursor>
{
private final HashMap<Long, ContactData> selectedContacts = new HashMap<Long, ContactData>();
private static final int MENU_OPTION_EXIT = 1;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.contact_selection_recent_activity);
public void onActivityCreated(Bundle icicle) {
super.onActivityCreated(icicle);
initializeResources();
displayContacts();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as);
return true;
initializeCursor();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case MENU_OPTION_EXIT: saveAndExit(); return true;
}
return false;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.contact_selection_recent_activity, container, false);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
saveAndExit();
return true;
}
return super.onKeyDown(keyCode, event);
public void onListItemClick(ListView l, View v, int position, long id) {
((CallItemView)v).selected();
}
private void saveAndExit() {
private void initializeCursor() {
setListAdapter(new ContactSelectionListAdapter(getActivity(), null));
this.getLoaderManager().initLoader(0, null, this);
}
public Recipients getSelectedContacts() {
List<Recipient> recipientList = new LinkedList<Recipient>();
for (ContactData contactData : selectedContacts.values()) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
}
}
Intent resultIntent = getIntent();
resultIntent.putExtra("recipients", new Recipients(recipientList));
if (getParent() == null) setResult(RESULT_OK, resultIntent);
else getParent().setResult(RESULT_OK, resultIntent);
finish();
return new Recipients(recipientList);
}
private void addSingleNumberContact(ContactData contactData) {
selectedContacts.put(contactData.id, contactData);
}
private void removeContact(ContactData contactData) {
selectedContacts.remove(contactData.id);
}
private void displayContacts() {
Cursor cursor = getContentResolver().query(Calls.CONTENT_URI, null, null, null, Calls.DEFAULT_SORT_ORDER);
startManagingCursor(cursor);
setListAdapter(new ContactSelectionListAdapter(this, cursor));
}
private void initializeResources() {
this.getListView().setFocusable(true);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
((CallItemView)v).selected();
}
private class ContactSelectionListAdapter extends CursorAdapter {
public ContactSelectionListAdapter(Context context, Cursor c) {
super(context, c);
}
@@ -164,11 +130,11 @@ public class ContactSelectionRecentActivity extends ListActivity {
String number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE));
long date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
((CallItemView)view).set(id, name, label, number, type, date);
}
}
}
}
private class CallItemView extends RelativeLayout {
private ContactData contactData;
private Context context;
@@ -176,7 +142,7 @@ public class ContactSelectionRecentActivity extends ListActivity {
private TextView date;
private TextView label;
private TextView number;
private CheckedTextView line1;
private CheckedTextView line1;
public CallItemView(Context context) {
super(context);
@@ -194,41 +160,59 @@ public class ContactSelectionRecentActivity extends ListActivity {
public void selected() {
line1.toggle();
if (line1.isChecked()) {
addSingleNumberContact(contactData);
} else {
removeContact(contactData);
}
}
}
public void set(long id, String name, String label, String number, int type, long date) {
if( name == null ) {
name = ContactAccessor.getInstance().getNameForNumber(ContactSelectionRecentActivity.this, number);
name = ContactAccessor.getInstance().getNameForNumber(getActivity(), number);
}
this.line1.setText((name == null || name.equals("")) ? number : name);
this.number.setText((name == null || name.equals("")) ? "" : number);
this.label.setText(label);
this.date.setText(DateUtils.getRelativeDateTimeString(context, date, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE));
if (type == Calls.INCOMING_TYPE || type == RedPhoneCallTypes.INCOMING) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_incoming_call));
else if (type == Calls.OUTGOING_TYPE || type == RedPhoneCallTypes.OUTGOING) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_outgoing_call));
else if (type == Calls.MISSED_TYPE || type == RedPhoneCallTypes.MISSED) callTypeIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_call_log_list_missed_call));
this.contactData = new ContactData();
if (name != null)
this.contactData.name = name;
this.contactData.id = id;
this.contactData.numbers = new LinkedList<ContactAccessor.NumberData>();
this.contactData.numbers.add(new NumberData(null, number));
if (selectedContacts.containsKey(id))
this.line1.setChecked(true);
else
this.line1.setChecked(false);
}
}
}
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(getActivity(), Calls.CONTENT_URI,
null, null, null,
Calls.DEFAULT_SORT_ORDER);
}
@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
((CursorAdapter)getListAdapter()).changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) {
((CursorAdapter)getListAdapter()).changeCursor(null);
}
}

View File

@@ -1,236 +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 java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.GroupData;
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckedTextView;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
/**
* An activity for selecting a list of "contact groups." Displayed
* by ContactSelectionActivity in a tabbed frame, and ultimately called
* by ComposeMessageActivity for selecting a list of recipients.
*
* @author Moxie Marlinspike
*
*/
public class GroupSelectionListActivity extends ListActivity {
private final HashMap<Long, GroupData> selectedGroups = new HashMap<Long, GroupData>();
private static final int MENU_OPTION_EXIT = 1;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.contact_selection_group_activity);
initializeResources();
displayGroups();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
menu.add(0, MENU_OPTION_EXIT, Menu.NONE, "Finished!").setIcon(android.R.drawable.ic_menu_set_as);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case MENU_OPTION_EXIT: saveAndExit(); return true;
}
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
saveAndExit();
return true;
}
return super.onKeyDown(keyCode, event);
}
private void saveAndExit() {
GroupAggregationHandler aggregator = new GroupAggregationHandler();
aggregator.aggregateContactsAndExit();
}
private void addGroup(GroupData groupData) {
selectedGroups.put(groupData.id, groupData);
}
private void removeGroup(GroupData groupData) {
selectedGroups.remove(groupData.id);
}
private void displayGroups() {
Cursor cursor = ContactAccessor.getInstance().getCursorForContactGroups(this);
startManagingCursor(cursor);
setListAdapter(new GroupSelectionListAdapter(this, cursor));
}
private void initializeResources() {
this.getListView().setFocusable(true);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
((GroupItemView)v).selected();
}
private class GroupSelectionListAdapter extends CursorAdapter {
public GroupSelectionListAdapter(Context context, Cursor cursor) {
super(context, cursor);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
GroupItemView view = new GroupItemView(context);
bindView(view, context, cursor);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
GroupData groupData = ContactAccessor.getInstance().getGroupData(GroupSelectionListActivity.this, cursor);
((GroupItemView)view).set(groupData);
}
}
private class GroupItemView extends LinearLayout {
private GroupData groupData;
private CheckedTextView name;
public GroupItemView(Context context) {
super(context);
LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
li.inflate(R.layout.contact_selection_group_item, this, true);
this.name = (CheckedTextView)findViewById(R.id.name);
}
public void selected() {
name.toggle();
if (name.isChecked()) {
addGroup(groupData);
} else {
removeGroup(groupData);
}
}
public void set(GroupData groupData) {
this.groupData = groupData;
if (selectedGroups.containsKey(groupData.id))
this.name.setChecked(true);
else
this.name.setChecked(false);
this.name.setText(groupData.name);
}
}
private class GroupAggregationHandler extends Handler implements Runnable {
private List<Recipient> recipientList = new LinkedList<Recipient>();
private ProgressDialog progressDialog;
public GroupAggregationHandler() {}
public void run() {
recipientList.clear();
for (GroupData groupData : selectedGroups.values()) {
List<ContactData> contactDataList = ContactAccessor.getInstance().getGroupMembership(GroupSelectionListActivity.this, groupData.id);
Log.w("GroupSelectionListActivity", "Got contacts in group: " + contactDataList.size());
for (ContactData contactData : contactDataList) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
}
}
}
this.obtainMessage().sendToTarget();
}
public void aggregateContactsAndExit() {
progressDialog = new ProgressDialog(GroupSelectionListActivity.this);
progressDialog.setTitle("Aggregating Contacts");
progressDialog.setMessage("Aggregating group contacts...");
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.show();
Log.w("GroupSelectionListActivity", "Showing group spinner...");
new Thread(this).start();
}
@Override
public void handleMessage(Message message) {
progressDialog.dismiss();
Intent resultIntent = getIntent();
resultIntent.putExtra("recipients", new Recipients(recipientList));
if (getParent() == null) setResult(RESULT_OK, resultIntent);
else getParent().setResult(RESULT_OK, resultIntent);
finish();
}
}
}

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,25 +10,25 @@
* 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 java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import java.util.LinkedList;
import java.util.List;
/**
* Android changed their contacts API pretty heavily between
@@ -36,37 +36,17 @@ import android.os.Parcelable;
* API operations, using a singleton pattern that will Class.forName
* the correct one so we don't trigger NoClassDefFound exceptions on
* old platforms.
*
*
* @author Moxie Marlinspike
*/
public abstract class ContactAccessor {
public abstract class ContactAccessor {
public static final int UNIQUE_ID = 0;
public static final int DISPLAY_NAME = 1;
private static ContactAccessor sInstance;
public static synchronized ContactAccessor getInstance() {
if (sInstance == null) {
String className;
private static final ContactAccessor sInstance = new ContactAccessorNewApi();
if (Integer.parseInt(Build.VERSION.SDK) <= Build.VERSION_CODES.DONUT)
className = "ContactAccessorOldApi";
else
className = "ContactAccessorNewApi";
try {
Class<? extends ContactAccessor> clazz =
Class.forName("org.thoughtcrime.securesms.contacts." + className )
.asSubclass(ContactAccessor.class);
sInstance = clazz.newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static synchronized ContactAccessor getInstance() {
return sInstance;
}
@@ -78,6 +58,8 @@ public abstract class ContactAccessor {
public abstract List<String> getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver);
public abstract List<ContactData> getGroupMembership(Context context, long groupId);
public abstract Cursor getCursorForContactGroups(Context context);
public abstract CursorLoader getCursorLoaderForContactGroups(Context context);
public abstract CursorLoader getCursorLoaderForContactsWithNumbers(Context context);
public abstract Cursor getCursorForContactsWithNumbers(Context context);
public abstract GroupData getGroupData(Context context, Cursor cursor);
public abstract ContactData getContactData(Context context, Cursor cursor);
@@ -85,9 +67,9 @@ public abstract class ContactAccessor {
public abstract CharSequence phoneTypeToString(Context mContext, int type, CharSequence label);
public abstract String getNameForNumber(Context context, String number);
public abstract Uri getContactsUri();
public static class NumberData implements Parcelable {
public static final Parcelable.Creator<NumberData> CREATOR = new Parcelable.Creator<NumberData>() {
public NumberData createFromParcel(Parcel in) {
return new NumberData(in);
@@ -105,12 +87,12 @@ public abstract class ContactAccessor {
this.type = type;
this.number = number;
}
public NumberData(Parcel in) {
number = in.readString();
type = in.readString();
}
public int describeContents() {
return 0;
}
@@ -120,14 +102,14 @@ public abstract class ContactAccessor {
dest.writeString(type);
}
}
public static class GroupData {
public long id;
public String name;
}
public static class ContactData implements Parcelable {
public static final Parcelable.Creator<ContactData> CREATOR = new Parcelable.Creator<ContactData>() {
public ContactData createFromParcel(Parcel in) {
return new ContactData(in);
@@ -143,18 +125,18 @@ public abstract class ContactAccessor {
public List<NumberData> numbers;
public ContactData() {}
public ContactData(Parcel in) {
id = in.readLong();
name = in.readString();
numbers = new LinkedList<NumberData>();
in.readTypedList(numbers, NumberData.CREATOR);
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(id);
dest.writeString(name);

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,21 +10,12 @@
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.util.Base64;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -39,17 +30,27 @@ import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.RawContacts;
import android.support.v4.content.CursorLoader;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import org.thoughtcrime.securesms.crypto.InvalidKeyException;
import org.thoughtcrime.securesms.util.Base64;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Interface into the Android 2.x+ contacts operations.
*
*
* @author Stuart Anderson
*/
public class ContactAccessorNewApi extends ContactAccessor {
private static final String SORT_ORDER = Contacts.TIMES_CONTACTED + " DESC," + Contacts.DISPLAY_NAME + "," + Phone.TYPE;
private static final String[] PROJECTION_PHONE = {
@@ -65,22 +66,22 @@ public class ContactAccessorNewApi extends ContactAccessor {
public List<String> getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) {
LinkedList<String> numberList = new LinkedList<String>();
Cursor cursor = null;
try {
cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(constraint)),
null, null, null, null);
cursor = contentResolver.query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(constraint)),
null, null, null, null);
while (cursor != null && cursor.moveToNext())
numberList.add(cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)));
} finally {
if (cursor != null)
cursor.close();
}
return numberList;
}
@Override
public Cursor getCursorForRecipientFilter(CharSequence constraint, ContentResolver mContentResolver) {
String phone = "";
@@ -97,22 +98,22 @@ public class ContactAccessorNewApi extends ContactAccessor {
}
}
}
Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(cons));
String selection = String.format("%s=%s OR %s=%s OR %s=%s",
Phone.TYPE,
Phone.TYPE_MOBILE,
Phone.TYPE,
Phone.TYPE_WORK_MOBILE,
Phone.TYPE,
Phone.TYPE_MMS);
Phone.TYPE,
Phone.TYPE_MOBILE,
Phone.TYPE,
Phone.TYPE_WORK_MOBILE,
Phone.TYPE,
Phone.TYPE_MMS);
Cursor phoneCursor = mContentResolver.query(uri,
PROJECTION_PHONE,
null,
null,
SORT_ORDER);
PROJECTION_PHONE,
null,
null,
SORT_ORDER);
if (phone.length() > 0) {
@@ -139,9 +140,9 @@ public class ContactAccessorNewApi extends ContactAccessor {
} else {
return phoneCursor;
}
}
@Override
public CharSequence phoneTypeToString( Context mContext, int type, CharSequence label ) {
return Phone.getTypeLabel(mContext.getResources(), type, label);
@@ -156,50 +157,50 @@ public class ContactAccessorNewApi extends ContactAccessor {
private long getContactIdFromLookupUri(Context context, Uri uri) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[] {ContactsContract.Contacts._ID}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getLong(0);
else
return -1;
} finally {
if (cursor != null)
cursor.close();
}
}
private ArrayList<Long> getRawContactIds(Context context, long contactId) {
Cursor cursor = null;
ArrayList<Long> rawContactIds = new ArrayList<Long>();
try {
cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID},
RawContacts.CONTACT_ID + " = ?", new String[] {contactId+""},
null);
cursor = context.getContentResolver().query(RawContacts.CONTENT_URI, new String[] {RawContacts._ID},
RawContacts.CONTACT_ID + " = ?", new String[] {contactId+""},
null);
if (cursor == null)
return rawContactIds;
while (cursor.moveToNext()) {
rawContactIds.add(new Long(cursor.getLong(0)));
}
}
} finally {
if (cursor != null)
cursor.close();
}
return rawContactIds;
}
@Override
public void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey) {
long contactId = getContactIdFromLookupUri(context, uri);
Log.w("ContactAccessorNewApi", "Got contact ID: " + contactId + " from uri: " + uri.toString());
ArrayList<Long> rawContactIds = getRawContactIds(context, contactId);
for (long rawContactId : rawContactIds) {
Log.w("ContactAccessorNewApi", "Inserting data for raw contact id: " + rawContactId);
ContentValues contentValues = new ContentValues();
@@ -208,9 +209,9 @@ public class ContactAccessorNewApi extends ContactAccessor {
contentValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
contentValues.put(Im.CUSTOM_PROTOCOL, "TextSecure-IdentityKey");
contentValues.put(Im.DATA, Base64.encodeBytes(identityKey.serialize()));
context.getContentResolver().insert(Data.CONTENT_URI, contentValues);
}
context.getContentResolver().insert(Data.CONTENT_URI, contentValues);
}
}
@Override
@@ -218,16 +219,16 @@ public class ContactAccessorNewApi extends ContactAccessor {
long contactId = getContactIdFromLookupUri(context, uri);
String selection = Im.CONTACT_ID + " = ? AND " + Im.PROTOCOL + " = ? AND " + Im.CUSTOM_PROTOCOL + " = ?";
String[] selectionArgs = new String[] {contactId+"", Im.PROTOCOL_CUSTOM+"", "TextSecure-IdentityKey"};
Cursor cursor = context.getContentResolver().query(Data.CONTENT_URI, null, selection, selectionArgs, null);
try {
if (cursor != null && cursor.moveToFirst()) {
String data = cursor.getString(cursor.getColumnIndexOrThrow(Im.DATA));
if (data != null)
if (data != null)
return new IdentityKey(Base64.decode(data), 0);
}
} catch (InvalidKeyException e) {
Log.w("ContactAccessorNewApi", e);
@@ -246,10 +247,10 @@ public class ContactAccessorNewApi extends ContactAccessor {
@Override
public String getNameFromContact(Context context, Uri uri) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME}, null, null, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getString(0);
@@ -257,24 +258,24 @@ public class ContactAccessorNewApi extends ContactAccessor {
if (cursor != null)
cursor.close();
}
return null;
}
private String getMobileNumberForId(Context context, long id) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ? AND " + Phone.TYPE + " = ?",
new String[] {id+"", Phone.TYPE_MOBILE+""}, null);
cursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ? AND " + Phone.TYPE + " = ?",
new String[] {id+"", Phone.TYPE_MOBILE+""}, null);
if (cursor != null && cursor.moveToFirst())
return cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER));
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
@@ -282,29 +283,41 @@ public class ContactAccessorNewApi extends ContactAccessor {
public NameAndNumber getNameAndNumberFromContact(Context context, Uri uri) {
Log.w("ContactAccessorNewApi", "Get name and number from: " + uri.toString());
Cursor cursor = null;
try {
NameAndNumber results = new NameAndNumber();
cursor = context.getContentResolver().query(uri, new String[] {Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
results.name = cursor.getString(1);
results.number = getMobileNumberForId(context, cursor.getLong(0));
return results;
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
@Override
public Cursor getCursorForContactsWithNumbers(Context context) {
public CursorLoader getCursorLoaderForContactsWithNumbers(Context context) {
Uri uri = ContactsContract.Contacts.CONTENT_URI;
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1";
return context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, selection, null, ContactsContract.Contacts.DISPLAY_NAME + " ASC");
return new CursorLoader(context, uri, null, selection, null,
ContactsContract.Contacts.DISPLAY_NAME + " ASC");
}
@Override
public Cursor getCursorForContactsWithNumbers(Context context) {
Uri uri = ContactsContract.Contacts.CONTENT_URI;
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = 1";
return context.getContentResolver().query(uri, null, selection, null,
ContactsContract.Contacts.DISPLAY_NAME + " ASC");
}
private ContactData getContactData(Context context, String displayName, long id) {
@@ -312,52 +325,58 @@ public class ContactAccessorNewApi extends ContactAccessor {
contactData.id = id;
contactData.name = displayName;
contactData.numbers = new LinkedList<NumberData>();
Cursor numberCursor = null;
try {
numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ?",
new String[] {contactData.id + ""}, null);
numberCursor = context.getContentResolver().query(Phone.CONTENT_URI, null, Phone.CONTACT_ID + " = ?",
new String[] {contactData.id + ""}, null);
while (numberCursor != null && numberCursor.moveToNext())
contactData.numbers.add(new NumberData(Phone.getTypeLabel(context.getResources(),
numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)),
numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL))).toString(),
numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER))));
contactData.numbers.add(new NumberData(Phone.getTypeLabel(context.getResources(),
numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phone.TYPE)),
numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.LABEL))).toString(),
numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phone.NUMBER))));
} finally {
if (numberCursor != null)
numberCursor.close();
}
return contactData;
}
@Override
public ContactData getContactData(Context context, Cursor cursor) {
return getContactData(context,
cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)),
cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)));
return getContactData(context,
cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)),
cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)));
}
@Override
public Cursor getCursorForContactGroups(Context context) {
return context.getContentResolver().query(ContactsContract.Groups.CONTENT_URI, null, null, null, ContactsContract.Groups.TITLE + " ASC");
}
@Override
public CursorLoader getCursorLoaderForContactGroups(Context context) {
return new CursorLoader(context, ContactsContract.Groups.CONTENT_URI,
null, null, null, ContactsContract.Groups.TITLE + " ASC");
}
@Override
public List<ContactData> getGroupMembership(Context context, long groupId) {
LinkedList<ContactData> contacts = new LinkedList<ContactData>();
LinkedList<ContactData> contacts = new LinkedList<ContactData>();
Cursor groupMembership = null;
try {
String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " +
String selection = ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? AND " +
ContactsContract.CommonDataKinds.GroupMembership.MIMETYPE + " = ?";
String[] args = new String[] {groupId+"",
ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE};
groupMembership = context.getContentResolver().query(Data.CONTENT_URI, null, selection, args, null);
while (groupMembership != null && groupMembership.moveToNext()) {
String displayName = groupMembership.getString(groupMembership.getColumnIndexOrThrow(Data.DISPLAY_NAME));
long contactId = groupMembership.getLong(groupMembership.getColumnIndexOrThrow(Data.CONTACT_ID));
@@ -368,16 +387,16 @@ public class ContactAccessorNewApi extends ContactAccessor {
if (groupMembership != null)
groupMembership.close();
}
return contacts;
}
@Override
public GroupData getGroupData(Context context, Cursor cursor) {
GroupData groupData = new GroupData();
groupData.id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID));
groupData.name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE));
return groupData;
}
@@ -393,7 +412,7 @@ public class ContactAccessorNewApi extends ContactAccessor {
if (cursor != null)
cursor.close();
}
return null;
}
@@ -401,5 +420,5 @@ public class ContactAccessorNewApi extends ContactAccessor {
public Uri getContactsUri() {
return ContactsContract.Contacts.CONTENT_URI;
}
}

View File

@@ -1,328 +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.contacts;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.thoughtcrime.securesms.crypto.IdentityKey;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MergeCursor;
import android.net.Uri;
import android.provider.Contacts;
import android.provider.Contacts.GroupMembership;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import android.widget.Toast;
/**
* A contact interface into the 1.x API for older clients.
*
* @author Stuart Anderson
*/
public class ContactAccessorOldApi extends ContactAccessor {
@SuppressWarnings("deprecation")
private static final String SORT_ORDER = Phones.NAME + "," + Phones.TYPE;
@SuppressWarnings("deprecation")
private static final String[] PROJECTION_PHONE = {
Phones._ID, // 0
Phones.PERSON_ID, // 1
Phones.TYPE, // 2
Phones.NUMBER, // 3
Phones.LABEL, // 4
Phones.DISPLAY_NAME, // 5
};
@SuppressWarnings("deprecation")
@Override
public Cursor getCursorForRecipientFilter(CharSequence constraint,
ContentResolver mContentResolver) {
String phone = "";
String wherePhone = null;
String cons = null;
if (constraint != null) {
cons = constraint.toString();
if (RecipientsAdapter.usefulAsDigits(cons)) {
phone = PhoneNumberUtils.convertKeypadLettersToDigits(cons);
if (phone.equals(cons)) {
phone = "";
} else {
phone = phone.trim();
}
}
}
String filter = DatabaseUtils.sqlEscapeString(cons + '%');
String filterLastName = DatabaseUtils.sqlEscapeString("% " + cons + '%');
StringBuilder s = new StringBuilder();
s.append("((name LIKE ");
s.append(filter);
s.append(") OR (name LIKE ");
s.append(filterLastName);
s.append(") OR (REPLACE(REPLACE(REPLACE(REPLACE(number, ' ', ''), '(', ''), ')', ''), '-', '') LIKE ");
s.append(filter);
s.append("))");
wherePhone = s.toString();
Cursor phoneCursor = mContentResolver.query(Phones.CONTENT_URI,
PROJECTION_PHONE,
wherePhone,
null,
SORT_ORDER);
//dumpCursor(phoneCursor);
if (phone.length() > 0) {
ArrayList result = new ArrayList();
result.add(Integer.valueOf(-1)); // ID
result.add(Long.valueOf(-1)); // CONTACT_ID
result.add(Integer.valueOf(Phones.TYPE_CUSTOM)); // TYPE
result.add(phone); // NUMBER
/*
* The "\u00A0" keeps Phone.getDisplayLabel() from deciding
* to display the default label ("Home") next to the transformation
* of the letters into numbers.
*/
result.add("\u00A0"); // LABEL
result.add(cons); // NAME
ArrayList<ArrayList> wrap = new ArrayList<ArrayList>();
wrap.add(result);
ArrayListCursor translated = new ArrayListCursor(PROJECTION_PHONE, wrap);
return new MergeCursor(new Cursor[] { translated, phoneCursor });
} else {
return phoneCursor;
}
}
@SuppressWarnings("deprecation")
@Override
public CharSequence phoneTypeToString(Context mContext, int type,
CharSequence label) {
return Phones.getDisplayLabel(mContext, type, label);
}
public static void dumpCursor( Cursor c ) {
c.moveToFirst();
Log.d( "DC", "Begin:" );
for( int i=0; i < c.getCount(); i++ ) {
String rowStr = "";
for( int j=0; j < c.getColumnCount(); j++ ) {
rowStr = rowStr + c.getColumnName(j) + "=" + c.getString(j) +" ";
}
Log.d( "DC", rowStr + "\n" );
c.moveToNext();
}
}
@Override
public Intent getIntentForContactSelection() {
return new Intent(Intent.ACTION_PICK, People.CONTENT_URI);
}
@Override
public void insertIdentityKey(Context context, Uri uri, IdentityKey identityKey) {
Toast.makeText(context, "Sorry, reading and writing identity keys to the contacts database is not supported on Android 1.X", Toast.LENGTH_LONG).show();
}
@Override
public IdentityKey importIdentityKey(Context context, Uri uri) {
Toast.makeText(context, "Sorry, reading and writing identity keys to the contacts database is not supported on Android 1.X", Toast.LENGTH_LONG).show();
return null;
}
@Override
public String getNameFromContact(Context context, Uri uri) {
// TODO Auto-generated method stub
return null;
}
@Override
public List<String> getNumbersForThreadSearchFilter(String constraint, ContentResolver contentResolver) {
LinkedList<String> numberList = new LinkedList<String>();
Cursor cursor = null;
try {
cursor = contentResolver.query(Uri.withAppendedPath(Contacts.People.CONTENT_FILTER_URI, Uri.encode(constraint)),
null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
String number = cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Phones.NUMBER));
if (number != null)
numberList.add(number);
}
} finally {
if (cursor != null)
cursor.close();
}
return numberList;
}
@Override
public NameAndNumber getNameAndNumberFromContact(Context context, Uri uri) {
Cursor cursor = null;
NameAndNumber result = new NameAndNumber();
try {
cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
result.name = cursor.getString(cursor.getColumnIndexOrThrow(People.NAME));
result.number = cursor.getString(cursor.getColumnIndexOrThrow(People.NUMBER));
return result;
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
@Override
public Cursor getCursorForContactsWithNumbers(Context context) {
return context.getContentResolver().query(People.CONTENT_URI,new String[]{People._ID,People.DISPLAY_NAME},
People.NUMBER + " NOT NULL", null, "UPPER( " + People.DISPLAY_NAME + " ) ASC");
}
@Override
public ContactData getContactData(Context context, Cursor cursor) {
ContactData contactData = new ContactData();
contactData.id = cursor.getLong(cursor.getColumnIndexOrThrow(People._ID));
contactData.name = cursor.getString(cursor.getColumnIndexOrThrow(People.DISPLAY_NAME));
contactData.numbers = getNumberDataForPersonId(context, contactData.id);
return contactData;
}
@Override
public Cursor getCursorForContactGroups(Context context) {
return context.getContentResolver().query(Contacts.Groups.CONTENT_URI, null, null, null, Contacts.Groups.NAME + " ASC");
}
private LinkedList<NumberData> getNumberDataForPersonId(Context context, long personId) {
LinkedList<NumberData> numbers = new LinkedList<NumberData>();
Cursor numberCursor = context.getContentResolver().query(Phones.CONTENT_URI, null,
Phones.PERSON_ID + " = ?",
new String[] {personId+""}, null);
try {
while (numberCursor != null && numberCursor.moveToNext()) {
numbers.add(new NumberData(Phones.getDisplayLabel(context, numberCursor.getInt(numberCursor.getColumnIndexOrThrow(Phones.TYPE)), "").toString(),
numberCursor.getString(numberCursor.getColumnIndexOrThrow(Phones.NUMBER))));
}
} finally {
if (numberCursor != null)
numberCursor.close();
}
return numbers;
}
private ContactData getContactDataFromGroupMembership(Context context, Cursor cursor) {
ContactData contactData = new ContactData();
contactData.id = cursor.getLong(cursor.getColumnIndexOrThrow(GroupMembership.PERSON_ID));
Cursor personCursor = context.getContentResolver().query(Uri.withAppendedPath(People.CONTENT_URI, contactData.id+""), null, null, null, null);
try {
if (personCursor == null || !personCursor.moveToFirst())
throw new AssertionError("Non-existent user in group?");
contactData.name = personCursor.getString(personCursor.getColumnIndexOrThrow(People.DISPLAY_NAME));
contactData.numbers = getNumberDataForPersonId(context, contactData.id);
return contactData;
} finally {
if (personCursor != null)
personCursor.close();
}
}
@Override
public List<ContactData> getGroupMembership(Context context, long groupId) {
LinkedList<ContactData> contacts = new LinkedList<ContactData>();
Cursor groupMembershipCursor = context.getContentResolver().query(Contacts.GroupMembership.CONTENT_URI, null,
GroupMembership.GROUP_ID + " = ?",
new String[] {groupId+""}, null);
try {
while (groupMembershipCursor != null && groupMembershipCursor.moveToNext()) {
contacts.add(getContactDataFromGroupMembership(context, groupMembershipCursor));
}
} finally {
if (groupMembershipCursor != null)
groupMembershipCursor.close();
}
return contacts;
}
@Override
public GroupData getGroupData(Context context, Cursor cursor) {
GroupData groupData = new GroupData();
groupData.id = cursor.getLong(cursor.getColumnIndexOrThrow(Contacts.Groups._ID));
groupData.name = cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Groups.NAME));
return groupData;
}
@Override
public String getNameForNumber(Context context, String number) {
Cursor cursor = context.getContentResolver().query(Contacts.Phones.CONTENT_URI, null,
Phones.NUMBER + " = ?",
new String[] {number}, null);
try {
if (cursor != null && cursor.moveToFirst())
return cursor.getString(cursor.getColumnIndexOrThrow(Contacts.Phones.DISPLAY_NAME));
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
@Override
public Uri getContactsUri() {
return Contacts.People.CONTENT_URI;
}
}

View File

@@ -1,6 +1,6 @@
/**
/**
* 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
@@ -10,20 +10,20 @@
* 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.recipients;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import android.os.Parcel;
import android.os.Parcelable;
import org.thoughtcrime.securesms.util.NumberUtil;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Recipients implements Parcelable {
@@ -37,73 +37,77 @@ public class Recipients implements Parcelable {
}
};
private List<Recipient> recipients;
public Recipients(List<Recipient> recipients) {
this.recipients = recipients;
}
public Recipients(Parcel in) {
this.recipients = new ArrayList<Recipient>();
in.readTypedList(recipients, Recipient.CREATOR);
}
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 boolean isEmailRecipient() {
for (Recipient recipient : recipients) {
if (NumberUtil.isValidEmail(recipient.getNumber()))
return true;
return true;
}
return false;
}
public boolean isEmpty() {
return this.recipients.isEmpty();
}
public boolean isSingleRecipient() {
return this.recipients.size() == 1;
}
public Recipient getPrimaryRecipient() {
if (!isEmpty())
return this.recipients.get(0);
else
return null;
}
public List<Recipient> getRecipientsList() {
return this.recipients;
}
public String[] toNumberStringArray() {
String[] recipientsArray = new String[recipients.size()];
Iterator<Recipient> iterator = recipients.iterator();
int i = 0;
while (iterator.hasNext())
recipientsArray[i++] = iterator.next().getNumber();
return recipientsArray;
}
public String toShortString() {
String fromString = "";
for (int i=0;i<recipients.size();i++) {
fromString += recipients.get(i).toShortString();
if (i != recipients.size() -1 )
fromString += ", ";
fromString += ", ";
}
return fromString;
}