blob: f236f58f79f96c7f6c682ef12e9e10d5a095f6c5 [file] [log] [blame]
/*
* Copyright (C) 2014 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.telecomm;
import android.telecomm.PhoneAccount;
import android.telecomm.PhoneAccountHandle;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.telecomm.TelecommManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
* delegate for all the account handling methods on {@link TelecommManager} as implemented in
* {@link TelecommServiceImpl}, with the notable exception that {@link TelecommServiceImpl} is
* responsible for security checking to make sure that the caller has proper authority over
* the {@code ComponentName}s they are declaring in their {@code PhoneAccountHandle}s.
*
* TODO(santoscordon): Replace this implementation with a proper database stored in a Telecomm
* provider.
*/
final class PhoneAccountRegistrar {
private static final String TELECOMM_PREFERENCES = "telecomm_prefs";
private static final String PREFERENCE_PHONE_ACCOUNTS = "phone_accounts";
private final Context mContext;
private final State mState;
PhoneAccountRegistrar(Context context) {
mContext = context;
mState = readState();
}
public PhoneAccountHandle getDefaultOutgoingPhoneAccount() {
if (mState.defaultOutgoing != null) {
// Return the registered outgoing default iff it still exists (we keep a sticky
// default to survive account deletion and re-addition)
for (int i = 0; i < mState.accounts.size(); i++) {
if (mState.accounts.get(i).getAccountHandle().equals(mState.defaultOutgoing)) {
return mState.defaultOutgoing;
}
}
// At this point, there was a registered default but it has been deleted; proceed
// as though there were no default
}
List<PhoneAccountHandle> enabled = getEnabledPhoneAccounts();
switch (enabled.size()) {
case 0:
// There are no accounts, so there can be no default
return null;
case 1:
// There is only one account, which is by definition the default
return enabled.get(0);
default:
// There are multiple accounts with no selected default
return null;
}
}
public void setDefaultOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
if (accountHandle == null) {
// Asking to clear the default outgoing is a valid request
mState.defaultOutgoing = null;
} else {
boolean found = false;
for (PhoneAccount m : mState.accounts) {
if (Objects.equals(accountHandle, m.getAccountHandle())) {
found = true;
break;
}
}
if (!found) {
Log.w(this, "Trying to set nonexistent default outgoing phone accountHandle %s",
accountHandle);
return;
}
mState.defaultOutgoing = accountHandle;
}
write();
}
public void setSimCallManager(PhoneAccountHandle callManager) {
if (callManager != null) {
PhoneAccount callManagerAccount = getPhoneAccount(callManager);
if (callManagerAccount == null) {
Log.d(this, "setSimCallManager: Nonexistent call manager: %s", callManager);
return;
} else if (!has(callManagerAccount, PhoneAccount.CAPABILITY_SIM_CALL_MANAGER)) {
Log.d(this, "setSimCallManager: Not a call manager: %s", callManagerAccount);
return;
}
}
mState.simCallManager = callManager;
write();
}
public PhoneAccountHandle getSimCallManager() {
return mState.simCallManager;
}
public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
List<PhoneAccountHandle> accountHandles = new ArrayList<>();
for (PhoneAccount m : mState.accounts) {
accountHandles.add(m.getAccountHandle());
}
return accountHandles;
}
public List<PhoneAccount> getAllPhoneAccounts() {
return new ArrayList<>(mState.accounts);
}
// TODO: Rename systemwide to "getCallProviderPhoneAccounts"?
public List<PhoneAccountHandle> getEnabledPhoneAccounts() {
return getCallProviderAccountHandles();
}
public PhoneAccount getPhoneAccount(PhoneAccountHandle handle) {
for (PhoneAccount m : mState.accounts) {
if (Objects.equals(handle, m.getAccountHandle())) {
return m;
}
}
return null;
}
// TODO: Should we implement an artificial limit for # of accounts associated with a single
// ComponentName?
public void registerPhoneAccount(PhoneAccount account) {
account = hackFixBabelAccount(account);
mState.accounts.add(account);
// Search for duplicates and remove any that are found.
for (int i = 0; i < mState.accounts.size() - 1; i++) {
if (Objects.equals(
account.getAccountHandle(), mState.accounts.get(i).getAccountHandle())) {
// replace existing entry.
mState.accounts.remove(i);
break;
}
}
write();
}
// STOPSHIP: Hack to edit the account registered by Babel so it shows up properly
private PhoneAccount hackFixBabelAccount(PhoneAccount account) {
String pkg = account.getAccountHandle().getComponentName().getPackageName();
return "com.google.android.talk".equals(pkg)
? new PhoneAccount(
account.getAccountHandle(),
account.getHandle(),
account.getSubscriptionNumber(),
PhoneAccount.CAPABILITY_SIM_CALL_MANAGER,
account.getIconResId(),
account.getLabel(),
account.getShortDescription(),
account.isVideoCallingSupported())
: account;
}
public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
for (int i = 0; i < mState.accounts.size(); i++) {
if (Objects.equals(accountHandle, mState.accounts.get(i).getAccountHandle())) {
mState.accounts.remove(i);
break;
}
}
write();
}
public void clearAccounts(String packageName) {
for (int i = 0; i < mState.accounts.size(); i++) {
if (Objects.equals(
packageName,
mState.accounts.get(i).getAccountHandle()
.getComponentName().getPackageName())) {
mState.accounts.remove(i);
}
}
write();
}
////////////////////////////////////////////////////////////////////////////////////////////////
// TODO: Add a corresponding has(...) method to class PhoneAccount itself and remove this one
// Return true iff the given account has all the specified capability flags
static boolean has(PhoneAccount account, int capability) {
return (account.getCapabilities() & capability) == capability;
}
private List<PhoneAccountHandle> getCallProviderAccountHandles() {
List<PhoneAccountHandle> accountHandles = new ArrayList<>();
for (PhoneAccount m : mState.accounts) {
if (has(m, PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
accountHandles.add(m.getAccountHandle());
}
}
return accountHandles;
}
/**
* The state of this {@code PhoneAccountRegistrar}.
*/
private static class State {
/**
* The account selected by the user to be employed by default for making outgoing calls.
* If the user has not made such a selection, then this is null.
*/
public PhoneAccountHandle defaultOutgoing = null;
/**
* A {@code PhoneAccount} having {@link PhoneAccount#CAPABILITY_SIM_CALL_MANAGER} which
* manages and optimizes a user's PSTN SIM connections.
*/
public PhoneAccountHandle simCallManager;
/**
* The complete list of {@code PhoneAccount}s known to the Telecomm subsystem.
*/
public final List<PhoneAccount> accounts = new ArrayList<>();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// State management
//
private void write() {
writeState(mState);
}
private State readState() {
try {
String serialized = getPreferences().getString(PREFERENCE_PHONE_ACCOUNTS, null);
Log.v(this, "read() obtained serialized state: %s", serialized);
State state = serialized == null
? new State()
: deserializeState(serialized);
Log.v(this, "read() obtained state: %s", state);
return state;
} catch (JSONException e) {
Log.e(this, e, "read");
return new State();
}
}
private boolean writeState(State state) {
try {
Log.v(this, "write() writing state: %s", state);
String serialized = serializeState(state);
Log.v(this, "write() writing serialized state: %s", serialized);
boolean success = getPreferences()
.edit()
.putString(PREFERENCE_PHONE_ACCOUNTS, serialized)
.commit();
Log.v(this, "serialized state was written with success = %b", success);
return success;
} catch (JSONException e) {
Log.e(this, e, "write");
return false;
}
}
private SharedPreferences getPreferences() {
return mContext.getSharedPreferences(TELECOMM_PREFERENCES, Context.MODE_PRIVATE);
}
private String serializeState(State s) throws JSONException {
// TODO: If this is used in production, remove the indent (=> do not pretty print)
return sStateJson.toJson(s).toString(2);
}
private State deserializeState(String s) throws JSONException {
return sStateJson.fromJson(new JSONObject(new JSONTokener(s)));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//
// JSON serialization
//
private interface Json<T> {
JSONObject toJson(T o) throws JSONException;
T fromJson(JSONObject json) throws JSONException;
}
private static final Json<State> sStateJson =
new Json<State>() {
private static final String DEFAULT_OUTGOING = "default_outgoing";
private static final String SIM_CALL_MANAGER = "sim_call_manager";
private static final String ACCOUNTS = "accounts";
@Override
public JSONObject toJson(State o) throws JSONException {
JSONObject json = new JSONObject();
if (o.defaultOutgoing != null) {
json.put(DEFAULT_OUTGOING, sPhoneAccountHandleJson.toJson(o.defaultOutgoing));
}
if (o.simCallManager != null) {
json.put(SIM_CALL_MANAGER, sPhoneAccountHandleJson.toJson(o.simCallManager));
}
JSONArray accounts = new JSONArray();
for (PhoneAccount m : o.accounts) {
accounts.put(sPhoneAccountJson.toJson(m));
}
json.put(ACCOUNTS, accounts);
return json;
}
@Override
public State fromJson(JSONObject json) throws JSONException {
State s = new State();
if (json.has(DEFAULT_OUTGOING)) {
try {
s.defaultOutgoing = sPhoneAccountHandleJson.fromJson(
(JSONObject) json.get(DEFAULT_OUTGOING));
} catch (Exception e) {
Log.e(this, e, "Extracting PhoneAccountHandle");
}
}
if (json.has(SIM_CALL_MANAGER)) {
try {
s.simCallManager = sPhoneAccountHandleJson.fromJson(
(JSONObject) json.get(SIM_CALL_MANAGER));
} catch (Exception e) {
Log.e(this, e, "Extracting PhoneAccountHandle");
}
}
if (json.has(ACCOUNTS)) {
JSONArray accounts = (JSONArray) json.get(ACCOUNTS);
for (int i = 0; i < accounts.length(); i++) {
try {
s.accounts.add(sPhoneAccountJson.fromJson(
(JSONObject) accounts.get(i)));
} catch (Exception e) {
Log.e(this, e, "Extracting phone account");
}
}
}
return s;
}
};
private static final Json<PhoneAccount> sPhoneAccountJson =
new Json<PhoneAccount>() {
private static final String ACCOUNT = "account";
private static final String HANDLE = "handle";
private static final String SUBSCRIPTION_NUMBER = "subscription_number";
private static final String CAPABILITIES = "capabilities";
private static final String ICON_RES_ID = "icon_res_id";
private static final String LABEL = "label";
private static final String SHORT_DESCRIPTION = "short_description";
private static final String VIDEO_CALLING_SUPPORTED = "video_calling_supported";
@Override
public JSONObject toJson(PhoneAccount o) throws JSONException {
return new JSONObject()
.put(ACCOUNT, sPhoneAccountHandleJson.toJson(o.getAccountHandle()))
.put(HANDLE, o.getHandle().toString())
.put(SUBSCRIPTION_NUMBER, o.getSubscriptionNumber())
.put(CAPABILITIES, o.getCapabilities())
.put(ICON_RES_ID, o.getIconResId())
.put(LABEL, o.getLabel())
.put(SHORT_DESCRIPTION, o.getShortDescription())
.put(VIDEO_CALLING_SUPPORTED, (Boolean) o.isVideoCallingSupported());
}
@Override
public PhoneAccount fromJson(JSONObject json) throws JSONException {
return new PhoneAccount(
sPhoneAccountHandleJson.fromJson((JSONObject) json.get(ACCOUNT)),
Uri.parse((String) json.get(HANDLE)),
(String) json.get(SUBSCRIPTION_NUMBER),
(int) json.get(CAPABILITIES),
(int) json.get(ICON_RES_ID),
(String) json.get(LABEL),
(String) json.get(SHORT_DESCRIPTION),
(Boolean) json.get(VIDEO_CALLING_SUPPORTED));
}
};
private static final Json<PhoneAccountHandle> sPhoneAccountHandleJson =
new Json<PhoneAccountHandle>() {
private static final String COMPONENT_NAME = "component_name";
private static final String ID = "id";
@Override
public JSONObject toJson(PhoneAccountHandle o) throws JSONException {
return new JSONObject()
.put(COMPONENT_NAME, o.getComponentName().flattenToString())
.put(ID, o.getId());
}
@Override
public PhoneAccountHandle fromJson(JSONObject json) throws JSONException {
return new PhoneAccountHandle(
ComponentName.unflattenFromString((String) json.get(COMPONENT_NAME)),
(String) json.get(ID));
}
};
}