Example #1
0
  @Override
  public void onBindViewHolder(GiphyViewHolder holder, int position) {
    GiphyImage image = images.get(position);

    holder.modelReady = false;
    holder.image = image;
    holder.thumbnail.setAspectRatio(image.getGifAspectRatio());
    holder.gifProgress.setVisibility(View.GONE);

    DrawableRequestBuilder<String> thumbnailRequest = Glide.with(context).load(image.getStillUrl());

    if (Util.isLowMemory(context)) {
      Glide.with(context)
          .load(image.getStillUrl())
          .placeholder(
              new ColorDrawable(
                  Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
          .diskCacheStrategy(DiskCacheStrategy.ALL)
          .into(holder.thumbnail);

      holder.setModelReady();
    } else {
      Glide.with(context)
          .load(image.getGifUrl())
          .thumbnail(thumbnailRequest)
          .placeholder(
              new ColorDrawable(
                  Util.getRandomElement(MaterialColor.values()).toConversationColor(context)))
          .diskCacheStrategy(DiskCacheStrategy.ALL)
          .listener(holder)
          .into(holder.thumbnail);
    }
  }
Example #2
0
  private void handleSmsRegistrationIntent(Intent intent) {
    markAsVerifying(true);

    String number = intent.getStringExtra("e164number");
    MasterSecret masterSecret = intent.getParcelableExtra("master_secret");
    int registrationId = TextSecurePreferences.getLocalRegistrationId(this);

    if (registrationId == 0) {
      registrationId = KeyHelper.generateRegistrationId(false);
      TextSecurePreferences.setLocalRegistrationId(this, registrationId);
    }

    try {
      String password = Util.getSecret(18);
      String signalingKey = Util.getSecret(52);

      initializeChallengeListener();

      setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number));
      TextSecureAccountManager accountManager =
          TextSecureCommunicationFactory.createManager(this, number, password);
      accountManager.requestSmsVerificationCode();

      setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
      String challenge = waitForChallenge();
      accountManager.verifyAccount(challenge, signalingKey, true, registrationId);

      handleCommonRegistration(masterSecret, accountManager, number);
      markAsVerified(number, password, signalingKey);

      setState(new RegistrationState(RegistrationState.STATE_COMPLETE, number));
      broadcastComplete(true);
    } catch (ExpectationFailedException efe) {
      Log.w("RegistrationService", efe);
      setState(new RegistrationState(RegistrationState.STATE_MULTI_REGISTERED, number));
      broadcastComplete(false);
    } catch (UnsupportedOperationException uoe) {
      Log.w("RegistrationService", uoe);
      setState(new RegistrationState(RegistrationState.STATE_GCM_UNSUPPORTED, number));
      broadcastComplete(false);
    } catch (AccountVerificationTimeoutException avte) {
      Log.w("RegistrationService", avte);
      setState(new RegistrationState(RegistrationState.STATE_TIMEOUT, number));
      broadcastComplete(false);
    } catch (IOException e) {
      Log.w("RegistrationService", e);
      setState(new RegistrationState(RegistrationState.STATE_NETWORK_ERROR, number));
      broadcastComplete(false);
    } finally {
      shutdownChallengeListener();
    }
  }
