diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8fe8fae..5ab5910 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -18,7 +18,7 @@ package="com.android.messaging" android:installLocation="internalOnly"> - + diff --git a/proguard.flags b/proguard.flags index 759f2d4..76b033c 100644 --- a/proguard.flags +++ b/proguard.flags @@ -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 *; } diff --git a/res/drawable-hdpi/ic_work_profile.png b/res/drawable-hdpi/ic_work_profile.png new file mode 100644 index 0000000..5c2e457 Binary files /dev/null and b/res/drawable-hdpi/ic_work_profile.png differ diff --git a/res/drawable-mdpi/ic_work_profile.png b/res/drawable-mdpi/ic_work_profile.png new file mode 100644 index 0000000..b7ffd2b Binary files /dev/null and b/res/drawable-mdpi/ic_work_profile.png differ diff --git a/res/drawable-xhdpi/ic_work_profile.png b/res/drawable-xhdpi/ic_work_profile.png new file mode 100644 index 0000000..b47b485 Binary files /dev/null and b/res/drawable-xhdpi/ic_work_profile.png differ diff --git a/res/drawable-xxhdpi/ic_work_profile.png b/res/drawable-xxhdpi/ic_work_profile.png new file mode 100644 index 0000000..de257e7 Binary files /dev/null and b/res/drawable-xxhdpi/ic_work_profile.png differ diff --git a/res/drawable-xxxhdpi/ic_work_profile.png b/res/drawable-xxxhdpi/ic_work_profile.png new file mode 100644 index 0000000..72292bd Binary files /dev/null and b/res/drawable-xxxhdpi/ic_work_profile.png differ diff --git a/res/layout/contact_list_item_view.xml b/res/layout/contact_list_item_view.xml index 3015ae3..71a7370 100644 --- a/res/layout/contact_list_item_view.xml +++ b/res/layout/contact_list_item_view.xml @@ -63,15 +63,30 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical" > - + + + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 270ea9e..5ff0eb7 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -188,4 +188,10 @@ 8dp 24sp + 16dp + 32dp + 18dp + 8dp + 14sp + diff --git a/res/values/strings.xml b/res/values/strings.xml index ec675ef..012d87f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -152,7 +152,7 @@ Sending… - Not sent. Touch to try again. + Not sent. Tap to try again. Not sent. Trying again… @@ -171,9 +171,9 @@ Couldn\'t download - Touch to try again + Tap to try again - Touch to download + Tap to download Download or delete @@ -296,7 +296,7 @@ Couldn\'t save audio. Try again. - Touch & hold + Tap & hold ,\u0020 @@ -348,7 +348,7 @@ Notifications turned on - All set. Touch Send again. + All set. Tap Send again. Messaging successfully set as the default SMS app. @@ -378,7 +378,7 @@ Failed message to %s: %s. Time: %s. Message to %s: %s. Time: %s. - Failed message. Touch to retry. + Failed message. Tap to retry. Conversation with %s @@ -905,7 +905,7 @@ Select SIM or edit subject - Touch and hold to record audio + Tap & hold to record audio Start new conversation @@ -931,7 +931,7 @@ Conversation deleted - Conversation deleted. Touch to show a different Messaging conversation + Conversation deleted. Tap to show a different Messaging conversation Blocked @@ -973,4 +973,6 @@ %s selected + + Work Profile contacts diff --git a/res/values/styles.xml b/res/values/styles.xml index d45f2e1..582c755 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -623,4 +623,14 @@ @android:color/white center + diff --git a/src/android/support/v7/mms/pdu/EncodedStringValue.java b/src/android/support/v7/mms/pdu/EncodedStringValue.java index d8fcacf..6a914ae 100644 --- a/src/android/support/v7/mms/pdu/EncodedStringValue.java +++ b/src/android/support/v7/mms/pdu/EncodedStringValue.java @@ -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; } diff --git a/src/com/android/messaging/datamodel/data/ContactListItemData.java b/src/com/android/messaging/datamodel/data/ContactListItemData.java index dcc7e20..b9c7e85 100644 --- a/src/com/android/messaging/datamodel/data/ContactListItemData.java +++ b/src/com/android/messaging/datamodel/data/ContactListItemData.java @@ -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; + } } diff --git a/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java b/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java index 21529c6..5fc2a19 100644 --- a/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java +++ b/src/com/android/messaging/ui/animation/PopupTransitionAnimation.java @@ -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); diff --git a/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java b/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java index 7df62de..051ebeb 100644 --- a/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java +++ b/src/com/android/messaging/ui/contact/ContactDropdownLayouter.java @@ -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; } diff --git a/src/com/android/messaging/ui/contact/ContactListItemView.java b/src/com/android/messaging/ui/contact/ContactListItemView.java index 6904da6..b0e8957 100644 --- a/src/com/android/messaging/ui/contact/ContactListItemView.java +++ b/src/com/android/messaging/ui/contact/ContactListItemView.java @@ -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()); diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java b/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java index 25f422e..1d91241 100644 --- a/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java +++ b/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java @@ -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 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 filteredResults = getFilteredResultsCursor(getContext(), - searchText); - final Cursor cursor = filteredResults.first; - final boolean sorted = filteredResults.second; - if (cursor != null) { - try { - final List entries = new ArrayList(); - // 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 entries = new ArrayList<>(); - HashSet 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); - } - 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 tempEntries = new ArrayList<>(); + HashSet 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 entries = (List) results.values; - updateEntries(entries); - } else { - updateEntries(Collections.emptyList()); + final ContactReceipientFilterResult contactReceipientFilterResult + = (ContactReceipientFilterResult) results.values; + if (contactReceipientFilterResult != null) { + mWorkDirectoryHeaderPos = contactReceipientFilterResult.workDirectoryPos; + if (contactReceipientFilterResult.recipientEntries != null) { + updateEntries(contactReceipientFilterResult.recipientEntries); + } else { + updateEntries(Collections.emptyList()); + } } } private class RecipientEntryComparator implements Comparator { + 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 recipientEntries; + + /** + * Index of row that showing work directory header. + */ + public final int workDirectoryPos; + + public ContactReceipientFilterResult(List 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; + } + } diff --git a/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java b/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java index ef7fcef..83b7be9 100644 --- a/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java +++ b/src/com/android/messaging/ui/conversationlist/ShareIntentActivity.java @@ -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 diff --git a/src/com/android/messaging/util/ContactUtil.java b/src/com/android/messaging/util/ContactUtil.java index 8555889..94af3ba 100644 --- a/src/com/android/messaging/util/ContactUtil.java +++ b/src/com/android/messaging/util/ContactUtil.java @@ -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(); + } } diff --git a/src/com/android/messaging/util/FileUtil.java b/src/com/android/messaging/util/FileUtil.java index 7c47ae9..e35e79b 100644 --- a/src/com/android/messaging/util/FileUtil.java +++ b/src/com/android/messaging/util/FileUtil.java @@ -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. diff --git a/src/com/android/messaging/util/OsUtil.java b/src/com/android/messaging/util/OsUtil.java index e45a63c..4890d08 100644 --- a/src/com/android/messaging/util/OsUtil.java +++ b/src/com/android/messaging/util/OsUtil.java @@ -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. */