diff options
Diffstat (limited to 'java/com/android/contacts/common/util')
16 files changed, 2063 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/util/AccountFilterUtil.java b/java/com/android/contacts/common/util/AccountFilterUtil.java new file mode 100644 index 000000000..18743c65e --- /dev/null +++ b/java/com/android/contacts/common/util/AccountFilterUtil.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import com.android.contacts.common.R; +import com.android.contacts.common.list.ContactListFilter; +import com.android.contacts.common.list.ContactListFilterController; + +/** Utility class for account filter manipulation. */ +public class AccountFilterUtil { + + public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter"; + private static final String TAG = AccountFilterUtil.class.getSimpleName(); + + /** + * Find TextView with the id "account_filter_header" and set correct text for the account filter + * header. + * + * @param filterContainer View containing TextView with id "account_filter_header" + * @return true when header text is set in the call. You may use this for conditionally showing or + * hiding this entire view. + */ + public static boolean updateAccountFilterTitleForPeople( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false); + } + + /** + * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean, + * boolean)}, but for Phone UI. + */ + public static boolean updateAccountFilterTitleForPhone( + View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) { + return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, true); + } + + private static boolean updateAccountFilterTitle( + View filterContainer, + ContactListFilter filter, + boolean showTitleForAllAccounts, + boolean forPhone) { + final Context context = filterContainer.getContext(); + final TextView headerTextView = + (TextView) filterContainer.findViewById(R.id.account_filter_header); + + boolean textWasSet = false; + if (filter != null) { + if (forPhone) { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_phones); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } else { + if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) { + if (showTitleForAllAccounts) { + headerTextView.setText(R.string.list_filter_all_accounts); + textWasSet = true; + } + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { + headerTextView.setText( + context.getString(R.string.listAllContactsInAccount, filter.accountName)); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + headerTextView.setText(R.string.listCustomView); + textWasSet = true; + } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { + headerTextView.setText(R.string.listSingleContact); + textWasSet = true; + } else { + Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected."); + } + } + } else { + Log.w(TAG, "Filter is null."); + } + return textWasSet; + } + + /** This will update filter via a given ContactListFilterController. */ + public static void handleAccountFilterResult( + ContactListFilterController filterController, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + final ContactListFilter filter = data.getParcelableExtra(EXTRA_CONTACT_LIST_FILTER); + if (filter == null) { + return; + } + if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) { + filterController.selectCustomFilter(); + } else { + filterController.setContactListFilter(filter, true); + } + } + } +} diff --git a/java/com/android/contacts/common/util/BitmapUtil.java b/java/com/android/contacts/common/util/BitmapUtil.java new file mode 100644 index 000000000..20f916a3f --- /dev/null +++ b/java/com/android/contacts/common/util/BitmapUtil.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +/** Provides static functions to decode bitmaps at the optimal size */ +public class BitmapUtil { + + private BitmapUtil() {} + + /** + * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually + * decode the picture, so it is pretty efficient to run. + */ + public static int getSmallerExtentFromBytes(byte[] bytes) { + final BitmapFactory.Options options = new BitmapFactory.Options(); + + // don't actually decode the picture, just return its bounds + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + + // test what the best sample size is + return Math.min(options.outWidth, options.outHeight); + } + + /** + * Finds the optimal sampleSize for loading the picture + * + * @param originalSmallerExtent Width or height of the picture, whichever is smaller + * @param targetExtent Width or height of the target view, whichever is bigger. + * <p>If either one of the parameters is 0 or smaller, no sampling is applied + */ + public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) { + // If we don't know sizes, we can't do sampling. + if (targetExtent < 1) { + return 1; + } + if (originalSmallerExtent < 1) { + return 1; + } + + // Test what the best sample size is. To do that, we find the sample size that gives us + // the best trade-off between resulting image size and memory requirement. We allow + // the down-sampled image to be 20% smaller than the target size. That way we can get around + // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at + // all. Why 20%? Why not. Prove me wrong. + int extent = originalSmallerExtent; + int sampleSize = 1; + while ((extent >> 1) >= targetExtent * 0.8f) { + sampleSize <<= 1; + extent >>= 1; + } + + return sampleSize; + } + + /** Decodes the bitmap with the given sample size */ + public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) { + final BitmapFactory.Options options; + if (sampleSize <= 1) { + options = null; + } else { + options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); + } + + /** + * Retrieves a copy of the specified drawable resource, rotated by a specified angle. + * + * @param resources The current resources. + * @param resourceId The resource ID of the drawable to rotate. + * @param angle The angle of rotation. + * @return Rotated drawable. + */ + public static Drawable getRotatedDrawable( + android.content.res.Resources resources, int resourceId, float angle) { + + // Get the original drawable and make a copy which will be rotated. + Bitmap original = BitmapFactory.decodeResource(resources, resourceId); + Bitmap rotated = + Bitmap.createBitmap(original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888); + + // Perform the rotation. + Canvas tempCanvas = new Canvas(rotated); + tempCanvas.rotate(angle, original.getWidth() / 2, original.getHeight() / 2); + tempCanvas.drawBitmap(original, 0, 0, null); + + return new BitmapDrawable(resources, rotated); + } + + /** + * Given an input bitmap, scales it to the given width/height and makes it round. + * + * @param input {@link Bitmap} to scale and crop + * @param targetWidth desired output width + * @param targetHeight desired output height + * @return output bitmap scaled to the target width/height and cropped to an oval. The cropping + * algorithm will try to fit as much of the input into the output as possible, while + * preserving the target width/height ratio. + */ + public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) { + if (input == null) { + return null; + } + final Bitmap.Config inputConfig = input.getConfig(); + final Bitmap result = + Bitmap.createBitmap( + targetWidth, targetHeight, inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(result); + final Paint paint = new Paint(); + canvas.drawARGB(0, 0, 0, 0); + paint.setAntiAlias(true); + final RectF dst = new RectF(0, 0, targetWidth, targetHeight); + canvas.drawOval(dst, paint); + + // Specifies that only pixels present in the destination (i.e. the drawn oval) should + // be overwritten with pixels from the input bitmap. + paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); + + final int inputWidth = input.getWidth(); + final int inputHeight = input.getHeight(); + + // Choose the largest scale factor that will fit inside the dimensions of the + // input bitmap. + final float scaleBy = + Math.min((float) inputWidth / targetWidth, (float) inputHeight / targetHeight); + + final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2); + final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2); + + final Rect src = + new Rect( + inputWidth / 2 - xCropAmountHalved, + inputHeight / 2 - yCropAmountHalved, + inputWidth / 2 + xCropAmountHalved, + inputHeight / 2 + yCropAmountHalved); + + canvas.drawBitmap(input, src, dst, paint); + return result; + } +} diff --git a/java/com/android/contacts/common/util/CommonDateUtils.java b/java/com/android/contacts/common/util/CommonDateUtils.java new file mode 100644 index 000000000..312e691f8 --- /dev/null +++ b/java/com/android/contacts/common/util/CommonDateUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** Common date utilities. */ +public class CommonDateUtils { + + // All the SimpleDateFormats in this class use the UTC timezone + public static final SimpleDateFormat NO_YEAR_DATE_FORMAT = + new SimpleDateFormat("--MM-dd", Locale.US); + public static final SimpleDateFormat FULL_DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static final SimpleDateFormat DATE_AND_TIME_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT = + new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + /** Exchange requires 8:00 for birthdays */ + public static final int DEFAULT_HOUR = 8; +} diff --git a/java/com/android/contacts/common/util/Constants.java b/java/com/android/contacts/common/util/Constants.java new file mode 100644 index 000000000..172e8c348 --- /dev/null +++ b/java/com/android/contacts/common/util/Constants.java @@ -0,0 +1,28 @@ +/* + * 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +public class Constants { + + /** + * Log tag for performance measurement. To enable: adb shell setprop log.tag.ContactsPerf VERBOSE + */ + public static final String PERFORMANCE_TAG = "ContactsPerf"; + + // Used for lookup URI that contains an encoded JSON string. + public static final String LOOKUP_URI_ENCODED = "encoded"; +} diff --git a/java/com/android/contacts/common/util/ContactDisplayUtils.java b/java/com/android/contacts/common/util/ContactDisplayUtils.java new file mode 100644 index 000000000..1586784db --- /dev/null +++ b/java/com/android/contacts/common/util/ContactDisplayUtils.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.contacts.common.util; + +import static android.provider.ContactsContract.CommonDataKinds.Phone; + +import android.content.Context; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TtsSpan; +import android.util.Log; +import android.util.Patterns; +import com.android.contacts.common.R; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import java.util.Objects; + +/** Methods for handling various contact data labels. */ +public class ContactDisplayUtils { + + public static final int INTERACTION_CALL = 1; + public static final int INTERACTION_SMS = 2; + private static final String TAG = ContactDisplayUtils.class.getSimpleName(); + + /** + * Checks if the given data type is a custom type. + * + * @param type Phone data type. + * @return {@literal true} if the type is custom. {@literal false} if not. + */ + public static boolean isCustomPhoneType(Integer type) { + return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT; + } + + /** + * Gets a display label for a given phone type. + * + * @param type The type of number. + * @param customLabel A custom label to use if the phone is determined to be of custom type + * determined by {@link #isCustomPhoneType(Integer))} + * @param interactionType whether this is a call or sms. Either {@link #INTERACTION_CALL} or + * {@link #INTERACTION_SMS}. + * @param context The application context. + * @return An appropriate string label + */ + public static CharSequence getLabelForCallOrSms( + Integer type, CharSequence customLabel, int interactionType, @NonNull Context context) { + Objects.requireNonNull(context); + + if (isCustomPhoneType(type)) { + return (customLabel == null) ? "" : customLabel; + } else { + int resId; + if (interactionType == INTERACTION_SMS) { + resId = getSmsLabelResourceId(type); + } else { + resId = getPhoneLabelResourceId(type); + if (interactionType != INTERACTION_CALL) { + Log.e( + TAG, + "Un-recognized interaction type: " + + interactionType + + ". Defaulting to ContactDisplayUtils.INTERACTION_CALL."); + } + } + + return context.getResources().getText(resId); + } + } + + /** + * Find a label for calling. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getPhoneLabelResourceId(Integer type) { + if (type == null) { + return R.string.call_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.call_home; + case Phone.TYPE_MOBILE: + return R.string.call_mobile; + case Phone.TYPE_WORK: + return R.string.call_work; + case Phone.TYPE_FAX_WORK: + return R.string.call_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.call_fax_home; + case Phone.TYPE_PAGER: + return R.string.call_pager; + case Phone.TYPE_OTHER: + return R.string.call_other; + case Phone.TYPE_CALLBACK: + return R.string.call_callback; + case Phone.TYPE_CAR: + return R.string.call_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.call_company_main; + case Phone.TYPE_ISDN: + return R.string.call_isdn; + case Phone.TYPE_MAIN: + return R.string.call_main; + case Phone.TYPE_OTHER_FAX: + return R.string.call_other_fax; + case Phone.TYPE_RADIO: + return R.string.call_radio; + case Phone.TYPE_TELEX: + return R.string.call_telex; + case Phone.TYPE_TTY_TDD: + return R.string.call_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.call_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.call_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.call_assistant; + case Phone.TYPE_MMS: + return R.string.call_mms; + default: + return R.string.call_custom; + } + } + + /** + * Find a label for sending an sms. + * + * @param type The type of number. + * @return An appropriate string label. + */ + public static int getSmsLabelResourceId(Integer type) { + if (type == null) { + return R.string.sms_other; + } + switch (type) { + case Phone.TYPE_HOME: + return R.string.sms_home; + case Phone.TYPE_MOBILE: + return R.string.sms_mobile; + case Phone.TYPE_WORK: + return R.string.sms_work; + case Phone.TYPE_FAX_WORK: + return R.string.sms_fax_work; + case Phone.TYPE_FAX_HOME: + return R.string.sms_fax_home; + case Phone.TYPE_PAGER: + return R.string.sms_pager; + case Phone.TYPE_OTHER: + return R.string.sms_other; + case Phone.TYPE_CALLBACK: + return R.string.sms_callback; + case Phone.TYPE_CAR: + return R.string.sms_car; + case Phone.TYPE_COMPANY_MAIN: + return R.string.sms_company_main; + case Phone.TYPE_ISDN: + return R.string.sms_isdn; + case Phone.TYPE_MAIN: + return R.string.sms_main; + case Phone.TYPE_OTHER_FAX: + return R.string.sms_other_fax; + case Phone.TYPE_RADIO: + return R.string.sms_radio; + case Phone.TYPE_TELEX: + return R.string.sms_telex; + case Phone.TYPE_TTY_TDD: + return R.string.sms_tty_tdd; + case Phone.TYPE_WORK_MOBILE: + return R.string.sms_work_mobile; + case Phone.TYPE_WORK_PAGER: + return R.string.sms_work_pager; + case Phone.TYPE_ASSISTANT: + return R.string.sms_assistant; + case Phone.TYPE_MMS: + return R.string.sms_mms; + default: + return R.string.sms_custom; + } + } + + /** + * Whether the given text could be a phone number. + * + * <p>Note this will miss many things that are legitimate phone numbers, for example, phone + * numbers with letters. + */ + public static boolean isPossiblePhoneNumber(CharSequence text) { + return text != null && Patterns.PHONE.matcher(text.toString()).matches(); + } + + /** + * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for the given + * phone number text wherever it is found within the message. + */ + public static Spannable getTelephoneTtsSpannable( + @Nullable String message, @Nullable String phoneNumber) { + if (message == null) { + return null; + } + final Spannable spannable = new SpannableString(message); + int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber); + while (start >= 0) { + final int end = start + phoneNumber.length(); + final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber); + spannable.setSpan( + ttsSpan, + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // this is consistenly done in a misleading way.. + start = message.indexOf(phoneNumber, end); + } + return spannable; + } + + /** + * Retrieves a string from a string template that takes 1 phone number as argument, span the + * number with a telephone {@link TtsSpan}, and return the spanned string. + * + * @param resources to retrieve the string from + * @param stringId ID of the string + * @param number to pass in the template + * @return CharSequence with the phone number wrapped in a TtsSpan + */ + public static CharSequence getTtsSpannedPhoneNumber( + Resources resources, int stringId, String number) { + String msg = resources.getString(stringId, number); + return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number); + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred display + * name. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredDisplayName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } + + /** + * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}. + * Defaults to the name that is non-null. + * + * @param namePrimary the primary name. + * @param nameAlternative the alternative name. + * @param contactsPreferences the ContactsPreferences used to determine the preferred sort order. + * @return namePrimary or nameAlternative depending on the value of displayOrderPreference. + */ + public static String getPreferredSortName( + String namePrimary, + String nameAlternative, + @Nullable ContactsPreferences contactsPreferences) { + if (contactsPreferences == null) { + return namePrimary != null ? namePrimary : nameAlternative; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { + return namePrimary; + } + + if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE + && !TextUtils.isEmpty(nameAlternative)) { + return nameAlternative; + } + + return namePrimary; + } +} diff --git a/java/com/android/contacts/common/util/ContactListViewUtils.java b/java/com/android/contacts/common/util/ContactListViewUtils.java new file mode 100644 index 000000000..278c27d5c --- /dev/null +++ b/java/com/android/contacts/common/util/ContactListViewUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.view.View; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.util.ViewUtil; + +/** Utilities for configuring ListViews with a card background. */ +public class ContactListViewUtils { + + // These two constants will help add more padding for the text inside the card. + private static final double TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO = 1.1; + + private static void addPaddingToView( + ListView listView, int parentWidth, int listSpaceWeight, int listViewWeight) { + if (listSpaceWeight > 0 && listViewWeight > 0) { + double paddingPercent = + (double) listSpaceWeight / (double) (listSpaceWeight * 2 + listViewWeight); + listView.setPadding( + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingTop(), + (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO), + listView.getPaddingBottom()); + // The EdgeEffect and ScrollBar need to span to the edge of the ListView's padding. + listView.setClipToPadding(false); + listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); + } + } + + /** + * Add padding to {@param listView} if this configuration has set both space weight and view + * weight on the layout. Use this util method instead of defining the padding in the layout file + * so that the {@param listView}'s padding can be set proportional to the card padding. + * + * @param listView ListView that we add padding to + * @param rootLayout layout that contains ListView and R.id.list_card + */ + public static void applyCardPaddingToView( + Resources resources, final ListView listView, final View rootLayout) { + // Set a padding on the list view so it appears in the center of the card + // in the layout if required. + final int listSpaceWeight = resources.getInteger(R.integer.contact_list_space_layout_weight); + final int listViewWeight = resources.getInteger(R.integer.contact_list_card_layout_weight); + if (listSpaceWeight > 0 && listViewWeight > 0) { + rootLayout.setBackgroundResource(0); + // Set the card view visible + View mCardView = rootLayout.findViewById(R.id.list_card); + if (mCardView == null) { + throw new RuntimeException( + "Your content must have a list card view who can be turned visible " + + "whenever it is necessary."); + } + mCardView.setVisibility(View.VISIBLE); + + // Add extra padding to the list view to make them appear in the center of the card. + // In order to avoid jumping, we skip drawing the next frame of the ListView. + ViewUtil.doOnPreDraw( + listView, + false, + new Runnable() { + @Override + public void run() { + // Use the rootLayout.getWidth() instead of listView.getWidth() since + // we sometimes hide the listView until we finish loading data. This would + // result in incorrect padding. + ContactListViewUtils.addPaddingToView( + listView, rootLayout.getWidth(), listSpaceWeight, listViewWeight); + } + }); + } + } +} diff --git a/java/com/android/contacts/common/util/ContactLoaderUtils.java b/java/com/android/contacts/common/util/ContactLoaderUtils.java new file mode 100644 index 000000000..e30971721 --- /dev/null +++ b/java/com/android/contacts/common/util/ContactLoaderUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.ContactsContract; +import android.provider.ContactsContract.RawContacts; + +/** Utility methods for the {@link ContactLoader}. */ +public final class ContactLoaderUtils { + + /** Static helper, not instantiable. */ + private ContactLoaderUtils() {} + + /** + * Transforms the given Uri and returns a Lookup-Uri that represents the contact. For legacy + * contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException} can be thrown + * if the URI is null or the authority is not recognized. + * + * <p>Do not call from the UI thread. + */ + @SuppressWarnings("deprecation") + public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) + throws IllegalArgumentException { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + + final String authority = uri.getAuthority(); + + // Current Style Uri? + if (ContactsContract.AUTHORITY.equals(authority)) { + final String type = resolver.getType(uri); + // Contact-Uri? Good, return it + if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) { + return uri; + } + + // RawContact-Uri? Transform it to ContactUri + if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) { + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + // Anything else? We don't know what this is + throw new IllegalArgumentException("uri format is unknown"); + } + + // Legacy Style? Convert to RawContact + final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY; + if (OBSOLETE_AUTHORITY.equals(authority)) { + // Legacy Format. Convert to RawContact-Uri and then lookup the contact + final long rawContactId = ContentUris.parseId(uri); + return RawContacts.getContactLookupUri( + resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); + } + + throw new IllegalArgumentException("uri authority is unknown"); + } +} diff --git a/java/com/android/contacts/common/util/DateUtils.java b/java/com/android/contacts/common/util/DateUtils.java new file mode 100644 index 000000000..1935d727a --- /dev/null +++ b/java/com/android/contacts/common/util/DateUtils.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2010 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.Context; +import android.text.format.DateFormat; +import android.text.format.Time; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +/** Utility methods for processing dates. */ +public class DateUtils { + + public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC"); + + /** + * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year. Let's + * add a one-off hack for that day of the year + */ + public static final String NO_YEAR_DATE_FEB29TH = "--02-29"; + + // Variations of ISO 8601 date format. Do not change the order - it does affect the + // result in ambiguous cases. + private static final SimpleDateFormat[] DATE_FORMATS = { + CommonDateUtils.FULL_DATE_FORMAT, + CommonDateUtils.DATE_AND_TIME_FORMAT, + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US), + new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US), + }; + + static { + for (SimpleDateFormat format : DATE_FORMATS) { + format.setLenient(true); + format.setTimeZone(UTC_TIMEZONE); + } + CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param string The string representation of the provided date + * @param mustContainYear If true, the string is parsed as a date containing a year. If false, the + * string is parsed into a valid date even if the year field is missing. + * @return A Calendar object corresponding to the date if the string is successfully parsed. If + * not, null is returned. + */ + public static Calendar parseDate(String string, boolean mustContainYear) { + ParsePosition parsePosition = new ParsePosition(0); + Date date; + if (!mustContainYear) { + final boolean noYearParsed; + // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately + if (NO_YEAR_DATE_FEB29TH.equals(string)) { + return getUtcDate(0, Calendar.FEBRUARY, 29); + } else { + synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) { + date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition); + } + noYearParsed = parsePosition.getIndex() == string.length(); + } + + if (noYearParsed) { + return getUtcDate(date, true); + } + } + for (int i = 0; i < DATE_FORMATS.length; i++) { + SimpleDateFormat f = DATE_FORMATS[i]; + synchronized (f) { + parsePosition.setIndex(0); + date = f.parse(string, parsePosition); + if (parsePosition.getIndex() == string.length()) { + return getUtcDate(date, false); + } + } + } + return null; + } + + private static final Calendar getUtcDate(Date date, boolean noYear) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.setTime(date); + if (noYear) { + calendar.set(Calendar.YEAR, 0); + } + return calendar; + } + + private static final Calendar getUtcDate(int year, int month, int dayOfMonth) { + final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US); + calendar.clear(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); + return calendar; + } + + public static boolean isYearSet(Calendar cal) { + // use the Calendar.YEAR field to track whether or not the year is set instead of + // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become + // true irregardless of what the previous value was + return cal.get(Calendar.YEAR) > 1; + } + + /** + * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with longForm + * set to {@code true} by default. + * + * @param context Valid context + * @param string String representation of a date to parse + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string) { + return formatDate(context, string, true); + } + + /** + * Parses the supplied string to see if it looks like a date. + * + * @param context Valid context + * @param string String representation of a date to parse + * @param longForm If true, return the date formatted into its long string representation. If + * false, return the date formatted using its short form representation (i.e. 12/11/2012) + * @return Returns the same date in a cleaned up format. If the supplied string does not look like + * a date, return it unchanged. + */ + public static String formatDate(Context context, String string, boolean longForm) { + if (string == null) { + return null; + } + + string = string.trim(); + if (string.length() == 0) { + return string; + } + final Calendar cal = parseDate(string, false); + + // we weren't able to parse the string successfully so just return it unchanged + if (cal == null) { + return string; + } + + final boolean isYearSet = isYearSet(cal); + final java.text.DateFormat outFormat; + if (!isYearSet) { + outFormat = getLocalizedDateFormatWithoutYear(context); + } else { + outFormat = + longForm ? DateFormat.getLongDateFormat(context) : DateFormat.getDateFormat(context); + } + synchronized (outFormat) { + outFormat.setTimeZone(UTC_TIMEZONE); + return outFormat.format(cal.getTime()); + } + } + + public static boolean isMonthBeforeDay(Context context) { + char[] dateFormatOrder = DateFormat.getDateFormatOrder(context); + for (int i = 0; i < dateFormatOrder.length; i++) { + if (dateFormatOrder[i] == 'd') { + return false; + } + if (dateFormatOrder[i] == 'M') { + return true; + } + } + return false; + } + + /** + * Returns a SimpleDateFormat object without the year fields by using a regular expression to + * eliminate the year in the string pattern. In the rare occurence that the resulting pattern + * cannot be reconverted into a SimpleDateFormat, it uses the provided context to determine + * whether the month field should be displayed before the day field, and returns either "MMMM dd" + * or "dd MMMM" converted into a SimpleDateFormat. + */ + public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) { + final String pattern = + ((SimpleDateFormat) SimpleDateFormat.getDateInstance(java.text.DateFormat.LONG)) + .toPattern(); + // Determine the correct regex pattern for year. + // Special case handling for Spanish locale by checking for "de" + final String yearPattern = + pattern.contains("de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*"; + try { + // Eliminate the substring in pattern that matches the format for that of year + return new SimpleDateFormat(pattern.replaceAll(yearPattern, "")); + } catch (IllegalArgumentException e) { + return new SimpleDateFormat(DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM"); + } + } + + /** + * Given a calendar (possibly containing only a day of the year), returns the earliest possible + * anniversary of the date that is equal to or after the current point in time if the date does + * not contain a year, or the date converted to the local time zone (if the date contains a year. + * + * @param target The date we wish to convert(in the UTC time zone). + * @return If date does not contain a year (year < 1900), returns the next earliest anniversary + * that is after the current point in time (in the local time zone). Otherwise, returns the + * adjusted Date in the local time zone. + */ + public static Date getNextAnnualDate(Calendar target) { + final Calendar today = Calendar.getInstance(); + today.setTime(new Date()); + + // Round the current time to the exact start of today so that when we compare + // today against the target date, both dates are set to exactly 0000H. + today.set(Calendar.HOUR_OF_DAY, 0); + today.set(Calendar.MINUTE, 0); + today.set(Calendar.SECOND, 0); + today.set(Calendar.MILLISECOND, 0); + + final boolean isYearSet = isYearSet(target); + final int targetYear = target.get(Calendar.YEAR); + final int targetMonth = target.get(Calendar.MONTH); + final int targetDay = target.get(Calendar.DAY_OF_MONTH); + final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29); + final GregorianCalendar anniversary = new GregorianCalendar(); + // Convert from the UTC date to the local date. Set the year to today's year if the + // there is no provided year (targetYear < 1900) + anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear, targetMonth, targetDay); + // If the anniversary's date is before the start of today and there is no year set, + // increment the year by 1 so that the returned date is always equal to or greater than + // today. If the day is a leap year, keep going until we get the next leap year anniversary + // Otherwise if there is already a year set, simply return the exact date. + if (!isYearSet) { + int anniversaryYear = today.get(Calendar.YEAR); + if (anniversary.before(today) || (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) { + // If the target date is not Feb 29, then set the anniversary to the next year. + // Otherwise, keep going until we find the next leap year (this is not guaranteed + // to be in 4 years time). + do { + anniversaryYear += 1; + } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear)); + anniversary.set(anniversaryYear, targetMonth, targetDay); + } + } + return anniversary.getTime(); + } + + /** + * Determine the difference, in days between two dates. Uses similar logic as the {@link + * android.text.format.DateUtils.getRelativeTimeSpanString} method. + * + * @param time Instance of time object to use for calculations. + * @param date1 First date to check. + * @param date2 Second date to check. + * @return The absolute difference in days between the two dates. + */ + public static int getDayDifference(Time time, long date1, long date2) { + time.set(date1); + int startDay = Time.getJulianDay(date1, time.gmtoff); + + time.set(date2); + int currentDay = Time.getJulianDay(date2, time.gmtoff); + + return Math.abs(currentDay - startDay); + } +} diff --git a/java/com/android/contacts/common/util/FabUtil.java b/java/com/android/contacts/common/util/FabUtil.java new file mode 100644 index 000000000..b1bb2e653 --- /dev/null +++ b/java/com/android/contacts/common/util/FabUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.ListView; +import com.android.contacts.common.R; +import com.android.dialer.compat.CompatUtils; + +/** Provides static functions to work with views */ +public class FabUtil { + + private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setOval(0, 0, view.getWidth(), view.getHeight()); + } + }; + + private FabUtil() {} + + /** + * Configures the floating action button, clipping it to a circle and setting its translation z + * + * @param fabView the float action button's view + * @param res the resources file + */ + public static void setupFloatingActionButton(View fabView, Resources res) { + if (CompatUtils.isLollipopCompatible()) { + fabView.setOutlineProvider(OVAL_OUTLINE_PROVIDER); + fabView.setTranslationZ( + res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z)); + } + } + + /** + * Adds padding to the bottom of the given {@link ListView} so that the floating action button + * does not obscure any content. + * + * @param listView to add the padding to + * @param res valid resources object + */ + public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) { + final int fabPadding = + res.getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding); + listView.setPaddingRelative( + listView.getPaddingStart(), + listView.getPaddingTop(), + listView.getPaddingEnd(), + listView.getPaddingBottom() + fabPadding); + listView.setClipToPadding(false); + } +} diff --git a/java/com/android/contacts/common/util/MaterialColorMapUtils.java b/java/com/android/contacts/common/util/MaterialColorMapUtils.java new file mode 100644 index 000000000..a2d9847ec --- /dev/null +++ b/java/com/android/contacts/common/util/MaterialColorMapUtils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2014 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Trace; +import com.android.contacts.common.R; + +public class MaterialColorMapUtils { + + private final TypedArray sPrimaryColors; + private final TypedArray sSecondaryColors; + + public MaterialColorMapUtils(Resources resources) { + sPrimaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors); + sSecondaryColors = + resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors_dark); + } + + public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) { + final int primaryColor = resources.getColor(R.color.quickcontact_default_photo_tint_color); + final int secondaryColor = + resources.getColor(R.color.quickcontact_default_photo_tint_color_dark); + return new MaterialPalette(primaryColor, secondaryColor); + } + + /** + * Returns the hue component of a color int. + * + * @return A value between 0.0f and 1.0f + */ + public static float hue(int color) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + int V = Math.max(b, Math.max(r, g)); + int temp = Math.min(b, Math.min(r, g)); + + float H; + + if (V == temp) { + H = 0; + } else { + final float vtemp = V - temp; + final float cr = (V - r) / vtemp; + final float cg = (V - g) / vtemp; + final float cb = (V - b) / vtemp; + + if (r == V) { + H = cb - cg; + } else if (g == V) { + H = 2 + cr - cb; + } else { + H = 4 + cg - cr; + } + + H /= 6.f; + if (H < 0) { + H++; + } + } + + return H; + } + + /** + * Return primary and secondary colors from the Material color palette that are similar to {@param + * color}. + */ + public MaterialPalette calculatePrimaryAndSecondaryColor(int color) { + Trace.beginSection("calculatePrimaryAndSecondaryColor"); + + final float colorHue = hue(color); + float minimumDistance = Float.MAX_VALUE; + int indexBestMatch = 0; + for (int i = 0; i < sPrimaryColors.length(); i++) { + final int primaryColor = sPrimaryColors.getColor(i, 0); + final float comparedHue = hue(primaryColor); + // No need to be perceptually accurate when calculating color distances since + // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change + // the mapping very often. + final float distance = Math.abs(comparedHue - colorHue); + if (distance < minimumDistance) { + minimumDistance = distance; + indexBestMatch = i; + } + } + + Trace.endSection(); + return new MaterialPalette( + sPrimaryColors.getColor(indexBestMatch, 0), sSecondaryColors.getColor(indexBestMatch, 0)); + } + + public static class MaterialPalette implements Parcelable { + + public static final Creator<MaterialPalette> CREATOR = + new Creator<MaterialPalette>() { + @Override + public MaterialPalette createFromParcel(Parcel in) { + return new MaterialPalette(in); + } + + @Override + public MaterialPalette[] newArray(int size) { + return new MaterialPalette[size]; + } + }; + public final int mPrimaryColor; + public final int mSecondaryColor; + + public MaterialPalette(int primaryColor, int secondaryColor) { + mPrimaryColor = primaryColor; + mSecondaryColor = secondaryColor; + } + + private MaterialPalette(Parcel in) { + mPrimaryColor = in.readInt(); + mSecondaryColor = in.readInt(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MaterialPalette other = (MaterialPalette) obj; + if (mPrimaryColor != other.mPrimaryColor) { + return false; + } + if (mSecondaryColor != other.mSecondaryColor) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mPrimaryColor; + result = prime * result + mSecondaryColor; + return result; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPrimaryColor); + dest.writeInt(mSecondaryColor); + } + } +} diff --git a/java/com/android/contacts/common/util/NameConverter.java b/java/com/android/contacts/common/util/NameConverter.java new file mode 100644 index 000000000..ae3275d14 --- /dev/null +++ b/java/com/android/contacts/common/util/NameConverter.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2011 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.net.Uri.Builder; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.text.TextUtils; +import com.android.contacts.common.model.dataitem.StructuredNameDataItem; +import java.util.Map; +import java.util.TreeMap; + +/** + * Utility class for converting between a display name and structured name (and vice-versa), via + * calls to the contact provider. + */ +public class NameConverter { + + /** The array of fields that comprise a structured name. */ + public static final String[] STRUCTURED_NAME_FIELDS = + new String[] { + StructuredName.PREFIX, + StructuredName.GIVEN_NAME, + StructuredName.MIDDLE_NAME, + StructuredName.FAMILY_NAME, + StructuredName.SUFFIX + }; + + /** + * Converts the given structured name (provided as a map from {@link StructuredName} fields to + * corresponding values) into a display name string. + * + * <p>Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. See ContactsProvider2.completeName() + * for the underlying method call. + * + * @param context Activity context. + * @param structuredName The structured name map to convert. + * @return The display name computed from the structured name map. + */ + public static String structuredNameToDisplayName( + Context context, Map<String, String> structuredName) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (structuredName.containsKey(key)) { + appendQueryParameter(builder, key, structuredName.get(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** + * Converts the given structured name (provided as ContentValues) into a display name string. + * + * @param context Activity context. + * @param values The content values containing values comprising the structured name. + */ + public static String structuredNameToDisplayName(Context context, ContentValues values) { + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + for (String key : STRUCTURED_NAME_FIELDS) { + if (values.containsKey(key)) { + appendQueryParameter(builder, key, values.getAsString(key)); + } + } + return fetchDisplayName(context, builder.build()); + } + + /** Helper method for fetching the display name via the given URI. */ + private static String fetchDisplayName(Context context, Uri uri) { + String displayName = null; + Cursor cursor = + context + .getContentResolver() + .query( + uri, + new String[] { + StructuredName.DISPLAY_NAME, + }, + null, + null, + null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + displayName = cursor.getString(0); + } + } finally { + cursor.close(); + } + } + return displayName; + } + + /** + * Converts the given display name string into a structured name (as a map from {@link + * StructuredName} fields to corresponding values). + * + * <p>Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @return The structured name map computed from the display name. + */ + public static Map<String, String> displayNameToStructuredName( + Context context, String displayName) { + Map<String, String> structuredName = new TreeMap<String, String>(); + Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name"); + + appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName); + Cursor cursor = + context + .getContentResolver() + .query(builder.build(), STRUCTURED_NAME_FIELDS, null, null, null); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) { + structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i)); + } + } + } finally { + cursor.close(); + } + } + return structuredName; + } + + /** + * Converts the given display name string into a structured name (inserting the structured values + * into a new or existing ContentValues object). + * + * <p>Note that this operates via a call back to the ContactProvider, but it does not access the + * database, so it should be safe to call from the UI thread. + * + * @param context Activity context. + * @param displayName The display name to convert. + * @param contentValues The content values object to place the structured name values into. If + * null, a new one will be created and returned. + * @return The ContentValues object containing the structured name fields derived from the display + * name. + */ + public static ContentValues displayNameToStructuredName( + Context context, String displayName, ContentValues contentValues) { + if (contentValues == null) { + contentValues = new ContentValues(); + } + Map<String, String> mapValues = displayNameToStructuredName(context, displayName); + for (String key : mapValues.keySet()) { + contentValues.put(key, mapValues.get(key)); + } + return contentValues; + } + + private static void appendQueryParameter(Builder builder, String field, String value) { + if (!TextUtils.isEmpty(value)) { + builder.appendQueryParameter(field, value); + } + } + + /** + * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues. Parsed + * data should be {@link StructuredName#PHONETIC_FAMILY_NAME}, {@link + * StructuredName#PHONETIC_MIDDLE_NAME}, and {@link StructuredName#PHONETIC_GIVEN_NAME}. If this + * method cannot parse given phoneticName, null values will be stored. + * + * @param phoneticName Phonetic name to be parsed + * @param values ContentValues to be used for storing data. If null, new instance will be created. + * @return ContentValues with parsed data. Those data can be null. + */ + public static StructuredNameDataItem parsePhoneticName( + String phoneticName, StructuredNameDataItem item) { + String family = null; + String middle = null; + String given = null; + + if (!TextUtils.isEmpty(phoneticName)) { + String[] strings = phoneticName.split(" ", 3); + switch (strings.length) { + case 1: + family = strings[0]; + break; + case 2: + family = strings[0]; + given = strings[1]; + break; + case 3: + family = strings[0]; + middle = strings[1]; + given = strings[2]; + break; + } + } + + if (item == null) { + item = new StructuredNameDataItem(); + } + item.setPhoneticFamilyName(family); + item.setPhoneticMiddleName(middle); + item.setPhoneticGivenName(given); + return item; + } + + /** Constructs and returns a phonetic full name from given parts. */ + public static String buildPhoneticName(String family, String middle, String given) { + if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle) || !TextUtils.isEmpty(given)) { + StringBuilder sb = new StringBuilder(); + if (!TextUtils.isEmpty(family)) { + sb.append(family.trim()).append(' '); + } + if (!TextUtils.isEmpty(middle)) { + sb.append(middle.trim()).append(' '); + } + if (!TextUtils.isEmpty(given)) { + sb.append(given.trim()).append(' '); + } + sb.setLength(sb.length() - 1); // Yank the last space + return sb.toString(); + } else { + return null; + } + } +} diff --git a/java/com/android/contacts/common/util/SearchUtil.java b/java/com/android/contacts/common/util/SearchUtil.java new file mode 100644 index 000000000..314d565b2 --- /dev/null +++ b/java/com/android/contacts/common/util/SearchUtil.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.support.annotation.VisibleForTesting; + +/** Methods related to search. */ +public class SearchUtil { + + /** + * Given a string with lines delimited with '\n', finds the matching line to the given substring. + * + * @param contents The string to search. + * @param substring The substring to search for. + * @return A MatchedLine object containing the matching line and the startIndex of the substring + * match within that line. + */ + public static MatchedLine findMatchingLine(String contents, String substring) { + final MatchedLine matched = new MatchedLine(); + + // Snippet may contain multiple lines separated by "\n". + // Locate the lines of the content that contain the substring. + final int index = SearchUtil.contains(contents, substring); + if (index != -1) { + // Match found. Find the corresponding line. + int start = index - 1; + while (start > -1) { + if (contents.charAt(start) == '\n') { + break; + } + start--; + } + int end = index + 1; + while (end < contents.length()) { + if (contents.charAt(end) == '\n') { + break; + } + end++; + } + matched.line = contents.substring(start + 1, end); + matched.startIndex = index - (start + 1); + } + return matched; + } + + /** + * Similar to String.contains() with two main differences: + * + * <p>1) Only searches token prefixes. A token is defined as any combination of letters or + * numbers. + * + * <p>2) Returns the starting index where the substring is found. + * + * @param value The string to search. + * @param substring The substring to look for. + * @return The starting index where the substring is found. {@literal -1} if substring is not + * found in value. + */ + @VisibleForTesting + static int contains(String value, String substring) { + if (value.length() < substring.length()) { + return -1; + } + + // i18n support + // Generate the code points for the substring once. + // There will be a maximum of substring.length code points. But may be fewer. + // Since the array length is not an accurate size, we need to keep a separate variable. + final int[] substringCodePoints = new int[substring.length()]; + int substringLength = 0; // may not equal substring.length()!! + for (int i = 0; i < substring.length(); ) { + final int codePoint = Character.codePointAt(substring, i); + substringCodePoints[substringLength] = codePoint; + substringLength++; + i += Character.charCount(codePoint); + } + + for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) { + int numMatch = 0; + for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) { + int valueCp = Character.toLowerCase(value.codePointAt(j)); + int substringCp = substringCodePoints[numMatch]; + if (valueCp != substringCp) { + break; + } + j += Character.charCount(valueCp); + } + if (numMatch == substringLength) { + return i; + } + } + return -1; + } + + /** + * Find the start of the next token. A token is composed of letters and numbers. Any other + * character are considered delimiters. + * + * @param line The string to search for the next token. + * @param startIndex The index to start searching. 0 based indexing. + * @return The index for the start of the next token. line.length() if next token not found. + */ + @VisibleForTesting + static int findNextTokenStart(String line, int startIndex) { + int index = startIndex; + + // If already in token, eat remainder of token. + while (index <= line.length()) { + if (index == line.length()) { + // No more tokens. + return index; + } + final int codePoint = line.codePointAt(index); + if (!Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + // Out of token, eat all consecutive delimiters. + while (index <= line.length()) { + if (index == line.length()) { + return index; + } + final int codePoint = line.codePointAt(index); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + index += Character.charCount(codePoint); + } + + return index; + } + + /** + * Anything other than letter and numbers are considered delimiters. Remove start and end + * delimiters since they are not relevant to search. + * + * @param query The query string to clean. + * @return The cleaned query. Empty string if all characters are cleaned out. + */ + public static String cleanStartAndEndOfSearchQuery(String query) { + int start = 0; + while (start < query.length()) { + int codePoint = query.codePointAt(start); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + start += Character.charCount(codePoint); + } + + if (start == query.length()) { + // All characters are delimiters. + return ""; + } + + int end = query.length() - 1; + while (end > -1) { + if (Character.isLowSurrogate(query.charAt(end))) { + // Assume valid i18n string. There should be a matching high surrogate before it. + end--; + } + int codePoint = query.codePointAt(end); + if (Character.isLetterOrDigit(codePoint)) { + break; + } + end--; + } + + // end is a letter or digit. + return query.substring(start, end + 1); + } + + public static class MatchedLine { + + public int startIndex = -1; + public String line; + + @Override + public String toString() { + return "MatchedLine{" + "line='" + line + '\'' + ", startIndex=" + startIndex + '}'; + } + } +} diff --git a/java/com/android/contacts/common/util/StopWatch.java b/java/com/android/contacts/common/util/StopWatch.java new file mode 100644 index 000000000..b944b9867 --- /dev/null +++ b/java/com/android/contacts/common/util/StopWatch.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.util.Log; +import java.util.ArrayList; + +/** A {@link StopWatch} records start, laps and stop, and print them to logcat. */ +public class StopWatch { + + private final String mLabel; + + private final ArrayList<Long> mTimes = new ArrayList<>(); + private final ArrayList<String> mLapLabels = new ArrayList<>(); + + private StopWatch(String label) { + mLabel = label; + lap(""); + } + + /** Create a new instance and start it. */ + public static StopWatch start(String label) { + return new StopWatch(label); + } + + /** Return a dummy instance that does no operations. */ + public static StopWatch getNullStopWatch() { + return NullStopWatch.INSTANCE; + } + + /** Record a lap. */ + public void lap(String lapLabel) { + mTimes.add(System.currentTimeMillis()); + mLapLabels.add(lapLabel); + } + + /** Stop it and log the result, if the total time >= {@code timeThresholdToLog}. */ + public void stopAndLog(String TAG, int timeThresholdToLog) { + + lap(""); + + final long start = mTimes.get(0); + final long stop = mTimes.get(mTimes.size() - 1); + + final long total = stop - start; + if (total < timeThresholdToLog) { + return; + } + + final StringBuilder sb = new StringBuilder(); + sb.append(mLabel); + sb.append(","); + sb.append(total); + sb.append(": "); + + long last = start; + for (int i = 1; i < mTimes.size(); i++) { + final long current = mTimes.get(i); + sb.append(mLapLabels.get(i)); + sb.append(","); + sb.append((current - last)); + sb.append(" "); + last = current; + } + Log.v(TAG, sb.toString()); + } + + private static class NullStopWatch extends StopWatch { + + public static final NullStopWatch INSTANCE = new NullStopWatch(); + + public NullStopWatch() { + super(null); + } + + @Override + public void lap(String lapLabel) { + // noop + } + + @Override + public void stopAndLog(String TAG, int timeThresholdToLog) { + // noop + } + } +} diff --git a/java/com/android/contacts/common/util/TelephonyManagerUtils.java b/java/com/android/contacts/common/util/TelephonyManagerUtils.java new file mode 100644 index 000000000..b664268ca --- /dev/null +++ b/java/com/android/contacts/common/util/TelephonyManagerUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +import android.content.Context; +import android.telephony.TelephonyManager; + +/** This class provides several TelephonyManager util functions. */ +public class TelephonyManagerUtils { + + /** + * Gets the voicemail tag from Telephony Manager. + * + * @param context Current application context + * @return Voicemail tag, the alphabetic identifier associated with the voice mail number. + */ + public static String getVoiceMailAlphaTag(Context context) { + final TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag(); + return voiceMailLabel; + } + + /** + * @param context Current application context. + * @return True if there is a subscription which supports video calls. False otherwise. + */ + public static boolean hasVideoCallSubscription(Context context) { + // TODO: Check the telephony manager's subscriptions to see if any support video calls. + return true; + } +} diff --git a/java/com/android/contacts/common/util/TrafficStatsTags.java b/java/com/android/contacts/common/util/TrafficStatsTags.java new file mode 100644 index 000000000..b0e7fb583 --- /dev/null +++ b/java/com/android/contacts/common/util/TrafficStatsTags.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2015 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.contacts.common.util; + +public class TrafficStatsTags { + + public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001; + public static final int TAG_MAX = 0x9999; +} diff --git a/java/com/android/contacts/common/util/UriUtils.java b/java/com/android/contacts/common/util/UriUtils.java new file mode 100644 index 000000000..4690942ba --- /dev/null +++ b/java/com/android/contacts/common/util/UriUtils.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 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 ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.contacts.common.util; + +import android.net.Uri; +import android.provider.ContactsContract; +import java.util.List; + +/** Utility methods for dealing with URIs. */ +public class UriUtils { + + /** Static helper, not instantiable. */ + private UriUtils() {} + + /** Checks whether two URI are equal, taking care of the case where either is null. */ + public static boolean areEqual(Uri uri1, Uri uri2) { + if (uri1 == null && uri2 == null) { + return true; + } + if (uri1 == null || uri2 == null) { + return false; + } + return uri1.equals(uri2); + } + + /** Parses a string into a URI and returns null if the given string is null. */ + public static Uri parseUriOrNull(String uriString) { + if (uriString == null) { + return null; + } + return Uri.parse(uriString); + } + + /** Converts a URI into a string, returns null if the given URI is null. */ + public static String uriToString(Uri uri) { + return uri == null ? null : uri.toString(); + } + + public static boolean isEncodedContactUri(Uri uri) { + if (uri == null) { + return false; + } + final String lastPathSegment = uri.getLastPathSegment(); + if (lastPathSegment == null) { + return false; + } + return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED); + } + + /** + * @return {@code uri} as-is if the authority is of contacts provider. Otherwise or {@code uri} is + * null, return null otherwise + */ + public static Uri nullForNonContactsUri(Uri uri) { + if (uri == null) { + return null; + } + return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null; + } + + /** Parses the given URI to determine the original lookup key of the contact. */ + public static String getLookupKeyFromUri(Uri lookupUri) { + // Would be nice to be able to persist the lookup key somehow to avoid having to parse + // the uri entirely just to retrieve the lookup key, but every uri is already parsed + // once anyway to check if it is an encoded JSON uri, so this has negligible effect + // on performance. + if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) { + final List<String> segments = lookupUri.getPathSegments(); + // This returns the third path segment of the uri, where the lookup key is located. + // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + return (segments.size() < 3) ? null : Uri.encode(segments.get(2)); + } else { + return null; + } + } +} |