Example #3
0
    private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) {
      long id = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.ID));
      long dateSent =
          cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_SENT));
      long dateReceived =
          cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
      long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
      long mailbox = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
      String address = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS));
      int addressDeviceId =
          cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS_DEVICE_ID));
      Recipients recipients = getRecipientsFor(address);

      String contentLocation =
          cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.CONTENT_LOCATION));
      String transactionId =
          cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.TRANSACTION_ID));
      long messageSize = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_SIZE));
      long expiry = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRY));
      int status = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.STATUS));
      int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.RECEIPT_COUNT));

      byte[] contentLocationBytes = null;
      byte[] transactionIdBytes = null;

      if (!TextUtils.isEmpty(contentLocation))
        contentLocationBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(contentLocation);

      if (!TextUtils.isEmpty(transactionId))
        transactionIdBytes = org.thoughtcrime.securesms.util.Util.toIsoBytes(transactionId);

      return new NotificationMmsMessageRecord(
          context,
          id,
          recipients,
          recipients.getPrimaryRecipient(),
          addressDeviceId,
          dateSent,
          dateReceived,
          receiptCount,
          threadId,
          contentLocationBytes,
          messageSize,
          expiry,
          status,
          transactionIdBytes,
          mailbox);
    }
  public Cursor getFilteredConversationList(List<String> filter) {
    if (filter == null || filter.size() == 0) return null;

    List<Long> rawRecipientIds =
        DatabaseFactory.getAddressDatabase(context).getCanonicalAddressIds(filter);

    if (rawRecipientIds == null || rawRecipientIds.size() == 0) return null;

    SQLiteDatabase db = databaseHelper.getReadableDatabase();
    List<List<Long>> partitionedRecipientIds = Util.partition(rawRecipientIds, 900);
    List<Cursor> cursors = new LinkedList<>();

    for (List<Long> recipientIds : partitionedRecipientIds) {
      String selection = RECIPIENT_IDS + " = ?";
      String[] selectionArgs = new String[recipientIds.size()];

      for (int i = 0; i < recipientIds.size() - 1; i++)
        selection += (" OR " + RECIPIENT_IDS + " = ?");

      int i = 0;
      for (long id : recipientIds) {
        selectionArgs[i++] = String.valueOf(id);
      }

      cursors.add(db.query(TABLE_NAME, null, selection, selectionArgs, null, null, DATE + " DESC"));
    }

    Cursor cursor =
        cursors.size() > 1
            ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()]))
            : cursors.get(0);
    setNotifyConverationListListeners(cursor);
    return cursor;
  }
  public static MasterSecret changeMasterSecretPassphrase(
      Context context, MasterSecret masterSecret, String newPassphrase) {
    try {
      byte[] combinedSecrets =
          Util.combine(
              masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());

      byte[] encryptionSalt = generateSalt();
      int iterations = generateIterationCount(newPassphrase, encryptionSalt);
      byte[] encryptedMasterSecret =
          encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
      byte[] macSalt = generateSalt();
      byte[] encryptedAndMacdMasterSecret =
          macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);

      save(context, "encryption_salt", encryptionSalt);
      save(context, "mac_salt", macSalt);
      save(context, "passphrase_iterations", iterations);
      save(context, "master_secret", encryptedAndMacdMasterSecret);
      save(context, "passphrase_initialized", true);

      return masterSecret;
    } catch (GeneralSecurityException gse) {
      throw new AssertionError(gse);
    }
  }
  public static MasterSecret generateMasterSecret(Context context, String passphrase) {
    try {
      byte[] encryptionSecret = generateEncryptionSecret();
      byte[] macSecret = generateMacSecret();
      byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
      byte[] encryptionSalt = generateSalt();
      int iterations = generateIterationCount(passphrase, encryptionSalt);
      byte[] encryptedMasterSecret =
          encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
      byte[] macSalt = generateSalt();
      byte[] encryptedAndMacdMasterSecret =
          macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);

      save(context, "encryption_salt", encryptionSalt);
      save(context, "mac_salt", macSalt);
      save(context, "passphrase_iterations", iterations);
      save(context, "master_secret", encryptedAndMacdMasterSecret);
      save(context, "passphrase_initialized", true);

      return new MasterSecret(
          new SecretKeySpec(encryptionSecret, "AES"), new SecretKeySpec(macSecret, "HmacSHA1"));
    } catch (GeneralSecurityException e) {
      Log.w("keyutil", e);
      return null;
    }
  }
Example #7
0
  private boolean isRelevant(Context context, Intent intent) {
    SmsMessage message = getSmsMessageFromIntent(intent);
    String messageBody = getSmsMessageBodyFromIntent(intent);

    if (message == null && messageBody == null) return false;

    if (isExemption(message, messageBody)) return false;

    if (!ApplicationMigrationService.isDatabaseImported(context)) return false;

    if (isChallenge(context, messageBody)) return false;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
        && SMS_RECEIVED_ACTION.equals(intent.getAction())
        && Util.isDefaultSmsProvider(context)) {
      return false;
    }

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT
        && TextSecurePreferences.isInterceptAllSmsEnabled(context)) {
      return true;
    }

    return false;
  }
