1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.messaging.datamodel.data;
18 
19 import android.content.ContentValues;
20 import android.content.res.Resources;
21 import android.database.Cursor;
22 import android.graphics.Color;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.support.v7.mms.MmsManager;
26 import android.telephony.SubscriptionInfo;
27 import android.text.TextUtils;
28 
29 import com.android.ex.chips.RecipientEntry;
30 import com.android.messaging.Factory;
31 import com.android.messaging.R;
32 import com.android.messaging.datamodel.DatabaseHelper;
33 import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
34 import com.android.messaging.datamodel.DatabaseWrapper;
35 import com.android.messaging.sms.MmsSmsUtils;
36 import com.android.messaging.util.Assert;
37 import com.android.messaging.util.PhoneUtils;
38 import com.android.messaging.util.TextUtil;
39 
40 /**
41  * A class that encapsulates all of the data for a specific participant in a conversation.
42  */
43 public class ParticipantData implements Parcelable {
44     // We always use -1 as default/invalid sub id although system may give us anything negative
45     public static final int DEFAULT_SELF_SUB_ID = MmsManager.DEFAULT_SUB_ID;
46 
47     // This needs to be something apart from valid or DEFAULT_SELF_SUB_ID
48     public static final int OTHER_THAN_SELF_SUB_ID = DEFAULT_SELF_SUB_ID - 1;
49 
50     // Active slot ids are non-negative. Using -1 to designate to inactive self participants.
51     public static final int INVALID_SLOT_ID = -1;
52 
53     // TODO: may make sense to move this to common place?
54     public static final long PARTICIPANT_CONTACT_ID_NOT_RESOLVED = -1;
55     public static final long PARTICIPANT_CONTACT_ID_NOT_FOUND = -2;
56 
57     public static class ParticipantsQuery {
58         public static final String[] PROJECTION = new String[] {
59             ParticipantColumns._ID,
60             ParticipantColumns.SUB_ID,
61             ParticipantColumns.SIM_SLOT_ID,
62             ParticipantColumns.NORMALIZED_DESTINATION,
63             ParticipantColumns.SEND_DESTINATION,
64             ParticipantColumns.DISPLAY_DESTINATION,
65             ParticipantColumns.FULL_NAME,
66             ParticipantColumns.FIRST_NAME,
67             ParticipantColumns.PROFILE_PHOTO_URI,
68             ParticipantColumns.CONTACT_ID,
69             ParticipantColumns.LOOKUP_KEY,
70             ParticipantColumns.BLOCKED,
71             ParticipantColumns.SUBSCRIPTION_COLOR,
72             ParticipantColumns.SUBSCRIPTION_NAME,
73             ParticipantColumns.CONTACT_DESTINATION,
74         };
75 
76         public static final int INDEX_ID                        = 0;
77         public static final int INDEX_SUB_ID                    = 1;
78         public static final int INDEX_SIM_SLOT_ID               = 2;
79         public static final int INDEX_NORMALIZED_DESTINATION    = 3;
80         public static final int INDEX_SEND_DESTINATION          = 4;
81         public static final int INDEX_DISPLAY_DESTINATION       = 5;
82         public static final int INDEX_FULL_NAME                 = 6;
83         public static final int INDEX_FIRST_NAME                = 7;
84         public static final int INDEX_PROFILE_PHOTO_URI         = 8;
85         public static final int INDEX_CONTACT_ID                = 9;
86         public static final int INDEX_LOOKUP_KEY                = 10;
87         public static final int INDEX_BLOCKED                   = 11;
88         public static final int INDEX_SUBSCRIPTION_COLOR        = 12;
89         public static final int INDEX_SUBSCRIPTION_NAME         = 13;
90         public static final int INDEX_CONTACT_DESTINATION       = 14;
91     }
92 
93     /**
94      * @return The MMS unknown sender participant entity
95      */
getUnknownSenderDestination()96     public static String getUnknownSenderDestination() {
97         // This is a hard coded string rather than a localized one because we don't want it to
98         // change when you change locale.
99         return "\u02BCUNKNOWN_SENDER!\u02BC";
100     }
101 
102     private String mParticipantId;
103     private int mSubId;
104     private int mSlotId;
105     private String mNormalizedDestination;
106     private String mSendDestination;
107     private String mDisplayDestination;
108     private String mContactDestination;
109     private String mFullName;
110     private String mFirstName;
111     private String mProfilePhotoUri;
112     private long mContactId;
113     private String mLookupKey;
114     private int mSubscriptionColor;
115     private String mSubscriptionName;
116     private boolean mIsEmailAddress;
117     private boolean mBlocked;
118 
119     // Don't call constructor directly
ParticipantData()120     private ParticipantData() {
121     }
122 
getFromCursor(final Cursor cursor)123     public static ParticipantData getFromCursor(final Cursor cursor) {
124         final ParticipantData pd = new ParticipantData();
125         pd.mParticipantId = cursor.getString(ParticipantsQuery.INDEX_ID);
126         pd.mSubId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
127         pd.mSlotId = cursor.getInt(ParticipantsQuery.INDEX_SIM_SLOT_ID);
128         pd.mNormalizedDestination = cursor.getString(
129                 ParticipantsQuery.INDEX_NORMALIZED_DESTINATION);
130         pd.mSendDestination = cursor.getString(ParticipantsQuery.INDEX_SEND_DESTINATION);
131         pd.mDisplayDestination = cursor.getString(ParticipantsQuery.INDEX_DISPLAY_DESTINATION);
132         pd.mContactDestination = cursor.getString(ParticipantsQuery.INDEX_CONTACT_DESTINATION);
133         pd.mFullName = cursor.getString(ParticipantsQuery.INDEX_FULL_NAME);
134         pd.mFirstName = cursor.getString(ParticipantsQuery.INDEX_FIRST_NAME);
135         pd.mProfilePhotoUri = cursor.getString(ParticipantsQuery.INDEX_PROFILE_PHOTO_URI);
136         pd.mContactId = cursor.getLong(ParticipantsQuery.INDEX_CONTACT_ID);
137         pd.mLookupKey = cursor.getString(ParticipantsQuery.INDEX_LOOKUP_KEY);
138         pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
139         pd.mBlocked = cursor.getInt(ParticipantsQuery.INDEX_BLOCKED) != 0;
140         pd.mSubscriptionColor = cursor.getInt(ParticipantsQuery.INDEX_SUBSCRIPTION_COLOR);
141         pd.mSubscriptionName = cursor.getString(ParticipantsQuery.INDEX_SUBSCRIPTION_NAME);
142         pd.maybeSetupUnknownSender();
143         return pd;
144     }
145 
getFromId(final DatabaseWrapper dbWrapper, final String participantId)146     public static ParticipantData getFromId(final DatabaseWrapper dbWrapper,
147             final String participantId) {
148         Cursor cursor = null;
149         try {
150             cursor = dbWrapper.query(DatabaseHelper.PARTICIPANTS_TABLE,
151                     ParticipantsQuery.PROJECTION,
152                     ParticipantColumns._ID + " =?",
153                     new String[] { participantId }, null, null, null);
154 
155             if (cursor.moveToFirst()) {
156                 return ParticipantData.getFromCursor(cursor);
157             } else {
158                 return null;
159             }
160         } finally {
161             if (cursor != null) {
162                 cursor.close();
163             }
164         }
165     }
166 
getFromRecipientEntry(final RecipientEntry recipientEntry)167     public static ParticipantData getFromRecipientEntry(final RecipientEntry recipientEntry) {
168         final ParticipantData pd = new ParticipantData();
169         pd.mParticipantId = null;
170         pd.mSubId = OTHER_THAN_SELF_SUB_ID;
171         pd.mSlotId = INVALID_SLOT_ID;
172         pd.mSendDestination = TextUtil.replaceUnicodeDigits(recipientEntry.getDestination());
173         pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
174         pd.mNormalizedDestination = pd.mIsEmailAddress ?
175                 pd.mSendDestination :
176                 PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
177         pd.mDisplayDestination = pd.mIsEmailAddress ?
178                 pd.mNormalizedDestination :
179                 PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
180         pd.mFullName = recipientEntry.getDisplayName();
181         pd.mFirstName = null;
182         pd.mProfilePhotoUri = (recipientEntry.getPhotoThumbnailUri() == null) ? null :
183                 recipientEntry.getPhotoThumbnailUri().toString();
184         pd.mContactId = recipientEntry.getContactId();
185         if (pd.mContactId < 0) {
186             // ParticipantData only supports real contact ids (>=0) based on faith that the contacts
187             // provider will continue to only use non-negative ids.  The UI uses contactId < 0 for
188             // special handling. We convert those to 'not resolved'
189             pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
190         }
191         pd.mLookupKey = recipientEntry.getLookupKey();
192         pd.mBlocked = false;
193         pd.mSubscriptionColor = Color.TRANSPARENT;
194         pd.mSubscriptionName = null;
195         pd.maybeSetupUnknownSender();
196         return pd;
197     }
198 
199     // Shared code for getFromRawPhoneBySystemLocale and getFromRawPhoneBySimLocale
getFromRawPhone(final String phoneNumber)200     private static ParticipantData getFromRawPhone(final String phoneNumber) {
201         Assert.isTrue(phoneNumber != null);
202         final ParticipantData pd = new ParticipantData();
203         pd.mParticipantId = null;
204         pd.mSubId = OTHER_THAN_SELF_SUB_ID;
205         pd.mSlotId = INVALID_SLOT_ID;
206         pd.mSendDestination = TextUtil.replaceUnicodeDigits(phoneNumber);
207         pd.mIsEmailAddress = MmsSmsUtils.isEmailAddress(pd.mSendDestination);
208         pd.mFullName = null;
209         pd.mFirstName = null;
210         pd.mProfilePhotoUri = null;
211         pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
212         pd.mLookupKey = null;
213         pd.mBlocked = false;
214         pd.mSubscriptionColor = Color.TRANSPARENT;
215         pd.mSubscriptionName = null;
216         return pd;
217     }
218 
219     /**
220      * Get an instance from a raw phone number and using system locale to normalize it.
221      *
222      * Use this when creating a participant that is for displaying UI and not associated
223      * with a specific SIM. For example, when creating a conversation using user entered
224      * phone number.
225      *
226      * @param phoneNumber The raw phone number
227      * @return instance
228      */
getFromRawPhoneBySystemLocale(final String phoneNumber)229     public static ParticipantData getFromRawPhoneBySystemLocale(final String phoneNumber) {
230         final ParticipantData pd = getFromRawPhone(phoneNumber);
231         pd.mNormalizedDestination = pd.mIsEmailAddress ?
232                 pd.mSendDestination :
233                 PhoneUtils.getDefault().getCanonicalBySystemLocale(pd.mSendDestination);
234         pd.mDisplayDestination = pd.mIsEmailAddress ?
235                 pd.mNormalizedDestination :
236                 PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
237         pd.maybeSetupUnknownSender();
238         return pd;
239     }
240 
241     /**
242      * Get an instance from a raw phone number and using SIM or system locale to normalize it.
243      *
244      * Use this when creating a participant that is associated with a specific SIM. For example,
245      * the sender of a received message or the recipient of a sending message that is already
246      * targeted at a specific SIM.
247      *
248      * @param phoneNumber The raw phone number
249      * @return instance
250      */
getFromRawPhoneBySimLocale( final String phoneNumber, final int subId)251     public static ParticipantData getFromRawPhoneBySimLocale(
252             final String phoneNumber, final int subId) {
253         final ParticipantData pd = getFromRawPhone(phoneNumber);
254         pd.mNormalizedDestination = pd.mIsEmailAddress ?
255                 pd.mSendDestination :
256                 PhoneUtils.get(subId).getCanonicalBySimLocale(pd.mSendDestination);
257         pd.mDisplayDestination = pd.mIsEmailAddress ?
258                 pd.mNormalizedDestination :
259                 PhoneUtils.getDefault().formatForDisplay(pd.mNormalizedDestination);
260         pd.maybeSetupUnknownSender();
261         return pd;
262     }
263 
getSelfParticipant(final int subId)264     public static ParticipantData getSelfParticipant(final int subId) {
265         Assert.isTrue(subId != OTHER_THAN_SELF_SUB_ID);
266         final ParticipantData pd = new ParticipantData();
267         pd.mParticipantId = null;
268         pd.mSubId = subId;
269         pd.mSlotId = INVALID_SLOT_ID;
270         pd.mIsEmailAddress = false;
271         pd.mSendDestination = null;
272         pd.mNormalizedDestination = null;
273         pd.mDisplayDestination = null;
274         pd.mFullName = null;
275         pd.mFirstName = null;
276         pd.mProfilePhotoUri = null;
277         pd.mContactId = PARTICIPANT_CONTACT_ID_NOT_RESOLVED;
278         pd.mLookupKey = null;
279         pd.mBlocked = false;
280         pd.mSubscriptionColor = Color.TRANSPARENT;
281         pd.mSubscriptionName = null;
282         return pd;
283     }
284 
maybeSetupUnknownSender()285     private void maybeSetupUnknownSender() {
286         if (isUnknownSender()) {
287             // Because your locale may change, we setup the display string for the unknown sender
288             // on the fly rather than relying on the version in the database.
289             final Resources resources = Factory.get().getApplicationContext().getResources();
290             mDisplayDestination = resources.getString(R.string.unknown_sender);
291             mFullName = mDisplayDestination;
292         }
293     }
294 
getNormalizedDestination()295     public String getNormalizedDestination() {
296         return mNormalizedDestination;
297     }
298 
getSendDestination()299     public String getSendDestination() {
300         return mSendDestination;
301     }
302 
getDisplayDestination()303     public String getDisplayDestination() {
304         return mDisplayDestination;
305     }
306 
getContactDestination()307     public String getContactDestination() {
308         return mContactDestination;
309     }
310 
getFullName()311     public String getFullName() {
312         return mFullName;
313     }
314 
getFirstName()315     public String getFirstName() {
316         return mFirstName;
317     }
318 
getDisplayName(final boolean preferFullName)319     public String getDisplayName(final boolean preferFullName) {
320         if (preferFullName) {
321             // Prefer full name over first name
322             if (!TextUtils.isEmpty(mFullName)) {
323                 return mFullName;
324             }
325             if (!TextUtils.isEmpty(mFirstName)) {
326                 return mFirstName;
327             }
328         } else {
329             // Prefer first name over full name
330             if (!TextUtils.isEmpty(mFirstName)) {
331                 return mFirstName;
332             }
333             if (!TextUtils.isEmpty(mFullName)) {
334                 return mFullName;
335             }
336         }
337 
338         // Fallback to the display destination
339         if (!TextUtils.isEmpty(mDisplayDestination)) {
340             return mDisplayDestination;
341         }
342 
343         return Factory.get().getApplicationContext().getResources().getString(
344                 R.string.unknown_sender);
345     }
346 
getProfilePhotoUri()347     public String getProfilePhotoUri() {
348         return mProfilePhotoUri;
349     }
350 
getContactId()351     public long getContactId() {
352         return mContactId;
353     }
354 
getLookupKey()355     public String getLookupKey() {
356         return mLookupKey;
357     }
358 
updatePhoneNumberForSelfIfChanged()359     public boolean updatePhoneNumberForSelfIfChanged() {
360         final String phoneNumber =
361                 PhoneUtils.get(mSubId).getCanonicalForSelf(true/*allowOverride*/);
362         boolean changed = false;
363         if (isSelf() && !TextUtils.equals(phoneNumber, mNormalizedDestination)) {
364             mNormalizedDestination = phoneNumber;
365             mSendDestination = phoneNumber;
366             mDisplayDestination = mIsEmailAddress ?
367                     phoneNumber :
368                     PhoneUtils.getDefault().formatForDisplay(phoneNumber);
369             changed = true;
370         }
371         return changed;
372     }
373 
updateSubscriptionInfoForSelfIfChanged(final SubscriptionInfo subscriptionInfo)374     public boolean updateSubscriptionInfoForSelfIfChanged(final SubscriptionInfo subscriptionInfo) {
375         boolean changed = false;
376         if (isSelf()) {
377             if (subscriptionInfo == null) {
378                 // The subscription is inactive. Check if the participant is still active.
379                 if (isActiveSubscription()) {
380                     mSlotId = INVALID_SLOT_ID;
381                     mSubscriptionColor = Color.TRANSPARENT;
382                     mSubscriptionName = "";
383                     changed = true;
384                 }
385             } else {
386                 final int slotId = subscriptionInfo.getSimSlotIndex();
387                 final int color = subscriptionInfo.getIconTint();
388                 final CharSequence name = subscriptionInfo.getDisplayName();
389                 if (mSlotId != slotId || mSubscriptionColor != color || mSubscriptionName != name) {
390                     mSlotId = slotId;
391                     mSubscriptionColor = color;
392                     mSubscriptionName = name.toString();
393                     changed = true;
394                 }
395             }
396         }
397         return changed;
398     }
399 
setFullName(final String fullName)400     public void setFullName(final String fullName) {
401         mFullName = fullName;
402     }
403 
setFirstName(final String firstName)404     public void setFirstName(final String firstName) {
405         mFirstName = firstName;
406     }
407 
setProfilePhotoUri(final String profilePhotoUri)408     public void setProfilePhotoUri(final String profilePhotoUri) {
409         mProfilePhotoUri = profilePhotoUri;
410     }
411 
setContactId(final long contactId)412     public void setContactId(final long contactId) {
413         mContactId = contactId;
414     }
415 
setLookupKey(final String lookupKey)416     public void setLookupKey(final String lookupKey) {
417         mLookupKey = lookupKey;
418     }
419 
setSendDestination(final String destination)420     public void setSendDestination(final String destination) {
421         mSendDestination = destination;
422     }
423 
setContactDestination(final String destination)424     public void setContactDestination(final String destination) {
425         mContactDestination = destination;
426     }
427 
getSubId()428     public int getSubId() {
429         return mSubId;
430     }
431 
432     /**
433      * @return whether this sub is active. Note that {@link ParticipantData#DEFAULT_SELF_SUB_ID} is
434      *         is considered as active if there is any active SIM.
435      */
isActiveSubscription()436     public boolean isActiveSubscription() {
437         return mSlotId != INVALID_SLOT_ID;
438     }
439 
isDefaultSelf()440     public boolean isDefaultSelf() {
441         return mSubId == ParticipantData.DEFAULT_SELF_SUB_ID;
442     }
443 
getSlotId()444     public int getSlotId() {
445         return mSlotId;
446     }
447 
448     /**
449      * Slot IDs in the subscription manager is zero-based, but we want to show it
450      * as 1-based in UI.
451      */
getDisplaySlotId()452     public int getDisplaySlotId() {
453         return getSlotId() + 1;
454     }
455 
getSubscriptionColor()456     public int getSubscriptionColor() {
457         Assert.isTrue(isActiveSubscription());
458         // Force the alpha channel to 0xff to ensure the returned color is solid.
459         return mSubscriptionColor | 0xff000000;
460     }
461 
getSubscriptionName()462     public String getSubscriptionName() {
463         Assert.isTrue(isActiveSubscription());
464         return mSubscriptionName;
465     }
466 
getId()467     public String getId() {
468         return mParticipantId;
469     }
470 
isSelf()471     public boolean isSelf() {
472         return (mSubId != OTHER_THAN_SELF_SUB_ID);
473     }
474 
isEmail()475     public boolean isEmail() {
476         return mIsEmailAddress;
477     }
478 
isContactIdResolved()479     public boolean isContactIdResolved() {
480         return (mContactId != PARTICIPANT_CONTACT_ID_NOT_RESOLVED);
481     }
482 
isBlocked()483     public boolean isBlocked() {
484         return mBlocked;
485     }
486 
isUnknownSender()487     public boolean isUnknownSender() {
488         final String unknownSender = ParticipantData.getUnknownSenderDestination();
489         return (TextUtils.equals(mSendDestination, unknownSender));
490     }
491 
toContentValues()492     public ContentValues toContentValues() {
493         final ContentValues values = new ContentValues();
494         values.put(ParticipantColumns.SUB_ID, mSubId);
495         values.put(ParticipantColumns.SIM_SLOT_ID, mSlotId);
496         values.put(DatabaseHelper.ParticipantColumns.SEND_DESTINATION, mSendDestination);
497 
498         if (!isUnknownSender()) {
499             values.put(DatabaseHelper.ParticipantColumns.DISPLAY_DESTINATION, mDisplayDestination);
500             values.put(DatabaseHelper.ParticipantColumns.NORMALIZED_DESTINATION,
501                     mNormalizedDestination);
502             values.put(ParticipantColumns.FULL_NAME, mFullName);
503             values.put(ParticipantColumns.FIRST_NAME, mFirstName);
504         }
505 
506         values.put(ParticipantColumns.PROFILE_PHOTO_URI, mProfilePhotoUri);
507         values.put(ParticipantColumns.CONTACT_ID, mContactId);
508         values.put(ParticipantColumns.LOOKUP_KEY, mLookupKey);
509         values.put(ParticipantColumns.BLOCKED, mBlocked);
510         values.put(ParticipantColumns.SUBSCRIPTION_COLOR, mSubscriptionColor);
511         values.put(ParticipantColumns.SUBSCRIPTION_NAME, mSubscriptionName);
512         return values;
513     }
514 
ParticipantData(final Parcel in)515     public ParticipantData(final Parcel in) {
516         mParticipantId = in.readString();
517         mSubId = in.readInt();
518         mSlotId = in.readInt();
519         mNormalizedDestination = in.readString();
520         mSendDestination = in.readString();
521         mDisplayDestination = in.readString();
522         mFullName = in.readString();
523         mFirstName = in.readString();
524         mProfilePhotoUri = in.readString();
525         mContactId = in.readLong();
526         mLookupKey = in.readString();
527         mIsEmailAddress = in.readInt() != 0;
528         mBlocked = in.readInt() != 0;
529         mSubscriptionColor = in.readInt();
530         mSubscriptionName = in.readString();
531     }
532 
533     @Override
describeContents()534     public int describeContents() {
535         return 0;
536     }
537 
538     @Override
writeToParcel(final Parcel dest, final int flags)539     public void writeToParcel(final Parcel dest, final int flags) {
540         dest.writeString(mParticipantId);
541         dest.writeInt(mSubId);
542         dest.writeInt(mSlotId);
543         dest.writeString(mNormalizedDestination);
544         dest.writeString(mSendDestination);
545         dest.writeString(mDisplayDestination);
546         dest.writeString(mFullName);
547         dest.writeString(mFirstName);
548         dest.writeString(mProfilePhotoUri);
549         dest.writeLong(mContactId);
550         dest.writeString(mLookupKey);
551         dest.writeInt(mIsEmailAddress ? 1 : 0);
552         dest.writeInt(mBlocked ? 1 : 0);
553         dest.writeInt(mSubscriptionColor);
554         dest.writeString(mSubscriptionName);
555     }
556 
557     public static final Parcelable.Creator<ParticipantData> CREATOR
558     = new Parcelable.Creator<ParticipantData>() {
559         @Override
560         public ParticipantData createFromParcel(final Parcel in) {
561             return new ParticipantData(in);
562         }
563 
564         @Override
565         public ParticipantData[] newArray(final int size) {
566             return new ParticipantData[size];
567         }
568     };
569 }
570