diff options
Diffstat (limited to 'java/com/android/dialer/app/contactinfo')
5 files changed, 732 insertions, 0 deletions
diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java new file mode 100644 index 000000000..4135cb7b8 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java @@ -0,0 +1,357 @@ +/* + * 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.dialer.app.contactinfo; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.ExpirableCache; +import java.lang.ref.WeakReference; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; + +/** + * This is a cache of contact details for the phone numbers in the c all log. The key is the phone + * number with the country in which teh call was placed or received. The content of the cache is + * expired (but not purged) whenever the application comes to the foreground. + * + * <p>This cache queues request for information and queries for information on a background thread, + * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction + * as needed. + * + * <p>TODO: Explore whether there is a pattern to remove external dependencies for starting and + * stopping the query thread. + */ +public class ContactInfoCache { + + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; + + private final ExpirableCache<NumberWithCountryIso, ContactInfo> mCache; + private final ContactInfoHelper mContactInfoHelper; + private final OnContactInfoChangedListener mOnContactInfoChangedListener; + private final BlockingQueue<ContactInfoRequest> mUpdateRequests; + private final Handler mHandler; + private QueryThread mContactInfoQueryThread; + private volatile boolean mRequestProcessingDisabled = false; + + private static class InnerHandler extends Handler { + + private final WeakReference<ContactInfoCache> contactInfoCacheWeakReference; + + public InnerHandler(WeakReference<ContactInfoCache> contactInfoCacheWeakReference) { + this.contactInfoCacheWeakReference = contactInfoCacheWeakReference; + } + + @Override + public void handleMessage(Message msg) { + ContactInfoCache reference = contactInfoCacheWeakReference.get(); + if (reference == null) { + return; + } + switch (msg.what) { + case REDRAW: + reference.mOnContactInfoChangedListener.onContactInfoChanged(); + break; + case START_THREAD: + reference.startRequestProcessing(); + } + } + } + + public ContactInfoCache( + @NonNull ExpirableCache<NumberWithCountryIso, ContactInfo> internalCache, + @NonNull ContactInfoHelper contactInfoHelper, + @NonNull OnContactInfoChangedListener listener) { + mCache = internalCache; + mContactInfoHelper = contactInfoHelper; + mOnContactInfoChangedListener = listener; + mUpdateRequests = new PriorityBlockingQueue<>(); + mHandler = new InnerHandler(new WeakReference<>(this)); + } + + public ContactInfo getValue( + String number, + String countryIso, + ContactInfo callLogContactInfo, + boolean remoteLookupIfNotFoundLocally) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue<ContactInfo> cachedInfo = mCache.getCachedValue(numberCountryIso); + ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); + if (cachedInfo == null) { + mCache.put(numberCountryIso, ContactInfo.EMPTY); + // Use the cached contact info from the call log. + info = callLogContactInfo; + // The db request should happen on a non-UI thread. + // Request the contact details immediately since they are currently missing. + int requestType = + remoteLookupIfNotFoundLocally + ? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE + : ContactInfoRequest.TYPE_LOCAL; + enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType); + // We will format the phone number when we make the background request. + } else { + if (cachedInfo.isExpired()) { + // The contact info is no longer up to date, we should request it. However, we + // do not need to request them immediately. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } else if (!callLogInfoMatches(callLogContactInfo, info)) { + // The call log information does not match the one we have, look it up again. + // We could simply update the call log directly, but that needs to be done in a + // background thread, so it is easier to simply request a new lookup, which will, as + // a side-effect, update the call log. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } + + if (info == ContactInfo.EMPTY) { + // Use the cached contact info from the call log. + info = callLogContactInfo; + } + } + return info; + } + + /** + * Queries the appropriate content provider for the contact associated with the number. + * + * <p>Upon completion it also updates the cache in the call log, if it is different from {@code + * callLogInfo}. + * + * <p>The number might be either a SIP address or a phone number. + * + * <p>It returns true if it updated the content of the cache and we should therefore tell the view + * to update its content. + */ + private boolean queryContactInfo(ContactInfoRequest request) { + ContactInfo info; + if (request.isLocalRequest()) { + info = mContactInfoHelper.lookupNumber(request.number, request.countryIso); + if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) { + if (!mContactInfoHelper.hasName(info)) { + enqueueRequest( + request.number, + request.countryIso, + request.callLogInfo, + true, + ContactInfoRequest.TYPE_REMOTE); + return false; + } + } + } else { + info = mContactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.countryIso); + } + + if (info == null) { + // The lookup failed, just return without requesting to update the view. + return false; + } + + // Check the existing entry in the cache: only if it has changed we should update the + // view. + NumberWithCountryIso numberCountryIso = + new NumberWithCountryIso(request.number, request.countryIso); + ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); + + final boolean isRemoteSource = info.sourceType != 0; + + // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} + // to avoid updating the data set for every new row that is scrolled into view. + + // Exception: Photo uris for contacts from remote sources are not cached in the call log + // cache, so we have to force a redraw for these contacts regardless. + boolean updated = + (existingInfo != ContactInfo.EMPTY || isRemoteSource) && !info.equals(existingInfo); + + // Store the data in the cache so that the UI thread can use to display it. Store it + // even if it has not changed so that it is marked as not expired. + mCache.put(numberCountryIso, info); + + // Update the call log even if the cache it is up-to-date: it is possible that the cache + // contains the value from a different call log entry. + mContactInfoHelper.updateCallLogContactInfo( + request.number, request.countryIso, info, request.callLogInfo); + if (!request.isLocalRequest()) { + mContactInfoHelper.updateCachedNumberLookupService(info); + } + return updated; + } + + /** + * After a delay, start the thread to begin processing requests. We perform lookups on a + * background thread, but this must be called to indicate the thread should be running. + */ + public void start() { + // Schedule a thread-creation message if the thread hasn't been created yet, as an + // optimization to queue fewer messages. + if (mContactInfoQueryThread == null) { + // TODO: Check whether this delay before starting to process is necessary. + mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); + } + } + + /** + * Stops the thread and clears the queue of messages to process. This cleans up the thread for + * lookups so that it is not perpetually running. + */ + public void stop() { + stopRequestProcessing(); + } + + /** + * Starts a background thread to process contact-lookup requests, unless one has already been + * started. + */ + private synchronized void startRequestProcessing() { + // For unit-testing. + if (mRequestProcessingDisabled) { + return; + } + + // If a thread is already started, don't start another. + if (mContactInfoQueryThread != null) { + return; + } + + mContactInfoQueryThread = new QueryThread(); + mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); + mContactInfoQueryThread.start(); + } + + public void invalidate() { + mCache.expireAll(); + stopRequestProcessing(); + } + + /** + * Stops the background thread that processes updates and cancels any pending requests to start + * it. + */ + private synchronized void stopRequestProcessing() { + // Remove any pending requests to start the processing thread. + mHandler.removeMessages(START_THREAD); + if (mContactInfoQueryThread != null) { + // Stop the thread; we are finished with it. + mContactInfoQueryThread.stopProcessing(); + mContactInfoQueryThread.interrupt(); + mContactInfoQueryThread = null; + } + } + + /** + * Enqueues a request to look up the contact details for the given phone number. + * + * <p>It also provides the current contact info stored in the call log for this number. + * + * <p>If the {@code immediate} parameter is true, it will start immediately the thread that looks + * up the contact information (if it has not been already started). Otherwise, it will be started + * with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MS}. + */ + private void enqueueRequest( + String number, + String countryIso, + ContactInfo callLogInfo, + boolean immediate, + @ContactInfoRequest.TYPE int type) { + ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type); + if (!mUpdateRequests.contains(request)) { + mUpdateRequests.offer(request); + } + + if (immediate) { + startRequestProcessing(); + } + } + + /** Checks whether the contact info from the call log matches the one from the contacts db. */ + private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { + // The call log only contains a subset of the fields in the contacts db. Only check those. + return TextUtils.equals(callLogInfo.name, info.name) + && callLogInfo.type == info.type + && TextUtils.equals(callLogInfo.label, info.label); + } + + /** Sets whether processing of requests for contact details should be enabled. */ + public void disableRequestProcessing() { + mRequestProcessingDisabled = true; + } + + @VisibleForTesting + public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + mCache.put(numberCountryIso, contactInfo); + } + + public interface OnContactInfoChangedListener { + + void onContactInfoChanged(); + } + + /* + * Handles requests for contact name and number type. + */ + private class QueryThread extends Thread { + + private volatile boolean mDone = false; + + public QueryThread() { + super("ContactInfoCache.QueryThread"); + } + + public void stopProcessing() { + mDone = true; + } + + @Override + public void run() { + boolean shouldRedraw = false; + while (true) { + // Check if thread is finished, and if so return immediately. + if (mDone) { + return; + } + + try { + ContactInfoRequest request = mUpdateRequests.take(); + shouldRedraw |= queryContactInfo(request); + if (shouldRedraw + && (mUpdateRequests.isEmpty() + || request.isLocalRequest() && !mUpdateRequests.peek().isLocalRequest())) { + shouldRedraw = false; + mHandler.sendEmptyMessage(REDRAW); + } + } catch (InterruptedException e) { + // Ignore and attempt to continue processing requests + } + } + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java new file mode 100644 index 000000000..5c2eb1dbb --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java @@ -0,0 +1,122 @@ +/* + * 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.dialer.app.contactinfo; + +import android.support.annotation.IntDef; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** A request for contact details for the given number, used by the ContactInfoCache. */ +public final class ContactInfoRequest implements Comparable<ContactInfoRequest> { + + private static final AtomicLong NEXT_SEQUENCE_NUMBER = new AtomicLong(0); + + private final long sequenceNumber; + + /** The number to look-up. */ + public final String number; + /** The country in which a call to or from this number was placed or received. */ + public final String countryIso; + /** The cached contact information stored in the call log. */ + public final ContactInfo callLogInfo; + + /** Is the request a remote lookup. Remote requests are treated as lower priority. */ + @TYPE public final int type; + + /** Specifies the type of the request is. */ + @IntDef( + value = { + TYPE_LOCAL, + TYPE_LOCAL_AND_REMOTE, + TYPE_REMOTE, + } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface TYPE {} + + public static final int TYPE_LOCAL = 0; + /** If cannot find the contact locally, do remote lookup later. */ + public static final int TYPE_LOCAL_AND_REMOTE = 1; + + public static final int TYPE_REMOTE = 2; + + public ContactInfoRequest( + String number, String countryIso, ContactInfo callLogInfo, @TYPE int type) { + this.sequenceNumber = NEXT_SEQUENCE_NUMBER.getAndIncrement(); + this.number = number; + this.countryIso = countryIso; + this.callLogInfo = callLogInfo; + this.type = type; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ContactInfoRequest)) { + return false; + } + + ContactInfoRequest other = (ContactInfoRequest) obj; + + if (!TextUtils.equals(number, other.number)) { + return false; + } + if (!TextUtils.equals(countryIso, other.countryIso)) { + return false; + } + if (!Objects.equals(callLogInfo, other.callLogInfo)) { + return false; + } + + if (type != other.type) { + return false; + } + + return true; + } + + public boolean isLocalRequest() { + return type == TYPE_LOCAL || type == TYPE_LOCAL_AND_REMOTE; + } + + @Override + public int hashCode() { + return Objects.hash(sequenceNumber, number, countryIso, callLogInfo, type); + } + + @Override + public int compareTo(ContactInfoRequest other) { + // Local query always comes first. + if (isLocalRequest() && !other.isLocalRequest()) { + return -1; + } + if (!isLocalRequest() && other.isLocalRequest()) { + return 1; + } + // First come first served. + return sequenceNumber < other.sequenceNumber ? -1 : 1; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java new file mode 100644 index 000000000..a8c718502 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java @@ -0,0 +1,129 @@ +/* + * 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.dialer.app.contactinfo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.app.R; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * Class to create the appropriate contact icon from a ContactInfo. This class is for synchronous, + * blocking calls to generate bitmaps, while ContactCommons.ContactPhotoManager is to cache, manage + * and update a ImageView asynchronously. + */ +public class ContactPhotoLoader { + + private final Context mContext; + private final ContactInfo mContactInfo; + + public ContactPhotoLoader(Context context, ContactInfo contactInfo) { + mContext = Objects.requireNonNull(context); + mContactInfo = Objects.requireNonNull(contactInfo); + } + + private static Bitmap drawableToBitmap(Drawable drawable, int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** Create a contact photo icon bitmap appropriate for the ContactInfo. */ + public Bitmap loadPhotoIcon() { + Assert.isWorkerThread(); + int photoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + return drawableToBitmap(getIcon(), photoSize, photoSize); + } + + @VisibleForTesting + Drawable getIcon() { + Drawable drawable = createPhotoIconDrawable(); + if (drawable == null) { + drawable = createLetterTileDrawable(); + } + return drawable; + } + + /** + * @return a {@link Drawable} of circular photo icon if the photo can be loaded, {@code null} + * otherwise. + */ + @Nullable + private Drawable createPhotoIconDrawable() { + if (mContactInfo.photoUri == null) { + return null; + } + try { + InputStream input = mContext.getContentResolver().openInputStream(mContactInfo.photoUri); + if (input == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: InputStream is null"); + return null; + } + Bitmap bitmap = BitmapFactory.decodeStream(input); + input.close(); + + if (bitmap == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: Bitmap is null"); + return null; + } + final RoundedBitmapDrawable drawable = + RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap); + drawable.setAntiAlias(true); + drawable.setCornerRadius(bitmap.getHeight() / 2); + return drawable; + } catch (IOException e) { + LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString()); + return null; + } + } + + /** @return a {@link LetterTileDrawable} based on the ContactInfo. */ + private Drawable createLetterTileDrawable() { + ContactInfoHelper helper = + new ContactInfoHelper(mContext, GeoUtil.getCurrentCountryIso(mContext)); + LetterTileDrawable drawable = new LetterTileDrawable(mContext.getResources()); + drawable.setCanonicalDialerLetterTileDetails( + mContactInfo.name, + mContactInfo.lookupKey, + LetterTileDrawable.SHAPE_CIRCLE, + helper.isBusiness(mContactInfo.sourceType) + ? LetterTileDrawable.TYPE_BUSINESS + : LetterTileDrawable.TYPE_DEFAULT); + return drawable; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java new file mode 100644 index 000000000..aed51b507 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java @@ -0,0 +1,67 @@ +/* + * 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.dialer.app.contactinfo; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.ExpirableCache; + +/** + * Fragment without any UI whose purpose is to retain an instance of {@link ExpirableCache} across + * configuration change through the use of {@link #setRetainInstance(boolean)}. This is done as + * opposed to implementing {@link android.os.Parcelable} as it is a less widespread change. + */ +public class ExpirableCacheHeadlessFragment extends Fragment { + + private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment"; + private static final int CONTACT_INFO_CACHE_SIZE = 100; + + private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache; + + @NonNull + public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) { + return attach(parentActivity.getSupportFragmentManager()); + } + + @NonNull + private static ExpirableCacheHeadlessFragment attach(FragmentManager fragmentManager) { + ExpirableCacheHeadlessFragment fragment = + (ExpirableCacheHeadlessFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (fragment == null) { + fragment = new ExpirableCacheHeadlessFragment(); + // Allowing state loss since in rare cases this is called after activity's state is saved and + // it's fine if the cache is lost. + fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG).commitNowAllowingStateLoss(); + } + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + setRetainInstance(true); + } + + public ExpirableCache<NumberWithCountryIso, ContactInfo> getRetainedCache() { + return retainedCache; + } +} diff --git a/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java new file mode 100644 index 000000000..a005c447d --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java @@ -0,0 +1,57 @@ +/* + * 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.dialer.app.contactinfo; + +import android.text.TextUtils; + +/** + * Stores a phone number of a call with the country code where it originally occurred. This object + * is used as a key in the {@code ContactInfoCache}. + * + * <p>The country does not necessarily specify the country of the phone number itself, but rather it + * is the country in which the user was in when the call was placed or received. + */ +public final class NumberWithCountryIso { + + public final String number; + public final String countryIso; + + public NumberWithCountryIso(String number, String countryIso) { + this.number = number; + this.countryIso = countryIso; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (!(o instanceof NumberWithCountryIso)) { + return false; + } + NumberWithCountryIso other = (NumberWithCountryIso) o; + return TextUtils.equals(number, other.number) && TextUtils.equals(countryIso, other.countryIso); + } + + @Override + public int hashCode() { + int numberHashCode = number == null ? 0 : number.hashCode(); + int countryHashCode = countryIso == null ? 0 : countryIso.hashCode(); + + return numberHashCode ^ countryHashCode; + } +} |