Example #8
0
  protected static byte[] parseResponse(InputStream is) throws IOException {
    InputStream in = new BufferedInputStream(is);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Util.copy(in, baos);

    Log.w(TAG, "Received full server response, " + baos.size() + " bytes");

    return baos.toByteArray();
  }
Example #9
0
  public void markIncomingNotificationReceived(long threadId) {
    notifyConversationListeners(threadId);
    DatabaseFactory.getThreadDatabase(context).update(threadId);

    if (org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)) {
      DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
    }

    jobManager.add(new TrimThreadJob(context, threadId));
  }
Example #10
0
  private long getThreadIdFor(IncomingMediaMessage retrieved)
      throws RecipientFormattingException, MmsException {
    if (retrieved.getGroupId() != null) {
      Recipients groupRecipients =
          RecipientFactory.getRecipientsFromString(context, retrieved.getGroupId(), true);
      return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
    }

    String localNumber;
    Set<String> group = new HashSet<>();

    if (retrieved.getAddresses().getFrom() == null) {
      throw new MmsException("FROM value in PduHeaders did not exist.");
    }

    group.add(retrieved.getAddresses().getFrom());

    if (TextSecurePreferences.isPushRegistered(context)) {
      localNumber = TextSecurePreferences.getLocalNumber(context);
    } else {
      localNumber = ServiceUtil.getTelephonyManager(context).getLine1Number();
    }

    for (String cc : retrieved.getAddresses().getCc()) {
      PhoneNumberUtil.MatchType match;

      if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH;
      else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, cc);

      if (match == PhoneNumberUtil.MatchType.NO_MATCH
          || match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) {
        group.add(cc);
      }
    }

    if (retrieved.getAddresses().getTo().size() > 1) {
      for (String to : retrieved.getAddresses().getTo()) {
        PhoneNumberUtil.MatchType match;

        if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH;
        else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, to);

        if (match == PhoneNumberUtil.MatchType.NO_MATCH
            || match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) {
          group.add(to);
        }
      }
    }

    String recipientsList = Util.join(group, ",");
    Recipients recipients =
        RecipientFactory.getRecipientsFromString(context, recipientsList, false);

    return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
  }
Example #11
0
 private long getThreadIdFor(@NonNull NotificationInd notification) {
   String fromString =
       notification.getFrom() != null && notification.getFrom().getTextString() != null
           ? Util.toIsoString(notification.getFrom().getTextString())
           : "";
   Recipients recipients = RecipientFactory.getRecipientsFromString(context, fromString, false);
   if (recipients.isEmpty())
     recipients =
         RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(), false);
   return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
 }
    private boolean isLocalNumber(Recipient recipient) {
      try {
        String localNumber = TextSecurePreferences.getLocalNumber(context);
        String e164Number = Util.canonicalizeNumber(context, recipient.getNumber());

        return e164Number != null && e164Number.equals(localNumber);
      } catch (InvalidNumberException e) {
        Log.w(TAG, e);
        return false;
      }
    }
Example #13
0
    public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
      this.context = context.getApplicationContext();
      this.groupContext = groupContext;

      if (groupContext == null || groupContext.getMembersList().isEmpty()) {
        this.members = null;
      } else {
        this.members =
            RecipientFactory.getRecipientsFromString(
                context, Util.join(groupContext.getMembersList(), ", "), true);
      }
    }
Example #14
0
    public File getFile(boolean forMms) throws ExecutionException, InterruptedException {
      synchronized (this) {
        while (!modelReady) {
          Util.wait(this, 0);
        }
      }

      return Glide.with(context)
          .load(forMms ? image.getGifMmsUrl() : image.getGifUrl())
          .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
          .get();
    }
