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

This commit is contained in:
Tony Mak
2016-06-29 21:01:09 +01:00
22 changed files with 425 additions and 110 deletions

View File

@@ -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" />

View File

@@ -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 *;
}

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

View File

@@ -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"

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"
/>

View File

@@ -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>

View File

@@ -152,7 +152,7 @@
<!-- While sending a message display this message. -->
<string name="message_status_sending">Sending&#8230;</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&#8230;</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 &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] -->
<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 &amp; 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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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

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
* 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();
}
}

View File

@@ -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.

View File

@@ -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.
*/