blob: b5314dd6d462ea1b12fff645a0588ad266f82a31 [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.content.Context;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import com.android.car.messenger.core.interfaces.AppFactory;
import com.android.car.messenger.core.models.UserAccount;
import com.android.car.messenger.impl.datamodels.UserAccountLiveData.UserAccountChangeList;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Get the UserAccount of the currently active remote bluetooth SIM(s). The records will be sorted
* by {@link SubscriptionInfo#getSimSlotIndex} then by {@link SubscriptionInfo#getSubscriptionId}.
*
* <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} or
* that the calling app has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges}).
* In the latter case, only records accessible to the calling app are returned.
*
* <p>Listens for changes via {@link OnSubscriptionsChangedListener} and the latest data can be
* observed and retrieved via {@link LiveData#getValue()}.
*
* <p>Sorts list of the currently {@link SubscriptionInfo} records available on the device
*
* <ul>
* <li>If the list is empty then there are no {@link SubscriptionInfo} records currently
* available.
* <li>if the list is non-empty the list is sorted by {@link SubscriptionInfo#getSimSlotIndex}
* then by {@link SubscriptionInfo#getSubscriptionId}.
* </ul>
*/
public class UserAccountLiveData extends LiveData<UserAccountChangeList> {
@NonNull private final SubscriptionManager mSubscriptionManager;
@NonNull
private final OnSubscriptionsChangedListener mOnChangeListener =
new OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
loadValue();
}
};
@Nullable private static UserAccountLiveData sInstance;
private UserAccountLiveData() {
Context context = AppFactory.get().getContext();
mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
mSubscriptionManager.addOnSubscriptionsChangedListener(mOnChangeListener);
loadValue();
}
/**
* Refresh the user accounts. Updates listeners if a change is found. Useful to call when
* something occurs that indicates a change in accounts, such as empty messages. This is useful
* as t here are occasions when the subscription on change listener is not called after a
* subscription is deleted.
*/
public void refresh() {
loadValue();
}
/** Gets the instance of {@link UserAccountLiveData} */
@NonNull
public static UserAccountLiveData getInstance() {
if (sInstance == null) {
sInstance = new UserAccountLiveData();
}
return sInstance;
}
private void loadValue() {
List<UserAccount> accounts =
getNullSafeSubscriptionInfoList().stream()
.map(
it -> {
int subscriptionId = it.getSubscriptionId();
String iccId = it.getIccId();
String displayName =
it.getDisplayName() != null
? it.getDisplayName().toString()
: "";
return new UserAccount(
subscriptionId, displayName, iccId, Instant.now());
})
.collect(Collectors.toList());
// get the removed accounts and added accounts.
Collection<UserAccount> prevUserAccounts = getValueOrEmpty().mAccounts;
Set<UserAccount> addedAccounts = getDifference(accounts, prevUserAccounts);
Set<UserAccount> removedAccounts = getDifference(prevUserAccounts, accounts);
if (addedAccounts.isEmpty() && removedAccounts.isEmpty()) {
// Return early if no new accounts were added or removed since last change list.
// However, if no account is found, post an empty changelist to allow
// the subscriber update the UI with "no account found or all accounts disconnected"
if (accounts.isEmpty()) {
postValue(new UserAccountChangeList());
}
return;
}
UserAccountChangeList newAccountChangeList = new UserAccountChangeList();
newAccountChangeList.mAccounts = accounts;
newAccountChangeList.mAddedAccounts = addedAccounts;
newAccountChangeList.mRemovedAccounts = removedAccounts;
postValue(newAccountChangeList);
}
/**
* Returns User Account with the given iccId
*
* @param iccId Maps to the {@link SubscriptionInfo#getIccId()}
*/
@Nullable
public static UserAccount getUserAccount(@NonNull String iccId) {
Collection<UserAccount> userAccounts = getValueOrEmpty().getAccounts();
for (UserAccount account : userAccounts) {
if (iccId.equals(account.getIccId())) {
return account;
}
}
return null;
}
/**
* Returns a list that contains a difference between the two lists - firstList - secondList =
* result This essentially points out which items or changes are not present in firstList.
*/
@NonNull
public static Set<UserAccount> getDifference(
@NonNull Collection<UserAccount> firstList,
@NonNull Collection<UserAccount> secondList) {
return firstList.stream()
.filter(it -> secondList.stream().noneMatch(item -> item.getId() == it.getId()))
.collect(Collectors.toSet());
}
/** A list of {@link UserAccount} with information on what changed */
public static class UserAccountChangeList {
@NonNull private Collection<UserAccount> mAccounts = new ArrayList<>();
@NonNull private Collection<UserAccount> mRemovedAccounts = new ArrayList<>();
@NonNull private Collection<UserAccount> mAddedAccounts = new ArrayList<>();
/** Get all user accounts */
@NonNull
public Collection<UserAccount> getAccounts() {
return mAccounts;
}
/** Get removed accounts for this change list */
@NonNull
public Stream<UserAccount> getRemovedAccounts() {
return mRemovedAccounts.stream();
}
/** Gets added accounts for this change list */
@NonNull
public Stream<UserAccount> getAddedAccounts() {
return mAddedAccounts.stream();
}
}
/** Returns the value or empty changelist, if value is null */
@NonNull
private static UserAccountChangeList getValueOrEmpty() {
UserAccountChangeList value = sInstance != null ? sInstance.getValue() : null;
if (value == null) {
value = new UserAccountChangeList();
}
return value;
}
/** Returns null safe subscription info list */
@NonNull
private List<SubscriptionInfo> getNullSafeSubscriptionInfoList() {
List<SubscriptionInfo> subscriptionInfos =
mSubscriptionManager.getActiveSubscriptionInfoList();
if (subscriptionInfos == null) {
return new ArrayList<>();
}
// The last added subscription is more likely the last device connection made
// and more likely relevant to the user.
// Reverse the subscription list to prioritize the last connected device.
Collections.reverse(subscriptionInfos);
return subscriptionInfos;
}
}