Example #15
0
  public Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification) {
    SQLiteDatabase db = databaseHelper.getWritableDatabase();
    MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
    long threadId = getThreadIdFor(notification);
    PduHeaders headers = notification.getPduHeaders();
    ContentValues contentValues = new ContentValues();
    ContentValuesBuilder contentBuilder = new ContentValuesBuilder(contentValues);

    Log.w(TAG, "Message received type: " + headers.getOctet(PduHeaders.MESSAGE_TYPE));

    contentBuilder.add(CONTENT_LOCATION, headers.getTextString(PduHeaders.CONTENT_LOCATION));
    contentBuilder.add(DATE_SENT, headers.getLongInteger(PduHeaders.DATE) * 1000L);
    contentBuilder.add(EXPIRY, headers.getLongInteger(PduHeaders.EXPIRY));
    contentBuilder.add(MESSAGE_SIZE, headers.getLongInteger(PduHeaders.MESSAGE_SIZE));
    contentBuilder.add(TRANSACTION_ID, headers.getTextString(PduHeaders.TRANSACTION_ID));
    contentBuilder.add(MESSAGE_TYPE, headers.getOctet(PduHeaders.MESSAGE_TYPE));

    if (headers.getEncodedStringValue(PduHeaders.FROM) != null) {
      contentBuilder.add(ADDRESS, headers.getEncodedStringValue(PduHeaders.FROM).getTextString());
    } else {
      contentBuilder.add(ADDRESS, null);
    }

    contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE);
    contentValues.put(THREAD_ID, threadId);
    contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED);
    contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp());
    contentValues.put(READ, Util.isDefaultSmsProvider(context) ? 0 : 1);

    if (!contentValues.containsKey(DATE_SENT))
      contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED));

    long messageId = db.insert(TABLE_NAME, null, contentValues);
    addressDatabase.insertAddressesForId(
        messageId, MmsAddresses.forFrom(Util.toIsoString(notification.getFrom().getTextString())));

    return new Pair<>(messageId, threadId);
  }
  public static MasterSecret getMasterSecret(Context context, String passphrase)
      throws InvalidPassphraseException {
    try {
      byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
      byte[] macSalt = retrieve(context, "mac_salt");
      int iterations = retrieve(context, "passphrase_iterations", 100);
      byte[] encryptedMasterSecret =
          verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
      byte[] encryptionSalt = retrieve(context, "encryption_salt");
      byte[] combinedSecrets =
          decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
      byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
      byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];

      return new MasterSecret(
          new SecretKeySpec(encryptionSecret, "AES"), new SecretKeySpec(macSecret, "HmacSHA1"));
    } catch (GeneralSecurityException e) {
      Log.w("keyutil", e);
      return null; // XXX
    } catch (IOException e) {
      Log.w("keyutil", e);
      return null; // XXX
    }
  }
Example #17
0
 private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) {
   SQLiteDatabase database = databaseHelper.getReadableDatabase();
   return database.rawQuery(
       "SELECT "
           + Util.join(MMS_PROJECTION, ",")
           + " FROM "
           + MmsDatabase.TABLE_NAME
           + " LEFT OUTER JOIN "
           + AttachmentDatabase.TABLE_NAME
           + " ON ("
           + MmsDatabase.TABLE_NAME
           + "."
           + MmsDatabase.ID
           + " = "
           + AttachmentDatabase.TABLE_NAME
           + "."
           + AttachmentDatabase.MMS_ID
           + ")"
           + " WHERE "
           + where,
       arguments);
 }
