blob: f7254185c6d8b30e871b605127bc23fb1ca5f1ae [file] [log] [blame]
/*
* Copyright (C) 2021 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.car.messenger.impl.datamodels;
import static com.android.car.messenger.impl.datamodels.util.ConversationFetchUtil.fetchConversation;
import static com.android.car.messenger.impl.datamodels.util.CursorUtils.DEFAULT_SORT_ORDER;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorIndexOutOfBoundsException;
import android.net.Uri;
import android.provider.Telephony;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.car.messenger.common.Conversation;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.core.shared.MessageConstants;
import com.android.car.messenger.core.util.CarStateListener;
import com.android.car.messenger.core.util.ConversationUtil;
import com.android.car.messenger.core.util.L;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
/**
* Publishes a stream of {@link Conversation} with unread messages that was received on the user
* device after the car's connection to the{@link UserAccount}.
*/
public class NewMessageLiveData extends ContentProviderLiveData<Conversation> {
@NonNull
private final UserAccountLiveData mUserAccountLiveData = UserAccountLiveData.getInstance();
@NonNull private Collection<UserAccount> mUserAccounts = new ArrayList<>();
@NonNull private final HashMap<Integer, Instant> mOffsetMap = new HashMap<>();
@NonNull
private static final String MESSAGE_QUERY =
Telephony.TextBasedSmsColumns.DATE
+ " > %d AND "
+ Telephony.TextBasedSmsColumns.SUBSCRIPTION_ID
+ " = %d";
@NonNull
private final CarStateListener mCarStateListener = AppFactory.get().getCarStateListener();
NewMessageLiveData() {
super(Telephony.Sms.CONTENT_URI, Telephony.Mms.CONTENT_URI, Telephony.MmsSms.CONTENT_URI);
}
@Override
protected void onActive() {
super.onActive();
addSource(
mUserAccountLiveData,
it -> {
mUserAccounts = it.getAccounts();
it.getRemovedAccounts()
.forEach(userAccount -> mOffsetMap.remove(userAccount.getId()));
});
if (getValue() == null) {
onDataChange();
}
}
@Override
protected void onInactive() {
super.onInactive();
removeSource(mUserAccountLiveData);
mUserAccounts.clear();
mOffsetMap.clear();
}
@Override
public void onDataChange() {
for (UserAccount userAccount : mUserAccounts) {
if (hasProjectionInForeground(userAccount)) {
continue;
}
Instant offset =
Objects.requireNonNull(
mOffsetMap.getOrDefault(
userAccount.getId(), userAccount.getConnectionTime()));
Cursor mmsCursor = getMmsCursor(userAccount, offset);
boolean foundNewMms = postNewMessageIfFound(mmsCursor, userAccount);
Cursor smsCursor = getSmsCursor(userAccount, offset);
boolean foundNewSms = postNewMessageIfFound(smsCursor, userAccount);
if (foundNewMms || foundNewSms) {
// onDataChange is called per one message insert,
// so once a new message is found we can exit early
break;
}
}
}
/** Post a new message if one is found, and returns true if so, false otherwise */
private boolean postNewMessageIfFound(
@Nullable Cursor cursor, @NonNull UserAccount userAccount) {
if (cursor == null || !cursor.moveToFirst()) {
return false;
}
String conversationId =
cursor.getString(cursor.getColumnIndex(Telephony.TextBasedSmsColumns.THREAD_ID));
Conversation conversation;
try {
conversation = fetchConversation(conversationId);
} catch (CursorIndexOutOfBoundsException e) {
L.w("Error occurred fetching conversation Id " + conversationId);
return false;
}
conversation.getExtras().putInt(MessageConstants.EXTRA_ACCOUNT_ID, userAccount.getId());
Instant offset =
Instant.ofEpochMilli(ConversationUtil.getConversationTimestamp(conversation));
mOffsetMap.put(userAccount.getId(), offset);
postValue(conversation);
return true;
}
/** Get the last message cursor, taking into account the last message posted */
@Nullable
private Cursor getMmsCursor(@NonNull UserAccount userAccount, @NonNull Instant offset) {
return getCursor(Telephony.Mms.Inbox.CONTENT_URI, userAccount, offset.getEpochSecond());
}
/** Get the last message cursor, taking into account the last message posted */
@Nullable
private Cursor getSmsCursor(@NonNull UserAccount userAccount, @NonNull Instant offset) {
return getCursor(Telephony.Sms.Inbox.CONTENT_URI, userAccount, offset.toEpochMilli());
}
/** Get the last message cursor, taking into account an offset and subscription id */
@Nullable
private Cursor getCursor(Uri uri, @NonNull UserAccount userAccount, long offset) {
Context context = AppFactory.get().getContext();
String query = String.format(Locale.ENGLISH, MESSAGE_QUERY, offset, userAccount.getId());
return context.getContentResolver()
.query(
uri,
new String[] {Telephony.TextBasedSmsColumns.THREAD_ID},
query,
/* selectionArgs= */ null,
DEFAULT_SORT_ORDER + " LIMIT 1");
}
private boolean hasProjectionInForeground(@NonNull UserAccount userAccount) {
return mCarStateListener.isProjectionInActiveForeground(userAccount.getIccId());
}
}