Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
Mike Dodd
2015-08-12 08:58:28 -07:00
parent 8b3e2b9c1b
commit 461a34b466
1645 changed files with 186271 additions and 0 deletions
@@ -0,0 +1,374 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.Telephony;
import android.text.TextUtils;
import android.util.Log;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/*
* Database helper class for looking up APNs. This database has a single table
* which stores the APNs that are initially created from an xml file.
*/
public class ApnDatabase extends SQLiteOpenHelper {
private static final int DB_VERSION = 3; // added sub_id columns
private static final String TAG = LogUtil.BUGLE_TAG;
private static final boolean DEBUG = false;
private static Context sContext;
private static ApnDatabase sApnDatabase;
private static final String APN_DATABASE_NAME = "apn.db";
/** table for carrier APN's */
public static final String APN_TABLE = "apn";
// APN table
private static final String APN_TABLE_SQL =
"CREATE TABLE " + APN_TABLE +
"(_id INTEGER PRIMARY KEY," +
Telephony.Carriers.NAME + " TEXT," +
Telephony.Carriers.NUMERIC + " TEXT," +
Telephony.Carriers.MCC + " TEXT," +
Telephony.Carriers.MNC + " TEXT," +
Telephony.Carriers.APN + " TEXT," +
Telephony.Carriers.USER + " TEXT," +
Telephony.Carriers.SERVER + " TEXT," +
Telephony.Carriers.PASSWORD + " TEXT," +
Telephony.Carriers.PROXY + " TEXT," +
Telephony.Carriers.PORT + " TEXT," +
Telephony.Carriers.MMSPROXY + " TEXT," +
Telephony.Carriers.MMSPORT + " TEXT," +
Telephony.Carriers.MMSC + " TEXT," +
Telephony.Carriers.AUTH_TYPE + " INTEGER," +
Telephony.Carriers.TYPE + " TEXT," +
Telephony.Carriers.CURRENT + " INTEGER," +
Telephony.Carriers.PROTOCOL + " TEXT," +
Telephony.Carriers.ROAMING_PROTOCOL + " TEXT," +
Telephony.Carriers.CARRIER_ENABLED + " BOOLEAN," +
Telephony.Carriers.BEARER + " INTEGER," +
Telephony.Carriers.MVNO_TYPE + " TEXT," +
Telephony.Carriers.MVNO_MATCH_DATA + " TEXT," +
Telephony.Carriers.SUBSCRIPTION_ID + " INTEGER DEFAULT " +
ParticipantData.DEFAULT_SELF_SUB_ID + ");";
public static final String[] APN_PROJECTION = {
Telephony.Carriers.TYPE, // 0
Telephony.Carriers.MMSC, // 1
Telephony.Carriers.MMSPROXY, // 2
Telephony.Carriers.MMSPORT, // 3
Telephony.Carriers._ID, // 4
Telephony.Carriers.CURRENT, // 5
Telephony.Carriers.NUMERIC, // 6
Telephony.Carriers.NAME, // 7
Telephony.Carriers.MCC, // 8
Telephony.Carriers.MNC, // 9
Telephony.Carriers.APN, // 10
Telephony.Carriers.SUBSCRIPTION_ID // 11
};
public static final int COLUMN_TYPE = 0;
public static final int COLUMN_MMSC = 1;
public static final int COLUMN_MMSPROXY = 2;
public static final int COLUMN_MMSPORT = 3;
public static final int COLUMN_ID = 4;
public static final int COLUMN_CURRENT = 5;
public static final int COLUMN_NUMERIC = 6;
public static final int COLUMN_NAME = 7;
public static final int COLUMN_MCC = 8;
public static final int COLUMN_MNC = 9;
public static final int COLUMN_APN = 10;
public static final int COLUMN_SUB_ID = 11;
public static final String[] APN_FULL_PROJECTION = {
Telephony.Carriers.NAME,
Telephony.Carriers.MCC,
Telephony.Carriers.MNC,
Telephony.Carriers.APN,
Telephony.Carriers.USER,
Telephony.Carriers.SERVER,
Telephony.Carriers.PASSWORD,
Telephony.Carriers.PROXY,
Telephony.Carriers.PORT,
Telephony.Carriers.MMSC,
Telephony.Carriers.MMSPROXY,
Telephony.Carriers.MMSPORT,
Telephony.Carriers.AUTH_TYPE,
Telephony.Carriers.TYPE,
Telephony.Carriers.PROTOCOL,
Telephony.Carriers.ROAMING_PROTOCOL,
Telephony.Carriers.CARRIER_ENABLED,
Telephony.Carriers.BEARER,
Telephony.Carriers.MVNO_TYPE,
Telephony.Carriers.MVNO_MATCH_DATA,
Telephony.Carriers.CURRENT,
Telephony.Carriers.SUBSCRIPTION_ID,
};
private static final String CURRENT_SELECTION = Telephony.Carriers.CURRENT + " NOT NULL";
/**
* ApnDatabase is initialized asynchronously from the application.onCreate
* To ensure that it works in a testing environment it needs to never access the factory context
*/
public static void initializeAppContext(final Context context) {
sContext = context;
}
private ApnDatabase() {
super(sContext, APN_DATABASE_NAME, null, DB_VERSION);
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase constructor");
}
}
public static ApnDatabase getApnDatabase() {
if (sApnDatabase == null) {
sApnDatabase = new ApnDatabase();
}
return sApnDatabase;
}
public static boolean doesDatabaseExist() {
final File dbFile = sContext.getDatabasePath(APN_DATABASE_NAME);
return dbFile.exists();
}
@Override
public void onCreate(final SQLiteDatabase db) {
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase onCreate");
}
// Build the table using defaults (apn info bundled with the app)
rebuildTables(db);
}
/**
* Get a copy of user changes in the old table
*
* @return The list of user changed apns
*/
public static List<ContentValues> loadUserDataFromOldTable(final SQLiteDatabase db) {
Cursor cursor = null;
try {
cursor = db.query(APN_TABLE,
APN_FULL_PROJECTION, CURRENT_SELECTION,
null/*selectionArgs*/,
null/*groupBy*/, null/*having*/, null/*orderBy*/);
if (cursor != null) {
final List<ContentValues> result = Lists.newArrayList();
while (cursor.moveToNext()) {
final ContentValues row = cursorToValues(cursor);
if (row != null) {
result.add(row);
}
}
return result;
}
} catch (final SQLiteException e) {
LogUtil.w(TAG, "ApnDatabase.loadUserDataFromOldTable: no old user data: " + e, e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
private static final String[] ID_PROJECTION = new String[]{Telephony.Carriers._ID};
private static final String ID_SELECTION = Telephony.Carriers._ID + "=?";
/**
* Store use changes of old table into the new apn table
*
* @param data The user changes
*/
public static void saveUserDataFromOldTable(
final SQLiteDatabase db, final List<ContentValues> data) {
if (data == null || data.size() < 1) {
return;
}
for (final ContentValues row : data) {
// Build query from the row data. It is an exact match, column by column,
// except the CURRENT column
final StringBuilder selectionBuilder = new StringBuilder();
final ArrayList<String> selectionArgs = Lists.newArrayList();
for (final String key : row.keySet()) {
if (!Telephony.Carriers.CURRENT.equals(key)) {
if (selectionBuilder.length() > 0) {
selectionBuilder.append(" AND ");
}
final String value = row.getAsString(key);
if (TextUtils.isEmpty(value)) {
selectionBuilder.append(key).append(" IS NULL");
} else {
selectionBuilder.append(key).append("=?");
selectionArgs.add(value);
}
}
}
Cursor cursor = null;
try {
cursor = db.query(APN_TABLE,
ID_PROJECTION,
selectionBuilder.toString(),
selectionArgs.toArray(new String[0]),
null/*groupBy*/, null/*having*/, null/*orderBy*/);
if (cursor != null && cursor.moveToFirst()) {
db.update(APN_TABLE, row, ID_SELECTION, new String[]{cursor.getString(0)});
} else {
// User APN does not exist, insert into the new table
row.put(Telephony.Carriers.NUMERIC,
PhoneUtils.canonicalizeMccMnc(
row.getAsString(Telephony.Carriers.MCC),
row.getAsString(Telephony.Carriers.MNC))
);
db.insert(APN_TABLE, null/*nullColumnHack*/, row);
}
} catch (final SQLiteException e) {
LogUtil.e(TAG, "ApnDatabase.saveUserDataFromOldTable: query error " + e, e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
// Convert Cursor to ContentValues
private static ContentValues cursorToValues(final Cursor cursor) {
final int columnCount = cursor.getColumnCount();
if (columnCount > 0) {
final ContentValues result = new ContentValues();
for (int i = 0; i < columnCount; i++) {
final String name = cursor.getColumnName(i);
final String value = cursor.getString(i);
result.put(name, value);
}
return result;
}
return null;
}
@Override
public void onOpen(final SQLiteDatabase db) {
super.onOpen(db);
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase onOpen");
}
}
@Override
public void close() {
super.close();
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase close");
}
}
private void rebuildTables(final SQLiteDatabase db) {
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase rebuildTables");
}
db.execSQL("DROP TABLE IF EXISTS " + APN_TABLE + ";");
db.execSQL(APN_TABLE_SQL);
loadApnTable(db);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase onUpgrade");
}
rebuildTables(db);
}
@Override
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
if (DEBUG) {
LogUtil.d(TAG, "ApnDatabase onDowngrade");
}
rebuildTables(db);
}
/**
* Load APN table from app resources
*/
private static void loadApnTable(final SQLiteDatabase db) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ApnDatabase loadApnTable");
}
final Resources r = sContext.getResources();
final XmlResourceParser parser = r.getXml(R.xml.apns);
final ApnsXmlProcessor processor = ApnsXmlProcessor.get(parser);
processor.setApnHandler(new ApnsXmlProcessor.ApnHandler() {
@Override
public void process(final ContentValues apnValues) {
db.insert(APN_TABLE, null/*nullColumnHack*/, apnValues);
}
});
try {
processor.process();
} catch (final Exception e) {
Log.e(TAG, "Got exception while loading APN database.", e);
} finally {
parser.close();
}
}
public static void forceBuildAndLoadApnTables() {
final SQLiteDatabase db = getApnDatabase().getWritableDatabase();
db.execSQL("DROP TABLE IF EXISTS " + APN_TABLE);
// Table(s) always need for JB MR1 for APN support for MMS because JB MR1 throws
// a SecurityException when trying to access the carriers table (which holds the
// APNs). Some JB MR2 devices also throw the security exception, so we're building
// the table for JB MR2, too.
db.execSQL(APN_TABLE_SQL);
loadApnTable(db);
}
/**
* Clear all tables
*/
public static void clearTables() {
final SQLiteDatabase db = getApnDatabase().getWritableDatabase();
db.execSQL("DROP TABLE IF EXISTS " + APN_TABLE);
db.execSQL(APN_TABLE_SQL);
}
}
@@ -0,0 +1,329 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.ContentValues;
import android.provider.Telephony;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.google.common.collect.Maps;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Map;
/*
* XML processor for the following files:
* 1. res/xml/apns.xml
* 2. res/xml/mms_config.xml (or related overlay files)
*/
class ApnsXmlProcessor {
public interface ApnHandler {
public void process(ContentValues apnValues);
}
public interface MmsConfigHandler {
public void process(String mccMnc, String key, String value, String type);
}
private static final String TAG = LogUtil.BUGLE_TAG;
private static final Map<String, String> APN_ATTRIBUTE_MAP = Maps.newHashMap();
static {
APN_ATTRIBUTE_MAP.put("mcc", Telephony.Carriers.MCC);
APN_ATTRIBUTE_MAP.put("mnc", Telephony.Carriers.MNC);
APN_ATTRIBUTE_MAP.put("carrier", Telephony.Carriers.NAME);
APN_ATTRIBUTE_MAP.put("apn", Telephony.Carriers.APN);
APN_ATTRIBUTE_MAP.put("mmsc", Telephony.Carriers.MMSC);
APN_ATTRIBUTE_MAP.put("mmsproxy", Telephony.Carriers.MMSPROXY);
APN_ATTRIBUTE_MAP.put("mmsport", Telephony.Carriers.MMSPORT);
APN_ATTRIBUTE_MAP.put("type", Telephony.Carriers.TYPE);
APN_ATTRIBUTE_MAP.put("user", Telephony.Carriers.USER);
APN_ATTRIBUTE_MAP.put("password", Telephony.Carriers.PASSWORD);
APN_ATTRIBUTE_MAP.put("authtype", Telephony.Carriers.AUTH_TYPE);
APN_ATTRIBUTE_MAP.put("mvno_match_data", Telephony.Carriers.MVNO_MATCH_DATA);
APN_ATTRIBUTE_MAP.put("mvno_type", Telephony.Carriers.MVNO_TYPE);
APN_ATTRIBUTE_MAP.put("protocol", Telephony.Carriers.PROTOCOL);
APN_ATTRIBUTE_MAP.put("bearer", Telephony.Carriers.BEARER);
APN_ATTRIBUTE_MAP.put("server", Telephony.Carriers.SERVER);
APN_ATTRIBUTE_MAP.put("roaming_protocol", Telephony.Carriers.ROAMING_PROTOCOL);
APN_ATTRIBUTE_MAP.put("proxy", Telephony.Carriers.PROXY);
APN_ATTRIBUTE_MAP.put("port", Telephony.Carriers.PORT);
APN_ATTRIBUTE_MAP.put("carrier_enabled", Telephony.Carriers.CARRIER_ENABLED);
}
private static final String TAG_APNS = "apns";
private static final String TAG_APN = "apn";
private static final String TAG_MMS_CONFIG = "mms_config";
// Handler to process one apn
private ApnHandler mApnHandler;
// Handler to process one mms_config key/value pair
private MmsConfigHandler mMmsConfigHandler;
private final StringBuilder mLogStringBuilder = new StringBuilder();
private final XmlPullParser mInputParser;
private ApnsXmlProcessor(XmlPullParser parser) {
mInputParser = parser;
mApnHandler = null;
mMmsConfigHandler = null;
}
public static ApnsXmlProcessor get(XmlPullParser parser) {
Assert.notNull(parser);
return new ApnsXmlProcessor(parser);
}
public ApnsXmlProcessor setApnHandler(ApnHandler handler) {
mApnHandler = handler;
return this;
}
public ApnsXmlProcessor setMmsConfigHandler(MmsConfigHandler handler) {
mMmsConfigHandler = handler;
return this;
}
/**
* Move XML parser forward to next event type or the end of doc
*
* @param eventType
* @return The final event type we meet
* @throws XmlPullParserException
* @throws IOException
*/
private int advanceToNextEvent(int eventType) throws XmlPullParserException, IOException {
for (;;) {
int nextEvent = mInputParser.next();
if (nextEvent == eventType
|| nextEvent == XmlPullParser.END_DOCUMENT) {
return nextEvent;
}
}
}
public void process() {
try {
// Find the first element
if (advanceToNextEvent(XmlPullParser.START_TAG) != XmlPullParser.START_TAG) {
throw new XmlPullParserException("ApnsXmlProcessor: expecting start tag @"
+ xmlParserDebugContext());
}
// A single ContentValues object for holding the parsing result of
// an apn element
final ContentValues values = new ContentValues();
String tagName = mInputParser.getName();
// Top level tag can be "apns" (apns.xml)
// or "mms_config" (mms_config.xml)
if (TAG_APNS.equals(tagName)) {
// For "apns", there could be "apn" or both "apn" and "mms_config"
for (;;) {
if (advanceToNextEvent(XmlPullParser.START_TAG) != XmlPullParser.START_TAG) {
break;
}
tagName = mInputParser.getName();
if (TAG_APN.equals(tagName)) {
processApn(values);
} else if (TAG_MMS_CONFIG.equals(tagName)) {
processMmsConfig();
}
}
} else if (TAG_MMS_CONFIG.equals(tagName)) {
// mms_config.xml resource
processMmsConfig();
}
} catch (IOException e) {
LogUtil.e(TAG, "ApnsXmlProcessor: I/O failure " + e, e);
} catch (XmlPullParserException e) {
LogUtil.e(TAG, "ApnsXmlProcessor: parsing failure " + e, e);
}
}
private Integer parseInt(String text, Integer defaultValue, String logHint) {
Integer value = defaultValue;
try {
value = Integer.parseInt(text);
} catch (Exception e) {
LogUtil.e(TAG,
"Invalid value " + text + "for" + logHint + " @" + xmlParserDebugContext());
}
return value;
}
private Boolean parseBoolean(String text, Boolean defaultValue, String logHint) {
Boolean value = defaultValue;
try {
value = Boolean.parseBoolean(text);
} catch (Exception e) {
LogUtil.e(TAG,
"Invalid value " + text + "for" + logHint + " @" + xmlParserDebugContext());
}
return value;
}
private static String xmlParserEventString(int event) {
switch (event) {
case XmlPullParser.START_DOCUMENT: return "START_DOCUMENT";
case XmlPullParser.END_DOCUMENT: return "END_DOCUMENT";
case XmlPullParser.START_TAG: return "START_TAG";
case XmlPullParser.END_TAG: return "END_TAG";
case XmlPullParser.TEXT: return "TEXT";
}
return Integer.toString(event);
}
/**
* @return The debugging information of the parser's current position
*/
private String xmlParserDebugContext() {
mLogStringBuilder.setLength(0);
if (mInputParser != null) {
try {
final int eventType = mInputParser.getEventType();
mLogStringBuilder.append(xmlParserEventString(eventType));
if (eventType == XmlPullParser.START_TAG
|| eventType == XmlPullParser.END_TAG
|| eventType == XmlPullParser.TEXT) {
mLogStringBuilder.append('<').append(mInputParser.getName());
for (int i = 0; i < mInputParser.getAttributeCount(); i++) {
mLogStringBuilder.append(' ')
.append(mInputParser.getAttributeName(i))
.append('=')
.append(mInputParser.getAttributeValue(i));
}
mLogStringBuilder.append("/>");
}
return mLogStringBuilder.toString();
} catch (XmlPullParserException e) {
LogUtil.e(TAG, "xmlParserDebugContext: " + e, e);
}
}
return "Unknown";
}
/**
* Process one apn
*
* @param apnValues Where we store the parsed apn
* @throws IOException
* @throws XmlPullParserException
*/
private void processApn(ContentValues apnValues) throws IOException, XmlPullParserException {
Assert.notNull(apnValues);
apnValues.clear();
// Collect all the attributes
for (int i = 0; i < mInputParser.getAttributeCount(); i++) {
final String key = APN_ATTRIBUTE_MAP.get(mInputParser.getAttributeName(i));
if (key != null) {
apnValues.put(key, mInputParser.getAttributeValue(i));
}
}
// Set numeric to be canonicalized mcc/mnc like "310120", always 6 digits
final String canonicalMccMnc = PhoneUtils.canonicalizeMccMnc(
apnValues.getAsString(Telephony.Carriers.MCC),
apnValues.getAsString(Telephony.Carriers.MNC));
apnValues.put(Telephony.Carriers.NUMERIC, canonicalMccMnc);
// Some of the values should not be string type, converting them to desired types
final String authType = apnValues.getAsString(Telephony.Carriers.AUTH_TYPE);
if (authType != null) {
apnValues.put(Telephony.Carriers.AUTH_TYPE, parseInt(authType, -1, "apn authtype"));
}
final String carrierEnabled = apnValues.getAsString(Telephony.Carriers.CARRIER_ENABLED);
if (carrierEnabled != null) {
apnValues.put(Telephony.Carriers.CARRIER_ENABLED,
parseBoolean(carrierEnabled, null, "apn carrierEnabled"));
}
final String bearer = apnValues.getAsString(Telephony.Carriers.BEARER);
if (bearer != null) {
apnValues.put(Telephony.Carriers.BEARER, parseInt(bearer, 0, "apn bearer"));
}
// We are at the end tag
if (mInputParser.next() != XmlPullParser.END_TAG) {
throw new XmlPullParserException("Apn: expecting end tag @"
+ xmlParserDebugContext());
}
// We are done parsing one APN, call the handler
if (mApnHandler != null) {
mApnHandler.process(apnValues);
}
}
/**
* Process one mms_config.
*
* @throws IOException
* @throws XmlPullParserException
*/
private void processMmsConfig()
throws IOException, XmlPullParserException {
// Get the mcc and mnc attributes
final String canonicalMccMnc = PhoneUtils.canonicalizeMccMnc(
mInputParser.getAttributeValue(null, "mcc"),
mInputParser.getAttributeValue(null, "mnc"));
// We are at the start tag
for (;;) {
int nextEvent;
// Skipping spaces
while ((nextEvent = mInputParser.next()) == XmlPullParser.TEXT) {
}
if (nextEvent == XmlPullParser.START_TAG) {
// Parse one mms config key/value
processMmsConfigKeyValue(canonicalMccMnc);
} else if (nextEvent == XmlPullParser.END_TAG) {
break;
} else {
throw new XmlPullParserException("MmsConfig: expecting start or end tag @"
+ xmlParserDebugContext());
}
}
}
/**
* Process one mms_config key/value pair
*
* @param mccMnc The mcc and mnc of this mms_config
* @throws IOException
* @throws XmlPullParserException
*/
private void processMmsConfigKeyValue(String mccMnc)
throws IOException, XmlPullParserException {
final String key = mInputParser.getAttributeValue(null, "name");
// We are at the start tag, the name of the tag is the type
// e.g. <int name="key">value</int>
final String type = mInputParser.getName();
int nextEvent = mInputParser.next();
String value = null;
if (nextEvent == XmlPullParser.TEXT) {
value = mInputParser.getText();
nextEvent = mInputParser.next();
}
if (nextEvent != XmlPullParser.END_TAG) {
throw new XmlPullParserException("ApnsXmlProcessor: expecting end tag @"
+ xmlParserDebugContext());
}
// We are done parsing one mms_config key/value, call the handler
if (mMmsConfigHandler != null) {
mMmsConfigHandler.process(mccMnc, key, value, type);
}
}
}
@@ -0,0 +1,646 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.Telephony;
import android.support.v7.mms.ApnSettingsLoader;
import android.support.v7.mms.MmsManager;
import android.text.TextUtils;
import android.util.SparseArray;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* APN loader for default SMS SIM
*
* This loader tries to load APNs from 3 sources in order:
* 1. Gservices setting
* 2. System APN table
* 3. Local APN table
*/
public class BugleApnSettingsLoader implements ApnSettingsLoader {
/**
* The base implementation of an APN
*/
private static class BaseApn implements Apn {
/**
* Create a base APN from parameters
*
* @param typesIn the APN type field
* @param mmscIn the APN mmsc field
* @param proxyIn the APN mmsproxy field
* @param portIn the APN mmsport field
* @return an instance of base APN, or null if any of the parameter is invalid
*/
public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn,
final String portIn) {
if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) {
return null;
}
String mmsc = trimWithNullCheck(mmscIn);
if (TextUtils.isEmpty(mmsc)) {
return null;
}
mmsc = trimV4AddrZeros(mmsc);
try {
new URI(mmsc);
} catch (final URISyntaxException e) {
return null;
}
String mmsProxy = trimWithNullCheck(proxyIn);
int mmsProxyPort = 80;
if (!TextUtils.isEmpty(mmsProxy)) {
mmsProxy = trimV4AddrZeros(mmsProxy);
final String portString = trimWithNullCheck(portIn);
if (portString != null) {
try {
mmsProxyPort = Integer.parseInt(portString);
} catch (final NumberFormatException e) {
// Ignore, just use 80 to try
}
}
}
return new BaseApn(mmsc, mmsProxy, mmsProxyPort);
}
private final String mMmsc;
private final String mMmsProxy;
private final int mMmsProxyPort;
public BaseApn(final String mmsc, final String proxy, final int port) {
mMmsc = mmsc;
mMmsProxy = proxy;
mMmsProxyPort = port;
}
@Override
public String getMmsc() {
return mMmsc;
}
@Override
public String getMmsProxy() {
return mMmsProxy;
}
@Override
public int getMmsProxyPort() {
return mMmsProxyPort;
}
@Override
public void setSuccess() {
// Do nothing
}
public boolean equals(final BaseApn other) {
return TextUtils.equals(mMmsc, other.getMmsc()) &&
TextUtils.equals(mMmsProxy, other.getMmsProxy()) &&
mMmsProxyPort == other.getMmsProxyPort();
}
}
/**
* The APN represented by the local APN table row
*/
private static class DatabaseApn implements Apn {
private static final ContentValues CURRENT_NULL_VALUE;
private static final ContentValues CURRENT_SET_VALUE;
static {
CURRENT_NULL_VALUE = new ContentValues(1);
CURRENT_NULL_VALUE.putNull(Telephony.Carriers.CURRENT);
CURRENT_SET_VALUE = new ContentValues(1);
CURRENT_SET_VALUE.put(Telephony.Carriers.CURRENT, "1"); // 1 for auto selected APN
}
private static final String CLEAR_UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?";
private static final String[] CLEAR_UPDATE_SELECTION_ARGS = new String[] { "1" };
private static final String SET_UPDATE_SELECTION = Telephony.Carriers._ID + " =?";
/**
* Create an APN loaded from local database
*
* @param apns the in-memory APN list
* @param typesIn the APN type field
* @param mmscIn the APN mmsc field
* @param proxyIn the APN mmsproxy field
* @param portIn the APN mmsport field
* @param rowId the APN's row ID in database
* @param current the value of CURRENT column in database
* @return an in-memory APN instance for database APN row, null if parameter invalid
*/
public static DatabaseApn from(final List<Apn> apns, final String typesIn,
final String mmscIn, final String proxyIn, final String portIn,
final long rowId, final int current) {
if (apns == null) {
return null;
}
final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
if (base == null) {
return null;
}
for (final ApnSettingsLoader.Apn apn : apns) {
if (apn instanceof DatabaseApn && ((DatabaseApn) apn).equals(base)) {
return null;
}
}
return new DatabaseApn(apns, base, rowId, current);
}
private final List<Apn> mApns;
private final BaseApn mBase;
private final long mRowId;
private int mCurrent;
public DatabaseApn(final List<Apn> apns, final BaseApn base, final long rowId,
final int current) {
mApns = apns;
mBase = base;
mRowId = rowId;
mCurrent = current;
}
@Override
public String getMmsc() {
return mBase.getMmsc();
}
@Override
public String getMmsProxy() {
return mBase.getMmsProxy();
}
@Override
public int getMmsProxyPort() {
return mBase.getMmsProxyPort();
}
@Override
public void setSuccess() {
moveToListHead();
setCurrentInDatabase();
}
/**
* Try to move this APN to the head of in-memory list
*/
private void moveToListHead() {
// If this is being marked as a successful APN, move it to the top of the list so
// next time it will be tried first
boolean moved = false;
synchronized (mApns) {
if (mApns.get(0) != this) {
mApns.remove(this);
mApns.add(0, this);
moved = true;
}
}
if (moved) {
LogUtil.d(LogUtil.BUGLE_TAG, "Set APN ["
+ "MMSC=" + getMmsc() + ", "
+ "PROXY=" + getMmsProxy() + ", "
+ "PORT=" + getMmsProxyPort() + "] to be first");
}
}
/**
* Try to set the APN to be CURRENT in its database table
*/
private void setCurrentInDatabase() {
synchronized (this) {
if (mCurrent > 0) {
// Already current
return;
}
mCurrent = 1;
}
LogUtil.d(LogUtil.BUGLE_TAG, "Set APN @" + mRowId + " to be CURRENT in local db");
final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase();
database.beginTransaction();
try {
// clear the previous current=1 apn
// we don't clear current=2 apn since it is manually selected by user
// and we should not override it.
database.update(ApnDatabase.APN_TABLE, CURRENT_NULL_VALUE,
CLEAR_UPDATE_SELECTION, CLEAR_UPDATE_SELECTION_ARGS);
// set this one to be current (1)
database.update(ApnDatabase.APN_TABLE, CURRENT_SET_VALUE, SET_UPDATE_SELECTION,
new String[] { Long.toString(mRowId) });
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
public boolean equals(final BaseApn other) {
if (other == null) {
return false;
}
return mBase.equals(other);
}
}
/**
* APN_TYPE_ALL is a special type to indicate that this APN entry can
* service all data connections.
*/
public static final String APN_TYPE_ALL = "*";
/** APN type for MMS traffic */
public static final String APN_TYPE_MMS = "mms";
private static final String[] APN_PROJECTION_SYSTEM = {
Telephony.Carriers.TYPE,
Telephony.Carriers.MMSC,
Telephony.Carriers.MMSPROXY,
Telephony.Carriers.MMSPORT,
};
private static final String[] APN_PROJECTION_LOCAL = {
Telephony.Carriers.TYPE,
Telephony.Carriers.MMSC,
Telephony.Carriers.MMSPROXY,
Telephony.Carriers.MMSPORT,
Telephony.Carriers.CURRENT,
Telephony.Carriers._ID,
};
private static final int COLUMN_TYPE = 0;
private static final int COLUMN_MMSC = 1;
private static final int COLUMN_MMSPROXY = 2;
private static final int COLUMN_MMSPORT = 3;
private static final int COLUMN_CURRENT = 4;
private static final int COLUMN_ID = 5;
private static final String SELECTION_APN = Telephony.Carriers.APN + "=?";
private static final String SELECTION_CURRENT = Telephony.Carriers.CURRENT + " IS NOT NULL";
private static final String SELECTION_NUMERIC = Telephony.Carriers.NUMERIC + "=?";
private static final String ORDER_BY = Telephony.Carriers.CURRENT + " DESC";
private final Context mContext;
// Cached APNs for subIds
private final SparseArray<List<ApnSettingsLoader.Apn>> mApnsCache;
public BugleApnSettingsLoader(final Context context) {
mContext = context;
mApnsCache = new SparseArray<>();
}
@Override
public List<ApnSettingsLoader.Apn> get(final String apnName) {
final int subId = PhoneUtils.getDefault().getEffectiveSubId(
ParticipantData.DEFAULT_SELF_SUB_ID);
List<ApnSettingsLoader.Apn> apns;
boolean didLoad = false;
synchronized (this) {
apns = mApnsCache.get(subId);
if (apns == null) {
apns = new ArrayList<>();
mApnsCache.put(subId, apns);
loadLocked(subId, apnName, apns);
didLoad = true;
}
}
if (didLoad) {
LogUtil.i(LogUtil.BUGLE_TAG, "Loaded " + apns.size() + " APNs");
}
return apns;
}
private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
// Try Gservices first
loadFromGservices(apns);
if (apns.size() > 0) {
return;
}
// Try system APN table
loadFromSystem(subId, apnName, apns);
if (apns.size() > 0) {
return;
}
// Try local APN table
loadFromLocalDatabase(apnName, apns);
if (apns.size() <= 0) {
LogUtil.w(LogUtil.BUGLE_TAG, "Failed to load any APN");
}
}
/**
* Load from Gservices if APN setting is set in Gservices
*
* @param apns the list used to return results
*/
private void loadFromGservices(final List<Apn> apns) {
final BugleGservices gservices = BugleGservices.get();
final String mmsc = gservices.getString(BugleGservicesKeys.MMS_MMSC, null);
if (TextUtils.isEmpty(mmsc)) {
return;
}
LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from gservices");
final String proxy = gservices.getString(BugleGservicesKeys.MMS_PROXY_ADDRESS, null);
final int port = gservices.getInt(BugleGservicesKeys.MMS_PROXY_PORT, -1);
final Apn apn = BaseApn.from("mms", mmsc, proxy, Integer.toString(port));
if (apn != null) {
apns.add(apn);
}
}
/**
* Load matching APNs from telephony provider.
* We try different combinations of the query to work around some platform quirks.
*
* @param subId the SIM subId
* @param apnName the APN name to match
* @param apns the list used to return results
*/
private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) {
Uri uri;
if (OsUtil.isAtLeastL_MR1() && subId != MmsManager.DEFAULT_SUB_ID) {
uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId);
} else {
uri = Telephony.Carriers.CONTENT_URI;
}
Cursor cursor = null;
try {
for (; ; ) {
// Try different combinations of queries. Some would work on some platforms.
// So we query each combination until we find one returns non-empty result.
cursor = querySystem(uri, true/*checkCurrent*/, apnName);
if (cursor != null) {
break;
}
cursor = querySystem(uri, false/*checkCurrent*/, apnName);
if (cursor != null) {
break;
}
cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/);
if (cursor != null) {
break;
}
cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/);
break;
}
} catch (final SecurityException e) {
// Can't access platform APN table, return directly
return;
}
if (cursor == null) {
return;
}
try {
if (cursor.moveToFirst()) {
final ApnSettingsLoader.Apn apn = BaseApn.from(
cursor.getString(COLUMN_TYPE),
cursor.getString(COLUMN_MMSC),
cursor.getString(COLUMN_MMSPROXY),
cursor.getString(COLUMN_MMSPORT));
if (apn != null) {
apns.add(apn);
}
}
} finally {
cursor.close();
}
}
/**
* Query system APN table
*
* @param uri The APN query URL to use
* @param checkCurrent If add "CURRENT IS NOT NULL" condition
* @param apnName The optional APN name for query condition
* @return A cursor of the query result. If a cursor is returned as not null, it is
* guaranteed to contain at least one row.
*/
private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) {
LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from system, "
+ "checkCurrent=" + checkCurrent + " apnName=" + apnName);
final StringBuilder selectionBuilder = new StringBuilder();
String[] selectionArgs = null;
if (checkCurrent) {
selectionBuilder.append(SELECTION_CURRENT);
}
apnName = trimWithNullCheck(apnName);
if (!TextUtils.isEmpty(apnName)) {
if (selectionBuilder.length() > 0) {
selectionBuilder.append(" AND ");
}
selectionBuilder.append(SELECTION_APN);
selectionArgs = new String[] { apnName };
}
try {
final Cursor cursor = SqliteWrapper.query(
mContext,
mContext.getContentResolver(),
uri,
APN_PROJECTION_SYSTEM,
selectionBuilder.toString(),
selectionArgs,
null/*sortOrder*/);
if (cursor == null || cursor.getCount() < 1) {
if (cursor != null) {
cursor.close();
}
LogUtil.w(LogUtil.BUGLE_TAG, "Query " + uri + " with apn " + apnName + " and "
+ (checkCurrent ? "checking CURRENT" : "not checking CURRENT")
+ " returned empty");
return null;
}
return cursor;
} catch (final SQLiteException e) {
LogUtil.w(LogUtil.BUGLE_TAG, "APN table query exception: " + e);
} catch (final SecurityException e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Platform restricts APN table access: " + e);
throw e;
}
return null;
}
/**
* Load matching APNs from local APN table.
* We try both using the APN name and not using the APN name.
*
* @param apnName the APN name
* @param apns the list of results to return
*/
private void loadFromLocalDatabase(final String apnName, final List<Apn> apns) {
LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from local APN table");
final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase();
final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.getDefault().getMccMnc());
Cursor cursor = null;
cursor = queryLocalDatabase(database, mccMnc, apnName);
if (cursor == null) {
cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/);
}
if (cursor == null) {
LogUtil.w(LogUtil.BUGLE_TAG, "Could not find any APN in local table");
return;
}
try {
while (cursor.moveToNext()) {
final Apn apn = DatabaseApn.from(apns,
cursor.getString(COLUMN_TYPE),
cursor.getString(COLUMN_MMSC),
cursor.getString(COLUMN_MMSPROXY),
cursor.getString(COLUMN_MMSPORT),
cursor.getLong(COLUMN_ID),
cursor.getInt(COLUMN_CURRENT));
if (apn != null) {
apns.add(apn);
}
}
} finally {
cursor.close();
}
}
/**
* Make a query of local APN table based on MCC/MNC and APN name, sorted by CURRENT
* column in descending order
*
* @param db the local database
* @param numeric the MCC/MNC string
* @param apnName the optional APN name to match
* @return the cursor of the query, null if no result
*/
private static Cursor queryLocalDatabase(final SQLiteDatabase db, final String numeric,
final String apnName) {
final String selection;
final String[] selectionArgs;
if (TextUtils.isEmpty(apnName)) {
selection = SELECTION_NUMERIC;
selectionArgs = new String[] { numeric };
} else {
selection = SELECTION_NUMERIC + " AND " + SELECTION_APN;
selectionArgs = new String[] { numeric, apnName };
}
Cursor cursor = null;
try {
cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs,
null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/);
} catch (final SQLiteException e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Local APN table does not exist. Try rebuilding.", e);
ApnDatabase.forceBuildAndLoadApnTables();
cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs,
null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/);
}
if (cursor == null || cursor.getCount() < 1) {
if (cursor != null) {
cursor.close();
}
LogUtil.w(LogUtil.BUGLE_TAG, "Query local APNs with apn " + apnName
+ " returned empty");
return null;
}
return cursor;
}
private static String trimWithNullCheck(final String value) {
return value != null ? value.trim() : null;
}
/**
* Trim leading zeros from IPv4 address strings
* Our base libraries will interpret that as octel..
* Must leave non v4 addresses and host names alone.
* For example, 192.168.000.010 -> 192.168.0.10
*
* @param addr a string representing an ip addr
* @return a string propertly trimmed
*/
private static String trimV4AddrZeros(final String addr) {
if (addr == null) {
return null;
}
final String[] octets = addr.split("\\.");
if (octets.length != 4) {
return addr;
}
final StringBuilder builder = new StringBuilder(16);
String result = null;
for (int i = 0; i < 4; i++) {
try {
if (octets[i].length() > 3) {
return addr;
}
builder.append(Integer.parseInt(octets[i]));
} catch (final NumberFormatException e) {
return addr;
}
if (i < 3) {
builder.append('.');
}
}
result = builder.toString();
return result;
}
/**
* Check if the APN contains the APN type we want
*
* @param types The string encodes a list of supported types
* @param requestType The type we want
* @return true if the input types string contains the requestType
*/
public static boolean isValidApnType(final String types, final String requestType) {
// If APN type is unspecified, assume APN_TYPE_ALL.
if (TextUtils.isEmpty(types)) {
return true;
}
for (final String t : types.split(",")) {
if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) {
return true;
}
}
return false;
}
/**
* Get the ID of first APN to try
*/
public static String getFirstTryApn(final SQLiteDatabase database, final String mccMnc) {
String key = null;
Cursor cursor = null;
try {
cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/);
if (cursor.moveToFirst()) {
key = cursor.getString(ApnDatabase.COLUMN_ID);
}
} catch (final Exception e) {
// Nothing to do
} finally {
if (cursor != null) {
cursor.close();
}
}
return key;
}
}
@@ -0,0 +1,201 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.support.v7.mms.CarrierConfigValuesLoader;
import android.util.SparseArray;
import com.android.messaging.R;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
/**
* Carrier configuration loader
*
* Loader tries to load from resources. If there is MMS API available, also
* load from system.
*/
public class BugleCarrierConfigValuesLoader implements CarrierConfigValuesLoader {
/*
* Key types
*/
public static final String KEY_TYPE_INT = "int";
public static final String KEY_TYPE_BOOL = "bool";
public static final String KEY_TYPE_STRING = "string";
private final Context mContext;
// Cached values for subIds
private final SparseArray<Bundle> mValuesCache;
public BugleCarrierConfigValuesLoader(final Context context) {
mContext = context;
mValuesCache = new SparseArray<>();
}
@Override
public Bundle get(int subId) {
subId = PhoneUtils.getDefault().getEffectiveSubId(subId);
Bundle values;
String loadSource = null;
synchronized (this) {
values = mValuesCache.get(subId);
if (values == null) {
values = new Bundle();
mValuesCache.put(subId, values);
loadSource = loadLocked(subId, values);
}
}
if (loadSource != null) {
LogUtil.i(LogUtil.BUGLE_TAG, "Carrier configs loaded: " + values
+ " from " + loadSource + " for subId=" + subId);
}
return values;
}
/**
* Clear the cache for reloading
*/
public void reset() {
synchronized (this) {
mValuesCache.clear();
}
}
/**
* Loading carrier config values
*
* @param subId which SIM to load for
* @param values the result to add to
* @return the source of the config, could be "resources" or "resources+system"
*/
private String loadLocked(final int subId, final Bundle values) {
// Load from resources in earlier platform
loadFromResources(subId, values);
if (OsUtil.isAtLeastL()) {
// Load from system to override if system API exists
loadFromSystem(subId, values);
return "resources+system";
}
return "resources";
}
/**
* Load from system, using MMS API
*
* @param subId which SIM to load for
* @param values the result to add to
*/
private static void loadFromSystem(final int subId, final Bundle values) {
try {
final Bundle systemValues =
PhoneUtils.get(subId).getSmsManager().getCarrierConfigValues();
if (systemValues != null) {
values.putAll(systemValues);
}
} catch (final Exception e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Calling system getCarrierConfigValues exception", e);
}
}
/**
* Load from SIM-dependent resources
*
* @param subId which SIM to load for
* @param values the result to add to
*/
private void loadFromResources(final int subId, final Bundle values) {
// Get a subscription-dependent context for loading the mms_config.xml
final Context subContext = getSubDepContext(mContext, subId);
// Load and parse the XML
XmlResourceParser parser = null;
try {
parser = subContext.getResources().getXml(R.xml.mms_config);
final ApnsXmlProcessor processor = ApnsXmlProcessor.get(parser);
processor.setMmsConfigHandler(new ApnsXmlProcessor.MmsConfigHandler() {
@Override
public void process(final String mccMnc, final String key, final String value,
final String type) {
update(values, type, key, value);
}
});
processor.process();
} catch (final Resources.NotFoundException e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Can not find mms_config.xml");
} finally {
if (parser != null) {
parser.close();
}
}
}
/**
* Get a subscription's Context so we can load resources from it
*
* @param context the sub-independent Context
* @param subId the SIM's subId
* @return the sub-dependent Context
*/
private static Context getSubDepContext(final Context context, final int subId) {
if (!OsUtil.isAtLeastL_MR1()) {
return context;
}
final int[] mccMnc = PhoneUtils.get(subId).getMccMnc();
final int mcc = mccMnc[0];
final int mnc = mccMnc[1];
final Configuration subConfig = new Configuration();
if (mcc == 0 && mnc == 0) {
Configuration config = context.getResources().getConfiguration();
subConfig.mcc = config.mcc;
subConfig.mnc = config.mnc;
} else {
subConfig.mcc = mcc;
subConfig.mnc = mnc;
}
return context.createConfigurationContext(subConfig);
}
/**
* Add or update a carrier config key/value pair to the Bundle
*
* @param values the result Bundle to add to
* @param type the value type
* @param key the key
* @param value the value
*/
public static void update(final Bundle values, final String type, final String key,
final String value) {
try {
if (KEY_TYPE_INT.equals(type)) {
values.putInt(key, Integer.parseInt(value));
} else if (KEY_TYPE_BOOL.equals(type)) {
values.putBoolean(key, Boolean.parseBoolean(value));
} else if (KEY_TYPE_STRING.equals(type)){
values.putString(key, value);
}
} catch (final NumberFormatException e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Add carrier values: "
+ "invalid " + key + "," + value + "," + type);
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.Context;
import android.support.v7.mms.UserAgentInfoLoader;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.VersionUtil;
/**
* User agent and UA profile URL loader
*/
public class BugleUserAgentInfoLoader implements UserAgentInfoLoader {
private static final String DEFAULT_USER_AGENT_PREFIX = "Bugle/";
private Context mContext;
private boolean mLoaded;
private String mUserAgent;
private String mUAProfUrl;
public BugleUserAgentInfoLoader(final Context context) {
mContext = context;
}
@Override
public String getUserAgent() {
load();
return mUserAgent;
}
@Override
public String getUAProfUrl() {
load();
return mUAProfUrl;
}
private void load() {
if (mLoaded) {
return;
}
boolean didLoad = false;
synchronized (this) {
if (!mLoaded) {
loadLocked();
mLoaded = true;
didLoad = true;
}
}
if (didLoad) {
LogUtil.i(LogUtil.BUGLE_TAG, "Loaded user agent info: "
+ "UA=" + mUserAgent + ", UAProfUrl=" + mUAProfUrl);
}
}
private void loadLocked() {
if (OsUtil.isAtLeastKLP()) {
// load the MMS User agent and UaProfUrl from TelephonyManager APIs
final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
Context.TELEPHONY_SERVICE);
mUserAgent = telephonyManager.getMmsUserAgent();
mUAProfUrl = telephonyManager.getMmsUAProfUrl();
}
// if user agent string isn't set, use the format "Bugle/<app_version>".
if (TextUtils.isEmpty(mUserAgent)) {
final String simpleVersionName = VersionUtil.getInstance(mContext).getSimpleName();
mUserAgent = DEFAULT_USER_AGENT_PREFIX + simpleVersionName;
}
// if the UAProfUrl isn't set, get it from Gservices
if (TextUtils.isEmpty(mUAProfUrl)) {
mUAProfUrl = BugleGservices.get().getString(
BugleGservicesKeys.MMS_UA_PROFILE_URL,
BugleGservicesKeys.MMS_UA_PROFILE_URL_DEFAULT);
}
}
}
File diff suppressed because it is too large Load Diff
+309
View File
@@ -0,0 +1,309 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.os.Bundle;
import android.support.v7.mms.CarrierConfigValuesLoader;
import android.telephony.SubscriptionInfo;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SafeAsyncTask;
import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* MMS configuration.
*
* This is now a wrapper around the BugleCarrierConfigValuesLoader, which does
* the actual loading and stores the values in a Bundle. This class provides getter
* methods for values used in the app, which is easier to use than the raw loader
* class.
*/
public class MmsConfig {
private static final String TAG = LogUtil.BUGLE_TAG;
private static final int DEFAULT_MAX_TEXT_LENGTH = 2000;
/*
* Key types
*/
public static final String KEY_TYPE_INT = "int";
public static final String KEY_TYPE_BOOL = "bool";
public static final String KEY_TYPE_STRING = "string";
private static final Map<String, String> sKeyTypeMap = Maps.newHashMap();
static {
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLED_MMS, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLED_TRANS_ID, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLED_NOTIFY_WAP_MMSC, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ALIAS_ENABLED, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ALLOW_ATTACH_AUDIO, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLE_MULTIPART_SMS, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLE_SMS_DELIVERY_REPORTS,
KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLE_GROUP_MMS, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION,
KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_CELL_BROADCAST_APP_LINKS, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES,
KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLE_MMS_READ_REPORTS, KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ENABLE_MMS_DELIVERY_REPORTS,
KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER,
KEY_TYPE_BOOL);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_HEIGHT, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_WIDTH, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_RECIPIENT_LIMIT, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ALIAS_MIN_CHARS, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_ALIAS_MAX_CHARS, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_THRESHOLD, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD,
KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_TEXT_SIZE, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_MAX_SUBJECT_LENGTH, KEY_TYPE_INT);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME, KEY_TYPE_STRING);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_HTTP_PARAMS, KEY_TYPE_STRING);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_EMAIL_GATEWAY_NUMBER, KEY_TYPE_STRING);
sKeyTypeMap.put(CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX, KEY_TYPE_STRING);
}
// A map that stores all MmsConfigs, one per active subscription. For pre-LMSim, this will
// contain just one entry with the default self sub id; for LMSim and above, this will contain
// all active sub ids but the default subscription id - the default subscription id will be
// resolved to an active sub id during runtime.
private static final Map<Integer, MmsConfig> sSubIdToMmsConfigMap = Maps.newHashMap();
// The fallback values
private static final MmsConfig sFallback =
new MmsConfig(ParticipantData.DEFAULT_SELF_SUB_ID, new Bundle());
// Per-subscription configuration values.
private final Bundle mValues;
private final int mSubId;
/**
* Retrieves the MmsConfig instance associated with the given {@code subId}
*/
public static MmsConfig get(final int subId) {
final int realSubId = PhoneUtils.getDefault().getEffectiveSubId(subId);
synchronized (sSubIdToMmsConfigMap) {
final MmsConfig mmsConfig = sSubIdToMmsConfigMap.get(realSubId);
if (mmsConfig == null) {
// The subId is no longer valid. Fall back to the default config.
LogUtil.e(LogUtil.BUGLE_TAG, "Get mms config failed: invalid subId. subId=" + subId
+ ", real subId=" + realSubId
+ ", map=" + sSubIdToMmsConfigMap.keySet());
return sFallback;
}
return mmsConfig;
}
}
private MmsConfig(final int subId, final Bundle values) {
mSubId = subId;
mValues = values;
}
/**
* Same as load() but doing it using an async thread from SafeAsyncTask thread pool.
*/
public static void loadAsync() {
SafeAsyncTask.executeOnThreadPool(new Runnable() {
@Override
public void run() {
load();
}
});
}
/**
* Reload the device and per-subscription settings.
*/
public static synchronized void load() {
final BugleCarrierConfigValuesLoader loader = Factory.get().getCarrierConfigValuesLoader();
// Rebuild the entire MmsConfig map.
sSubIdToMmsConfigMap.clear();
loader.reset();
if (OsUtil.isAtLeastL_MR1()) {
final List<SubscriptionInfo> subInfoRecords =
PhoneUtils.getDefault().toLMr1().getActiveSubscriptionInfoList();
if (subInfoRecords == null) {
LogUtil.w(TAG, "Loading mms config failed: no active SIM");
return;
}
for (SubscriptionInfo subInfoRecord : subInfoRecords) {
final int subId = subInfoRecord.getSubscriptionId();
final Bundle values = loader.get(subId);
addMmsConfig(new MmsConfig(subId, values));
}
} else {
final Bundle values = loader.get(ParticipantData.DEFAULT_SELF_SUB_ID);
addMmsConfig(new MmsConfig(ParticipantData.DEFAULT_SELF_SUB_ID, values));
}
}
private static void addMmsConfig(MmsConfig mmsConfig) {
Assert.isTrue(OsUtil.isAtLeastL_MR1() !=
(mmsConfig.mSubId == ParticipantData.DEFAULT_SELF_SUB_ID));
sSubIdToMmsConfigMap.put(mmsConfig.mSubId, mmsConfig);
}
public int getSmsToMmsTextThreshold() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_THRESHOLD,
CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_THRESHOLD_DEFAULT);
}
public int getSmsToMmsTextLengthThreshold() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD,
CarrierConfigValuesLoader.CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_DEFAULT);
}
public int getMaxMessageSize() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE,
CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE_DEFAULT);
}
/**
* Return the largest MaxMessageSize for any subid
*/
public static int getMaxMaxMessageSize() {
int maxMax = 0;
for (MmsConfig config : sSubIdToMmsConfigMap.values()) {
maxMax = Math.max(maxMax, config.getMaxMessageSize());
}
return maxMax > 0 ? maxMax : sFallback.getMaxMessageSize();
}
public boolean getTransIdEnabled() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ENABLED_TRANS_ID,
CarrierConfigValuesLoader.CONFIG_ENABLED_TRANS_ID_DEFAULT);
}
public String getEmailGateway() {
return mValues.getString(CarrierConfigValuesLoader.CONFIG_EMAIL_GATEWAY_NUMBER,
CarrierConfigValuesLoader.CONFIG_EMAIL_GATEWAY_NUMBER_DEFAULT);
}
public int getMaxImageHeight() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_HEIGHT,
CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_HEIGHT_DEFAULT);
}
public int getMaxImageWidth() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_WIDTH,
CarrierConfigValuesLoader.CONFIG_MAX_IMAGE_WIDTH_DEFAULT);
}
public int getRecipientLimit() {
final int limit = mValues.getInt(CarrierConfigValuesLoader.CONFIG_RECIPIENT_LIMIT,
CarrierConfigValuesLoader.CONFIG_RECIPIENT_LIMIT_DEFAULT);
return limit < 0 ? Integer.MAX_VALUE : limit;
}
public int getMaxTextLimit() {
final int max = mValues.getInt(CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_TEXT_SIZE,
CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_TEXT_SIZE_DEFAULT);
return max > -1 ? max : DEFAULT_MAX_TEXT_LENGTH;
}
public boolean getMultipartSmsEnabled() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ENABLE_MULTIPART_SMS,
CarrierConfigValuesLoader.CONFIG_ENABLE_MULTIPART_SMS_DEFAULT);
}
public boolean getSendMultipartSmsAsSeparateMessages() {
return mValues.getBoolean(
CarrierConfigValuesLoader.CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES,
CarrierConfigValuesLoader.CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_DEFAULT);
}
public boolean getSMSDeliveryReportsEnabled() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ENABLE_SMS_DELIVERY_REPORTS,
CarrierConfigValuesLoader.CONFIG_ENABLE_SMS_DELIVERY_REPORTS_DEFAULT);
}
public boolean getNotifyWapMMSC() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ENABLED_NOTIFY_WAP_MMSC,
CarrierConfigValuesLoader.CONFIG_ENABLED_NOTIFY_WAP_MMSC_DEFAULT);
}
public boolean isAliasEnabled() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ALIAS_ENABLED,
CarrierConfigValuesLoader.CONFIG_ALIAS_ENABLED_DEFAULT);
}
public int getAliasMinChars() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_ALIAS_MIN_CHARS,
CarrierConfigValuesLoader.CONFIG_ALIAS_MIN_CHARS_DEFAULT);
}
public int getAliasMaxChars() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_ALIAS_MAX_CHARS,
CarrierConfigValuesLoader.CONFIG_ALIAS_MAX_CHARS_DEFAULT);
}
public boolean getAllowAttachAudio() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ALLOW_ATTACH_AUDIO,
CarrierConfigValuesLoader.CONFIG_ALLOW_ATTACH_AUDIO_DEFAULT);
}
public int getMaxSubjectLength() {
return mValues.getInt(CarrierConfigValuesLoader.CONFIG_MAX_SUBJECT_LENGTH,
CarrierConfigValuesLoader.CONFIG_MAX_SUBJECT_LENGTH_DEFAULT);
}
public boolean getGroupMmsEnabled() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_ENABLE_GROUP_MMS,
CarrierConfigValuesLoader.CONFIG_ENABLE_GROUP_MMS_DEFAULT);
}
public boolean getSupportMmsContentDisposition() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION,
CarrierConfigValuesLoader.CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT);
}
public boolean getShowCellBroadcast() {
return mValues.getBoolean(CarrierConfigValuesLoader.CONFIG_CELL_BROADCAST_APP_LINKS,
CarrierConfigValuesLoader.CONFIG_CELL_BROADCAST_APP_LINKS_DEFAULT);
}
public Object getValue(final String key) {
return mValues.get(key);
}
public Set<String> keySet() {
return mValues.keySet();
}
public static String getKeyType(final String key) {
return sKeyTypeMap.get(key);
}
public void update(final String type, final String key, final String value) {
BugleCarrierConfigValuesLoader.update(mValues, type, key, value);
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.Assert;
/**
* Exception for MMS failures
*/
public class MmsFailureException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Hint of how we should retry in case of failure. Take values defined in MmsUtils.
*/
public final int retryHint;
/**
* If set, provides a more detailed reason for the failure.
*/
public final int rawStatus;
private void checkRetryHint() {
Assert.isTrue(retryHint == MmsUtils.MMS_REQUEST_AUTO_RETRY
|| retryHint == MmsUtils.MMS_REQUEST_MANUAL_RETRY
|| retryHint == MmsUtils.MMS_REQUEST_NO_RETRY);
}
/**
* Creates a new MmsFailureException.
*
* @param retryHint Hint for how to retry
*/
public MmsFailureException(final int retryHint) {
super();
this.retryHint = retryHint;
checkRetryHint();
this.rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
}
public MmsFailureException(final int retryHint, final int rawStatus) {
super();
this.retryHint = retryHint;
checkRetryHint();
this.rawStatus = rawStatus;
}
/**
* Creates a new MmsFailureException with the specified detail message.
*
* @param retryHint Hint for how to retry
* @param message the detail message.
*/
public MmsFailureException(final int retryHint, String message) {
super(message);
this.retryHint = retryHint;
checkRetryHint();
this.rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
}
/**
* Creates a new MmsFailureException with the specified cause.
*
* @param retryHint Hint for how to retry
* @param cause the cause.
*/
public MmsFailureException(final int retryHint, Throwable cause) {
super(cause);
this.retryHint = retryHint;
checkRetryHint();
this.rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
}
/**
* Creates a new MmsFailureException
* with the specified detail message and cause.
*
* @param retryHint Hint for how to retry
* @param message the detail message.
* @param cause the cause.
*/
public MmsFailureException(final int retryHint, String message, Throwable cause) {
super(message, cause);
this.retryHint = retryHint;
checkRetryHint();
this.rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED;
}
}
@@ -0,0 +1,312 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.mms.MmsManager;
import android.telephony.SmsManager;
import com.android.messaging.datamodel.MmsFileProvider;
import com.android.messaging.datamodel.action.SendMessageAction;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.mmslib.InvalidHeaderValueException;
import com.android.messaging.mmslib.pdu.AcknowledgeInd;
import com.android.messaging.mmslib.pdu.EncodedStringValue;
import com.android.messaging.mmslib.pdu.GenericPdu;
import com.android.messaging.mmslib.pdu.NotifyRespInd;
import com.android.messaging.mmslib.pdu.PduComposer;
import com.android.messaging.mmslib.pdu.PduHeaders;
import com.android.messaging.mmslib.pdu.PduParser;
import com.android.messaging.mmslib.pdu.RetrieveConf;
import com.android.messaging.mmslib.pdu.SendConf;
import com.android.messaging.mmslib.pdu.SendReq;
import com.android.messaging.receiver.SendStatusReceiver;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Class that sends chat message via MMS.
*
* The interface emulates a blocking send similar to making an HTTP request.
*/
public class MmsSender {
private static final String TAG = LogUtil.BUGLE_TAG;
/**
* Send an MMS message.
*
* @param context Context
* @param messageUri The unique URI of the message for identifying it during sending
* @param sendReq The SendReq PDU of the message
* @throws MmsFailureException
*/
public static void sendMms(final Context context, final int subId, final Uri messageUri,
final SendReq sendReq, final Bundle sentIntentExras) throws MmsFailureException {
sendMms(context,
subId,
messageUri,
null /* locationUrl */,
sendReq,
true /* responseImportant */,
sentIntentExras);
}
/**
* Send NotifyRespInd (response to mms auto download).
*
* @param context Context
* @param subId subscription to use to send the response
* @param transactionId The transaction id of the MMS message
* @param contentLocation The url of the MMS message
* @param status The status to send with the NotifyRespInd
* @throws MmsFailureException
* @throws InvalidHeaderValueException
*/
public static void sendNotifyResponseForMmsDownload(final Context context, final int subId,
final byte[] transactionId, final String contentLocation, final int status)
throws MmsFailureException, InvalidHeaderValueException {
// Create the M-NotifyResp.ind
final NotifyRespInd notifyRespInd = new NotifyRespInd(
PduHeaders.CURRENT_MMS_VERSION, transactionId, status);
final Uri messageUri = Uri.parse(contentLocation);
// Pack M-NotifyResp.ind and send it
sendMms(context,
subId,
messageUri,
MmsConfig.get(subId).getNotifyWapMMSC() ? contentLocation : null,
notifyRespInd,
false /* responseImportant */,
null /* sentIntentExtras */);
}
/**
* Send AcknowledgeInd (response to mms manual download). Ignore failures.
*
* @param context Context
* @param subId The SIM's subId we are currently using
* @param transactionId The transaction id of the MMS message
* @param contentLocation The url of the MMS message
* @throws MmsFailureException
* @throws InvalidHeaderValueException
*/
public static void sendAcknowledgeForMmsDownload(final Context context, final int subId,
final byte[] transactionId, final String contentLocation)
throws MmsFailureException, InvalidHeaderValueException {
final String selfNumber = PhoneUtils.get(subId).getCanonicalForSelf(true/*allowOverride*/);
// Create the M-Acknowledge.ind
final AcknowledgeInd acknowledgeInd = new AcknowledgeInd(PduHeaders.CURRENT_MMS_VERSION,
transactionId);
acknowledgeInd.setFrom(new EncodedStringValue(selfNumber));
final Uri messageUri = Uri.parse(contentLocation);
// Sending
sendMms(context,
subId,
messageUri,
MmsConfig.get(subId).getNotifyWapMMSC() ? contentLocation : null,
acknowledgeInd,
false /*responseImportant*/,
null /* sentIntentExtras */);
}
/**
* Send a generic PDU.
*
* @param context Context
* @param messageUri The unique URI of the message for identifying it during sending
* @param locationUrl The optional URL to send to
* @param pdu The PDU to send
* @param responseImportant If the sending response is important. Responses to the
* Sending of AcknowledgeInd and NotifyRespInd are not important.
* @throws MmsFailureException
*/
private static void sendMms(final Context context, final int subId, final Uri messageUri,
final String locationUrl, final GenericPdu pdu, final boolean responseImportant,
final Bundle sentIntentExtras) throws MmsFailureException {
// Write PDU to temporary file to send to platform
final Uri contentUri = writePduToTempFile(context, pdu, subId);
// Construct PendingIntent that will notify us when message sending is complete
final Intent sentIntent = new Intent(SendStatusReceiver.MMS_SENT_ACTION,
messageUri,
context,
SendStatusReceiver.class);
sentIntent.putExtra(SendMessageAction.EXTRA_CONTENT_URI, contentUri);
sentIntent.putExtra(SendMessageAction.EXTRA_RESPONSE_IMPORTANT, responseImportant);
if (sentIntentExtras != null) {
sentIntent.putExtras(sentIntentExtras);
}
final PendingIntent sentPendingIntent = PendingIntent.getBroadcast(
context,
0 /*request code*/,
sentIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
// Send the message
MmsManager.sendMultimediaMessage(subId, context, contentUri, locationUrl,
sentPendingIntent);
}
private static Uri writePduToTempFile(final Context context, final GenericPdu pdu, int subId)
throws MmsFailureException {
final Uri contentUri = MmsFileProvider.buildRawMmsUri();
final File tempFile = MmsFileProvider.getFile(contentUri);
FileOutputStream writer = null;
try {
// Ensure rawmms directory exists
tempFile.getParentFile().mkdirs();
writer = new FileOutputStream(tempFile);
final byte[] pduBytes = new PduComposer(context, pdu).make();
if (pduBytes == null) {
throw new MmsFailureException(
MmsUtils.MMS_REQUEST_NO_RETRY, "Failed to compose PDU");
}
if (pduBytes.length > MmsConfig.get(subId).getMaxMessageSize()) {
throw new MmsFailureException(
MmsUtils.MMS_REQUEST_NO_RETRY,
MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG);
}
writer.write(pduBytes);
} catch (final IOException e) {
if (tempFile != null) {
tempFile.delete();
}
LogUtil.e(TAG, "Cannot create temporary file " + tempFile.getAbsolutePath(), e);
throw new MmsFailureException(
MmsUtils.MMS_REQUEST_AUTO_RETRY, "Cannot create raw mms file");
} catch (final OutOfMemoryError e) {
if (tempFile != null) {
tempFile.delete();
}
LogUtil.e(TAG, "Out of memory in composing PDU", e);
throw new MmsFailureException(
MmsUtils.MMS_REQUEST_MANUAL_RETRY,
MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG);
} finally {
if (writer != null) {
try {
writer.close();
} catch (final IOException e) {
// no action we can take here
}
}
}
return contentUri;
}
public static SendConf parseSendConf(byte[] response, int subId) {
if (response != null) {
final GenericPdu respPdu = new PduParser(
response, MmsConfig.get(subId).getSupportMmsContentDisposition()).parse();
if (respPdu != null) {
if (respPdu instanceof SendConf) {
return (SendConf) respPdu;
} else {
LogUtil.e(TAG, "MmsSender: send response not SendConf");
}
} else {
// Invalid PDU
LogUtil.e(TAG, "MmsSender: send invalid response");
}
}
// Empty or invalid response
return null;
}
/**
* Download an MMS message.
*
* @param context Context
* @param contentLocation The url of the MMS message
* @throws MmsFailureException
* @throws InvalidHeaderValueException
*/
public static void downloadMms(final Context context, final int subId,
final String contentLocation, Bundle extras) throws MmsFailureException,
InvalidHeaderValueException {
final Uri requestUri = Uri.parse(contentLocation);
final Uri contentUri = MmsFileProvider.buildRawMmsUri();
final Intent downloadedIntent = new Intent(SendStatusReceiver.MMS_DOWNLOADED_ACTION,
requestUri,
context,
SendStatusReceiver.class);
downloadedIntent.putExtra(SendMessageAction.EXTRA_CONTENT_URI, contentUri);
if (extras != null) {
downloadedIntent.putExtras(extras);
}
final PendingIntent downloadedPendingIntent = PendingIntent.getBroadcast(
context,
0 /*request code*/,
downloadedIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
MmsManager.downloadMultimediaMessage(subId, context, contentLocation, contentUri,
downloadedPendingIntent);
}
public static RetrieveConf parseRetrieveConf(byte[] data, int subId) {
if (data != null) {
final GenericPdu pdu = new PduParser(
data, MmsConfig.get(subId).getSupportMmsContentDisposition()).parse();
if (pdu != null) {
if (pdu instanceof RetrieveConf) {
return (RetrieveConf) pdu;
} else {
LogUtil.e(TAG, "MmsSender: downloaded pdu not RetrieveConf: "
+ pdu.getClass().getName());
}
} else {
LogUtil.e(TAG, "MmsSender: downloaded pdu could not be parsed (invalid)");
}
}
LogUtil.e(TAG, "MmsSender: downloaded pdu is empty");
return null;
}
// Process different result code from platform MMS service
public static int getErrorResultStatus(int resultCode, int httpStatusCode) {
Assert.isFalse(resultCode == Activity.RESULT_OK);
switch (resultCode) {
case SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS:
case SmsManager.MMS_ERROR_IO_ERROR:
return MmsUtils.MMS_REQUEST_AUTO_RETRY;
case SmsManager.MMS_ERROR_INVALID_APN:
case SmsManager.MMS_ERROR_CONFIGURATION_ERROR:
case SmsManager.MMS_ERROR_NO_DATA_NETWORK:
case SmsManager.MMS_ERROR_UNSPECIFIED:
return MmsUtils.MMS_REQUEST_MANUAL_RETRY;
case SmsManager.MMS_ERROR_HTTP_FAILURE:
if (httpStatusCode == 404) {
return MmsUtils.MMS_REQUEST_NO_RETRY;
} else {
return MmsUtils.MMS_REQUEST_AUTO_RETRY;
}
default:
return MmsUtils.MMS_REQUEST_MANUAL_RETRY;
}
}
}
@@ -0,0 +1,204 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Patterns;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.util.LogUtil;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility functions for the Messaging Service
*/
public class MmsSmsUtils {
private MmsSmsUtils() {
// Forbidden being instantiated.
}
// An alias (or commonly called "nickname") is:
// Nickname must begin with a letter.
// Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
public static boolean isAlias(final String string, final int subId) {
if (!MmsConfig.get(subId).isAliasEnabled()) {
return false;
}
final int len = string == null ? 0 : string.length();
if (len < MmsConfig.get(subId).getAliasMinChars() ||
len > MmsConfig.get(subId).getAliasMaxChars()) {
return false;
}
if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter
return false;
}
for (int i = 1; i < len; i++) {
final char c = string.charAt(i);
if (!(Character.isLetterOrDigit(c) || c == '.')) {
return false;
}
}
return true;
}
/**
* mailbox = name-addr
* name-addr = [display-name] angle-addr
* angle-addr = [CFWS] "<" addr-spec ">" [CFWS]
*/
public static final Pattern NAME_ADDR_EMAIL_PATTERN =
Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
public static String extractAddrSpec(final String address) {
final Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
if (match.matches()) {
return match.group(2);
}
return address;
}
/**
* Returns true if the address is an email address
*
* @param address the input address to be tested
* @return true if address is an email address
*/
public static boolean isEmailAddress(final String address) {
if (TextUtils.isEmpty(address)) {
return false;
}
final String s = extractAddrSpec(address);
final Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
return match.matches();
}
/**
* Returns true if the number is a Phone number
*
* @param number the input number to be tested
* @return true if number is a Phone number
*/
public static boolean isPhoneNumber(final String number) {
if (TextUtils.isEmpty(number)) {
return false;
}
final Matcher match = Patterns.PHONE.matcher(number);
return match.matches();
}
/**
* Check if MMS is required when sending to email address
*
* @param destinationHasEmailAddress destination includes an email address
* @return true if MMS is required.
*/
public static boolean getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress,
final int subId) {
if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway())) {
return false;
} else {
return destinationHasEmailAddress;
}
}
/**
* Helper functions for the "threads" table used by MMS and SMS.
*/
public static final class Threads implements android.provider.Telephony.ThreadsColumns {
private static final String[] ID_PROJECTION = { BaseColumns._ID };
private static final Uri THREAD_ID_CONTENT_URI = Uri.parse(
"content://mms-sms/threadID");
public static final Uri CONTENT_URI = Uri.withAppendedPath(
android.provider.Telephony.MmsSms.CONTENT_URI, "conversations");
// No one should construct an instance of this class.
private Threads() {
}
/**
* This is a single-recipient version of
* getOrCreateThreadId. It's convenient for use with SMS
* messages.
*/
public static long getOrCreateThreadId(final Context context, final String recipient) {
final Set<String> recipients = new HashSet<String>();
recipients.add(recipient);
return getOrCreateThreadId(context, recipients);
}
/**
* Given the recipients list and subject of an unsaved message,
* return its thread ID. If the message starts a new thread,
* allocate a new thread ID. Otherwise, use the appropriate
* existing thread ID.
*
* Find the thread ID of the same set of recipients (in
* any order, without any additions). If one
* is found, return it. Otherwise, return a unique thread ID.
*/
public static long getOrCreateThreadId(
final Context context, final Set<String> recipients) {
final Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
for (String recipient : recipients) {
if (isEmailAddress(recipient)) {
recipient = extractAddrSpec(recipient);
}
uriBuilder.appendQueryParameter("recipient", recipient);
}
final Uri uri = uriBuilder.build();
//if (DEBUG) Rlog.v(TAG, "getOrCreateThreadId uri: " + uri);
final Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
uri, ID_PROJECTION, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getLong(0);
} else {
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG,
"getOrCreateThreadId returned no rows!");
}
} finally {
cursor.close();
}
}
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "getOrCreateThreadId failed with "
+ LogUtil.sanitizePII(recipients.toString()));
throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2015 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.messaging.sms;
/**
* A generic Exception for errors in sending SMS
*/
class SmsException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Creates a new SmsException.
*/
public SmsException() {
super();
}
/**
* Creates a new SmsException with the specified detail message.
*
* @param message the detail message.
*/
public SmsException(String message) {
super(message);
}
/**
* Creates a new SmsException with the specified cause.
*
* @param cause the cause.
*/
public SmsException(Throwable cause) {
super(cause);
}
/**
* Creates a new SmsException with the specified detail message and cause.
*
* @param message the detail message.
* @param cause the cause.
*/
public SmsException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,166 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.content.res.Resources;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class handling message cleanup when storage is low
*/
public class SmsReleaseStorage {
/**
* Class representing a time duration specified by Gservices
*/
public static class Duration {
// Time duration unit types
public static final int UNIT_WEEK = 'w';
public static final int UNIT_MONTH = 'm';
public static final int UNIT_YEAR = 'y';
// Number of units
public final int mCount;
// Unit type: week, month or year
public final int mUnit;
public Duration(final int count, final int unit) {
mCount = count;
mUnit = unit;
}
}
private static final String TAG = LogUtil.BUGLE_TAG;
private static final Duration DEFAULT_DURATION = new Duration(1, Duration.UNIT_MONTH);
private static final Pattern DURATION_PATTERN = Pattern.compile("([1-9]+\\d*)(w|m|y)");
/**
* Parse message retaining time duration specified by Gservices
*
* @return The parsed time duration from Gservices
*/
public static Duration parseMessageRetainingDuration() {
final String smsAutoDeleteMessageRetainingDuration =
BugleGservices.get().getString(
BugleGservicesKeys.SMS_STORAGE_PURGING_MESSAGE_RETAINING_DURATION,
BugleGservicesKeys.SMS_STORAGE_PURGING_MESSAGE_RETAINING_DURATION_DEFAULT);
final Matcher matcher = DURATION_PATTERN.matcher(smsAutoDeleteMessageRetainingDuration);
try {
if (matcher.matches()) {
return new Duration(
Integer.parseInt(matcher.group(1)),
matcher.group(2).charAt(0));
}
} catch (final NumberFormatException e) {
// Nothing to do
}
LogUtil.e(TAG, "SmsAutoDelete: invalid duration " +
smsAutoDeleteMessageRetainingDuration);
return DEFAULT_DURATION;
}
/**
* Get string representation of the time duration
*
* @param duration
* @return
*/
public static String getMessageRetainingDurationString(final Duration duration) {
final Resources resources = Factory.get().getApplicationContext().getResources();
switch (duration.mUnit) {
case Duration.UNIT_WEEK:
return resources.getQuantityString(
R.plurals.week_count, duration.mCount, duration.mCount);
case Duration.UNIT_MONTH:
return resources.getQuantityString(
R.plurals.month_count, duration.mCount, duration.mCount);
case Duration.UNIT_YEAR:
return resources.getQuantityString(
R.plurals.year_count, duration.mCount, duration.mCount);
}
throw new IllegalArgumentException(
"SmsAutoDelete: invalid duration unit " + duration.mUnit);
}
// Time conversations
private static final long WEEK_IN_MILLIS = 7 * 24 * 3600 * 1000L;
private static final long MONTH_IN_MILLIS = 30 * 24 * 3600 * 1000L;
private static final long YEAR_IN_MILLIS = 365 * 24 * 3600 * 1000L;
/**
* Convert time duration to time in milliseconds
*
* @param duration
* @return
*/
public static long durationToTimeInMillis(final Duration duration) {
switch (duration.mUnit) {
case Duration.UNIT_WEEK:
return duration.mCount * WEEK_IN_MILLIS;
case Duration.UNIT_MONTH:
return duration.mCount * MONTH_IN_MILLIS;
case Duration.UNIT_YEAR:
return duration.mCount * YEAR_IN_MILLIS;
}
return -1L;
}
/**
* Delete message actions:
* 0: delete media messages
* 1: delete old messages
*
* @param actionIndex The index of the delete action to perform
* @param durationInMillis The time duration for retaining messages
*/
public static void deleteMessages(final int actionIndex, final long durationInMillis) {
int deleted = 0;
switch (actionIndex) {
case 0: {
// Delete media
deleted = MmsUtils.deleteMediaMessages();
break;
}
case 1: {
// Delete old messages
final long now = System.currentTimeMillis();
final long cutOffTimestampInMillis = now - durationInMillis;
// Delete messages from telephony provider
deleted = MmsUtils.deleteMessagesOlderThan(cutOffTimestampInMillis);
break;
}
default: {
LogUtil.e(TAG, "SmsStorageStatusManager: invalid action " + actionIndex);
break;
}
}
if (deleted > 0) {
// Kick off a sync to update local db.
SyncManager.sync();
}
}
}
@@ -0,0 +1,315 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.receiver.SendStatusReceiver;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
/**
* Class that sends chat message via SMS.
*
* The interface emulates a blocking sending similar to making an HTTP request.
* It calls the SmsManager to send a (potentially multipart) message and waits
* on the sent status on each part. The waiting has a timeout so it won't wait
* forever. Once the sent status of all parts received, the call returns.
* A successful sending requires success status for all parts. Otherwise, we
* pick the highest level of failure as the error for the whole message, which
* is used to determine if we need to retry the sending.
*/
public class SmsSender {
private static final String TAG = LogUtil.BUGLE_TAG;
public static final String EXTRA_PART_ID = "part_id";
/*
* A map for pending sms messages. The key is the random request UUID.
*/
private static ConcurrentHashMap<Uri, SendResult> sPendingMessageMap =
new ConcurrentHashMap<Uri, SendResult>();
private static final Random RANDOM = new Random();
// Whether we should send multipart SMS as separate messages
private static Boolean sSendMultipartSmsAsSeparateMessages = null;
/**
* Class that holds the sent status for all parts of a multipart message sending
*/
public static class SendResult {
// Failure levels, used by the caller of the sender.
// For temporary failures, possibly we could retry the sending
// For permanent failures, we probably won't retry
public static final int FAILURE_LEVEL_NONE = 0;
public static final int FAILURE_LEVEL_TEMPORARY = 1;
public static final int FAILURE_LEVEL_PERMANENT = 2;
// Tracking the remaining pending parts in sending
private int mPendingParts;
// Tracking the highest level of failure among all parts
private int mHighestFailureLevel;
public SendResult(final int numOfParts) {
Assert.isTrue(numOfParts > 0);
mPendingParts = numOfParts;
mHighestFailureLevel = FAILURE_LEVEL_NONE;
}
// Update the sent status of one part
public void setPartResult(final int resultCode) {
mPendingParts--;
setHighestFailureLevel(resultCode);
}
public boolean hasPending() {
return mPendingParts > 0;
}
public int getHighestFailureLevel() {
return mHighestFailureLevel;
}
private int getFailureLevel(final int resultCode) {
switch (resultCode) {
case Activity.RESULT_OK:
return FAILURE_LEVEL_NONE;
case SmsManager.RESULT_ERROR_NO_SERVICE:
return FAILURE_LEVEL_TEMPORARY;
case SmsManager.RESULT_ERROR_RADIO_OFF:
return FAILURE_LEVEL_PERMANENT;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
return FAILURE_LEVEL_PERMANENT;
default: {
LogUtil.e(TAG, "SmsSender: Unexpected sent intent resultCode = " + resultCode);
return FAILURE_LEVEL_PERMANENT;
}
}
}
private void setHighestFailureLevel(final int resultCode) {
final int level = getFailureLevel(resultCode);
if (level > mHighestFailureLevel) {
mHighestFailureLevel = level;
}
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("SendResult:");
sb.append("Pending=").append(mPendingParts).append(",");
sb.append("HighestFailureLevel=").append(mHighestFailureLevel);
return sb.toString();
}
}
public static void setResult(final Uri requestId, final int resultCode,
final int errorCode, final int partId, int subId) {
if (resultCode != Activity.RESULT_OK) {
LogUtil.e(TAG, "SmsSender: failure in sending message part. "
+ " requestId=" + requestId + " partId=" + partId
+ " resultCode=" + resultCode + " errorCode=" + errorCode);
if (errorCode != SendStatusReceiver.NO_ERROR_CODE) {
final Context context = Factory.get().getApplicationContext();
UiUtils.showToastAtBottom(getSendErrorToastMessage(context, subId, errorCode));
}
} else {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SmsSender: received sent result. " + " requestId=" + requestId
+ " partId=" + partId + " resultCode=" + resultCode);
}
}
if (requestId != null) {
final SendResult result = sPendingMessageMap.get(requestId);
if (result != null) {
synchronized (result) {
result.setPartResult(resultCode);
if (!result.hasPending()) {
result.notifyAll();
}
}
} else {
LogUtil.e(TAG, "SmsSender: ignoring sent result. " + " requestId=" + requestId
+ " partId=" + partId + " resultCode=" + resultCode);
}
}
}
private static String getSendErrorToastMessage(final Context context, final int subId,
final int errorCode) {
final String carrierName = PhoneUtils.get(subId).getCarrierName();
if (TextUtils.isEmpty(carrierName)) {
return context.getString(R.string.carrier_send_error_unknown_carrier, errorCode);
} else {
return context.getString(R.string.carrier_send_error, carrierName, errorCode);
}
}
// This should be called from a RequestWriter queue thread
public static SendResult sendMessage(final Context context, final int subId, String dest,
String message, final String serviceCenter, final boolean requireDeliveryReport,
final Uri messageUri) throws SmsException {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SmsSender: sending message. " +
"dest=" + dest + " message=" + message +
" serviceCenter=" + serviceCenter +
" requireDeliveryReport=" + requireDeliveryReport +
" requestId=" + messageUri);
}
if (TextUtils.isEmpty(message)) {
throw new SmsException("SmsSender: empty text message");
}
// Get the real dest and message for email or alias if dest is email or alias
// Or sanitize the dest if dest is a number
if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway()) &&
(MmsSmsUtils.isEmailAddress(dest) || MmsSmsUtils.isAlias(dest, subId))) {
// The original destination (email address) goes with the message
message = dest + " " + message;
// the new address is the email gateway #
dest = MmsConfig.get(subId).getEmailGateway();
} else {
// remove spaces and dashes from destination number
// (e.g. "801 555 1212" -> "8015551212")
// (e.g. "+8211-123-4567" -> "+82111234567")
dest = PhoneNumberUtils.stripSeparators(dest);
}
if (TextUtils.isEmpty(dest)) {
throw new SmsException("SmsSender: empty destination address");
}
// Divide the input message by SMS length limit
final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
final ArrayList<String> messages = smsManager.divideMessage(message);
if (messages == null || messages.size() < 1) {
throw new SmsException("SmsSender: fails to divide message");
}
// Prepare the send result, which collects the send status for each part
final SendResult pendingResult = new SendResult(messages.size());
sPendingMessageMap.put(messageUri, pendingResult);
// Actually send the sms
sendInternal(
context, subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri);
// Wait for pending intent to come back
synchronized (pendingResult) {
final long smsSendTimeoutInMillis = BugleGservices.get().getLong(
BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS,
BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT);
final long beginTime = SystemClock.elapsedRealtime();
long waitTime = smsSendTimeoutInMillis;
// We could possibly be woken up while still pending
// so make sure we wait the full timeout period unless
// we have the send results of all parts.
while (pendingResult.hasPending() && waitTime > 0) {
try {
pendingResult.wait(waitTime);
} catch (final InterruptedException e) {
LogUtil.e(TAG, "SmsSender: sending wait interrupted");
}
waitTime = smsSendTimeoutInMillis - (SystemClock.elapsedRealtime() - beginTime);
}
}
// Either we timed out or have all the results (success or failure)
sPendingMessageMap.remove(messageUri);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SmsSender: sending completed. " +
"dest=" + dest + " message=" + message + " result=" + pendingResult);
}
return pendingResult;
}
// Actually sending the message using SmsManager
private static void sendInternal(final Context context, final int subId, String dest,
final ArrayList<String> messages, final String serviceCenter,
final boolean requireDeliveryReport, final Uri messageUri) throws SmsException {
Assert.notNull(context);
final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
final int messageCount = messages.size();
final ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount);
final ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messageCount);
for (int i = 0; i < messageCount; i++) {
// Make pending intents different for each message part
final int partId = (messageCount <= 1 ? 0 : i + 1);
if (requireDeliveryReport && (i == (messageCount - 1))) {
// TODO we only care about the delivery status of the last part
// Shall we have better tracking of delivery status of all parts?
deliveryIntents.add(PendingIntent.getBroadcast(
context,
partId,
getSendStatusIntent(context, SendStatusReceiver.MESSAGE_DELIVERED_ACTION,
messageUri, partId, subId),
0/*flag*/));
} else {
deliveryIntents.add(null);
}
sentIntents.add(PendingIntent.getBroadcast(
context,
partId,
getSendStatusIntent(context, SendStatusReceiver.MESSAGE_SENT_ACTION,
messageUri, partId, subId),
0/*flag*/));
}
if (sSendMultipartSmsAsSeparateMessages == null) {
sSendMultipartSmsAsSeparateMessages = MmsConfig.get(subId)
.getSendMultipartSmsAsSeparateMessages();
}
try {
if (sSendMultipartSmsAsSeparateMessages) {
// If multipart sms is not supported, send them as separate messages
for (int i = 0; i < messageCount; i++) {
smsManager.sendTextMessage(dest,
serviceCenter,
messages.get(i),
sentIntents.get(i),
deliveryIntents.get(i));
}
} else {
smsManager.sendMultipartTextMessage(
dest, serviceCenter, messages, sentIntents, deliveryIntents);
}
} catch (final Exception e) {
throw new SmsException("SmsSender: caught exception in sending " + e);
}
}
private static Intent getSendStatusIntent(final Context context, final String action,
final Uri requestUri, final int partId, final int subId) {
// Encode requestId in intent data
final Intent intent = new Intent(action, requestUri, context, SendStatusReceiver.class);
intent.putExtra(SendStatusReceiver.EXTRA_PART_ID, partId);
intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId);
return intent;
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.PendingIntentConstants;
import com.android.messaging.util.PhoneUtils;
/**
* Class that handles SMS auto delete and notification when storage is low
*/
public class SmsStorageStatusManager {
/**
* Handles storage low signal for SMS
*/
public static void handleStorageLow() {
if (!PhoneUtils.getDefault().isSmsEnabled()) {
return;
}
// TODO: Auto-delete messages, when that setting exists and is enabled
// Notify low storage for SMS
postStorageLowNotification();
}
/**
* Handles storage OK signal for SMS
*/
public static void handleStorageOk() {
if (!PhoneUtils.getDefault().isSmsEnabled()) {
return;
}
cancelStorageLowNotification();
}
/**
* Post sms storage low notification
*/
private static void postStorageLowNotification() {
final Context context = Factory.get().getApplicationContext();
final Resources resources = context.getResources();
final PendingIntent pendingIntent = UIIntents.get()
.getPendingIntentForLowStorageNotifications(context);
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
builder.setContentTitle(resources.getString(R.string.sms_storage_low_title))
.setTicker(resources.getString(R.string.sms_storage_low_notification_ticker))
.setSmallIcon(R.drawable.ic_failed_light)
.setPriority(Notification.PRIORITY_DEFAULT)
.setOngoing(true) // Can't be swiped off
.setAutoCancel(false) // Don't auto cancel
.setContentIntent(pendingIntent);
final NotificationCompat.BigTextStyle bigTextStyle =
new NotificationCompat.BigTextStyle(builder);
bigTextStyle.bigText(resources.getString(R.string.sms_storage_low_text));
final Notification notification = bigTextStyle.build();
final NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(Factory.get().getApplicationContext());
notificationManager.notify(getNotificationTag(),
PendingIntentConstants.SMS_STORAGE_LOW_NOTIFICATION_ID, notification);
}
/**
* Cancel the notification
*/
public static void cancelStorageLowNotification() {
final NotificationManagerCompat notificationManager =
NotificationManagerCompat.from(Factory.get().getApplicationContext());
notificationManager.cancel(getNotificationTag(),
PendingIntentConstants.SMS_STORAGE_LOW_NOTIFICATION_ID);
}
private static String getNotificationTag() {
return Factory.get().getApplicationContext().getPackageName() + ":smsstoragelow";
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2015 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.messaging.sms;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Hacky way to call the hidden SystemProperties class API
*/
class SystemProperties {
private static Method sSystemPropertiesGetMethod = null;
public static String get(final String name) {
if (sSystemPropertiesGetMethod == null) {
try {
final Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
if (systemPropertiesClass != null) {
sSystemPropertiesGetMethod =
systemPropertiesClass.getMethod("get", String.class);
}
} catch (final ClassNotFoundException e) {
// Nothing to do
} catch (final NoSuchMethodException e) {
// Nothing to do
}
}
if (sSystemPropertiesGetMethod != null) {
try {
return (String) sSystemPropertiesGetMethod.invoke(null, name);
} catch (final IllegalArgumentException e) {
// Nothing to do
} catch (final IllegalAccessException e) {
// Nothing to do
} catch (final InvocationTargetException e) {
// Nothing to do
}
}
return null;
}
}