Example #18
0
  protected Pair<Long, Long> insertMessageInbox(IncomingTextMessage message, long type) {
    if (message.isJoined()) {
      type = (type & (Types.TOTAL_MASK - Types.BASE_TYPE_MASK)) | Types.JOINED_TYPE;
    } else if (message.isPreKeyBundle()) {
      type |= Types.KEY_EXCHANGE_BIT | Types.KEY_EXCHANGE_BUNDLE_BIT;
    } else if (message.isSecureMessage()) {
      type |= Types.SECURE_MESSAGE_BIT;
    } else if (message.isGroup()) {
      type |= Types.SECURE_MESSAGE_BIT;
      if (((IncomingGroupMessage) message).isUpdate()) type |= Types.GROUP_UPDATE_BIT;
      else if (((IncomingGroupMessage) message).isQuit()) type |= Types.GROUP_QUIT_BIT;
    } else if (message.isEndSession()) {
      type |= Types.SECURE_MESSAGE_BIT;
      type |= Types.END_SESSION_BIT;
    }

    if (message.isPush()) type |= Types.PUSH_MESSAGE_BIT;

    Recipients recipients;

    if (message.getSender() != null) {
      recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), true);
    } else {
      Log.w(TAG, "Sender is null, returning unknown recipient");
      recipients =
          RecipientFactory.getRecipientsFor(context, Recipient.getUnknownRecipient(), false);
    }

    Recipients groupRecipients;

    if (message.getGroupId() == null) {
      groupRecipients = null;
    } else {
      groupRecipients =
          RecipientFactory.getRecipientsFromString(context, message.getGroupId(), true);
    }

    boolean unread =
        org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context)
            || message.isSecureMessage()
            || message.isPreKeyBundle();

    long threadId;

    if (groupRecipients == null)
      threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
    else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);

    ContentValues values = new ContentValues(6);
    values.put(ADDRESS, message.getSender());
    values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId());
    values.put(DATE_RECEIVED, System.currentTimeMillis());
    values.put(DATE_SENT, message.getSentTimestampMillis());
    values.put(PROTOCOL, message.getProtocol());
    values.put(READ, unread ? 0 : 1);

    if (!TextUtils.isEmpty(message.getPseudoSubject()))
      values.put(SUBJECT, message.getPseudoSubject());

    values.put(REPLY_PATH_PRESENT, message.isReplyPathPresent());
    values.put(SERVICE_CENTER, message.getServiceCenterAddress());
    values.put(BODY, message.getMessageBody());
    values.put(TYPE, type);
    values.put(THREAD_ID, threadId);

    SQLiteDatabase db = databaseHelper.getWritableDatabase();
    long messageId = db.insert(TABLE_NAME, null, values);

    if (unread) {
      DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
    }

    DatabaseFactory.getThreadDatabase(context).update(threadId, true);
    notifyConversationListeners(threadId);
    jobManager.add(new TrimThreadJob(context, threadId));

    return new Pair<>(messageId, threadId);
  }
/**
 * List adapter to display all contacts and their related information
 *
 * @author Jake McGinty
 */
public class ContactSelectionListAdapter extends CursorAdapter implements StickyListHeadersAdapter {
  private static final String TAG = "ContactListAdapter";

  private static final ExecutorService photoResolver = Util.newSingleThreadedLifoExecutor();

  private static final int STYLE_ATTRIBUTES[] =
      new int[] {
        R.attr.contact_selection_push_user,
        R.attr.contact_selection_lay_user,
        R.attr.contact_selection_label_text
      };

  private int TYPE_COLUMN = -1;
  private int NAME_COLUMN = -1;
  private int NUMBER_COLUMN = -1;
  private int NUMBER_TYPE_COLUMN = -1;
  private int ID_COLUMN = -1;

  private final Context context;
  private final boolean multiSelect;
  private final LayoutInflater li;
  private final TypedArray drawables;
  private final Bitmap defaultPhoto;
  private final Bitmap defaultCroppedPhoto;
  private final int scaledPhotoSize;

  private final HashMap<Long, ContactAccessor.ContactData> selectedContacts =
      new HashMap<Long, ContactAccessor.ContactData>();

