content: /*
   * Copyright (C) 2019 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.telephony.ims;
  
  import static android.provider.Telephony.RcsColumns.Rcs1To1ThreadColumns.FALLBACK_THREAD_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.Rcs1To1ThreadColumns.RCS_1_TO_1_THREAD_URI;
  import static android.provider.Telephony.RcsColumns.RcsEventTypes.ICON_CHANGED_EVENT_TYPE;
  import static android.provider.Telephony.RcsColumns.RcsEventTypes.NAME_CHANGED_EVENT_TYPE;
  import static android.provider.Telephony.RcsColumns.RcsEventTypes.PARTICIPANT_JOINED_EVENT_TYPE;
  import static android.provider.Telephony.RcsColumns.RcsEventTypes.PARTICIPANT_LEFT_EVENT_TYPE;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.CONTENT_TYPE_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.CONTENT_URI_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.DURATION_MILLIS_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.FILE_SIZE_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.FILE_TRANSFER_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.FILE_TRANSFER_URI;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.HEIGHT_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.PREVIEW_TYPE_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.PREVIEW_URI_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.SESSION_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.SUCCESSFULLY_TRANSFERRED_BYTES;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.TRANSFER_STATUS_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsFileTransferColumns.WIDTH_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsGroupThreadColumns.CONFERENCE_URI_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsGroupThreadColumns.GROUP_ICON_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsGroupThreadColumns.GROUP_NAME_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsGroupThreadColumns.OWNER_PARTICIPANT_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsGroupThreadColumns.RCS_GROUP_THREAD_URI;
  import static android.provider.Telephony.RcsColumns.RcsIncomingMessageColumns.ARRIVAL_TIMESTAMP_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsIncomingMessageColumns.INCOMING_MESSAGE_URI;
  import static android.provider.Telephony.RcsColumns.RcsIncomingMessageColumns.SEEN_TIMESTAMP_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsIncomingMessageColumns.SENDER_PARTICIPANT_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.GLOBAL_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.LATITUDE_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.LONGITUDE_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.MESSAGE_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.MESSAGE_TEXT_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.ORIGINATION_TIMESTAMP_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.STATUS_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageColumns.SUB_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsMessageDeliveryColumns.DELIVERED_TIMESTAMP_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.CANONICAL_ADDRESS_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_ALIAS_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_PARTICIPANT_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsParticipantColumns.RCS_PARTICIPANT_URI;
  import static android.provider.Telephony.RcsColumns.RcsParticipantEventColumns.NEW_ALIAS_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadColumns.RCS_THREAD_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadEventColumns.DESTINATION_PARTICIPANT_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadEventColumns.NEW_ICON_URI_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadEventColumns.NEW_NAME_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadEventColumns.SOURCE_PARTICIPANT_ID_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsThreadEventColumns.TIMESTAMP_COLUMN;
  import static android.provider.Telephony.RcsColumns.RcsUnifiedThreadColumns.THREAD_TYPE_GROUP;
  import static android.provider.Telephony.RcsColumns.TRANSACTION_FAILED;
  import static android.telephony.ims.RcsEventQueryParams.EVENT_QUERY_PARAMETERS_KEY;
  import static android.telephony.ims.RcsMessageQueryParams.MESSAGE_QUERY_PARAMETERS_KEY;
  import static android.telephony.ims.RcsParticipantQueryParams.PARTICIPANT_QUERY_PARAMETERS_KEY;
  import static android.telephony.ims.RcsQueryContinuationToken.QUERY_CONTINUATION_TOKEN;
  import static android.telephony.ims.RcsThreadQueryParams.THREAD_QUERY_PARAMETERS_KEY;
  
  import static com.android.internal.telephony.ims.RcsMessageStoreUtil.getMessageTableUri;
  import static com.android.internal.telephony.ims.RcsParticipantQueryHelper.getUriForParticipant;
  import static com.android.internal.telephony.ims.RcsThreadQueryHelper.get1To1ThreadUri;
  import static com.android.internal.telephony.ims.RcsThreadQueryHelper.getAllParticipantsInThreadUri;
  import static com.android.internal.telephony.ims.RcsThreadQueryHelper.getGroupThreadUri;
  import static com.android.internal.telephony.ims.RcsThreadQueryHelper.getParticipantInThreadUri;
  
  import android.content.ContentResolver;
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.provider.Telephony;
  import android.telephony.Rlog;
  import android.telephony.ims.RcsEventQueryParams;
  import android.telephony.ims.RcsEventQueryResultDescriptor;
  import android.telephony.ims.RcsFileTransferCreationParams;
  import android.telephony.ims.RcsIncomingMessageCreationParams;
  import android.telephony.ims.RcsMessageQueryParams;
  import android.telephony.ims.RcsMessageQueryResultParcelable;
  import android.telephony.ims.RcsMessageSnippet;
  import android.telephony.ims.RcsMessageStore;
  import android.telephony.ims.RcsOutgoingMessageCreationParams;
  import android.telephony.ims.RcsParticipantQueryParams;
  import android.telephony.ims.RcsParticipantQueryResultParcelable;
  import android.telephony.ims.RcsQueryContinuationToken;
  import android.telephony.ims.RcsThreadQueryParams;
  import android.telephony.ims.RcsThreadQueryResultParcelable;
  import android.telephony.ims.aidl.IRcs;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  /**
   * Backing implementation of {@link RcsMessageStore}.
   */
  public class RcsMessageStoreController extends IRcs.Stub {
  
      static final String TAG = "RcsMsgStoreController";
      private static final String RCS_SERVICE_NAME = "ircs";
  
      private static RcsMessageStoreController sInstance;
  
      private final Context mContext;
      private final ContentResolver mContentResolver;
      private final RcsParticipantQueryHelper mParticipantQueryHelper;
      private final RcsMessageQueryHelper mMessageQueryHelper;
      private final RcsEventQueryHelper mEventQueryHelper;
      private final RcsThreadQueryHelper mThreadQueryHelper;
      private final RcsMessageStoreUtil mMessageStoreUtil;
  
      /**
       * Initialize the instance. Should only be called once.
       */
      public static RcsMessageStoreController init(Context context) {
          synchronized (RcsMessageStoreController.class) {
              if (sInstance == null) {
                  sInstance = new RcsMessageStoreController(context);
                  if (ServiceManager.getService(RCS_SERVICE_NAME) == null) {
                      ServiceManager.addService(RCS_SERVICE_NAME, sInstance);
                  }
              } else {
                  Rlog.e(TAG, "init() called multiple times! sInstance = " + sInstance);
              }
          }
          return sInstance;
      }
  
      interface ThrowingSupplier<T> {
          T get() throws RemoteException;
      }
  
      interface ThrowingRunnable {
          void run() throws RemoteException;
      }
  
      private void performWriteOperation(String callingPackage, ThrowingRunnable fn) {
          RcsPermissions.checkWritePermissions(mContext, callingPackage);
  
          try {
              fn.run();
          } catch (RemoteException e) {
              throw new RuntimeException(e);
          }
      }
  
      private <T> T performCreateOperation(String callingPackage, ThrowingSupplier<T> fn) {
          RcsPermissions.checkWritePermissions(mContext, callingPackage);
  
          try {
              return fn.get();
          } catch (RemoteException e) {
              throw new RuntimeException(e);
          }
      }
  
      private <T> T performReadOperation(String callingPackage, ThrowingSupplier<T> fn) {
          RcsPermissions.checkReadPermissions(mContext, callingPackage);
  
          try {
              return fn.get();
          } catch (RemoteException e) {
              throw new RuntimeException(e);
          }
      }
  
      @VisibleForTesting
      public RcsMessageStoreController(Context context) {
          mContext = context;
          mContentResolver = context.getContentResolver();
          mParticipantQueryHelper = new RcsParticipantQueryHelper(mContentResolver);
          mMessageQueryHelper = new RcsMessageQueryHelper(mContentResolver);
          mThreadQueryHelper = new RcsThreadQueryHelper(mContentResolver, mParticipantQueryHelper);
          mEventQueryHelper = new RcsEventQueryHelper(mContentResolver);
          mMessageStoreUtil = new RcsMessageStoreUtil(mContentResolver);
      }
  
      @Override
      public boolean deleteThread(int threadId, int threadType, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              int deletionCount = mContentResolver.delete(
                      threadType == THREAD_TYPE_GROUP ? RCS_GROUP_THREAD_URI : RCS_1_TO_1_THREAD_URI,
                      RCS_THREAD_ID_COLUMN + "=?",
                      new String[]{Integer.toString(threadId)});
  
              return deletionCount > 0;
          });
      }
  
      @Override
      public RcsMessageSnippet getMessageSnippet(int threadId, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              // TODO - implement
              return null;
          });
      }
  
      @Override
      public RcsThreadQueryResultParcelable getRcsThreads(RcsThreadQueryParams queryParameters,
              String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(THREAD_QUERY_PARAMETERS_KEY, queryParameters);
              return mThreadQueryHelper.performThreadQuery(bundle);
          });
      }
  
      @Override
      public RcsThreadQueryResultParcelable getRcsThreadsWithToken(
              RcsQueryContinuationToken continuationToken, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(QUERY_CONTINUATION_TOKEN, continuationToken);
              return mThreadQueryHelper.performThreadQuery(bundle);
          });
      }
  
      @Override
      public RcsParticipantQueryResultParcelable getParticipants(
              RcsParticipantQueryParams queryParameters, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(PARTICIPANT_QUERY_PARAMETERS_KEY, queryParameters);
              return mParticipantQueryHelper.performParticipantQuery(bundle);
          });
      }
  
      @Override
      public RcsParticipantQueryResultParcelable getParticipantsWithToken(
              RcsQueryContinuationToken continuationToken, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(QUERY_CONTINUATION_TOKEN, continuationToken);
              return mParticipantQueryHelper.performParticipantQuery(bundle);
          });
      }
  
      @Override
      public RcsMessageQueryResultParcelable getMessages(RcsMessageQueryParams queryParams,
              String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(MESSAGE_QUERY_PARAMETERS_KEY, queryParams);
              return mMessageQueryHelper.performMessageQuery(bundle);
          });
      }
  
      @Override
      public RcsMessageQueryResultParcelable getMessagesWithToken(
              RcsQueryContinuationToken continuationToken, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(QUERY_CONTINUATION_TOKEN, continuationToken);
              return mMessageQueryHelper.performMessageQuery(bundle);
          });
      }
  
      @Override
      public RcsEventQueryResultDescriptor getEvents(RcsEventQueryParams queryParameters,
              String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(EVENT_QUERY_PARAMETERS_KEY, queryParameters);
              return mEventQueryHelper.performEventQuery(bundle);
          });
      }
  
      @Override
      public RcsEventQueryResultDescriptor getEventsWithToken(
              RcsQueryContinuationToken continuationToken, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Bundle bundle = new Bundle();
              bundle.putParcelable(QUERY_CONTINUATION_TOKEN, continuationToken);
              return mEventQueryHelper.performEventQuery(bundle);
          });
      }
  
      @Override
      public int createRcs1To1Thread(int recipientId, String callingPackage) {
          return performCreateOperation(callingPackage,
                  () -> mThreadQueryHelper.create1To1Thread(recipientId));
      }
  
      @Override
      public int createGroupThread(int[] participantIds, String groupName, Uri groupIcon,
              String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              int groupThreadId = mThreadQueryHelper.createGroupThread(groupName, groupIcon);
              if (groupThreadId <= 0) {
                  throw new RemoteException("Could not create RcsGroupThread.");
              }
  
              // Insert participants
              // TODO(123718879): Instead of adding participants here, add them under RcsProvider
              //  under one transaction
              if (participantIds != null) {
                  for (int participantId : participantIds) {
                      addParticipantToGroupThread(groupThreadId, participantId, callingPackage);
                  }
              }
  
              return groupThreadId;
          });
      }
  
      /**
       * TODO(109759350) Instead of sending the update query directly to RcsProvider, this function
       * orchestrates between RcsProvider and MmsSmsProvider. This is because we are not fully decided
       * on whether we should have RCS storage in a separate database file.
       */
      @Override
      public int createRcsParticipant(String canonicalAddress, String alias, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues contentValues = new ContentValues();
  
              long canonicalAddressId = Telephony.RcsColumns.RcsCanonicalAddressHelper
                      .getOrCreateCanonicalAddressId(mContentResolver, canonicalAddress);
  
              if (canonicalAddressId == TRANSACTION_FAILED) {
                  throw new RemoteException("Could not create or make canonical address entry");
              }
  
              contentValues.put(CANONICAL_ADDRESS_ID_COLUMN, canonicalAddressId);
              contentValues.put(RCS_ALIAS_COLUMN, alias);
  
              // TODO (123719857) - Disallow creation of duplicate participants
              Uri newParticipantUri = mContentResolver.insert(RCS_PARTICIPANT_URI, contentValues);
              int newParticipantRowId;
  
              if (newParticipantUri == null) {
                  throw new RemoteException("Error inserting new participant into RcsProvider");
              }
  
              try {
                  newParticipantRowId = Integer.parseInt(newParticipantUri.getLastPathSegment());
              } catch (NumberFormatException e) {
                  throw new RemoteException(
                          "Uri returned after creating a participant is malformed: "
                                  + newParticipantUri);
              }
  
              return newParticipantRowId;
          });
      }
  
      @Override
      public String getRcsParticipantCanonicalAddress(int participantId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(RCS_PARTICIPANT_URI,
                          Telephony.CanonicalAddressesColumns.ADDRESS, RCS_PARTICIPANT_ID_COLUMN,
                          participantId));
      }
  
      @Override
      public String getRcsParticipantAlias(int participantId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(RCS_PARTICIPANT_URI,
                          RCS_ALIAS_COLUMN,
                          RCS_PARTICIPANT_ID_COLUMN, participantId));
      }
  
      @Override
      public void setRcsParticipantAlias(int id, String alias, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(getUriForParticipant(id),
                          RCS_ALIAS_COLUMN,
                          alias, "Could not update RCS participant alias"));
      }
  
      @Override
      public String getRcsParticipantContactId(int participantId, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              // TODO - implement
              return null;
          });
      }
  
      @Override
      public void setRcsParticipantContactId(int participantId, String contactId,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> {
              // TODO - implement
          });
      }
  
      @Override
      public void set1To1ThreadFallbackThreadId(int rcsThreadId, long fallbackId,
              String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(get1To1ThreadUri(rcsThreadId),
                          FALLBACK_THREAD_ID_COLUMN, fallbackId, "Could not set fallback thread ID"));
      }
  
      @Override
      public long get1To1ThreadFallbackThreadId(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(RCS_1_TO_1_THREAD_URI,
                          FALLBACK_THREAD_ID_COLUMN,
                          RCS_THREAD_ID_COLUMN, rcsThreadId));
      }
  
      @Override
      public int get1To1ThreadOtherParticipantId(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              Uri uri = get1To1ThreadUri(rcsThreadId);
              String[] projection = new String[]{RCS_PARTICIPANT_ID_COLUMN};
              try (Cursor cursor = mContentResolver.query(uri, projection, null, null)) {
                  if (cursor == null || cursor.getCount() != 1) {
                      throw new RemoteException("Could not get the thread recipient");
                  }
                  cursor.moveToNext();
                  return cursor.getInt(
                          cursor.getColumnIndex(RCS_PARTICIPANT_ID_COLUMN));
              }
          });
      }
  
      @Override
      public void setGroupThreadName(int rcsThreadId, String groupName, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(getGroupThreadUri(rcsThreadId),
                          GROUP_NAME_COLUMN, groupName, "Could not update group name"));
      }
  
      @Override
      public String getGroupThreadName(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(RCS_GROUP_THREAD_URI,
                          GROUP_NAME_COLUMN,
                          RCS_THREAD_ID_COLUMN, rcsThreadId));
      }
  
      @Override
      public void setGroupThreadIcon(int rcsThreadId, Uri groupIcon, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(getGroupThreadUri(rcsThreadId),
                          GROUP_ICON_COLUMN, groupIcon, "Could not update group icon"));
      }
  
      @Override
      public Uri getGroupThreadIcon(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getUriValueFromTableRow(RCS_GROUP_THREAD_URI,
                          GROUP_ICON_COLUMN,
                          RCS_THREAD_ID_COLUMN, rcsThreadId));
      }
  
      @Override
      public void setGroupThreadOwner(int rcsThreadId, int participantId, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(getGroupThreadUri(rcsThreadId),
                          OWNER_PARTICIPANT_COLUMN, participantId, "Could not set the group owner"));
      }
  
      @Override
      public int getGroupThreadOwner(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(RCS_GROUP_THREAD_URI,
                          OWNER_PARTICIPANT_COLUMN,
                          RCS_THREAD_ID_COLUMN, rcsThreadId));
      }
  
      @Override
      public void setGroupThreadConferenceUri(int rcsThreadId, Uri uri, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mMessageStoreUtil.updateValueOfProviderUri(getGroupThreadUri(rcsThreadId),
                          CONFERENCE_URI_COLUMN, uri, "Could not set the conference URI for group"));
      }
  
      @Override
      public Uri getGroupThreadConferenceUri(int rcsThreadId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getUriValueFromTableRow(RCS_GROUP_THREAD_URI,
                          CONFERENCE_URI_COLUMN, RCS_THREAD_ID_COLUMN, rcsThreadId));
      }
  
      @Override
      public void addParticipantToGroupThread(int rcsThreadId, int participantId,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> {
              ContentValues contentValues = new ContentValues(2);
              contentValues.put(RCS_THREAD_ID_COLUMN, rcsThreadId);
              contentValues.put(RCS_PARTICIPANT_ID_COLUMN, participantId);
  
              mContentResolver.insert(getAllParticipantsInThreadUri(rcsThreadId), contentValues);
          });
      }
  
      @Override
      public void removeParticipantFromGroupThread(int rcsThreadId, int participantId,
              String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mContentResolver.delete(getParticipantInThreadUri(rcsThreadId, participantId),
                          null,
                          null));
      }
  
      @Override
      public int addIncomingMessage(int rcsThreadId,
              RcsIncomingMessageCreationParams rcsIncomingMessageCreationParams,
              String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues contentValues = new ContentValues();
  
              contentValues.put(ARRIVAL_TIMESTAMP_COLUMN,
                      rcsIncomingMessageCreationParams.getArrivalTimestamp());
              contentValues.put(SEEN_TIMESTAMP_COLUMN,
                      rcsIncomingMessageCreationParams.getSeenTimestamp());
              contentValues.put(SENDER_PARTICIPANT_ID_COLUMN,
                      rcsIncomingMessageCreationParams.getSenderParticipantId());
  
              mMessageQueryHelper.createContentValuesForGenericMessage(contentValues, rcsThreadId,
                      rcsIncomingMessageCreationParams);
  
              return addMessage(rcsThreadId, true, contentValues);
          });
      }
  
      @Override
      public int addOutgoingMessage(int rcsThreadId,
              RcsOutgoingMessageCreationParams rcsOutgoingMessageCreationParameters,
              String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues contentValues = new ContentValues();
  
              mMessageQueryHelper.createContentValuesForGenericMessage(contentValues, rcsThreadId,
                      rcsOutgoingMessageCreationParameters);
  
              return addMessage(rcsThreadId, false, contentValues);
          });
      }
  
      private int addMessage(int rcsThreadId, boolean isIncoming, ContentValues contentValues)
              throws RemoteException {
          Uri uri = mContentResolver.insert(mMessageQueryHelper.getMessageInsertionUri(isIncoming),
                  contentValues);
  
          if (uri == null) {
              throw new RemoteException(
                      "Could not create message on thread, threadId: " + rcsThreadId);
          }
  
          return Integer.parseInt(uri.getLastPathSegment());
      }
  
      @Override
      public void deleteMessage(int messageId, boolean isIncoming, int rcsThreadId, boolean isGroup,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mContentResolver.delete(
                  mMessageQueryHelper.getMessageDeletionUri(messageId, isIncoming, rcsThreadId,
                          isGroup),
                  null, null));
      }
  
      @Override
      public void setMessageSubId(int messageId, boolean isIncoming, int subId,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming), SUB_ID_COLUMN,
                  subId, "Could not set subscription ID for message"));
      }
  
      @Override
      public int getMessageSubId(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(getMessageTableUri(isIncoming),
                          SUB_ID_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setMessageStatus(int messageId, boolean isIncoming, int status,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming), STATUS_COLUMN,
                  status, "Could not set the status for message"));
      }
  
      @Override
      public int getMessageStatus(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(getMessageTableUri(isIncoming),
                          STATUS_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setMessageOriginationTimestamp(int messageId, boolean isIncoming,
              long originationTimestamp, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  ORIGINATION_TIMESTAMP_COLUMN, originationTimestamp,
                  "Could not set the origination timestamp for message"));
      }
  
      @Override
      public long getMessageOriginationTimestamp(int messageId, boolean isIncoming,
              String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(getMessageTableUri(isIncoming),
                          ORIGINATION_TIMESTAMP_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setGlobalMessageIdForMessage(int messageId, boolean isIncoming, String globalId,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  GLOBAL_ID_COLUMN,
                  globalId, "Could not set the global ID for message"));
      }
  
      @Override
      public String getGlobalMessageIdForMessage(int messageId, boolean isIncoming,
              String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(getMessageTableUri(isIncoming),
                          GLOBAL_ID_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setMessageArrivalTimestamp(int messageId, boolean isIncoming,
              long arrivalTimestamp, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  ARRIVAL_TIMESTAMP_COLUMN, arrivalTimestamp,
                  "Could not update the arrival timestamp for message"));
      }
  
      @Override
      public long getMessageArrivalTimestamp(int messageId, boolean isIncoming,
              String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(getMessageTableUri(isIncoming),
                          ARRIVAL_TIMESTAMP_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setMessageSeenTimestamp(int messageId, boolean isIncoming,
              long notifiedTimestamp, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  SEEN_TIMESTAMP_COLUMN, notifiedTimestamp,
                  "Could not set the notified timestamp for message"));
      }
  
      @Override
      public long getMessageSeenTimestamp(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(getMessageTableUri(isIncoming),
                          SEEN_TIMESTAMP_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public int[] getMessageRecipients(int messageId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageQueryHelper.getDeliveryParticipantsForMessage(messageId));
      }
  
      @Override
      public long getOutgoingDeliveryDeliveredTimestamp(int messageId, int participantId,
              String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageQueryHelper.getLongValueFromDelivery(messageId, participantId,
                          DELIVERED_TIMESTAMP_COLUMN));
      }
  
      @Override
      public void setOutgoingDeliveryDeliveredTimestamp(int messageId, int participantId,
              long deliveredTimestamp, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageDeliveryUri(messageId, participantId),
                  DELIVERED_TIMESTAMP_COLUMN, deliveredTimestamp,
                  "Could not update the delivered timestamp for outgoing delivery"));
      }
  
      @Override
      public long getOutgoingDeliverySeenTimestamp(int messageId, int participantId,
              String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageQueryHelper.getLongValueFromDelivery(messageId, participantId,
                          SEEN_TIMESTAMP_COLUMN));
      }
  
      @Override
      public void setOutgoingDeliverySeenTimestamp(int messageId, int participantId,
              long seenTimestamp, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageDeliveryUri(messageId, participantId),
                  SEEN_TIMESTAMP_COLUMN, seenTimestamp,
                  "Could not update the seen timestamp for outgoing delivery"));
      }
  
      @Override
      public int getOutgoingDeliveryStatus(int messageId, int participantId, String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              // TODO - implement
              return 0;
          });
      }
  
      @Override
      public void setOutgoingDeliveryStatus(int messageId, int participantId, int status,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> {
              // TODO - implement
          });
      }
  
      @Override
      public void setTextForMessage(int messageId, boolean isIncoming, String text,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  MESSAGE_TEXT_COLUMN,
                  text, "Could not set the text for message"));
      }
  
      @Override
      public String getTextForMessage(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(getMessageTableUri(isIncoming),
                          MESSAGE_TEXT_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setLatitudeForMessage(int messageId, boolean isIncoming, double latitude,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming), LATITUDE_COLUMN,
                  latitude, "Could not update latitude for message"));
      }
  
      @Override
      public double getLatitudeForMessage(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getDoubleValueFromTableRow(getMessageTableUri(isIncoming),
                          LATITUDE_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void setLongitudeForMessage(int messageId, boolean isIncoming, double longitude,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getMessageUpdateUri(messageId, isIncoming),
                  LONGITUDE_COLUMN,
                  longitude, "Could not set longitude for message"));
      }
  
      @Override
      public double getLongitudeForMessage(int messageId, boolean isIncoming, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getDoubleValueFromTableRow(getMessageTableUri(isIncoming),
                          LONGITUDE_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public int[] getFileTransfersAttachedToMessage(int messageId, boolean isIncoming,
              String callingPackage) {
          return performReadOperation(callingPackage, () -> {
              // TODO - implement
              return new int[0];
          });
      }
  
      @Override
      public int getSenderParticipant(int messageId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(INCOMING_MESSAGE_URI,
                          SENDER_PARTICIPANT_ID_COLUMN, MESSAGE_ID_COLUMN, messageId));
      }
  
      @Override
      public void deleteFileTransfer(int partId, String callingPackage) {
          performWriteOperation(callingPackage,
                  () -> mContentResolver.delete(mMessageQueryHelper.getFileTransferUpdateUri(partId),
                          null,
                          null));
      }
  
      @Override
      public int storeFileTransfer(int messageId, boolean isIncoming,
              RcsFileTransferCreationParams fileTransferCreationParameters, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues contentValues = mMessageQueryHelper.getContentValuesForFileTransfer(
                      fileTransferCreationParameters);
              Uri uri = mContentResolver.insert(
                      mMessageQueryHelper.getFileTransferInsertionUri(messageId), contentValues);
  
              if (uri != null) {
                  return Integer.parseInt(uri.getLastPathSegment());
              }
  
              return TRANSACTION_FAILED;
          });
      }
  
      @Override
      public void setFileTransferSessionId(int partId, String sessionId, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), SESSION_ID_COLUMN,
                  sessionId,
                  "Could not set session ID for file transfer"));
      }
  
      @Override
      public String getFileTransferSessionId(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(FILE_TRANSFER_URI,
                          SESSION_ID_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferContentUri(int partId, Uri contentUri, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), CONTENT_URI_COLUMN,
                  contentUri, "Could not set content URI for file transfer"));
      }
  
      @Override
      public Uri getFileTransferContentUri(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getUriValueFromTableRow(FILE_TRANSFER_URI,
                          CONTENT_URI_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferContentType(int partId, String contentType, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), CONTENT_TYPE_COLUMN,
                  contentType, "Could not set content type for file transfer"));
      }
  
      @Override
      public String getFileTransferContentType(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(FILE_TRANSFER_URI,
                          CONTENT_TYPE_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferFileSize(int partId, long fileSize, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), FILE_SIZE_COLUMN,
                  fileSize,
                  "Could not set file size for file transfer"));
      }
  
      @Override
      public long getFileTransferFileSize(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(FILE_TRANSFER_URI,
                          FILE_SIZE_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferTransferOffset(int partId, long transferOffset,
              String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId),
                  SUCCESSFULLY_TRANSFERRED_BYTES,
                  transferOffset, "Could not set transfer offset for file transfer"));
      }
  
      @Override
      public long getFileTransferTransferOffset(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(FILE_TRANSFER_URI,
                          SUCCESSFULLY_TRANSFERRED_BYTES,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferStatus(int partId, int transferStatus, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), TRANSFER_STATUS_COLUMN,
                  transferStatus, "Could not set transfer status for file transfer"));
      }
  
      @Override
      public int getFileTransferStatus(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(FILE_TRANSFER_URI, STATUS_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferWidth(int partId, int width, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), WIDTH_COLUMN, width,
                  "Could not set width of file transfer"));
      }
  
      @Override
      public int getFileTransferWidth(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(FILE_TRANSFER_URI, WIDTH_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferHeight(int partId, int height, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), HEIGHT_COLUMN, height,
                  "Could not set height of file transfer"));
      }
  
      @Override
      public int getFileTransferHeight(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getIntValueFromTableRow(FILE_TRANSFER_URI, HEIGHT_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferLength(int partId, long length, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), DURATION_MILLIS_COLUMN,
                  length,
                  "Could not set length of file transfer"));
      }
  
      @Override
      public long getFileTransferLength(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getLongValueFromTableRow(FILE_TRANSFER_URI,
                          DURATION_MILLIS_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferPreviewUri(int partId, Uri uri, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), PREVIEW_URI_COLUMN, uri,
                  "Could not set preview URI of file transfer"));
      }
  
      @Override
      public Uri getFileTransferPreviewUri(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getUriValueFromTableRow(FILE_TRANSFER_URI,
                          DURATION_MILLIS_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public void setFileTransferPreviewType(int partId, String type, String callingPackage) {
          performWriteOperation(callingPackage, () -> mMessageStoreUtil.updateValueOfProviderUri(
                  mMessageQueryHelper.getFileTransferUpdateUri(partId), PREVIEW_TYPE_COLUMN, type,
                  "Could not set preview type of file transfer"));
      }
  
      @Override
      public String getFileTransferPreviewType(int partId, String callingPackage) {
          return performReadOperation(callingPackage,
                  () -> mMessageStoreUtil.getStringValueFromTableRow(FILE_TRANSFER_URI,
                          PREVIEW_TYPE_COLUMN,
                          FILE_TRANSFER_ID_COLUMN, partId));
      }
  
      @Override
      public int createGroupThreadNameChangedEvent(long timestamp, int threadId,
              int originationParticipantId, String newName, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues eventSpecificValues = new ContentValues();
              eventSpecificValues.put(NEW_NAME_COLUMN, newName);
  
              return mEventQueryHelper.createGroupThreadEvent(NAME_CHANGED_EVENT_TYPE, timestamp,
                      threadId, originationParticipantId, eventSpecificValues);
          });
      }
  
      @Override
      public int createGroupThreadIconChangedEvent(long timestamp, int threadId,
              int originationParticipantId, Uri newIcon, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues eventSpecificValues = new ContentValues();
              eventSpecificValues.put(NEW_ICON_URI_COLUMN,
                      newIcon == null ? null : newIcon.toString());
  
              return mEventQueryHelper.createGroupThreadEvent(ICON_CHANGED_EVENT_TYPE, timestamp,
                      threadId, originationParticipantId, eventSpecificValues);
          });
      }
  
      @Override
      public int createGroupThreadParticipantJoinedEvent(long timestamp, int threadId,
              int originationParticipantId, int participantId, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues eventSpecificValues = new ContentValues();
              eventSpecificValues.put(DESTINATION_PARTICIPANT_ID_COLUMN, participantId);
  
              return mEventQueryHelper.createGroupThreadEvent(PARTICIPANT_JOINED_EVENT_TYPE,
                      timestamp,
                      threadId, originationParticipantId, eventSpecificValues);
          });
      }
  
      @Override
      public int createGroupThreadParticipantLeftEvent(long timestamp, int threadId,
              int originationParticipantId, int participantId, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues eventSpecificValues = new ContentValues();
              eventSpecificValues.put(DESTINATION_PARTICIPANT_ID_COLUMN, participantId);
  
              return mEventQueryHelper.createGroupThreadEvent(PARTICIPANT_LEFT_EVENT_TYPE, timestamp,
                      threadId, originationParticipantId, eventSpecificValues);
          });
      }
  
      @Override
      public int createParticipantAliasChangedEvent(long timestamp, int participantId,
              String newAlias, String callingPackage) {
          return performCreateOperation(callingPackage, () -> {
              ContentValues contentValues = new ContentValues(4);
              contentValues.put(TIMESTAMP_COLUMN, timestamp);
              contentValues.put(SOURCE_PARTICIPANT_ID_COLUMN, participantId);
              contentValues.put(NEW_ALIAS_COLUMN, newAlias);
  
              Uri uri = mContentResolver.insert(
                      mEventQueryHelper.getParticipantEventInsertionUri(participantId),
                      contentValues);
  
              if (uri == null) {
                  throw new RemoteException(
                          "Could not create RcsParticipantAliasChangedEvent with participant id: "
                                  + participantId);
              }
  
              return Integer.parseInt(uri.getLastPathSegment());
          });
      }
  }
  