Merge remote-tracking branch 'goog/nyc-mr1-dev' into aosp/master
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
package="com.android.messaging"
|
||||
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 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
# Keep methods that have the @VisibleForAnimation annotation
|
||||
-keep @interface com.android.messaging.annotation.VisibleForAnimation
|
||||
-keep interface com.android.messaging.annotation.VisibleForAnimation
|
||||
-keepclassmembers class * {
|
||||
@com.android.messaging.annotation.VisibleForAnimation *;
|
||||
}
|
||||
|
||||
BIN
res/drawable-hdpi/ic_work_profile.png
Normal file
BIN
res/drawable-hdpi/ic_work_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 B |
BIN
res/drawable-mdpi/ic_work_profile.png
Normal file
BIN
res/drawable-mdpi/ic_work_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 B |
BIN
res/drawable-xhdpi/ic_work_profile.png
Normal file
BIN
res/drawable-xhdpi/ic_work_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 223 B |
BIN
res/drawable-xxhdpi/ic_work_profile.png
Normal file
BIN
res/drawable-xxhdpi/ic_work_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 B |
BIN
res/drawable-xxxhdpi/ic_work_profile.png
Normal file
BIN
res/drawable-xxxhdpi/ic_work_profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 B |
@@ -63,15 +63,30 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical" >
|
||||
<TextView
|
||||
android:id="@+id/contact_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="4dp"
|
||||
android:singleLine="true"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
style="@style/ContactListItem" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:id="@+id/contact_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingBottom="4dp"
|
||||
android:singleLine="true"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
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
|
||||
android:orientation="horizontal"
|
||||
|
||||
24
res/layout/work_directory_header.xml
Normal file
24
res/layout/work_directory_header.xml
Normal 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"
|
||||
/>
|
||||
@@ -188,4 +188,10 @@
|
||||
<dimen name="fastscroll_preview_margin_left_right">8dp</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>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<!-- While sending a message display this message. -->
|
||||
<string name="message_status_sending">Sending…</string>
|
||||
<!-- 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. -->
|
||||
<string name="message_status_send_retrying">Not sent. Trying again…</string>
|
||||
<!-- When showing resend action display this message. -->
|
||||
@@ -171,9 +171,9 @@
|
||||
<!-- Title line for MMS which failed to download. -->
|
||||
<string name="message_title_download_failed">Couldn\'t download</string>
|
||||
<!-- 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. -->
|
||||
<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. -->
|
||||
<string name="message_status_download_action">Download or delete</string>
|
||||
<!-- Timestamp line to display while downloading a message. -->
|
||||
@@ -296,7 +296,7 @@
|
||||
<!-- Error occurred while recording audio -->
|
||||
<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 -->
|
||||
<string name="audio_picker_hint_text">Touch & hold</string>
|
||||
<string name="audio_picker_hint_text">Tap & hold</string>
|
||||
|
||||
<!-- An enumeration comma for separating multiple names in notifications. [CHAR LIMIT=2] -->
|
||||
<string name="enumeration_comma">,\u0020</string>
|
||||
@@ -348,7 +348,7 @@
|
||||
<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. -->
|
||||
<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. -->
|
||||
<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_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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
<!-- 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 & hold to record audio</string>
|
||||
|
||||
<!-- Content description for new conversation button in desktop widget -->
|
||||
<string name="widget_new_conversation_content_description">Start new conversation</string>
|
||||
@@ -931,7 +931,7 @@
|
||||
<string name="conversation_deleted">Conversation deleted</string>
|
||||
<!-- Displayed when user adds a new conversation widget. Tapping on the widget in this
|
||||
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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
@@ -623,4 +623,14 @@
|
||||
<item name="android:textColor">@android:color/white</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</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>
|
||||
|
||||
@@ -145,7 +145,7 @@ public class EncodedStringValue implements Cloneable {
|
||||
}
|
||||
try {
|
||||
return new String(mData, CharacterSets.MIMENAME_ISO_8859_1);
|
||||
} catch (UnsupportedEncodingException _) {
|
||||
} catch (UnsupportedEncodingException e2) {
|
||||
return new String(mData); // system default encoding.
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ public class EncodedStringValue implements Cloneable {
|
||||
try {
|
||||
ret[i] = new EncodedStringValue(mCharacterSet,
|
||||
temp[i].getBytes());
|
||||
} catch (NullPointerException _) {
|
||||
} catch (NullPointerException e) {
|
||||
// Can't arrive here
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ public class ContactListItemData {
|
||||
// existing chip for which we show full contact detail for the selected contact).
|
||||
private boolean mSingleRecipient;
|
||||
|
||||
// Is the contact in managed profile.
|
||||
private boolean mIsWorkContact;
|
||||
|
||||
/**
|
||||
* Bind to a contact cursor in the contact list.
|
||||
*/
|
||||
@@ -77,6 +80,8 @@ public class ContactListItemData {
|
||||
mRecipientEntry = ContactUtil.createRecipientEntry(displayName,
|
||||
DisplayNameSources.STRUCTURED_NAME, destination, destinationType, destinationLabel,
|
||||
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.
|
||||
*/
|
||||
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());
|
||||
mRecipientEntry = entry;
|
||||
mStyledName = styledName;
|
||||
mStyledDestination = styledDestination;
|
||||
mAlphabetHeader = null;
|
||||
mSingleRecipient = singleRecipient;
|
||||
mIsWorkContact = isWorkContact;
|
||||
}
|
||||
|
||||
public CharSequence getDisplayName() {
|
||||
@@ -157,4 +164,11 @@ public class ContactListItemData {
|
||||
public RecipientEntry getRecipientEntry() {
|
||||
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();
|
||||
mPopupRect = new Rect();
|
||||
mActionBarRect = new Rect();
|
||||
final Activity activity = (Activity) viewToAnimate.getRootView().getContext();
|
||||
mActionBarView = activity.getWindow().getDecorView().findViewById(
|
||||
mActionBarView = viewToAnimate.getRootView().findViewById(
|
||||
android.support.v7.appcompat.R.id.action_bar);
|
||||
mRectEvaluator = RectEvaluatorCompat.create();
|
||||
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.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
|
||||
/**
|
||||
* An implementation for {@link DropdownChipLayouter}. Layouts the dropdown
|
||||
@@ -99,8 +100,9 @@ public class ContactDropdownLayouter extends DropdownChipLayouter {
|
||||
Assert.isTrue(itemView instanceof ContactListItemView);
|
||||
final ContactListItemView contactListItemView = (ContactListItemView) itemView;
|
||||
contactListItemView.setImageClickHandlerDisabled(true);
|
||||
boolean isWorkContact = ContactUtil.isEnterpriseContactId(entry.getContactId());
|
||||
contactListItemView.bind(entry, styledResults[0], styledResults[1],
|
||||
mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT));
|
||||
mClivHostInterface, (type == AdapterType.SINGLE_RECIPIENT), isWorkContact);
|
||||
return itemView;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
private TextView mAlphabetHeaderTextView;
|
||||
private ContactIconView mContactIconView;
|
||||
private ImageView mContactCheckmarkView;
|
||||
private ImageView mWorkProfileIcon;
|
||||
private HostInterface mHostInterface;
|
||||
private boolean mShouldShowAlphabetHeader;
|
||||
|
||||
@@ -69,6 +70,7 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mAlphabetHeaderTextView = (TextView) findViewById(R.id.alphabet_header);
|
||||
mContactIconView = (ContactIconView) findViewById(R.id.contact_icon);
|
||||
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
|
||||
* 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.
|
||||
* @param isWorkContact whether the contact is in managed profile.
|
||||
*/
|
||||
public void bind(final RecipientEntry recipientEntry, final CharSequence styledName,
|
||||
final CharSequence styledDestination, final HostInterface hostInterface,
|
||||
final boolean isSingleRecipient) {
|
||||
mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient);
|
||||
final boolean isSingleRecipient, final boolean isWorkContact) {
|
||||
mData.bind(recipientEntry, styledName, styledDestination, isSingleRecipient, isWorkContact);
|
||||
mHostInterface = hostInterface;
|
||||
mShouldShowAlphabetHeader = false;
|
||||
updateViewAppearance();
|
||||
@@ -152,7 +155,11 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactDetailsTextView.setVisibility(VISIBLE);
|
||||
mContactDetailTypeTextView.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (mData.getIsWorkContact()) {
|
||||
mWorkProfileIcon.setVisibility(VISIBLE);
|
||||
} else {
|
||||
mWorkProfileIcon.setVisibility(GONE);
|
||||
}
|
||||
if (mShouldShowAlphabetHeader) {
|
||||
mAlphabetHeaderTextView.setVisibility(VISIBLE);
|
||||
mAlphabetHeaderTextView.setText(mData.getAlphabetHeader());
|
||||
|
||||
@@ -22,18 +22,24 @@ import android.support.v4.util.Pair;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Rfc822Token;
|
||||
import android.text.util.Rfc822Tokenizer;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.ex.chips.BaseRecipientAdapter;
|
||||
import com.android.ex.chips.RecipientAlternatesAdapter;
|
||||
import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.BugleGservices;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.text.Collator;
|
||||
@@ -53,6 +59,18 @@ import java.util.Map;
|
||||
* for {@link ContactRecipientAutoCompleteView}
|
||||
*/
|
||||
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,
|
||||
final ContactListItemView.HostInterface 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) {
|
||||
super(context, preferredMaxResultCount, queryMode);
|
||||
setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
|
||||
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,6 +100,7 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
|
||||
* results.
|
||||
*/
|
||||
public class ContactFilter extends Filter {
|
||||
|
||||
// Used to sort filtered contacts when it has combined results from email and phone.
|
||||
private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
|
||||
|
||||
@@ -95,21 +115,45 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
|
||||
* return the merged results.
|
||||
*/
|
||||
@DoesNotRunOnMainThread
|
||||
private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context,
|
||||
final String searchText) {
|
||||
private CursorResult getFilteredResultsCursor(final String searchText) {
|
||||
Assert.isNotMainThread();
|
||||
if (BugleGservices.get().getBoolean(
|
||||
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
|
||||
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
|
||||
return Pair.create((Cursor) new MergeCursor(new Cursor[] {
|
||||
ContactUtil.filterPhones(getContext(), searchText)
|
||||
.performSynchronousQuery(),
|
||||
ContactUtil.filterEmails(getContext(), searchText)
|
||||
.performSynchronousQuery()
|
||||
}), false /* the merged cursor is not sorted */);
|
||||
|
||||
final Cursor personalFilterPhonesCursor = ContactUtil
|
||||
.filterPhones(getContext(), searchText).performSynchronousQuery();
|
||||
final Cursor personalFilterEmailsCursor = ContactUtil
|
||||
.filterEmails(getContext(), searchText).performSynchronousQuery();
|
||||
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 {
|
||||
return Pair.create(ContactUtil.filterDestination(getContext(), searchText)
|
||||
.performSynchronousQuery(), true);
|
||||
final Cursor personalFilterDestinationCursor = ContactUtil
|
||||
.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,44 +172,57 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
|
||||
|
||||
// Query for auto-complete results, since performFiltering() is not done on the
|
||||
// main thread, perform the cursor loader queries directly.
|
||||
final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
|
||||
searchText);
|
||||
final Cursor cursor = filteredResults.first;
|
||||
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
|
||||
// destination as a suggestion item to the drop down.
|
||||
if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
|
||||
entries.add(ContactRecipientEntryUtils
|
||||
.constructSendToDestinationEntry(searchText));
|
||||
}
|
||||
final CursorResult cursorResult = getFilteredResultsCursor(searchText);
|
||||
final List<RecipientEntry> entries = new ArrayList<>();
|
||||
|
||||
HashSet<Long> existingContactIds = new HashSet<Long>();
|
||||
while (cursor.moveToNext()) {
|
||||
// Make sure there's only one first-level contact (i.e. contact for which
|
||||
// we show the avatar picture and name) for every contact id.
|
||||
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
final boolean isFirstLevel = !existingContactIds.contains(contactId);
|
||||
if (isFirstLevel) {
|
||||
existingContactIds.add(contactId);
|
||||
}
|
||||
entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
|
||||
isFirstLevel));
|
||||
}
|
||||
// First check if the constraint is a valid SMS destination. If so, add the
|
||||
// destination as a suggestion item to the drop down.
|
||||
if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
|
||||
entries.add(ContactRecipientEntryUtils
|
||||
.constructSendToDestinationEntry(searchText));
|
||||
}
|
||||
|
||||
if (!sorted) {
|
||||
Collections.sort(entries, mComparator);
|
||||
}
|
||||
results.values = entries;
|
||||
results.count = 1;
|
||||
|
||||
} finally {
|
||||
cursor.close();
|
||||
// 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()) {
|
||||
// Make sure there's only one first-level contact (i.e. contact for
|
||||
// which we show the avatar picture and name) for every contact id.
|
||||
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
||||
final boolean isFirstLevel = !existingContactIds.contains(contactId);
|
||||
if (isFirstLevel) {
|
||||
existingContactIds.add(contactId);
|
||||
}
|
||||
tempEntries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
|
||||
isFirstLevel));
|
||||
}
|
||||
|
||||
if (!cursorResult.isSorted) {
|
||||
Collections.sort(tempEntries, mComparator);
|
||||
}
|
||||
entries.addAll(tempEntries);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
results.values = new ContactReceipientFilterResult(entries, workDirectoryHeaderPos);
|
||||
results.count = 1;
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -174,16 +231,20 @@ public final class ContactRecipientAdapter extends BaseRecipientAdapter {
|
||||
mCurrentConstraint = constraint;
|
||||
clearTempEntries();
|
||||
|
||||
if (results.values != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
final List<RecipientEntry> entries = (List<RecipientEntry>) results.values;
|
||||
updateEntries(entries);
|
||||
} else {
|
||||
updateEntries(Collections.<RecipientEntry>emptyList());
|
||||
final ContactReceipientFilterResult contactReceipientFilterResult
|
||||
= (ContactReceipientFilterResult) results.values;
|
||||
if (contactReceipientFilterResult != null) {
|
||||
mWorkDirectoryHeaderPos = contactReceipientFilterResult.workDirectoryPos;
|
||||
if (contactReceipientFilterResult.recipientEntries != null) {
|
||||
updateEntries(contactReceipientFilterResult.recipientEntries);
|
||||
} else {
|
||||
updateEntries(Collections.<RecipientEntry>emptyList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipientEntryComparator implements Comparator<RecipientEntry> {
|
||||
|
||||
private final Collator mCollator;
|
||||
|
||||
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
|
||||
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.LogUtil;
|
||||
import com.android.messaging.util.MediaMetadataRetrieverWrapper;
|
||||
import com.android.messaging.util.FileUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -158,8 +159,12 @@ public class ShareIntentActivity extends BaseBugleActivity implements
|
||||
}
|
||||
|
||||
private void addSharedImagePartToDraft(final String contentType, final Uri imageUri) {
|
||||
mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType,
|
||||
imageUri));
|
||||
if (FileUtil.isInPrivateDir(imageUri)) {
|
||||
Assert.fail("Cannot send private file " + imageUri.toString());
|
||||
} else {
|
||||
mDraftMessage.addPart(PendingAttachmentData.createPendingAttachmentData(contentType,
|
||||
imageUri));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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
|
||||
* phone number. In case search is on contact name, all matching contact's phone number
|
||||
@@ -282,19 +294,29 @@ public class ContactUtil {
|
||||
*/
|
||||
@VisibleForTesting
|
||||
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()) {
|
||||
return CursorQueryData.getEmptyQueryData();
|
||||
}
|
||||
|
||||
final Uri uri = Phone.CONTENT_FILTER_URI.buildUpon()
|
||||
.appendPath(query).appendQueryParameter(
|
||||
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
|
||||
.build();
|
||||
|
||||
return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null,
|
||||
Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId);
|
||||
return new CursorQueryData(context,
|
||||
phoneFilterUri,
|
||||
PhoneQuery.PROJECTION, null, null,
|
||||
PhoneQuery.SORT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -336,17 +358,29 @@ public class ContactUtil {
|
||||
*/
|
||||
@VisibleForTesting
|
||||
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()) {
|
||||
return CursorQueryData.getEmptyQueryData();
|
||||
}
|
||||
|
||||
final Uri uri = Email.CONTENT_FILTER_URI.buildUpon()
|
||||
.appendPath(query).appendQueryParameter(
|
||||
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
|
||||
.build();
|
||||
|
||||
return new CursorQueryData(context, uri, EmailQuery.PROJECTION, null, null,
|
||||
EmailQuery.SORT_KEY);
|
||||
final Uri filterEmailsUri = buildDirectorySearchUri(filterEmailsBaseUri, query,
|
||||
directoryId);
|
||||
return new CursorQueryData(context,
|
||||
filterEmailsUri,
|
||||
PhoneQuery.PROJECTION, null, null,
|
||||
PhoneQuery.SORT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -485,25 +519,15 @@ public class ContactUtil {
|
||||
* Returns if a given contact id belongs to managed profile.
|
||||
*/
|
||||
public static boolean isEnterpriseContactId(final long contactId) {
|
||||
return isWorkProfileSupported()
|
||||
&& 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);
|
||||
return OsUtil.isAtLeastL() && ContactsContract.Contacts.isEnterpriseContactId(contactId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Email lookup uri that will query both primary and corp profile
|
||||
*/
|
||||
private static Uri getEmailContentLookupUri() {
|
||||
if (isWorkProfileSupported() && OsUtil.isAtLeastM()) {
|
||||
// TODO: use Email.ENTERPRISE_CONTENT_LOOKUP_URI, which will be available in M SDK API
|
||||
return Uri.parse("content://com.android.contacts/data/emails/lookup_enterprise");
|
||||
if (OsUtil.isAtLeastM()) {
|
||||
return Email.ENTERPRISE_CONTENT_LOOKUP_URI;
|
||||
}
|
||||
return Email.CONTENT_LOOKUP_URI;
|
||||
}
|
||||
@@ -512,8 +536,7 @@ public class ContactUtil {
|
||||
* Returns PhoneLookup URI.
|
||||
*/
|
||||
public static Uri getPhoneLookupUri() {
|
||||
// Apply it to M only
|
||||
if (isWorkProfileSupported() && OsUtil.isAtLeastM()) {
|
||||
if (OsUtil.isAtLeastM()) {
|
||||
return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
|
||||
}
|
||||
return PhoneLookup.CONTENT_FILTER_URI;
|
||||
@@ -522,4 +545,12 @@ public class ContactUtil {
|
||||
public static boolean hasReadContactsPermission() {
|
||||
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;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
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
|
||||
* directory.
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.support.v4.os.BuildCompat;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
|
||||
@@ -41,6 +42,7 @@ public class OsUtil {
|
||||
private static boolean sIsAtLeastL;
|
||||
private static boolean sIsAtLeastL_MR1;
|
||||
private static boolean sIsAtLeastM;
|
||||
private static boolean sIsAtLeastN;
|
||||
|
||||
private static Boolean sIsSecondaryUser = null;
|
||||
|
||||
@@ -54,6 +56,7 @@ public class OsUtil {
|
||||
sIsAtLeastL = v >= android.os.Build.VERSION_CODES.LOLLIPOP;
|
||||
sIsAtLeastL_MR1 = v >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
|
||||
sIsAtLeastM = v >= android.os.Build.VERSION_CODES.M;
|
||||
sIsAtLeastN = BuildCompat.isAtLeastN();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +123,14 @@ public class OsUtil {
|
||||
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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user