  public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) {
    super(context, cursor, 0);
    this.context = context;
    this.li = LayoutInflater.from(context);
    this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
    this.multiSelect = multiSelect;
    this.defaultPhoto = ContactPhotoFactory.getDefaultContactPhoto(context);
    this.scaledPhotoSize =
        context.getResources().getDimensionPixelSize(R.dimen.contact_selection_photo_size);
    this.defaultCroppedPhoto =
        BitmapUtil.getScaledCircleCroppedBitmap(defaultPhoto, scaledPhotoSize);
  }

  public static class ViewHolder {
    public CheckBox checkBox;
    public TextView name;
    public TextView number;
    public ImageView contactPhoto;
    public int position;
  }

  public static class DataHolder {
    public int type;
    public String name;
    public String number;
    public int numberType;
    public long id;
  }

  public static class HeaderViewHolder {
    TextView text;
  }

  @Override
  public View newView(Context context, Cursor cursor, ViewGroup parent) {
    final View v = li.inflate(R.layout.push_contact_selection_list_item, parent, false);
    final ViewHolder holder = new ViewHolder();

    if (v != null) {
      holder.name = (TextView) v.findViewById(R.id.name);
      holder.number = (TextView) v.findViewById(R.id.number);
      holder.checkBox = (CheckBox) v.findViewById(R.id.check_box);
      holder.contactPhoto = (ImageView) v.findViewById(R.id.contact_photo_image);

      if (!multiSelect) holder.checkBox.setVisibility(View.GONE);

      v.setTag(R.id.holder_tag, holder);
      v.setTag(R.id.contact_info_tag, new DataHolder());
    }
    return v;
  }

  @Override
  public void bindView(View view, Context context, Cursor cursor) {
    final DataHolder contactData = (DataHolder) view.getTag(R.id.contact_info_tag);
    final ViewHolder holder = (ViewHolder) view.getTag(R.id.holder_tag);
    if (holder == null) {
      Log.w(TAG, "ViewHolder was null. This should not happen.");
      return;
    }
    if (contactData == null) {
      Log.w(TAG, "DataHolder was null. This should not happen.");
      return;
    }
    if (ID_COLUMN < 0) {
      populateColumnIndices(cursor);
    }

    contactData.type = cursor.getInt(TYPE_COLUMN);
    contactData.name = cursor.getString(NAME_COLUMN);
    contactData.number = cursor.getString(NUMBER_COLUMN);
    contactData.numberType = cursor.getInt(NUMBER_TYPE_COLUMN);
    contactData.id = cursor.getLong(ID_COLUMN);

    if (contactData.type != ContactsDatabase.PUSH_TYPE) {
      holder.name.setTextColor(drawables.getColor(1, 0xff000000));
      holder.number.setTextColor(drawables.getColor(1, 0xff000000));
    } else {
      holder.name.setTextColor(drawables.getColor(0, 0xa0000000));
      holder.number.setTextColor(drawables.getColor(0, 0xa0000000));
    }

    if (selectedContacts.containsKey(contactData.id)) {
      holder.checkBox.setChecked(true);
    } else {
      holder.checkBox.setChecked(false);
    }

    holder.name.setText(contactData.name);

    if (contactData.number == null || contactData.number.isEmpty()) {
      holder.name.setEnabled(false);
      holder.number.setText("");
    } else if (contactData.type == ContactsDatabase.PUSH_TYPE) {
      holder.number.setText(contactData.number);
    } else {
      final CharSequence label =
          ContactsContract.CommonDataKinds.Phone.getTypeLabel(
              context.getResources(), contactData.numberType, "");
      final CharSequence numberWithLabel = contactData.number + "  " + label;
      final Spannable numberLabelSpan = new SpannableString(numberWithLabel);
      numberLabelSpan.setSpan(
          new ForegroundColorSpan(drawables.getColor(2, 0xff444444)),
          contactData.number.length(),
          numberWithLabel.length(),
          Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
      holder.number.setText(numberLabelSpan);
    }
    holder.contactPhoto.setImageBitmap(defaultCroppedPhoto);
    loadBitmap(contactData.number, holder.contactPhoto);
  }

  @Override
  public View getHeaderView(int i, View convertView, ViewGroup viewGroup) {
    final Cursor c = getCursor();
    final HeaderViewHolder holder;
    if (convertView == null) {
      holder = new HeaderViewHolder();
      convertView = li.inflate(R.layout.push_contact_selection_list_header, viewGroup, false);
      holder.text = (TextView) convertView.findViewById(R.id.text);
      convertView.setTag(holder);
    } else {
      holder = (HeaderViewHolder) convertView.getTag();
    }
    c.moveToPosition(i);

    final int type = c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN));
    final int headerTextRes;
    switch (type) {
      case 1:
        headerTextRes = R.string.contact_selection_list__header_textsecure_users;
        break;
      default:
        headerTextRes = R.string.contact_selection_list__header_other;
        break;
    }
    holder.text.setText(headerTextRes);
    return convertView;
  }

  @Override
  public long getHeaderId(int i) {
    final Cursor c = getCursor();
    c.moveToPosition(i);
    return c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN));
  }

  public boolean cancelPotentialWork(String number, ImageView imageView) {
    final TaggedFutureTask<?> bitmapWorkerTask = AsyncDrawable.getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
      final Object tag = bitmapWorkerTask.getTag();
      if (tag != null && !tag.equals(number)) {
        bitmapWorkerTask.cancel(true);
      } else {
        return false;
      }
    }
    return true;
  }

  public void loadBitmap(String number, ImageView imageView) {
    if (cancelPotentialWork(number, imageView)) {
      final BitmapWorkerRunnable runnable =
          new BitmapWorkerRunnable(context, imageView, defaultPhoto, number, scaledPhotoSize);
      final TaggedFutureTask<?> task = new TaggedFutureTask<Void>(runnable, null, number);
      final AsyncDrawable asyncDrawable =
          new AsyncDrawable(context.getResources(), defaultCroppedPhoto, task);

      imageView.setImageDrawable(asyncDrawable);
      if (!task.isCancelled()) photoResolver.execute(new FutureTask<Void>(task, null));
    }
  }

  public Map<Long, ContactAccessor.ContactData> getSelectedContacts() {
    return selectedContacts;
  }

  private void populateColumnIndices(final Cursor cursor) {
    this.TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN);
    this.NAME_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN);
    this.NUMBER_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN);
    this.NUMBER_TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN);
    this.ID_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN);
  }
}
public class RecipientProvider {

