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 */ 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