Merge remote-tracking branch 'goog/nyc-mr1-dev' into aosp/master

This commit is contained in:
Tony Mak
2016-06-29 20:11:17 +00:00
22 changed files with 425 additions and 110 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
package="com.android.messaging" package="com.android.messaging"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="23" /> <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="24" />
<!-- Application holds CPU wakelock while working in background --> <!-- Application holds CPU wakelock while working in background -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
+1 -1
View File
@@ -23,7 +23,7 @@
} }
# Keep methods that have the @VisibleForAnimation annotation # Keep methods that have the @VisibleForAnimation annotation
-keep @interface com.android.messaging.annotation.VisibleForAnimation -keep interface com.android.messaging.annotation.VisibleForAnimation
-keepclassmembers class * { -keepclassmembers class * {
@com.android.messaging.annotation.VisibleForAnimation *; @com.android.messaging.annotation.VisibleForAnimation *;
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

+15
View File
@@ -63,16 +63,31 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" > android:layout_gravity="center_vertical" >
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/contact_name" android:id="@+id/contact_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingBottom="4dp" android:paddingBottom="4dp"
android:singleLine="true" android:singleLine="true"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
style="@style/ContactListItem" /> style="@style/ContactListItem" />
<ImageView android:id="@+id/work_profile_icon"
android:src="@drawable/ic_work_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
android:layout_gravity="center_vertical"
android:visibility="gone" />
</LinearLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2009 The Android Open Source 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 A`NY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Layout used for list section separators. -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="@string/work_directory_display_name"
style="@style/DirectoryHeaderStyle"
/>
+6
View File
@@ -188,4 +188,10 @@
<dimen name="fastscroll_preview_margin_left_right">8dp</dimen> <dimen name="fastscroll_preview_margin_left_right">8dp</dimen>
<dimen name="fastscroll_preview_text_size">24sp</dimen> <dimen name="fastscroll_preview_text_size">24sp</dimen>
<dimen name="directory_header_padding_start">16dp</dimen>
<dimen name="directory_header_padding_end">32dp</dimen>
<dimen name="directory_header_padding_top">18dp</dimen>
<dimen name="directory_header_padding_bottom">8dp</dimen>
<dimen name="directory_header_text_size">14sp</dimen>
</resources> </resources>
+10 -8
View File
@@ -152,7 +152,7 @@
<!-- While sending a message display this message. --> <!-- While sending a message display this message. -->
<string name="message_status_sending">Sending&#8230;</string> <string name="message_status_sending">Sending&#8230;</string>
<!-- When sending a message failed display this message. --> <!-- When sending a message failed display this message. -->
<string name="message_status_send_failed">Not sent. Touch to try again.</string> <string name="message_status_send_failed">Not sent. Tap to try again.</string>
<!-- When retrying sending for a message. --> <!-- When retrying sending for a message. -->
<string name="message_status_send_retrying">Not sent. Trying again&#8230;</string> <string name="message_status_send_retrying">Not sent. Trying again&#8230;</string>
<!-- When showing resend action display this message. --> <!-- When showing resend action display this message. -->
@@ -171,9 +171,9 @@
<!-- Title line for MMS which failed to download. --> <!-- Title line for MMS which failed to download. -->
<string name="message_title_download_failed">Couldn\'t download</string> <string name="message_title_download_failed">Couldn\'t download</string>
<!-- Timestamp line for MMS which failed to download. --> <!-- Timestamp line for MMS which failed to download. -->
<string name="message_status_download_failed">Touch to try again</string> <string name="message_status_download_failed">Tap to try again</string>
<!-- Timestamp line for MMS which failed to download. --> <!-- Timestamp line for MMS which failed to download. -->
<string name="message_status_download">Touch to download</string> <string name="message_status_download">Tap to download</string>
<!-- Timestamp line for MMS which is selected. --> <!-- Timestamp line for MMS which is selected. -->
<string name="message_status_download_action">Download or delete</string> <string name="message_status_download_action">Download or delete</string>
<!-- Timestamp line to display while downloading a message. --> <!-- Timestamp line to display while downloading a message. -->
@@ -296,7 +296,7 @@
<!-- Error occurred while recording audio --> <!-- Error occurred while recording audio -->
<string name="audio_recording_error"> Couldn\'t save audio. Try again.</string> <string name="audio_recording_error"> Couldn\'t save audio. Try again.</string>
<!-- Hint text on the audio recorder that instructs user how to start recording --> <!-- Hint text on the audio recorder that instructs user how to start recording -->
<string name="audio_picker_hint_text">Touch &amp; hold</string> <string name="audio_picker_hint_text">Tap &amp; hold</string>
<!-- An enumeration comma for separating multiple names in notifications. [CHAR LIMIT=2] --> <!-- An enumeration comma for separating multiple names in notifications. [CHAR LIMIT=2] -->
<string name="enumeration_comma">,\u0020</string> <string name="enumeration_comma">,\u0020</string>
@@ -348,7 +348,7 @@
<string name="notification_on_toast_message">Notifications turned on</string> <string name="notification_on_toast_message">Notifications turned on</string>
<!-- Toast shown when the user tries to send a message, and then sets Bugle as the default SMS app. --> <!-- Toast shown when the user tries to send a message, and then sets Bugle as the default SMS app. -->
<string name="toast_after_setting_default_sms_app_for_message_send">All set. Touch Send again.</string> <string name="toast_after_setting_default_sms_app_for_message_send">All set. Tap Send again.</string>
<!-- Toast shown when the user successfully sets Bugle as the default SMS app. --> <!-- Toast shown when the user successfully sets Bugle as the default SMS app. -->
<string name="toast_after_setting_default_sms_app">Messaging successfully set as the default SMS app.</string> <string name="toast_after_setting_default_sms_app">Messaging successfully set as the default SMS app.</string>
@@ -378,7 +378,7 @@
<string name="group_outgoing_failed_message_prefix">Failed message to <xliff:g id="group">%s</xliff:g>: <xliff:g id="message">%s</xliff:g>. Time: <xliff:g id="time">%s</xliff:g>.</string> <string name="group_outgoing_failed_message_prefix">Failed message to <xliff:g id="group">%s</xliff:g>: <xliff:g id="message">%s</xliff:g>. Time: <xliff:g id="time">%s</xliff:g>.</string>
<string name="group_outgoing_successful_message_prefix">Message to <xliff:g id="group">%s</xliff:g>: <xliff:g id="message">%s</xliff:g>. Time: <xliff:g id="time">%s</xliff:g>.</string> <string name="group_outgoing_successful_message_prefix">Message to <xliff:g id="group">%s</xliff:g>: <xliff:g id="message">%s</xliff:g>. Time: <xliff:g id="time">%s</xliff:g>.</string>
<!-- Accessibility description for conversation list for failed messages --> <!-- Accessibility description for conversation list for failed messages -->
<string name="failed_message_content_description">Failed message. Touch to retry.</string> <string name="failed_message_content_description">Failed message. Tap to retry.</string>
<!-- Accessibility name for a group conversation --> <!-- Accessibility name for a group conversation -->
<string name="group_conversation_description">Conversation with <xliff:g id="participants">%s</xliff:g></string> <string name="group_conversation_description">Conversation with <xliff:g id="participants">%s</xliff:g></string>
@@ -905,7 +905,7 @@
<string name="send_button_long_click_description_with_sim_selector">Select SIM or edit subject</string> <string name="send_button_long_click_description_with_sim_selector">Select SIM or edit subject</string>
<!-- Content description for the audio record view --> <!-- Content description for the audio record view -->
<string name="audio_record_view_content_description">Touch and hold to record audio</string> <string name="audio_record_view_content_description">Tap &amp; hold to record audio</string>
<!-- Content description for new conversation button in desktop widget --> <!-- Content description for new conversation button in desktop widget -->
<string name="widget_new_conversation_content_description">Start new conversation</string> <string name="widget_new_conversation_content_description">Start new conversation</string>
@@ -931,7 +931,7 @@
<string name="conversation_deleted">Conversation deleted</string> <string name="conversation_deleted">Conversation deleted</string>
<!-- Displayed when user adds a new conversation widget. Tapping on the widget in this <!-- Displayed when user adds a new conversation widget. Tapping on the widget in this
mode will bring user to the conversation selection screen --> mode will bring user to the conversation selection screen -->
<string name="tap_to_configure">Conversation deleted. Touch to show a different Messaging conversation</string> <string name="tap_to_configure">Conversation deleted. Tap to show a different Messaging conversation</string>
<!-- Toast message telling the user that someone was blocked --> <!-- Toast message telling the user that someone was blocked -->
<string name="update_destination_blocked">Blocked</string> <string name="update_destination_blocked">Blocked</string>
@@ -973,4 +973,6 @@
<!-- The accessibility text read when the sim chooser pops up to read the current selected sim --> <!-- The accessibility text read when the sim chooser pops up to read the current selected sim -->
<string name="selected_sim_content_message"><xliff:g id="selected_sim">%s</xliff:g> selected</string> <string name="selected_sim_content_message"><xliff:g id="selected_sim">%s</xliff:g> selected</string>
<string name="work_directory_display_name">Work Profile contacts</string>
</resources> </resources>
+10
View File
@@ -623,4 +623,14 @@
<item name="android:textColor">@android:color/white</item> <item name="android:textColor">@android:color/white</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
<style name="DirectoryHeaderStyle">
<item name="android:paddingLeft">@dimen/directory_header_padding_start</item>
<item name="android:paddingStart">@dimen/directory_header_padding_start</item>
<item name="android:paddingRight">@dimen/directory_header_padding_end</item>
<item name="android:paddingEnd">@dimen/directory_header_padding_end</item>
<item name="android:paddingTop">@dimen/directory_header_padding_top</item>
<item name="android:paddingBottom">@dimen/directory_header_padding_bottom</item>
<item name="android:textSize">@dimen/directory_header_text_size</item>
<item name="android:textStyle">bold</item>
</style>
</resources> </resources>
@@ -145,7 +145,7 @@ public class EncodedStringValue implements Cloneable {
} }
try { try {
return new String(mData, CharacterSets.MIMENAME_ISO_8859_1); return new String(mData, CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException _) { } catch (UnsupportedEncodingException e2) {
return new String(mData); // system default encoding. return new String(mData); // system default encoding.
} }
} }
@@ -216,7 +216,7 @@ public class EncodedStringValue implements Cloneable {
try { try {
ret[i] = new EncodedStringValue(mCharacterSet, ret[i] = new EncodedStringValue(mCharacterSet,
temp[i].getBytes()); temp[i].getBytes());
} catch (NullPointerException _) { } catch (NullPointerException e) {
// Can't arrive here // Can't arrive here
return null; return null;
} }
@@ -46,6 +46,9 @@ public class ContactListItemData {
// existing chip for which we show full contact detail for the selected contact). // existing chip for which we show full contact detail for the selected contact).
private boolean mSingleRecipient; private boolean mSingleRecipient;
// Is the contact in managed profile.
private boolean mIsWorkContact;
/** /**
* Bind to a contact cursor in the contact list. * Bind to a contact cursor in the contact list.
*/ */
@@ -77,6 +80,8 @@ public class ContactListItemData {
mRecipientEntry = ContactUtil.createRecipientEntry(displayName, mRecipientEntry = ContactUtil.createRecipientEntry(displayName,
DisplayNameSources.STRUCTURED_NAME, destination, destinationType, destinationLabel, DisplayNameSources.STRUCTURED_NAME, destination, destinationType, destinationLabel,
contactId, lookupKey, dataId, photoThumbnailUri, isFirstLevel); contactId, lookupKey, dataId, photoThumbnailUri, isFirstLevel);
mIsWorkContact = ContactUtil.isEnterpriseContactId(contactId);
} }
/** /**
@@ -84,13 +89,15 @@ public class ContactListItemData {
* optional styled name & destination for showing bold search match. * optional styled name & destination for showing bold search match.
*/ */
public void bind(final RecipientEntry entry, final CharSequence styledName, public void bind(final RecipientEntry entry, final CharSequence styledName,
final CharSequence styledDestination, final boolean singleRecipient) { final CharSequence styledDestination, final boolean singleRecipient,
final boolean isWorkContact) {
Assert.isTrue(entry.isValid()); Assert.isTrue(entry.isValid());
mRecipientEntry = entry; mRecipientEntry = entry;
mStyledName = styledName; mStyledName = styledName;
mStyledDestination = styledDestination; mStyledDestination = styledDestination;
mAlphabetHeader = null; mAlphabetHeader = null;
mSingleRecipient = singleRecipient; mSingleRecipient = singleRecipient;
mIsWorkContact = isWorkContact;
} }
public CharSequence getDisplayName() { public CharSequence getDisplayName() {
@@ -157,4 +164,11 @@ public class ContactListItemData {
public RecipientEntry getRecipientEntry() { public RecipientEntry getRecipientEntry() {
return mRecipientEntry; return mRecipientEntry;
} }
/**
* @return whether the contact is in managed profile.
*/
public boolean getIsWorkContact() {
return mIsWorkContact;
}
} }
@@ -79,8 +79,7 @@ public class PopupTransitionAnimation extends Animation {
mDestRect = new Rect(); mDestRect = new Rect();
mPopupRect = new Rect(); mPopupRect = new Rect();
mActionBarRect = new Rect(); mActionBarRect = new Rect();
final Activity activity = (Activity) viewToAnimate.getRootView().getContext(); mActionBarView = viewToAnimate.getRootView().findViewById(
mActionBarView = activity.getWindow().getDecorView().findViewById(
android.support.v7.appcompat.R.id.action_bar); android.support.v7.appcompat.R.id.action_bar);
mRectEvaluator = RectEvaluatorCompat.create(); mRectEvaluator = RectEvaluatorCompat.create();
setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION); setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
@@ -34,6 +34,7 @@ import com.android.messaging.ui.ContactIconView;
import com.android.messaging.util.Assert; import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil; import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.ContactRecipientEntryUtils; import com.android.messaging.util.ContactRecipientEntryUtils;
import com.android.messaging.util.ContactUtil;
/** /**
* An implementation for {@link DropdownChipLayouter}. Layouts the dropdown * An implementation for {@link DropdownChipLayouter}. Layouts the dropdown
@@ -99,8 +100,9 @@ public class ContactDropdownLayouter extends DropdownChipLayouter {
Assert.isTrue(itemView instanceof ContactListItemView); Assert.isTrue(itemView instanceof ContactListItemView);
final ContactListItemView contactListItemView = (ContactListItemView) itemView; final ContactListItemView contactListItemView = (ContactListItemView) itemView;
contactListItemView.setImageClickHandlerDisabled(true); contactListItemView.setImageClickHandlerDisabled(true);
boolean isWorkContact = ContactUtil.isEnterpriseContactId(entry.getContactId());
contactListItemView.bind(entry, styledResults[0], styledResults[1], contactListItemView.bind(entry, styledResults[0], styledResults[1],
mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT)); mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT), isWorkContact);
return itemView; return itemView;
} }
@@ -53,6 +53,7 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
private TextView mAlphabetHeaderTextView; private TextView mAlphabetHeaderTextView;
private ContactIconView mContactIconView; private ContactIconView mContactIconView;
private ImageView mContactCheckmarkView; private ImageView mContactCheckmarkView;
private ImageView mWorkProfileIcon;
private HostInterface mHostInterface; private HostInterface mHostInterface;
private boolean mShouldShowAlphabetHeader; private boolean mShouldShowAlphabetHeader;
@@ -69,6 +70,7 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mAlphabetHeaderTextView = (TextView) findViewById(R.id.alphabet_header); mAlphabetHeaderTextView = (TextView) findViewById(R.id.alphabet_header);
mContactIconView = (ContactIconView) findViewById(R.id.contact_icon); mContactIconView = (ContactIconView) findViewById(R.id.contact_icon);
mContactCheckmarkView = (ImageView) findViewById(R.id.contact_checkmark); mContactCheckmarkView = (ImageView) findViewById(R.id.contact_checkmark);
mWorkProfileIcon = (ImageView) findViewById(R.id.work_profile_icon);
} }
/** /**
@@ -100,11 +102,12 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
* @param isSingleRecipient whether this item is shown as the only line item in the single * @param isSingleRecipient whether this item is shown as the only line item in the single
* recipient drop down from the chips view. If this is the case, we always show the * recipient drop down from the chips view. If this is the case, we always show the
* contact avatar even if it's not a first-level entry. * contact avatar even if it's not a first-level entry.
* @param isWorkContact whether the contact is in managed profile.
*/ */
public void bind(final RecipientEntry recipientEntry, final CharSequence styledName, public void bind(final RecipientEntry recipientEntry, final CharSequence styledName,
final CharSequence styledDestination, final HostInterface hostInterface, final CharSequence styledDestination, final HostInterface hostInterface,
final boolean isSingleRecipient) { final boolean isSingleRecipient, final boolean isWorkContact) {
mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient); mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient, isWorkContact);
mHostInterface = hostInterface; mHostInterface = hostInterface;
mShouldShowAlphabetHeader = false; mShouldShowAlphabetHeader = false;
updateViewAppearance(); updateViewAppearance();
@@ -152,7 +155,11 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mContactDetailsTextView.setVisibility(VISIBLE); mContactDetailsTextView.setVisibility(VISIBLE);
mContactDetailTypeTextView.setVisibility(VISIBLE); mContactDetailTypeTextView.setVisibility(VISIBLE);
} }
if (mData.getIsWorkContact()) {
mWorkProfileIcon.setVisibility(VISIBLE);
} else {
mWorkProfileIcon.setVisibility(GONE);
}
if (mShouldShowAlphabetHeader) { if (mShouldShowAlphabetHeader) {
mAlphabetHeaderTextView.setVisibility(VISIBLE); mAlphabetHeaderTextView.setVisibility(VISIBLE);
mAlphabetHeaderTextView.setText(mData.getAlphabetHeader()); mAlphabetHeaderTextView.setText(mData.getAlphabetHeader());
@@ -22,18 +22,24 @@ import android.support.v4.util.Pair;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.util.Rfc822Token; import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer; import android.text.util.Rfc822Tokenizer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter; import android.widget.Filter;
import android.widget.TextView;
import com.android.ex.chips.BaseRecipientAdapter; import com.android.ex.chips.BaseRecipientAdapter;
import com.android.ex.chips.RecipientAlternatesAdapter; import com.android.ex.chips.RecipientAlternatesAdapter;
import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback; import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
import com.android.ex.chips.RecipientEntry; import com.android.ex.chips.RecipientEntry;
import com.android.messaging.R;
import com.android.messaging.util.Assert; import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread; import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.BugleGservices; import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.ContactRecipientEntryUtils; import com.android.messaging.util.ContactRecipientEntryUtils;
import com.android.messaging.util.ContactUtil; import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.PhoneUtils;
import java.text.Collator; import java.text.Collator;
@@ -53,6 +59,18 @@ import java.util.Map;
* for {@link ContactRecipientAutoCompleteView} * for {@link ContactRecipientAutoCompleteView}
*/ */
public final class ContactRecipientAdapter extends BaseRecipientAdapter { public final class ContactRecipientAdapter extends BaseRecipientAdapter {
private static final int WORD_DIRECTORY_HEADER_POS_NONE = -1;
/**
* Stores the index of work directory header.
*/
private int mWorkDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
private final LayoutInflater mInflater;
/**
* Type of directory entry.
*/
private static final int ENTRY_TYPE_DIRECTORY = RecipientEntry.ENTRY_TYPE_SIZE;
public ContactRecipientAdapter(final Context context, public ContactRecipientAdapter(final Context context,
final ContactListItemView.HostInterface clivHost) { final ContactListItemView.HostInterface clivHost) {
this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost); this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
@@ -62,6 +80,7 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
final int queryMode, final ContactListItemView.HostInterface clivHost) { final int queryMode, final ContactListItemView.HostInterface clivHost) {
super(context, preferredMaxResultCount, queryMode); super(context, preferredMaxResultCount, queryMode);
setPhotoManager(new ContactRecipientPhotoManager(context, clivHost)); setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} }
@Override @Override
@@ -81,6 +100,7 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
* results. * results.
*/ */
public class ContactFilter extends Filter { public class ContactFilter extends Filter {
// Used to sort filtered contacts when it has combined results from email and phone. // Used to sort filtered contacts when it has combined results from email and phone.
private final RecipientEntryComparator mComparator = new RecipientEntryComparator(); private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
@@ -95,21 +115,45 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
* return the merged results. * return the merged results.
*/ */
@DoesNotRunOnMainThread @DoesNotRunOnMainThread
private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context, private CursorResult getFilteredResultsCursor(final String searchText) {
final String searchText) {
Assert.isNotMainThread(); Assert.isNotMainThread();
if (BugleGservices.get().getBoolean( if (BugleGservices.get().getBoolean(
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS, BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) { BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
return Pair.create((Cursor) new MergeCursor(new Cursor[] {
ContactUtil.filterPhones(getContext(), searchText) final Cursor personalFilterPhonesCursor = ContactUtil
.performSynchronousQuery(), .filterPhones(getContext(), searchText).performSynchronousQuery();
ContactUtil.filterEmails(getContext(), searchText) final Cursor personalFilterEmailsCursor = ContactUtil
.performSynchronousQuery() .filterEmails(getContext(), searchText).performSynchronousQuery();
}), false /* the merged cursor is not sorted */); final Cursor personalCursor = new MergeCursor(
new Cursor[]{personalFilterEmailsCursor, personalFilterPhonesCursor});
final CursorResult cursorResult =
new CursorResult(personalCursor, false /* sorted */);
if (OsUtil.isAtLeastN()) {
// Including enterprise result starting from N.
final Cursor enterpriseFilterPhonesCursor = ContactUtil.filterPhonesEnterprise(
getContext(), searchText).performSynchronousQuery();
final Cursor enterpriseFilterEmailsCursor = ContactUtil.filterEmailsEnterprise(
getContext(), searchText).performSynchronousQuery();
final Cursor enterpriseCursor = new MergeCursor(
new Cursor[]{enterpriseFilterEmailsCursor,
enterpriseFilterPhonesCursor});
cursorResult.enterpriseCursor = enterpriseCursor;
}
return cursorResult;
} else { } else {
return Pair.create(ContactUtil.filterDestination(getContext(), searchText) final Cursor personalFilterDestinationCursor = ContactUtil
.performSynchronousQuery(), true); .filterDestination(getContext(), searchText).performSynchronousQuery();
final CursorResult cursorResult = new CursorResult(personalFilterDestinationCursor,
true);
if (OsUtil.isAtLeastN()) {
// Including enterprise result starting from N.
final Cursor enterpriseFilterDestinationCursor = ContactUtil
.filterDestinationEnterprise(getContext(), searchText)
.performSynchronousQuery();
cursorResult.enterpriseCursor = enterpriseFilterDestinationCursor;
}
return cursorResult;
} }
} }
@@ -128,13 +172,9 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
// Query for auto-complete results, since performFiltering() is not done on the // Query for auto-complete results, since performFiltering() is not done on the
// main thread, perform the cursor loader queries directly. // main thread, perform the cursor loader queries directly.
final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
searchText); final CursorResult cursorResult = getFilteredResultsCursor(searchText);
final Cursor cursor = filteredResults.first; final List<RecipientEntry> entries = new ArrayList<>();
final boolean sorted = filteredResults.second;
if (cursor != null) {
try {
final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
// First check if the constraint is a valid SMS destination. If so, add the // First check if the constraint is a valid SMS destination. If so, add the
// destination as a suggestion item to the drop down. // destination as a suggestion item to the drop down.
@@ -143,29 +183,46 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
.constructSendToDestinationEntry(searchText)); .constructSendToDestinationEntry(searchText));
} }
HashSet<Long> existingContactIds = new HashSet<Long>(); // Only show work directory header if more than one result in work directory.
int workDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
if (cursorResult.enterpriseCursor != null
&& cursorResult.enterpriseCursor.getCount() > 0) {
if (cursorResult.personalCursor != null) {
workDirectoryHeaderPos = entries.size();
workDirectoryHeaderPos += cursorResult.personalCursor.getCount();
}
}
final Cursor[] cursors = new Cursor[]{cursorResult.personalCursor,
cursorResult.enterpriseCursor};
for (Cursor cursor : cursors) {
if (cursor != null) {
try {
final List<RecipientEntry> tempEntries = new ArrayList<>();
HashSet<Long> existingContactIds = new HashSet<>();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
// Make sure there's only one first-level contact (i.e. contact for which // Make sure there's only one first-level contact (i.e. contact for
// we show the avatar picture and name) for every contact id. // which we show the avatar picture and name) for every contact id.
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID); final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
final boolean isFirstLevel = !existingContactIds.contains(contactId); final boolean isFirstLevel = !existingContactIds.contains(contactId);
if (isFirstLevel) { if (isFirstLevel) {
existingContactIds.add(contactId); existingContactIds.add(contactId);
} }
entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor, tempEntries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
isFirstLevel)); isFirstLevel));
} }
if (!sorted) { if (!cursorResult.isSorted) {
Collections.sort(entries, mComparator); Collections.sort(tempEntries, mComparator);
} }
results.values = entries; entries.addAll(tempEntries);
results.count = 1;
} finally { } finally {
cursor.close(); cursor.close();
} }
} }
}
results.values = new ContactReceipientFilterResult(entries, workDirectoryHeaderPos);
results.count = 1;
return results; return results;
} }
@@ -174,16 +231,20 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
mCurrentConstraint = constraint; mCurrentConstraint = constraint;
clearTempEntries(); clearTempEntries();
if (results.values != null) { final ContactReceipientFilterResult contactReceipientFilterResult
@SuppressWarnings("unchecked") = (ContactReceipientFilterResult) results.values;
final List<RecipientEntry> entries = (List<RecipientEntry>) results.values; if (contactReceipientFilterResult != null) {
updateEntries(entries); mWorkDirectoryHeaderPos = contactReceipientFilterResult.workDirectoryPos;
if (contactReceipientFilterResult.recipientEntries != null) {
updateEntries(contactReceipientFilterResult.recipientEntries);
} else { } else {
updateEntries(Collections.<RecipientEntry>emptyList()); updateEntries(Collections.<RecipientEntry>emptyList());
} }
} }
}
private class RecipientEntryComparator implements Comparator<RecipientEntry> { private class RecipientEntryComparator implements Comparator<RecipientEntry> {
private final Collator mCollator; private final Collator mCollator;
public RecipientEntryComparator() { public RecipientEntryComparator() {
@@ -237,6 +298,38 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
} }
} }
} }
private class CursorResult {
public final Cursor personalCursor;
public Cursor enterpriseCursor;
public final boolean isSorted;
public CursorResult(Cursor personalCursor, boolean isSorted) {
this.personalCursor = personalCursor;
this.isSorted = isSorted;
}
}
private class ContactReceipientFilterResult {
/**
* Recipient entries in all directories.
*/
public final List<RecipientEntry> recipientEntries;
/**
* Index of row that showing work directory header.
*/
public final int workDirectoryPos;
public ContactReceipientFilterResult(List<RecipientEntry> recipientEntries,
int workDirectoryPos) {
this.recipientEntries = recipientEntries;
this.workDirectoryPos = workDirectoryPos;
}
}
} }
/** /**
@@ -283,4 +376,81 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
// report matches // report matches
callback.matchesFound(recipientEntries); callback.matchesFound(recipientEntries);
} }
/**
* We handle directory header here and then delegate the work of creating recipient views to
* the {@link BaseRecipientAdapter}. Please notice that we need to fix the position
* before passing to {@link BaseRecipientAdapter} because it is not aware of the existence of
* directory headers.
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView;
if (isDirectoryEntry(position)) {
if (convertView == null) {
textView = (TextView) mInflater.inflate(R.layout.work_directory_header, parent,
false);
} else {
textView = (TextView) convertView;
}
return textView;
}
return super.getView(fixPosition(position), convertView, parent);
}
@Override
public RecipientEntry getItem(int position) {
if (isDirectoryEntry(position)) {
return null;
}
return super.getItem(fixPosition(position));
}
@Override
public int getViewTypeCount() {
return RecipientEntry.ENTRY_TYPE_SIZE + 1;
}
@Override
public int getItemViewType(int position) {
if (isDirectoryEntry(position)) {
return ENTRY_TYPE_DIRECTORY;
}
return super.getItemViewType(fixPosition(position));
}
@Override
public boolean isEnabled(int position) {
if (isDirectoryEntry(position)) {
return false;
}
return super.isEnabled(fixPosition(position));
}
@Override
public int getCount() {
return super.getCount() + ((hasWorkDirectoryHeader()) ? 1 : 0);
}
private boolean isDirectoryEntry(int position) {
return position == mWorkDirectoryHeaderPos;
}
/**
* @return the position of items without counting directory headers.
*/
private int fixPosition(int position) {
if (hasWorkDirectoryHeader()) {
Assert.isTrue(position != mWorkDirectoryHeaderPos);
if (position > mWorkDirectoryHeaderPos) {
return position - 1;
}
}
return position;
}
private boolean hasWorkDirectoryHeader() {
return mWorkDirectoryHeaderPos != WORD_DIRECTORY_HEADER_POS_NONE;
}
} }
@@ -34,6 +34,7 @@ import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType; import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil; import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaMetadataRetrieverWrapper; import com.android.messaging.util.MediaMetadataRetrieverWrapper;
import com.android.messaging.util.FileUtil;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -158,9 +159,13 @@ public class ShareIntentActivity extends BaseBugleActivity implements
} }
private void addSharedImagePartToDraft(final String contentType, final Uri imageUri) { private void addSharedImagePartToDraft(final String contentType, final Uri imageUri) {
if (FileUtil.isInPrivateDir(imageUri)) {
Assert.fail("Cannot send private file " + imageUri.toString());
} else {
mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType, mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType,
imageUri)); imageUri));
} }
}
@Override @Override
public void onConversationClick(final ConversationListItemData conversationListItemData) { public void onConversationClick(final ConversationListItemData conversationListItemData) {
+62 -31
View File
@@ -273,6 +273,18 @@ public class ContactUtil {
} }
} }
/**
* Get a list of destinations (phone, email) matching the partial destination in work profile.
*/
public static CursorQueryData filterDestinationEnterprise(final Context context,
final String destination) {
if (shouldFilterForEmail(destination)) {
return ContactUtil.filterEmailsEnterprise(context, destination);
} else {
return ContactUtil.filterPhonesEnterprise(context, destination);
}
}
/** /**
* Get a list of phones matching a search criteria. The search may be on contact name or * Get a list of phones matching a search criteria. The search may be on contact name or
* phone number. In case search is on contact name, all matching contact's phone number * phone number. In case search is on contact name, all matching contact's phone number
@@ -282,19 +294,29 @@ public class ContactUtil {
*/ */
@VisibleForTesting @VisibleForTesting
public static CursorQueryData filterPhones(final Context context, final String query) { public static CursorQueryData filterPhones(final Context context, final String query) {
return filterPhonesInternal(context, Phone.CONTENT_FILTER_URI, query, Directory.DEFAULT);
}
/**
* Similar to {@link #filterPhones(Context, String)}, but search in work profile instead.
*/
public static CursorQueryData filterPhonesEnterprise(final Context context,
final String query) {
return filterPhonesInternal(context, Phone.ENTERPRISE_CONTENT_FILTER_URI, query,
Directory.ENTERPRISE_DEFAULT);
}
private static CursorQueryData filterPhonesInternal(final Context context,
final Uri phoneFilterBaseUri, final String query, final long directoryId) {
if (!ContactUtil.hasReadContactsPermission()) { if (!ContactUtil.hasReadContactsPermission()) {
return CursorQueryData.getEmptyQueryData(); return CursorQueryData.getEmptyQueryData();
} }
Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId);
final Uri uri = Phone.CONTENT_FILTER_URI.buildUpon() return new CursorQueryData(context,
.appendPath(query).appendQueryParameter( phoneFilterUri,
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) PhoneQuery.PROJECTION, null, null,
.build();
return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null,
PhoneQuery.SORT_KEY); PhoneQuery.SORT_KEY);
} }
/** /**
* Lookup a phone based on a phone number. Supplied phone should be a relatively complete * Lookup a phone based on a phone number. Supplied phone should be a relatively complete
* phone number for this to succeed. PhoneLookup URI will apply some smartness to do a * phone number for this to succeed. PhoneLookup URI will apply some smartness to do a
@@ -336,17 +358,29 @@ public class ContactUtil {
*/ */
@VisibleForTesting @VisibleForTesting
public static CursorQueryData filterEmails(final Context context, final String query) { public static CursorQueryData filterEmails(final Context context, final String query) {
return filterEmailsInternal(context, Email.CONTENT_FILTER_URI, query, Directory.DEFAULT);
}
/**
* Similar to {@link #filterEmails(Context, String)}, but search in work profile instead.
*/
public static CursorQueryData filterEmailsEnterprise(final Context context,
final String query) {
return filterEmailsInternal(context, Email.ENTERPRISE_CONTENT_FILTER_URI, query,
Directory.ENTERPRISE_DEFAULT);
}
private static CursorQueryData filterEmailsInternal(final Context context,
final Uri filterEmailsBaseUri, final String query, final long directoryId) {
if (!ContactUtil.hasReadContactsPermission()) { if (!ContactUtil.hasReadContactsPermission()) {
return CursorQueryData.getEmptyQueryData(); return CursorQueryData.getEmptyQueryData();
} }
final Uri filterEmailsUri = buildDirectorySearchUri(filterEmailsBaseUri, query,
final Uri uri = Email.CONTENT_FILTER_URI.buildUpon() directoryId);
.appendPath(query).appendQueryParameter( return new CursorQueryData(context,
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) filterEmailsUri,
.build(); PhoneQuery.PROJECTION, null, null,
PhoneQuery.SORT_KEY);
return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null,
EmailQuery.SORT_KEY);
} }
/** /**
@@ -485,25 +519,15 @@ public class ContactUtil {
* Returns if a given contact id belongs to managed profile. * Returns if a given contact id belongs to managed profile.
*/ */
public static boolean isEnterpriseContactId(final long contactId) { public static boolean isEnterpriseContactId(final long contactId) {
return isWorkProfileSupported() return OsUtil.isAtLeastL() && ContactsContract.Contacts.isEnterpriseContactId(contactId);
&& ContactsContract.Contacts.isEnterpriseContactId(contactId);
}
/**
* Returns if managed profile is supported.
*/
public static boolean isWorkProfileSupported() {
final PackageManager pm = Factory.get().getApplicationContext().getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_MANAGED_USERS);
} }
/** /**
* Returns Email lookup uri that will query both primary and corp profile * Returns Email lookup uri that will query both primary and corp profile
*/ */
private static Uri getEmailContentLookupUri() { private static Uri getEmailContentLookupUri() {
if (isWorkProfileSupported() && OsUtil.isAtLeastM()) { if (OsUtil.isAtLeastM()) {
// TODO: use Email.ENTERPRISE_CONTENT_LOOKUP_URI, which will be available in M SDK API return Email.ENTERPRISE_CONTENT_LOOKUP_URI;
return Uri.parse("content://com.android.contacts/data/emails/lookup_enterprise");
} }
return Email.CONTENT_LOOKUP_URI; return Email.CONTENT_LOOKUP_URI;
} }
@@ -512,8 +536,7 @@ public class ContactUtil {
* Returns PhoneLookup URI. * Returns PhoneLookup URI.
*/ */
public static Uri getPhoneLookupUri() { public static Uri getPhoneLookupUri() {
// Apply it to M only if (OsUtil.isAtLeastM()) {
if (isWorkProfileSupported() && OsUtil.isAtLeastM()) {
return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
} }
return PhoneLookup.CONTENT_FILTER_URI; return PhoneLookup.CONTENT_FILTER_URI;
@@ -522,4 +545,12 @@ public class ContactUtil {
public static boolean hasReadContactsPermission() { public static boolean hasReadContactsPermission() {
return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS); return OsUtil.hasPermission(Manifest.permission.READ_CONTACTS);
} }
private static Uri buildDirectorySearchUri(final Uri uri, final String query,
final long directoryId) {
return uri.buildUpon()
.appendPath(query).appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
.build();
}
} }
@@ -16,7 +16,11 @@
package com.android.messaging.util; package com.android.messaging.util;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import com.android.messaging.Factory; import com.android.messaging.Factory;
@@ -116,6 +120,21 @@ public class FileUtil {
} }
} }
private static boolean isFileUri(final Uri uri) {
return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE);
}
// Checks if the file is in /data, and don't allow any app to send personal information.
// We're told it's possible to create world readable hardlinks to other apps private data
// so we ban all /data file uris.
public static boolean isInPrivateDir(Uri uri) {
if (!isFileUri(uri)) {
return false;
}
final File file = new File(uri.getPath());
return FileUtil.isSameOrSubDirectory(Environment.getDataDirectory(), file);
}
/** /**
* Checks, whether the child directory is the same as, or a sub-directory of the base * Checks, whether the child directory is the same as, or a sub-directory of the base
* directory. * directory.
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.UserHandle; import android.os.UserHandle;
import android.os.UserManager; import android.os.UserManager;
import android.support.v4.os.BuildCompat;
import com.android.messaging.Factory; import com.android.messaging.Factory;
@@ -41,6 +42,7 @@ public class OsUtil {
private static boolean sIsAtLeastL; private static boolean sIsAtLeastL;
private static boolean sIsAtLeastL_MR1; private static boolean sIsAtLeastL_MR1;
private static boolean sIsAtLeastM; private static boolean sIsAtLeastM;
private static boolean sIsAtLeastN;
private static Boolean sIsSecondaryUser = null; private static Boolean sIsSecondaryUser = null;
@@ -54,6 +56,7 @@ public class OsUtil {
sIsAtLeastL = v >= android.os.Build.VERSION_CODES.LOLLIPOP; sIsAtLeastL = v >= android.os.Build.VERSION_CODES.LOLLIPOP;
sIsAtLeastL_MR1 = v >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1; sIsAtLeastL_MR1 = v >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
sIsAtLeastM = v >= android.os.Build.VERSION_CODES.M; sIsAtLeastM = v >= android.os.Build.VERSION_CODES.M;
sIsAtLeastN = BuildCompat.isAtLeastN();
} }
/** /**
@@ -120,6 +123,14 @@ public class OsUtil {
return sIsAtLeastM; return sIsAtLeastM;
} }
/**
* @return True if the version of Android that we're running on is at least N
* (API level 24).
*/
public static boolean isAtLeastN() {
return sIsAtLeastN;
}
/** /**
* @return The Android API version of the OS that we're currently running on. * @return The Android API version of the OS that we're currently running on.
*/ */