  private static final String TAG = RecipientProvider.class.getSimpleName();

  private static final RecipientCache recipientCache = new RecipientCache();
  private static final RecipientsCache recipientsCache = new RecipientsCache();
  private static final ExecutorService asyncRecipientResolver =
      Util.newSingleThreadedLifoExecutor();

  private static final String[] CALLER_ID_PROJECTION =
      new String[] {
        PhoneLookup.DISPLAY_NAME, PhoneLookup.LOOKUP_KEY, PhoneLookup._ID, PhoneLookup.NUMBER
      };

  Recipient getRecipient(Context context, long recipientId, boolean asynchronous) {
    Recipient cachedRecipient = recipientCache.get(recipientId);
    if (cachedRecipient != null && !cachedRecipient.isStale()) return cachedRecipient;

    String number = CanonicalAddressDatabase.getInstance(context).getAddressFromId(recipientId);

    if (asynchronous) {
      cachedRecipient =
          new Recipient(
              recipientId,
              number,
              cachedRecipient,
              getRecipientDetailsAsync(context, recipientId, number));
    } else {
      cachedRecipient =
          new Recipient(recipientId, getRecipientDetailsSync(context, recipientId, number));
    }

    recipientCache.set(recipientId, cachedRecipient);
    return cachedRecipient;
  }

  Recipients getRecipients(Context context, long[] recipientIds, boolean asynchronous) {
    Recipients cachedRecipients = recipientsCache.get(new RecipientIds(recipientIds));
    if (cachedRecipients != null && !cachedRecipients.isStale()) return cachedRecipients;

    List<Recipient> recipientList = new LinkedList<>();

    for (long recipientId : recipientIds) {
      recipientList.add(getRecipient(context, recipientId, asynchronous));
    }

    if (asynchronous)
      cachedRecipients =
          new Recipients(
              recipientList,
              cachedRecipients,
              getRecipientsPreferencesAsync(context, recipientIds));
    else
      cachedRecipients =
          new Recipients(recipientList, getRecipientsPreferencesSync(context, recipientIds));

    recipientsCache.set(new RecipientIds(recipientIds), cachedRecipients);
    return cachedRecipients;
  }

  void clearCache() {
    recipientCache.reset();
    recipientsCache.reset();
  }

  private @NonNull ListenableFutureTask<RecipientDetails> getRecipientDetailsAsync(
      final Context context, final long recipientId, final String number) {
    Callable<RecipientDetails> task =
        new Callable<RecipientDetails>() {
          @Override
          public RecipientDetails call() throws Exception {
            return getRecipientDetailsSync(context, recipientId, number);
          }
        };

    ListenableFutureTask<RecipientDetails> future = new ListenableFutureTask<>(task);
    asyncRecipientResolver.submit(future);
    return future;
  }

  private @NonNull RecipientDetails getRecipientDetailsSync(
      Context context, long recipientId, String number) {
    if (GroupUtil.isEncodedGroup(number)) return getGroupRecipientDetails(context, number);
    else return getIndividualRecipientDetails(context, recipientId, number);
  }

  private @NonNull RecipientDetails getIndividualRecipientDetails(
      Context context, long recipientId, String number) {
    Optional<RecipientsPreferences> preferences =
        DatabaseFactory.getRecipientPreferenceDatabase(context)
            .getRecipientsPreferences(new long[] {recipientId});
    MaterialColor color = preferences.isPresent() ? preferences.get().getColor() : null;
    Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
    Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null);

