Permalink
Browse files

Mms: Add the ability to select recipients from a checkbox list

When creating a new message you have to write some letters from the
contacts you want to add and select them by clicking on it.

This can be annoying if you want to send your message to several contacts.
With this change you can manually select numbers from contacts and groups
from a checkbox list.

Change-Id: I03681f22a0d949679e2a9cc95c551e4c216b6300

Conflicts:
	res/values/cm_strings.xml
  • Loading branch information...
1 parent 57cf337 commit 67ccdbc7becfa64293a04a86ceb96c59b6ea308d @championswimmer championswimmer committed Jan 5, 2014
View
6 AndroidManifest.xml
@@ -170,6 +170,12 @@
android:theme="@style/MmsHoloTheme"
android:label="@string/edit_slideshow_activity" />
+ <activity android:name=".ui.SelectRecipientsList"
+ android:theme="@style/MmsHoloTheme"
+ android:label="@string/select_recipients"
+ android:parentActivityName=".ui.ComposeMessageActivity">
+ </activity>
+
<activity android:name=".ui.RecipientListActivity"
android:theme="@style/MmsHoloTheme"
android:label="@string/recipient_list_activity" />
View
BIN res/drawable-hdpi/ic_recipients_add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN res/drawable-mdpi/ic_menu_done_holo_light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN res/drawable-mdpi/ic_recipients_add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN res/drawable-xhdpi/ic_menu_done_holo_light.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN res/drawable-xhdpi/ic_recipients_add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
19 res/layout/recipients_editor.xml
@@ -19,8 +19,8 @@
-->
<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="horizontal">
<com.android.mms.ui.RecipientsEditor
@@ -29,17 +29,28 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textFilter"
+ android:nextFocusRight="@+id/select_recipients_button"
android:layout_weight="1"
- android:textColor="#000000"
+ android:textColor="@color/compose_message_edit_text"
/>
<ImageButton android:id="@+id/recipients_picker"
android:src="@mipmap/ic_launcher_contacts"
android:layout_marginLeft="5dip"
android:layout_width="50dip"
- android:layout_height="fill_parent"
+ android:layout_height="match_parent"
android:layout_weight="0"
android:visibility="gone"
/>
+ <ImageButton
+ android:id="@+id/recipients_selector"
+ android:nextFocusLeft="@+id/recipients_editor"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:padding="12dip"
+ android:src="@drawable/ic_recipients_add"
+ />
+
</LinearLayout>
View
114 res/layout/select_recipients_list_item.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 The CyanogenMod Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<com.android.mms.ui.SelectRecipientsListItem
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+
+ <TextView
+ android:id="@+id/separator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="?android:attr/listSeparatorTextViewStyle" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal" >
+
+ <android.widget.QuickContactBadge
+ android:id="@+id/avatar"
+ android:layout_width="@dimen/avatar_width_height"
+ android:layout_height="@dimen/avatar_width_height"
+ android:layout_gravity="center_vertical"
+ style="?android:attr/quickContactBadgeStyleWindowLarge" />
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_margin="8dp" >
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textAllCaps="true"
+ android:textSize="12sp"
+ android:singleLine="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentBottom="true" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/label"
+ android:ellipsize="marquee" />
+
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_below="@id/name"
+ android:layout_toLeftOf="@id/label"
+ android:layout_alignWithParentIfMissing="true"
+ android:gravity="center_vertical"
+ android:ellipsize="end" />
+
+ <CheckBox
+ android:id="@+id/checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@id/label"
+ android:layout_alignWithParentIfMissing="true"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_centerVertical="true"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentRight="true" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/footer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/listDivider" />
+
+</com.android.mms.ui.SelectRecipientsListItem>
View
58 res/layout/select_recipients_list_screen.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 The CyanogenMod Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center" >
+
+ <LinearLayout
+ android:id="@+id/progress_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical" >
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@android:id/list"
+ style="?android:attr/listViewWhiteStyle"
+ android:listSelector="@android:color/transparent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:drawSelectorOnTop="false"
+ android:scrollbarStyle="outsideOverlay"
+ android:background="@color/background"
+ android:cacheColorHint="@color/background"
+ android:fadingEdgeLength="16dip"
+ android:paddingLeft="16dip"
+ android:paddingRight="32dip" />
+
+ <TextView
+ android:id="@+id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+</LinearLayout>
View
10 res/values/cm_strings.xml
@@ -124,4 +124,14 @@
<string name="pref_summary_sms_split_counter">Adds counter to message ex. (1/2)</string>
+ <!-- Multi recipients -->
+ <string name="select_recipients">Select recipients</string>
+ <string name="groups_header">Groups</string>
+ <string name="no_recipients">No contacts found</string>
+ <string name="no_recipients_mobile_only">No matching contacts found. Try without the \'Mobile numbers only\' filter</string>
+ <string name="menu_done">Done</string>
+ <string name="menu_mobile">Mobile numbers only</string>
+ <string name="menu_groups">Show groups</string>
+ <string name="fastscroll_index_groups">GR</string> <!-- CHAR LIMIT=3 -->
+
</resources>
View
153 src/com/android/mms/data/Group.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.data;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract.Groups;
+import android.util.Log;
+
+import com.android.mms.LogTag;
+
+public class Group {
+ private static final String TAG = "Mms/Group";
+
+ private static final String[] PROJECTION = new String[] {
+ Groups._ID,
+ Groups.TITLE,
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.DATA_SET,
+ Groups.SUMMARY_COUNT,
+ };
+
+ private static final String SELECTION = Groups.ACCOUNT_TYPE + " NOT NULL AND "
+ + Groups.ACCOUNT_NAME + " NOT NULL AND "
+ + Groups.AUTO_ADD + "=0 AND "
+ + Groups.DELETED + "=0 AND "
+ + Groups.SUMMARY_COUNT + "!=0";
+
+ private static final String SORT = Groups.ACCOUNT_TYPE + ", "
+ + Groups.ACCOUNT_NAME + ", "
+ + Groups.DATA_SET + ", "
+ + Groups.TITLE + " COLLATE LOCALIZED ASC";
+
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_GROUP_TITLE = 1;
+ private static final int COLUMN_ACCOUNT_NAME = 2;
+ private static final int COLUMN_ACCOUNT_TYPE = 3;
+ private static final int COLUMN_DATA_SET = 4;
+ private static final int COLUMN_SUMMARY_COUNT = 5;
+
+ private long mId;
+ private String mTitle;
+ private String mAccountName;
+ private String mAccountType;
+ private String mDataSet;
+ private int mSummaryCount;
+ private ArrayList<PhoneNumber> mPhoneNumbers;
+
+ private boolean mIsChecked;
+
+ private Group(Context context, Cursor c) {
+ mId = c.getLong(COLUMN_ID);
+ mTitle = c.getString(COLUMN_GROUP_TITLE);
+ mAccountName = c.getString(COLUMN_ACCOUNT_NAME);
+ mAccountType = c.getString(COLUMN_ACCOUNT_TYPE);
+ mDataSet = c.getString(COLUMN_DATA_SET);
+ mSummaryCount = c.getInt(COLUMN_SUMMARY_COUNT);
+ mPhoneNumbers = new ArrayList<PhoneNumber>();
+
+ if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
+ Log.d(TAG, "Create Group: recipient=" + mTitle + ", groupId=" + mId);
+ }
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public String getAccountType() {
+ return mAccountType;
+ }
+
+ public String getDataSet() {
+ return mDataSet;
+ }
+
+ public int getSummaryCount() {
+ return mSummaryCount;
+ }
+
+ public ArrayList<PhoneNumber> getPhoneNumbers() {
+ return mPhoneNumbers;
+ }
+
+ public void addPhoneNumber(PhoneNumber phoneNumber) {
+ if (!mPhoneNumbers.contains(phoneNumber)) {
+ mPhoneNumbers.add(phoneNumber);
+ }
+ }
+
+ /**
+ * Returns true if this group is selected for a multi-operation.
+ */
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ }
+
+ /**
+ * Get all groups
+ */
+ public static ArrayList<Group> getGroups(Context context) {
+ final Cursor cursor = context.getContentResolver().query(Groups.CONTENT_SUMMARY_URI,
+ PROJECTION, SELECTION, null, SORT);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ if (cursor.getCount() == 0) {
+ cursor.close();
+ return null;
+ }
+
+ ArrayList<Group> groups = new ArrayList<Group>();
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ groups.add(new Group(context, cursor));
+ }
+ cursor.close();
+
+ return groups;
+ }
+}
View
96 src/com/android/mms/data/GroupMembership.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.data;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Data;
+import android.util.Log;
+
+import com.android.mms.LogTag;
+
+public class GroupMembership {
+ private static final String TAG = "Mms/GroupMembership";
+
+ private static final String[] PROJECTION = new String[] {
+ CommonDataKinds.GroupMembership._ID,
+ CommonDataKinds.GroupMembership.CONTACT_ID,
+ CommonDataKinds.GroupMembership.GROUP_ROW_ID
+ };
+
+ private static final String SELECTION =
+ CommonDataKinds.GroupMembership.MIMETYPE + " = '" +
+ CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE + "'";
+
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_GM_CONTACT_ID = 1;
+ private static final int COLUMN_GM_GROUP_ID = 2;
+
+ private long mContactId;
+ private long mGroupId;
+
+ private GroupMembership(Context context, Cursor c) {
+ mContactId = c.getLong(COLUMN_GM_CONTACT_ID);
+ mGroupId = c.getLong(COLUMN_GM_GROUP_ID);
+
+ if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
+ Log.d(TAG, "Create groupMembership: groupId=" + mGroupId
+ + ", contactId=" + mContactId);
+ }
+ }
+
+ public long getContactId() {
+ return mContactId;
+ }
+
+ public long getGroupId() {
+ return mGroupId;
+ }
+
+ /**
+ * Get all groupMembership
+ * @param context
+ * @return all groupMembership
+ */
+ public static ArrayList<GroupMembership> getGroupMemberships(Context context) {
+ final Cursor cursor = context.getContentResolver().query(Data.CONTENT_URI,
+ PROJECTION, SELECTION, null, null);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ if (cursor.getCount() == 0) {
+ // No results to process
+ cursor.close();
+ return null;
+ }
+
+ ArrayList<GroupMembership> groupMemberships = new ArrayList<GroupMembership>();
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ groupMemberships.add(new GroupMembership(context, cursor));
+ }
+ cursor.close();
+
+ return groupMemberships;
+ }
+}
View
275 src/com/android/mms/data/PhoneNumber.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Preferences;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import com.android.mms.LogTag;
+import com.android.mms.ui.SelectRecipientsList;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeSet;
+
+/**
+ * An interface for finding information about phone numbers
+ */
+public class PhoneNumber implements Comparable<PhoneNumber> {
+ private static final String TAG = "Mms/PhoneNumber";
+
+ private static final String[] PROJECTION = new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.DISPLAY_NAME_PRIMARY,
+ Phone.IS_SUPER_PRIMARY,
+ Phone.CONTACT_ID
+ };
+
+ private static final String[] PROJECTION_ALT = new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.DISPLAY_NAME_ALTERNATIVE,
+ Phone.IS_SUPER_PRIMARY,
+ Phone.CONTACT_ID
+ };
+
+ private static final String SELECTION = Phone.NUMBER + " NOT NULL";
+ private static final String SELECTION_MOBILE_ONLY = SELECTION +
+ " AND " + Phone.TYPE + "=" + Phone.TYPE_MOBILE;
+
+ private static final int COLUMN_ID = 0;
+ private static final int COLUMN_NUMBER = 1;
+ private static final int COLUMN_TYPE = 2;
+ private static final int COLUMN_LABEL = 3;
+ private static final int COLUMN_DISPLAY_NAME = 4;
+ private static final int COLUMN_IS_SUPER_PRIMARY = 5;
+ private static final int COLUMN_CONTACT_ID = 6;
+
+ private long mId;
+ private String mNumber;
+ private int mType;
+ private String mLabel;
+ private String mName;
+ private boolean mIsDefault;
+ private long mContactId;
+ private ArrayList<Group> mGroups;
+ private boolean mIsChecked;
+ private String mSectionIndex;
+
+ private PhoneNumber(Context context, Cursor c, String sectionIndex) {
+ mId = c.getLong(COLUMN_ID);
+ mNumber = c.getString(COLUMN_NUMBER);
+ mType = c.getInt(COLUMN_TYPE);
+ mLabel = c.getString(COLUMN_LABEL);
+ mName = c.getString(COLUMN_DISPLAY_NAME);
+ mContactId = c.getLong(COLUMN_CONTACT_ID);
+ mIsDefault = c.getInt(COLUMN_IS_SUPER_PRIMARY) != 0;
+ mGroups = new ArrayList<Group>();
+ mSectionIndex = sectionIndex;
+
+ if (Log.isLoggable(LogTag.THREAD_CACHE, Log.VERBOSE)) {
+ Log.d(TAG, "Create phone number: recipient=" + mName + ", recipientId="
+ + mId + ", recipientNumber=" + mNumber);
+ }
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getNumber() {
+ return mNumber;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getSectionIndex() {
+ return mSectionIndex;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public boolean isDefault() {
+ return mIsDefault;
+ }
+
+ public long getContactId() {
+ return mContactId;
+ }
+
+ public ArrayList<Group> getGroups() {
+ return mGroups;
+ }
+
+ public void addGroup(Group group) {
+ if (!mGroups.contains(group)) {
+ mGroups.add(group);
+ }
+ }
+
+ /**
+ * Returns true if this phone number is selected for a multi-operation.
+ */
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ }
+
+ /**
+ * The primary key of a recipient is its number
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof PhoneNumber) {
+ PhoneNumber other = (PhoneNumber) obj;
+ return mContactId == other.mContactId
+ && PhoneNumberUtils.compare(mNumber, other.mNumber);
+ } else if (obj instanceof String) {
+ return PhoneNumberUtils.compare(mNumber, (String) obj);
+ }
+ return false;
+ }
+
+ @Override
+ public int compareTo(PhoneNumber other) {
+ int result = mName.compareTo(other.mName);
+ if (result != 0) {
+ return result;
+ }
+ if (mIsDefault != other.mIsDefault) {
+ return mIsDefault ? -1 : 1;
+ }
+ result = mNumber.compareTo(other.mNumber);
+ if (result != 0) {
+ return result;
+ }
+ if (mContactId != other.mContactId) {
+ return mContactId < other.mContactId ? -1 : 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Get all possible recipients (groups and contacts with phone number(s) only)
+ * @param context
+ * @return all possible recipients
+ */
+ public static ArrayList<PhoneNumber> getPhoneNumbers(Context context) {
+ final ContentResolver resolver = context.getContentResolver();
+ boolean useAlternative = Settings.System.getInt(resolver, Preferences.DISPLAY_ORDER,
+ Preferences.DISPLAY_ORDER_PRIMARY) == Preferences.DISPLAY_ORDER_ALTERNATIVE;
+ final Uri uri = Phone.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean mobileOnly = prefs.getBoolean(SelectRecipientsList.PREF_MOBILE_NUMBERS_ONLY, true);
+ final Cursor cursor = resolver.query(uri,
+ useAlternative ? PROJECTION_ALT : PROJECTION,
+ mobileOnly ? SELECTION_MOBILE_ONLY : SELECTION, null,
+ useAlternative ? Phone.SORT_KEY_ALTERNATIVE : Phone.SORT_KEY_PRIMARY);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ if (cursor.getCount() == 0) {
+ cursor.close();
+ return null;
+ }
+
+ Bundle bundle = cursor.getExtras();
+ String[] sections = null;
+ int[] counts = null;
+
+ if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
+ sections = bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+ counts = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+ }
+
+ // As we can't sort by super primary state when using the index extra query parameter,
+ // we have to group by contact in a first pass and sort by primary state in a second pass
+ ArrayList<Long> contactIdOrder = new ArrayList<Long>();
+ HashMap<Long, TreeSet<PhoneNumber>> numbers = new HashMap<Long, TreeSet<PhoneNumber>>();
+ int section = 0, sectionPosition = 0;
+ long lastContactId = -1;
+
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ String sectionIndex = null;
+ if (sections != null) {
+ sectionIndex = sections[section];
+ sectionPosition++;
+ if (sectionPosition >= counts[section]) {
+ section++;
+ sectionPosition = 0;
+ }
+ }
+
+ PhoneNumber number = new PhoneNumber(context, cursor, sectionIndex);
+ if (!contactIdOrder.contains(number.mContactId)) {
+ contactIdOrder.add(number.mContactId);
+ }
+ TreeSet<PhoneNumber> numbersByContact = numbers.get(number.mContactId);
+ if (numbersByContact == null) {
+ numbersByContact = new TreeSet<PhoneNumber>();
+ numbers.put(number.mContactId, numbersByContact);
+ }
+ numbersByContact.add(number);
+ }
+ cursor.close();
+
+ // Construct the final list
+ ArrayList<PhoneNumber> phoneNumbers = new ArrayList<PhoneNumber>();
+
+ for (Long contactId : contactIdOrder) {
+ TreeSet<PhoneNumber> numbersByContact = numbers.get(contactId);
+ // In case there wasn't a primary number, we declare the first item to by default
+ numbersByContact.first().mIsDefault = true;
+ for (PhoneNumber number : numbersByContact) {
+ phoneNumbers.add(number);
+ }
+ }
+
+ return phoneNumbers;
+ }
+}
View
159 src/com/android/mms/data/RecipientsListLoader.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.data;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.android.mms.ui.SelectRecipientsList;
+
+public class RecipientsListLoader extends AsyncTaskLoader<ArrayList<RecipientsListLoader.Result>> {
+ private ArrayList<Result> mResults;
+
+ public static class Result {
+ public PhoneNumber phoneNumber;
+ public Group group;
+ }
+
+ public RecipientsListLoader(Context context) {
+ super(context);
+ }
+
+ @Override
+ public ArrayList<Result> loadInBackground() {
+ final Context context = getContext();
+ ArrayList<PhoneNumber> phoneNumbers = PhoneNumber.getPhoneNumbers(context);
+ if (phoneNumbers == null) {
+ return new ArrayList<Result>();
+ }
+
+ // Get things ready
+ ArrayList<Result> results = new ArrayList<Result>();
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean showGroups = prefs.getBoolean(SelectRecipientsList.PREF_SHOW_GROUPS, true);
+
+ if (showGroups) {
+ ArrayList<Group> groups = Group.getGroups(context);
+ ArrayList<GroupMembership> groupMemberships = GroupMembership.getGroupMemberships(context);
+ Map<Long, ArrayList<Long>> groupIdWithContactsId = new HashMap<Long, ArrayList<Long>>();
+
+ // Store GID with all its CIDs
+ if (groups != null && groupMemberships != null) {
+ for (GroupMembership membership : groupMemberships) {
+ Long gid = membership.getGroupId();
+ Long uid = membership.getContactId();
+
+ if (!groupIdWithContactsId.containsKey(gid)) {
+ groupIdWithContactsId.put(gid, new ArrayList<Long>());
+ }
+
+ if (!groupIdWithContactsId.get(gid).contains(uid)) {
+ groupIdWithContactsId.get(gid).add(uid);
+ }
+ }
+
+ // For each PhoneNumber, find its GID, and add it to correct Group
+ for (PhoneNumber phoneNumber : phoneNumbers) {
+ long cid = phoneNumber.getContactId();
+
+ for (Map.Entry<Long, ArrayList<Long>> entry : groupIdWithContactsId.entrySet()) {
+ if (!entry.getValue().contains(cid)) {
+ continue;
+ }
+ for (Group group : groups) {
+ if (group.getId() == entry.getKey()) {
+ group.addPhoneNumber(phoneNumber);
+ phoneNumber.addGroup(group);
+ }
+ }
+ }
+ }
+
+ // Add the groups to the list first
+ for (Group group : groups) {
+ // Due to filtering there may be groups that have contacts, but no
+ // phone numbers. Filter those.
+ if (!group.getPhoneNumbers().isEmpty()) {
+ Result result = new Result();
+ result.group = group;
+ results.add(result);
+ }
+ }
+ }
+ }
+
+ // Add phone numbers to the list
+ for (PhoneNumber phoneNumber : phoneNumbers) {
+ Result result = new Result();
+ result.phoneNumber = phoneNumber;
+ results.add(result);
+ }
+
+ // We are done
+ return results;
+ }
+
+ // Called when there is new data to deliver to the client. The
+ // super class will take care of delivering it; the implementation
+ // here just adds a little more logic.
+ @Override
+ public void deliverResult(ArrayList<Result> results) {
+ mResults = results;
+
+ if (isStarted()) {
+ // If the Loader is started, immediately deliver its results.
+ super.deliverResult(results);
+ }
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mResults != null) {
+ // If we currently have a result available, deliver it immediately.
+ deliverResult(mResults);
+ }
+
+ if (takeContentChanged() || mResults == null) {
+ // If the data has changed since the last time it was loaded
+ // or is not currently available, start a load.
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ // Attempt to cancel the current load task if possible.
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+
+ // At this point we can release the resources associated if needed.
+ mResults = null;
+ }
+}
View
32 src/com/android/mms/ui/ComposeMessageActivity.java
@@ -34,6 +34,7 @@
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
@@ -198,6 +199,7 @@
public static final int REQUEST_CODE_ECM_EXIT_DIALOG = 107;
public static final int REQUEST_CODE_ADD_CONTACT = 108;
public static final int REQUEST_CODE_PICK = 109;
+ public static final int REQUEST_CODE_ADD_RECIPIENTS = 111;
private static final String TAG = "Mms/compose";
@@ -309,6 +311,7 @@
private RecipientsEditor mRecipientsEditor; // UI control for editing recipients
private ImageButton mRecipientsPicker; // UI control for recipients picker
+ private ImageButton mRecipientsSelector; // UI control for recipients selector
// For HW keyboard, 'mIsKeyboardOpen' indicates if the HW keyboard is open.
// For SW keyboard, 'mIsKeyboardOpen' should always be true.
@@ -1891,12 +1894,17 @@ private void initRecipientsEditor() {
View stubView = stub.inflate();
mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
+ mRecipientsSelector = (ImageButton) stubView.findViewById(R.id.recipients_selector);
+ mRecipientsSelector.setVisibility(View.VISIBLE);
} else {
mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
mRecipientsEditor.setVisibility(View.VISIBLE);
mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
+ mRecipientsSelector = (ImageButton)findViewById(R.id.recipients_selector);
+ mRecipientsSelector.setVisibility(View.VISIBLE);
}
mRecipientsPicker.setOnClickListener(this);
+ mRecipientsSelector.setOnClickListener(this);
mRecipientsEditor.setAdapter(new ChipsRecipientAdapter(this));
mRecipientsEditor.populate(recipients);
@@ -3181,12 +3189,29 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
break;
+ case REQUEST_CODE_ADD_RECIPIENTS:
+ insertNumbersIntoRecipientsEditor(
+ data.getStringArrayListExtra(SelectRecipientsList.EXTRA_RECIPIENTS));
+ break;
+
default:
if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode);
break;
}
}
+ private void insertNumbersIntoRecipientsEditor(final ArrayList<String> numbers) {
+ ContactList list = ContactList.getByNumbers(numbers, true);
+ ContactList existing = mRecipientsEditor.constructContactsFromInput(true);
+ for (Contact contact : existing) {
+ if (!contact.existsInDatabase()) {
+ list.add(contact);
+ }
+ }
+ mRecipientsEditor.setText(null);
+ mRecipientsEditor.populate(list);
+ }
+
private void processPickResult(final Intent data) {
// The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
// multiple phone picker.
@@ -3558,8 +3583,13 @@ private void drawTopPanel(boolean showSubjectEditor) {
public void onClick(View v) {
if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) {
confirmSendMessageIfNeeded();
- } else if ((v == mRecipientsPicker)) {
+ } else if (v == mRecipientsPicker) {
launchMultiplePhonePicker();
+ } else if (v == mRecipientsSelector) {
+ Intent intent = new Intent(ComposeMessageActivity.this, SelectRecipientsList.class);
+ ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
+ intent.putExtra(SelectRecipientsList.EXTRA_RECIPIENTS, contacts.getNumbers());
+ startActivityForResult(intent, REQUEST_CODE_ADD_RECIPIENTS);
}
}
View
297 src/com/android/mms/ui/SelectRecipientsList.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.ui;
+
+import android.app.ActionBar;
+import android.app.ListActivity;
+import android.app.LoaderManager;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.mms.R;
+import com.android.mms.data.Group;
+import com.android.mms.data.PhoneNumber;
+import com.android.mms.data.RecipientsListLoader;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class SelectRecipientsList extends ListActivity implements
+ AdapterView.OnItemClickListener,
+ LoaderManager.LoaderCallbacks<ArrayList<RecipientsListLoader.Result>> {
+ private static final int MENU_DONE = 0;
+ private static final int MENU_MOBILE = 1;
+ private static final int MENU_GROUPS = 2;
+
+ public static final String EXTRA_RECIPIENTS = "recipients";
+ public static final String PREF_MOBILE_NUMBERS_ONLY = "pref_key_mobile_numbers_only";
+ public static final String PREF_SHOW_GROUPS = "pref_key_show_groups";
+
+ private SelectRecipientsListAdapter mListAdapter;
+ private HashSet<PhoneNumber> mCheckedPhoneNumbers;
+ private boolean mMobileOnly = true;
+ private boolean mShowGroups = true;
+ private View mProgressSpinner;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_recipients_list_screen);
+
+ mProgressSpinner = findViewById(R.id.progress_spinner);
+
+ // List view
+ ListView listView = getListView();
+ listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
+ listView.setFastScrollEnabled(true);
+ listView.setFastScrollAlwaysVisible(true);
+ listView.setDivider(null);
+ listView.setDividerHeight(0);
+ listView.setEmptyView(findViewById(R.id.empty));
+ listView.setOnItemClickListener(this);
+
+ // Get things ready
+ mCheckedPhoneNumbers = new HashSet<PhoneNumber>();
+ getLoaderManager().initLoader(0, null, this);
+
+ ActionBar mActionBar = getActionBar();
+ if (mActionBar != null) {
+ mActionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mListAdapter != null) {
+ unbindListItems();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Load the required preference values
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mMobileOnly = sharedPreferences.getBoolean(PREF_MOBILE_NUMBERS_ONLY, true);
+ mShowGroups = sharedPreferences.getBoolean(PREF_SHOW_GROUPS, true);
+
+ menu.add(0, MENU_DONE, 0, R.string.menu_done)
+ .setIcon(R.drawable.ic_menu_done_holo_light)
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT)
+ .setVisible(false);
+
+ menu.add(0, MENU_MOBILE, 0, R.string.menu_mobile)
+ .setCheckable(true)
+ .setChecked(mMobileOnly)
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ menu.add(0, MENU_GROUPS, 0, R.string.menu_groups)
+ .setCheckable(true)
+ .setChecked(mShowGroups)
+ .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(MENU_DONE).setVisible(mCheckedPhoneNumbers.size() > 0);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ switch (item.getItemId()) {
+ case MENU_DONE:
+ ArrayList<String> numbers = new ArrayList<String>();
+ for (PhoneNumber phoneNumber : mCheckedPhoneNumbers) {
+ if (phoneNumber.isChecked()) {
+ numbers.add(phoneNumber.getNumber());
+ }
+ }
+
+ // Pass the resulting set of numbers back
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_RECIPIENTS, numbers);
+ setResult(RESULT_OK, intent);
+ finish();
+ return true;
+ case MENU_MOBILE:
+ // If it was checked before it should be unchecked now and vice versa
+ mMobileOnly = !mMobileOnly;
+ item.setChecked(mMobileOnly);
+ prefs.edit().putBoolean(PREF_MOBILE_NUMBERS_ONLY, mMobileOnly).commit();
+
+ // Restart the loader to reflect the change
+ getLoaderManager().restartLoader(0, null, this);
+ return true;
+ case MENU_GROUPS:
+ // If it was checked before it should be unchecked now and vice versa
+ mShowGroups = !mShowGroups;
+ item.setChecked(mShowGroups);
+ prefs.edit().putBoolean(PREF_SHOW_GROUPS, mShowGroups).commit();
+
+ // Restart the loader to reflect the change
+ getLoaderManager().restartLoader(0, null, this);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long arg) {
+ RecipientsListLoader.Result item =
+ (RecipientsListLoader.Result) adapter.getItemAtPosition(position);
+
+ if (item.group != null) {
+ checkGroup(item.group, !item.group.isChecked());
+ } else {
+ checkPhoneNumber(item.phoneNumber, !item.phoneNumber.isChecked());
+ updateGroupCheckStateForNumber(item.phoneNumber, null);
+ }
+
+ invalidateOptionsMenu();
+ mListAdapter.notifyDataSetChanged();
+ }
+
+ private void checkPhoneNumber(PhoneNumber phoneNumber, boolean check) {
+ phoneNumber.setChecked(check);
+ if (check) {
+ mCheckedPhoneNumbers.add(phoneNumber);
+ } else {
+ mCheckedPhoneNumbers.remove(phoneNumber);
+ }
+ }
+
+ private void updateGroupCheckStateForNumber(PhoneNumber phoneNumber, Group excludedGroup) {
+ ArrayList<Group> phoneGroups = phoneNumber.getGroups();
+ if (phoneGroups == null) {
+ return;
+ }
+
+ if (phoneNumber.isChecked() && phoneNumber.isDefault()) {
+ for (Group group : phoneGroups) {
+ if (group == excludedGroup) {
+ continue;
+ }
+ boolean checked = true;
+ for (PhoneNumber number : group.getPhoneNumbers()) {
+ if (number.isDefault() && !number.isChecked()) {
+ checked = false;
+ break;
+ }
+ }
+ group.setChecked(checked);
+ }
+ } else if (!phoneNumber.isChecked()) {
+ for (Group group : phoneGroups) {
+ if (group != excludedGroup) {
+ group.setChecked(false);
+ }
+ }
+ }
+ }
+
+ private void checkGroup(Group group, boolean check) {
+ group.setChecked(check);
+ ArrayList<PhoneNumber> phoneNumbers = group.getPhoneNumbers();
+
+ if (phoneNumbers != null) {
+ for (PhoneNumber phoneNumber : phoneNumbers) {
+ if (phoneNumber.isDefault()) {
+ checkPhoneNumber(phoneNumber, check);
+ updateGroupCheckStateForNumber(phoneNumber, group);
+ }
+ }
+ }
+ }
+
+ private void unbindListItems() {
+ final ListView listView = getListView();
+ final int count = listView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ mListAdapter.unbindView(listView.getChildAt(i));
+ }
+ }
+
+ @Override
+ public Loader<ArrayList<RecipientsListLoader.Result>> onCreateLoader(int id, Bundle args) {
+ // Show the progress indicator
+ mProgressSpinner.setVisibility(View.VISIBLE);
+ return new RecipientsListLoader(this);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<ArrayList<RecipientsListLoader.Result>> loader,
+ ArrayList<RecipientsListLoader.Result> data) {
+ // We have an old list, get rid of it before we start again
+ if (mListAdapter != null) {
+ mListAdapter.notifyDataSetInvalidated();
+ unbindListItems();
+ }
+
+ // Hide the progress indicator
+ mProgressSpinner.setVisibility(View.GONE);
+
+ // Create and set the list adapter
+ mListAdapter = new SelectRecipientsListAdapter(this, data);
+
+ if (getIntent() != null) {
+ String[] initialRecipients = getIntent().getStringArrayExtra(EXTRA_RECIPIENTS);
+ if (initialRecipients != null) {
+ for (String recipient : initialRecipients) {
+ for (RecipientsListLoader.Result result : data) {
+ if (result.phoneNumber != null && result.phoneNumber.equals(recipient)) {
+ checkPhoneNumber(result.phoneNumber, true);
+ updateGroupCheckStateForNumber(result.phoneNumber, null);
+ break;
+ }
+ }
+ }
+ invalidateOptionsMenu();
+ }
+ setIntent(null);
+ }
+
+ if (mListAdapter == null) {
+ // We have no numbers to show, indicate it
+ TextView emptyText = (TextView) getListView().getEmptyView();
+ emptyText.setText(mMobileOnly ?
+ R.string.no_recipients_mobile_only : R.string.no_recipients);
+ } else {
+ setListAdapter(mListAdapter);
+ getListView().setRecyclerListener(mListAdapter);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<ArrayList<RecipientsListLoader.Result>> data) {
+ mListAdapter.notifyDataSetInvalidated();
+ }
+}
View
174 src/com/android/mms/ui/SelectRecipientsListAdapter.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.ui;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ArrayAdapter;
+import android.widget.SectionIndexer;
+
+import com.android.mms.R;
+import com.android.mms.data.Group;
+import com.android.mms.data.PhoneNumber;
+import com.android.mms.data.RecipientsListLoader;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SelectRecipientsListAdapter extends ArrayAdapter<RecipientsListLoader.Result>
+ implements SectionIndexer, AbsListView.RecyclerListener {
+ private final LayoutInflater mInflater;
+ private String[] mSections;
+ private int[] mPositions;
+
+ public SelectRecipientsListAdapter(Context context,
+ List<RecipientsListLoader.Result> items) {
+ super(context, R.layout.select_recipients_list_item, items);
+ mInflater = LayoutInflater.from(context);
+
+ ArrayList<String> sections = new ArrayList<String>();
+ ArrayList<Integer> positions = new ArrayList<Integer>();
+ String groupIndex = context.getString(R.string.fastscroll_index_groups);
+ String lastSectionIndex = null;
+ boolean hasSections = true;
+
+ for (int i = 0; i < items.size(); i++) {
+ RecipientsListLoader.Result item = items.get(i);
+ String index;
+
+ if (item.phoneNumber == null) {
+ index = groupIndex;
+ } else {
+ index = item.phoneNumber.getSectionIndex();
+ }
+
+ if (index == null) {
+ hasSections = false;
+ break;
+ }
+
+ if (!TextUtils.equals(index, lastSectionIndex)) {
+ sections.add(index);
+ positions.add(i);
+ lastSectionIndex = index;
+ }
+ }
+
+ if (!hasSections) {
+ sections.clear();
+ sections.add("");
+ positions.clear();
+ positions.add(0);
+ }
+
+ int count = sections.size();
+ mSections = new String[count];
+ mPositions = new int[count];
+ for (int i = 0; i < count; i++) {
+ mSections[i] = sections.get(i);
+ mPositions[i] = positions.get(i);
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup viewGroup) {
+ SelectRecipientsListItem view;
+
+ if (convertView == null) {
+ view = (SelectRecipientsListItem) mInflater.inflate(
+ R.layout.select_recipients_list_item, viewGroup, false);
+ } else {
+ view = (SelectRecipientsListItem) convertView;
+ }
+
+ bindView(position, view);
+ return view;
+ }
+
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ unbindView(view);
+ }
+
+ public void unbindView(View view) {
+ if (view instanceof SelectRecipientsListItem) {
+ SelectRecipientsListItem srli = (SelectRecipientsListItem) view;
+ srli.unbind();
+ }
+ }
+
+ private void bindView(int position, SelectRecipientsListItem view) {
+ final RecipientsListLoader.Result item = getItem(position);
+
+ if (item.group == null) {
+ PhoneNumber phoneNumber = item.phoneNumber;
+ PhoneNumber lastNumber = position != 0
+ ? getItem(position - 1).phoneNumber : null;
+ PhoneNumber nextNumber = position != getCount() - 1
+ ? getItem(position + 1).phoneNumber : null;
+ long contactId = phoneNumber.getContactId();
+ long lastContactId = lastNumber != null ? lastNumber.getContactId() : -1;
+ long nextContactId = nextNumber != null ? nextNumber.getContactId() : -1;
+
+ boolean showHeader = Arrays.binarySearch(mPositions, position) >= 0;
+ boolean showFooter = contactId != nextContactId;
+ boolean isFirst = contactId != lastContactId;
+
+ view.bind(getContext(), phoneNumber, showHeader, showFooter, isFirst);
+ } else {
+ view.bind(getContext(), item.group, position == 0);
+ }
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if (section < 0 || section >= mSections.length) {
+ return -1;
+ }
+
+ return mPositions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ if (position < 0 || position >= getCount()) {
+ return -1;
+ }
+
+ int index = Arrays.binarySearch(mPositions, position);
+
+ /*
+ * Consider this example: section positions are 0, 3, 5; the supplied
+ * position is 4. The section corresponding to position 4 starts at
+ * position 3, so the expected return value is 1. Binary search will not
+ * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+ * To get from that number to the expected value of 1 we need to negate
+ * and subtract 2.
+ */
+ return index >= 0 ? index : -index - 2;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSections;
+ }
+}
View
185 src/com/android/mms/ui/SelectRecipientsListItem.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2013 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mms.ui;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.mms.R;
+import com.android.mms.data.Contact;
+import com.android.mms.data.Group;
+import com.android.mms.data.PhoneNumber;
+
+public class SelectRecipientsListItem extends LinearLayout implements Contact.UpdateListener {
+
+ private static Drawable sDefaultContactImage;
+
+ private static final int MSG_UPDATE_AVATAR = 1;
+
+ private static Handler sHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_AVATAR:
+ SelectRecipientsListItem item = (SelectRecipientsListItem) msg.obj;
+ item.updateAvatarView();
+ break;
+ }
+ }
+ };
+
+ private View mHeader;
+ private View mFooter;
+ private TextView mSeparator;
+ private TextView mNameView;
+ private TextView mNumberView;
+ private TextView mLabelView;
+ private QuickContactBadge mAvatarView;
+ private CheckBox mCheckBox;
+
+ private Contact mContact;
+
+ public SelectRecipientsListItem(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ if (sDefaultContactImage == null) {
+ sDefaultContactImage =
+ context.getResources().getDrawable(R.drawable.ic_contact_picture);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mHeader = findViewById(R.id.header);
+ mFooter = findViewById(R.id.footer);
+ mSeparator = (TextView) findViewById(R.id.separator);
+ mNameView = (TextView) findViewById(R.id.name);
+ mNumberView = (TextView) findViewById(R.id.number);
+ mLabelView = (TextView) findViewById(R.id.label);
+ mAvatarView = (QuickContactBadge) findViewById(R.id.avatar);
+ mCheckBox = (CheckBox) findViewById(R.id.checkbox);
+ }
+
+ @Override
+ public void onUpdate(Contact updated) {
+ if (updated == mContact) {
+ sHandler.obtainMessage(MSG_UPDATE_AVATAR, this).sendToTarget();
+ }
+ }
+
+ private void updateAvatarView() {
+ if (mContact == null) {
+ // we were unbound in the meantime
+ return;
+ }
+
+ Drawable avatarDrawable = mContact.getAvatar(mContext, sDefaultContactImage);
+
+ if (mContact.existsInDatabase()) {
+ mAvatarView.assignContactUri(mContact.getUri());
+ } else {
+ mAvatarView.assignContactFromPhone(mContact.getNumber(), true);
+ }
+
+ mAvatarView.setImageDrawable(avatarDrawable);
+ mAvatarView.setVisibility(View.VISIBLE);
+ }
+
+ public final void bind(Context context, final PhoneNumber phoneNumber,
+ boolean showHeader, boolean showFooter, boolean isFirst) {
+ if (showHeader) {
+ String index = phoneNumber.getSectionIndex();
+ mHeader.setVisibility(View.VISIBLE);
+ mSeparator.setText(index != null ? index.toUpperCase() : "");
+ } else {
+ mHeader.setVisibility(View.GONE);
+ }
+
+ if (showFooter) {
+ mFooter.setVisibility(View.VISIBLE);
+ } else {
+ mFooter.setVisibility(View.GONE);
+ }
+
+ if (isFirst) {
+ mNameView.setVisibility(View.VISIBLE);
+
+ if (mContact == null) {
+ mContact = Contact.get(phoneNumber.getNumber(), false);
+ Contact.addListener(this);
+ }
+ updateAvatarView();
+ } else {
+ mNameView.setVisibility(View.GONE);
+ mAvatarView.setVisibility(View.INVISIBLE);
+ }
+
+ mNumberView.setText(phoneNumber.getNumber());
+ mNameView.setText(phoneNumber.getName());
+ mLabelView.setText(Phone.getTypeLabel(getResources(),
+ phoneNumber.getType(), phoneNumber.getLabel()));
+ mLabelView.setVisibility(View.VISIBLE);
+ mCheckBox.setChecked(phoneNumber.isChecked());
+ }
+
+ public final void bind(Context context, final Group group, boolean showHeader) {
+ if (showHeader) {
+ mHeader.setVisibility(View.VISIBLE);
+ mSeparator.setText(R.string.groups_header);
+ } else {
+ mHeader.setVisibility(View.GONE);
+ }
+
+ mFooter.setVisibility(View.VISIBLE);
+ mNameView.setVisibility(View.VISIBLE);
+
+ SpannableStringBuilder groupTitle = new SpannableStringBuilder(group.getTitle());
+ int before = groupTitle.length();
+
+ groupTitle.append(" ");
+ groupTitle.append(Integer.toString(group.getSummaryCount()));
+ groupTitle.setSpan(new ForegroundColorSpan(
+ context.getResources().getColor(R.color.message_count_color)),
+ before, groupTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ mNameView.setText(groupTitle);
+
+ mNumberView.setVisibility(View.VISIBLE);
+ mNumberView.setText(group.getAccountName());
+ mLabelView.setVisibility(View.GONE);
+ mCheckBox.setChecked(group.isChecked());
+ mAvatarView.setVisibility(View.GONE);
+ }
+
+ public void unbind() {
+ Contact.removeListener(this);
+ mContact = null;
+ }
+}

0 comments on commit 67ccdbc

Please sign in to comment.