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
17package com.android.messaging.datamodel.data;
18
19import android.content.ContentValues;
20import android.content.res.Resources;
21import android.database.Cursor;
22import android.graphics.Color;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.support.v7.mms.MmsManager;
26import android.telephony.SubscriptionInfo;
27import android.text.TextUtils;
28
29import com.android.ex.chips.RecipientEntry;
30import com.android.messaging.Factory;
31import com.android.messaging.R;
32import com.android.messaging.datamodel.DatabaseHelper;
33import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
34import com.android.messaging.datamodel.DatabaseWrapper;
35import com.android.messaging.sms.MmsSmsUtils;
36import com.android.messaging.util.Assert;
37import com.android.messaging.util.PhoneUtils;
38import com.android.messaging.util.TextUtil;
39
40/**
41 * A class that encapsulates all of the data for a specific participant in a conversation.
42 */
43public 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     */
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
120    private ParticipantData() {
121    }
122
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
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
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
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     */
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     */
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
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
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
295    public String getNormalizedDestination() {
296        return mNormalizedDestination;
297    }
298
299    public String getSendDestination() {
300        return mSendDestination;
301    }
302
303    public String getDisplayDestination() {
304        return mDisplayDestination;
305    }
306
307    public String getContactDestination() {
308        return mContactDestination;
309    }
310
311    public String getFullName() {
312        return mFullName;
313    }
314
315    public String getFirstName() {
316        return mFirstName;
317    }
318
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
347    public String getProfilePhotoUri() {
348        return mProfilePhotoUri;
349    }
350
351    public long getContactId() {
352        return mContactId;
353    }
354
355    public String getLookupKey() {
356        return mLookupKey;
357    }
358
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
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
400    public void setFullName(final String fullName) {
401        mFullName = fullName;
402    }
403
404    public void setFirstName(final String firstName) {
405        mFirstName = firstName;
406    }
407
408    public void setProfilePhotoUri(final String profilePhotoUri) {
409        mProfilePhotoUri = profilePhotoUri;
410    }
411
412    public void setContactId(final long contactId) {
413        mContactId = contactId;
414    }
415
416    public void setLookupKey(final String lookupKey) {
417        mLookupKey = lookupKey;
418    }
419
420    public void setSendDestination(final String destination) {
421        mSendDestination = destination;
422    }
423
424    public void setContactDestination(final String destination) {
425        mContactDestination = destination;
426    }
427
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     */
436    public boolean isActiveSubscription() {
437        return mSlotId != INVALID_SLOT_ID;
438    }
439
440    public boolean isDefaultSelf() {
441        return mSubId == ParticipantData.DEFAULT_SELF_SUB_ID;
442    }
443
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     */
452    public int getDisplaySlotId() {
453        return getSlotId() + 1;
454    }
455
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
462    public String getSubscriptionName() {
463        Assert.isTrue(isActiveSubscription());
464        return mSubscriptionName;
465    }
466
467    public String getId() {
468        return mParticipantId;
469    }
470
471    public boolean isSelf() {
472        return (mSubId != OTHER_THAN_SELF_SUB_ID);
473    }
474
475    public boolean isEmail() {
476        return mIsEmailAddress;
477    }
478
479    public boolean isContactIdResolved() {
480        return (mContactId != PARTICIPANT_CONTACT_ID_NOT_RESOLVED);
481    }
482
483    public boolean isBlocked() {
484        return mBlocked;
485    }
486
487    public boolean isUnknownSender() {
488        final String unknownSender = ParticipantData.getUnknownSenderDestination();
489        return (TextUtils.equals(mSendDestination, unknownSender));
490    }
491
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
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
534    public int describeContents() {
535        return 0;
536    }
537
538    @Override
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