    try {
      if (cursor != null && cursor.moveToFirst()) {
        Uri contactUri = Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1));
        String name = cursor.getString(3).equals(cursor.getString(0)) ? null : cursor.getString(0);
        ContactPhoto contactPhoto =
            ContactPhotoFactory.getContactPhoto(
                context, Uri.withAppendedPath(Contacts.CONTENT_URI, cursor.getLong(2) + ""), name);

        return new RecipientDetails(
            cursor.getString(0), cursor.getString(3), contactUri, contactPhoto, color);
      }
    } finally {
      if (cursor != null) cursor.close();
    }

    return new RecipientDetails(
        null, number, null, ContactPhotoFactory.getDefaultContactPhoto(null), color);
  }

  private @NonNull RecipientDetails getGroupRecipientDetails(Context context, String groupId) {
    try {
      GroupDatabase.GroupRecord record =
          DatabaseFactory.getGroupDatabase(context).getGroup(GroupUtil.getDecodedId(groupId));

      if (record != null) {
        ContactPhoto contactPhoto = ContactPhotoFactory.getGroupContactPhoto(record.getAvatar());
        return new RecipientDetails(record.getTitle(), groupId, null, contactPhoto, null);
      }

      return new RecipientDetails(
          null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null);
    } catch (IOException e) {
      Log.w("RecipientProvider", e);
      return new RecipientDetails(
          null, groupId, null, ContactPhotoFactory.getDefaultGroupPhoto(), null);
    }
  }

  private @Nullable RecipientsPreferences getRecipientsPreferencesSync(
      Context context, long[] recipientIds) {
    return DatabaseFactory.getRecipientPreferenceDatabase(context)
        .getRecipientsPreferences(recipientIds)
        .orNull();
  }

  private ListenableFutureTask<RecipientsPreferences> getRecipientsPreferencesAsync(
      final Context context, final long[] recipientIds) {
    ListenableFutureTask<RecipientsPreferences> task =
        new ListenableFutureTask<>(
            new Callable<RecipientsPreferences>() {
              @Override
              public RecipientsPreferences call() throws Exception {
                return getRecipientsPreferencesSync(context, recipientIds);
              }
            });

    asyncRecipientResolver.execute(task);

    return task;
  }

  public static class RecipientDetails {
    @Nullable public final String name;
    @NonNull public final String number;
    @NonNull public final ContactPhoto avatar;
    @Nullable public final Uri contactUri;
    @Nullable public final MaterialColor color;

    public RecipientDetails(
        @Nullable String name,
        @NonNull String number,
        @Nullable Uri contactUri,
        @NonNull ContactPhoto avatar,
        @Nullable MaterialColor color) {
      this.name = name;
      this.number = number;
      this.avatar = avatar;
      this.contactUri = contactUri;
      this.color = color;
    }
  }

  private static class RecipientIds {
    private final long[] ids;

    private RecipientIds(long[] ids) {
      this.ids = ids;
    }

    public boolean equals(Object other) {
      if (other == null || !(other instanceof RecipientIds)) return false;
      return Arrays.equals(this.ids, ((RecipientIds) other).ids);
    }

    public int hashCode() {
      return Arrays.hashCode(ids);
    }
  }

  private static class RecipientCache {

    private final Map<Long, Recipient> cache = new LRUCache<>(1000);

    public synchronized Recipient get(long recipientId) {
      return cache.get(recipientId);
    }

    public synchronized void set(long recipientId, Recipient recipient) {
      cache.put(recipientId, recipient);
    }

    public synchronized void reset() {
      for (Recipient recipient : cache.values()) {
        recipient.setStale();
      }
    }
  }

  private static class RecipientsCache {

    private final Map<RecipientIds, Recipients> cache = new LRUCache<>(1000);

    public synchronized Recipients get(RecipientIds ids) {
      return cache.get(ids);
    }

    public synchronized void set(RecipientIds ids, Recipients recipients) {
      cache.put(ids, recipients);
    }

    public synchronized void reset() {
      for (Recipients recipients : cache.values()) {
        recipients.setStale();
      }
    }
  }
}